Files
flight-sim/tests/model-geometry.mjs
T
Quintine 8c19cdaae6 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.
2026-08-24 17:58:58 +10:00

74 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);