feat(city): add landing gear, cockpit glass, and winglets to aircraft

Implement proper ground clearance and landing gear for all aircraft
types (airliner, regional, prop). Add cockpit glass at the nose,
winglets at wing tips, and configurable propeller blade length.
Wheels now touch the runway surface (y=0) with the fuselage lifted
by clearance values.
This commit is contained in:
2026-10-31 22:40:17 -03:00
parent ecf5291027
commit e230c391ad
2 changed files with 61 additions and 4 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

+61 -4
View File
@@ -2602,9 +2602,11 @@ function runwayMarkings(designators) {
function buildAircraft(kind, livery) {
const g = new THREE.Group();
const dims = {
airliner: { L: 16, r: 0.95, span: 15, chord: 2.6, tail: 3.6, fin: 2.8, engines: true },
regional: { L: 12, r: 0.75, span: 11.6, chord: 2.1, tail: 2.8, fin: 2.2, engines: true },
prop: { L: 9.5, r: 0.65, span: 12, chord: 1.9, tail: 2.4, fin: 1.9, engines: false, prop: true },
// clearance: fuselage centreline height — the wheels (bottom at
// y≈0) carry the airframe above the pavement instead of dragging.
airliner: { L: 16, r: 0.95, span: 15, chord: 2.6, tail: 3.6, fin: 2.8, engines: true, clearance: 1.75, wheel: 0.34 },
regional: { L: 12, r: 0.75, span: 11.6, chord: 2.1, tail: 2.8, fin: 2.2, engines: true, clearance: 1.4, wheel: 0.3 },
prop: { L: 9.5, r: 0.65, span: 12, chord: 1.9, tail: 2.4, fin: 1.9, engines: false, prop: true, blade: 2.6, clearance: 1.5, wheel: 0.28 },
};
const d = dims[kind] || dims.airliner;
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xf3f5f8, roughness: 0.35, metalness: 0.45 });
@@ -2660,8 +2662,9 @@ function buildAircraft(kind, livery) {
if (d.prop) {
const prop = new THREE.Group();
const bladeLen = d.blade ?? 3.4;
for (let i = 0; i < 3; i++) {
const blade = new THREE.Mesh(new THREE.BoxGeometry(0.12, 3.4, 0.16), darkMat);
const blade = new THREE.Mesh(new THREE.BoxGeometry(0.12, bladeLen, 0.16), darkMat);
blade.rotation.x = (i * Math.PI * 2) / 3;
prop.add(blade);
}
@@ -2670,6 +2673,19 @@ function buildAircraft(kind, livery) {
g.userData.prop = prop;
}
// Cockpit glass at the nose (rider sits above the nose gear).
const glassMat = new THREE.MeshStandardMaterial({ color: 0x0e1620, roughness: 0.2, metalness: 0.8 });
const cockpit = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.4, 0.62), glassMat);
cockpit.position.set(d.L * 0.44, d.r * 0.5, 0);
g.add(cockpit);
// Winglets at the tips (airline livery accent).
for (const side of [1, -1]) {
const winglet = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.55, 0.12), accentMat);
winglet.position.set(d.L * 0.04, 0.1, side * (d.span / 2));
g.add(winglet);
}
const beacon = new THREE.Mesh(
new THREE.SphereGeometry(0.2, 8, 8),
new THREE.MeshStandardMaterial({ color: 0xff3333, emissive: 0xff3333, emissiveIntensity: 2 })
@@ -2677,7 +2693,34 @@ function buildAircraft(kind, livery) {
beacon.position.set(d.L * 0.08, d.r + 0.3, 0);
g.add(beacon);
g.userData.beacon = beacon;
// Ride on the landing gear: lift the whole airframe so the wheels
// (added next, ground-based) carry it above the pavement.
for (const child of g.children) {
child.position.y += d.clearance;
}
// ---- Landing gear: nose leg + two mains; wheel bottoms touch y=0 ----
const gearMat = new THREE.MeshStandardMaterial({ color: 0x2a2e36, roughness: 0.45, metalness: 0.6 });
const addGear = (gx, gz, wr, strutR) => {
const strutBottom = wr * 0.5;
const top = d.clearance - d.r * 0.35;
const strutH = top - strutBottom;
const strut = new THREE.Mesh(new THREE.CylinderGeometry(strutR, strutR * 1.2, strutH, 8), gearMat);
strut.position.set(gx, strutBottom + strutH / 2, gz);
g.add(strut);
const wheel = new THREE.Mesh(new THREE.CylinderGeometry(wr, wr, wr * 0.75, 12), gearMat);
wheel.rotation.x = Math.PI / 2; // axle along Z — rolls along the runway
// Centre at half the tyre thickness → the bottom sits exactly on y=0.
wheel.position.set(gx, wr * 0.375, gz);
g.add(wheel);
};
addGear(d.L * 0.4, 0, d.wheel * 0.85, 0.06); // nose gear
addGear(d.L * 0.06, d.span * 0.2, d.wheel, 0.085); // main right
addGear(d.L * 0.06, -d.span * 0.2, d.wheel, 0.085); // main left
g.userData.kind = kind;
g.userData.gear = { kind: 'wheels', clearance: d.clearance };
g.traverse(o => { if (o.isMesh) o.castShadow = true; });
return g;
}
@@ -2753,6 +2796,12 @@ function buildHelicopter(livery) {
beacon.position.set(0, 2.0, 0);
g.add(beacon);
g.userData.beacon = beacon;
// Ride on the skids: lift the fuselage so the skid tubes rest on y=0.
for (const child of g.children) {
child.position.y += 1.4;
}
g.userData.gear = { kind: 'skids', clearance: 1.4 };
g.userData.kind = 'helicopter';
g.traverse(o => { if (o.isMesh) o.castShadow = true; });
return g;
@@ -3280,6 +3329,14 @@ function initAirplanes(cfg) {
takeoffs: flights.takeoffs,
landings: flights.landings,
states: aircraft.map(a => a.callsign + ':' + a.state),
gear: aircraft.map(a => ({
kind: a.mesh.userData.kind,
gear: a.mesh.userData.gear?.kind || 'none',
state: a.state,
x: +a.p[0].toFixed(1),
y: +a.p[1].toFixed(2),
z: +a.p[2].toFixed(1),
})),
};
},
update(dt) {