3D flight simulator: 6-DOF physics, three.js r160, browser HUD

- Detailed flight dynamics: aero lift/drag/side-force with smooth stall,
  control-surface moments, stability derivatives, engine spool with prop
  torque and gyro precession, inertial coupling, ISA atmosphere, ground
  effect, 3-point gear handling, landing vs crash classification.
- Pure ESM, zero build step; three.js vendored locally. Deterministic
  simulation; physics core is Node-testable (14 node:test cases).
- Procedural 8 km world with runway, water, sky dome, cloud wrap; DOM
  instrument panel (attitude indicator, tapes, VSI, lamps, stall warning).
- Controls: WASD/arrows, Q/E rudder, Shift/Ctrl throttle, F flaps, G gear,
  B brakes, C camera (chase/cockpit/orbit), R reset, P pause, D debug.
This commit is contained in:
Quintine committed 2026-08-24 17:58:58 +10:00
commit 8c19cdaae6
18 files changed
+4499

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
// Geometry sanity: the wing/stab chords must run AFT (−X), and flaps/
// ailerons/elevator must hinge at the actual trailing edges. Runs the vendored
// three.js in Node — no DOM needed.
import * as THREE from '../vendor/three.module.min.js';
import { buildAircraftModel } from '../src/aircraft-model.js';
const { group, animate } = buildAircraftModel();
const bbox = (m) => new THREE.Box3().setFromObject(m); // fresh Box3 per call
let fail = 0;
const check = (name, cond, detail) => {
console.log((cond ? 'PASS' : 'FAIL') + ' ' + name + (detail ? ' (' + detail + ')' : ''));
if (!cond) fail++;
};
// Identify meshes by geometry parameters.
const meshes = [];
group.traverse((o) => { if (o.isMesh) meshes.push(o); });
const byBox = (w, h, d) => meshes.filter((m) => {
const p = m.geometry.parameters;
return p && p.width === w && p.height === h && p.depth === d;
});
const wing = meshes.find((m) => m.geometry.type === 'ExtrudeGeometry' && Math.abs(m.position.y) < 0.001 && m.position.x > 0 && m.position.z > 0.5);
const stab = meshes.find((m) => m.geometry.type === 'ExtrudeGeometry' && m.position.x < -2 && m.position.z > -0.2);
const flaps = byBox(0.45, 1.15, 0.045);
const ails = byBox(0.42, 2.0, 0.045);
const elevs = byBox(0.32, 4.0, 0.06);
if (!wing || !stab) { console.log('FAIL could not locate wing/stab meshes'); process.exit(1); }
const wb = bbox(wing), sb = bbox(stab);
console.log('wing bbox: x [' + wb.min.x.toFixed(2) + ', ' + wb.max.x.toFixed(2) + '] z [' + wb.min.z.toFixed(2) + ', ' + wb.max.z.toFixed(2) + ']');
console.log('stab bbox: x [' + sb.min.x.toFixed(2) + ', ' + sb.max.x.toFixed(2) + ']');
check('wing chord runs AFT (LE x=0.65 > TE x≈−1.30)', Math.abs(wb.max.x - 0.65) < 0.02 && wb.min.x < -1.25, 'LE=' + wb.max.x.toFixed(2) + ' TE=' + wb.min.x.toFixed(2));
const wingTE = wb.min.x;
for (const f of flaps) {
const fb = bbox(f);
check('flap hinges at wing TE and sweeps AFT', Math.abs(fb.max.x - wingTE) < 0.03 && fb.min.x < fb.max.x - 0.4,
'hinge=' + fb.max.x.toFixed(2) + ' aftEnd=' + fb.min.x.toFixed(2));
check('flap sits behind the wing LEADING edge (not at the front!)', fb.max.x < wb.max.x - 1.5);
}
for (const a of ails) {
const ab = bbox(a);
check('aileron hinges at wing TE and sweeps AFT', ab.max.x > -1.35 && ab.max.x < -1.1 && ab.min.x < ab.max.x - 0.35,
'hinge=' + ab.max.x.toFixed(2));
check('aileron behind the wing LE', ab.max.x < wb.max.x - 1.5);
}
check('two flaps + two ailerons built', flaps.length === 2 && ails.length === 2, flaps.length + '/' + ails.length);
const elev = elevs[0];
if (elev) {
const eb = bbox(elev);
check('elevator hinges at stab TE (−3.08) and sweeps AFT',
Math.abs(eb.max.x - sb.min.x) < 0.03 && eb.min.x < eb.max.x - 0.25,
'hinge=' + eb.max.x.toFixed(2) + ' stabTE=' + sb.min.x.toFixed(2));
}
// Deflection directions: drive animate() and read the sign of each surface.
const inputs = { elevator: 1, aileron: 1, rudder: 0.5, flaps: 1, gear: true, brakes: false };
animate({ rpm: 0.5, flaps: 1, gearDown: true }, inputs, 0.1);
const find = (w, h, d) => byBox(w, h, d)[0];
const elevMesh = find(0.32, 4.0, 0.06);
const flapMesh = find(0.45, 1.15, 0.045);
const ailS = ails.find((m) => m.position.y > 0);
const ailP = ails.find((m) => m.position.y < 0);
check('elevator TE UP on nose-up command (rotation.y < 0)', elevMesh.rotation.y < 0, elevMesh.rotation.y.toFixed(3));
check('flaps TE DOWN when deployed (rotation.y > 0)', flapMesh.rotation.y > 0, flapMesh.rotation.y.toFixed(3));
check('starboard aileron TE UP on positive input (rotation.y < 0)', ailS.rotation.y < 0, ailS.rotation.y.toFixed(3));
check('port aileron TE DOWN on positive input (rotation.y > 0)', ailP.rotation.y > 0, ailP.rotation.y.toFixed(3));
console.log(fail === 0 ? '\nALL GEOMETRY CHECKS PASSED' : '\n' + fail + ' CHECK(S) FAILED');
process.exit(fail === 0 ? 0 : 1);