diff --git a/.playwright-mcp/aeronave-trem-de-pouso.png b/.playwright-mcp/aeronave-trem-de-pouso.png new file mode 100644 index 0000000..1195aee Binary files /dev/null and b/.playwright-mcp/aeronave-trem-de-pouso.png differ diff --git a/public/js/city/app.js b/public/js/city/app.js index d76b361..f72aabd 100755 --- a/public/js/city/app.js +++ b/public/js/city/app.js @@ -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) {