feat(flights): aircraft draw life-size fleet models chosen by type

Flights, Military Flights and Local ADS-B draw each aircraft with the fleet
model its ICAO type, class or emitter category selects, scaled to the type's
length, built from the up-front loaded bytes. The eight third-party aircraft
models and their attribution are removed; every shipped model is now the
project's own.
This commit is contained in:
Sameh Khamis committed 2026-11-13 07:16:25 -08:00
1 parent f6760ad96c
commit fcc44d0eaf
45 files changed
+970 -1371

No files matched your search

+3 -3
View File
@@ -40,7 +40,7 @@
* the DISPLAYED (render-delayed) time within 35° — catches
* sign/offset inversion in the whole chain (chord lags the
* tangent by ≤ half a segment ≈ 22.5°)
* Both layers: flights (airplane.glb, offset 180°) then military (jet.glb,
* Both layers: flights (fleet models, offset 180°) then military (fleet models,
* normalized nose -X, offset 180°).
*
* VISUAL — with the turning flights plane tracked in 3D, capture screenshots
@@ -721,7 +721,7 @@ async function main() {
// ========================================================================
// Phase 1 — FLIGHTS layer: track TRN001, sample displayed course 30 s
// ========================================================================
console.log('\nPhase 1 — flights layer (airplane.glb, heading offset 180°)');
console.log('\nPhase 1 — flights layer (fleet models, heading offset 180°)');
await page.evaluate((icao) => {
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
}, TURN.flights[0].icao);
@@ -807,7 +807,7 @@ async function main() {
// ========================================================================
// Phase 2 — MILITARY layer: track TRNMIL1 (flights layer auto-clears)
// ========================================================================
console.log('\nPhase 2 — military layer (jet.glb, heading offset 180°)');
console.log('\nPhase 2 — military layer (fleet models, heading offset 180°)');
await page.evaluate((hex) => {
window.__godsEyeView.dataManager.layers.get('military').module.trackById(hex);
}, TURN.military[0].hex);
+1 -1
View File
@@ -75,7 +75,7 @@ try {
}
const position = C.Cartesian3.fromDegrees(0, 0, 10000);
const model = await C.Model.fromGltfAsync({
url: '/models/airplane.glb',
url: '/models/fleet/aircraft/a320.glb',
modelMatrix: C.Transforms.eastNorthUpToFixedFrame(position),
});
scene.primitives.add(model);
+32 -33
View File
@@ -15,9 +15,9 @@
* per-class glyph (image data-URI === aircraftIcon(klass),
* imported from src) and per-class scale (flights:
* CLASS_SCALE_2D; military: 0.7 × CLASS_SCALE_2D)
* B3 models : with 3D on, fleet Cesium.Model.scale === the layer's
* MODEL_SCALE (both 1; legacy scale is baked into the GLB)
* × CLASS_SCALE_3D[klass] per icao; tracked too
* B3 models : with 3D on, fleet Cesium.Model.scale === the life-size
* scale of the fleet model the aircraft's type or category
* selects (aircraftModelSpec) per icao; tracked too
* B4 clean : no console errors
*
* Visual output (qa-shots/b5/, untracked): top-down 2D spreads of both rows,
@@ -33,15 +33,18 @@ import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
import { classifyAircraft, CLASS_SCALE_2D, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
// Hangar fleet (2026-08-16): real per-class GLBs render at scale 1; military
// heavies (airliner/quadjet/glider) render airplane.glb at 1 x class; only
// fastjet/unknown keep the per-layer jet/airplane MODEL_SCALE formula.
const flightsWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1 : FL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
const MIL_PLANE_CLASSES = new Set(['airliner', 'quadjet', 'glider']);
const militaryWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1
: MIL_PLANE_CLASSES.has(klass) ? MIL_PLANE_MODEL_SCALE * CLASS_SCALE_3D[klass]
: MIL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
import { classifyAircraft, CLASS_SCALE_2D } from '../src/data/aircraftClass.js';
import { aircraftModelSpec } from '../src/data/fleetModels.js';
// Every aircraft draws its fleet model life size: its type's length over the
// model's built length, chosen by type code (military) or category (flights).
const FLEET_SET = Object.freeze({
catalog: JSON.parse(fs.readFileSync(new URL('../public/models/fleet/catalog.json', import.meta.url), 'utf8')),
types: JSON.parse(fs.readFileSync(new URL('../public/models/fleet/types.json', import.meta.url), 'utf8')),
});
const flightsWantScale = (icao) =>
aircraftModelSpec(FLEET_SET, { category: SPRITES.flights.find((p) => p.icao === icao)?.category }).scale;
const militaryWantScale = (hex) =>
aircraftModelSpec(FLEET_SET, { typeCode: SPRITES.military.find((p) => p.hex === hex)?.t }).scale;
import { aircraftIcon } from '../src/data/aircraftIcons.js';
// ---------------------------------------------------------------------------
@@ -58,9 +61,6 @@ const APP_URL = getOpt('--url', 'http://localhost:4173');
const HEADFUL = getFlag('--headful');
const SHOT_DIR = path.resolve('qa-shots/b5');
const FL_MODEL_SCALE = 1; // flights.js (airplane.glb is transform-applied and meter-scale)
const MIL_PLANE_MODEL_SCALE = 1; // military heavy classes share the same baked airplane.glb
const MIL_MODEL_SCALE = 1; // militaryFlights.js (jet.glb is already real-world scale, native radius ~29.8 m)
const MIL_BILLBOARD_SCALE = 0.7; // militaryFlights BILLBOARD_SCALE
const CHROME_EXECUTABLE_CANDIDATES = [
@@ -134,10 +134,9 @@ const SPRITES = {
// course 0 like the rest — keeps the row a clean size-comparison line (2D
// rotation is demoed by caa004 and gated by qa-heading-b3 anyway).
{ hex: 'dda008', flight: 'ML-FJ', t: 'F16', lon0: ROW_LON0 + 7 * ROW_STEP_DEG, lat0: 30.28, courseDeg: 0, speedMps: 80, altFt: 15000 },
// 3D cluster: C17 vs F16 (tracked) vs B744 vs C130. jet.glb is a
// real-world-scale asset (native bounding radius ~29.8) rendered at
// MODEL_SCALE 1, so models are ~22–43 m radius — same 150 m spacing and
// 450–900 m shot ranges as the flights cluster.
// 3D cluster: C17 vs F16 (tracked) vs B744 vs C130, each drawn life size
// (~8–36 m radius) — same 150 m spacing and 450–900 m shot ranges as the
// flights cluster.
{ hex: 'ddb001', flight: '3DMWID', t: 'C17', lon0: CLUSTER_LON0 - mDegLon(150, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb002', flight: '3DMFJ', t: 'F16', lon0: CLUSTER_LON0, lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
{ hex: 'ddb003', flight: '3DMQUA', t: 'B744', lon0: CLUSTER_LON0 + mDegLon(150, 30.05), lat0: 30.05, courseDeg: 0, speedMps: 60, altFt: 12000 },
@@ -464,7 +463,7 @@ async function main() {
// ========================================================================
// B3 + 3D screenshots — flights cluster
// ========================================================================
console.log('\n3D — flights cluster (airplane.glb, per-class scale)');
console.log('\n3D — flights cluster (fleet models, life size)');
await page.evaluate(() => {
const dm = window.__godsEyeView.dataManager;
dm.layers.get('flights').module.setParams({ models3d: true });
@@ -481,20 +480,20 @@ async function main() {
const flModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
const modelScaleBad = [];
for (const [id, want] of [
['cbb001', flightsWantScale(expectedFlights.get('cbb001'))],
['cbb003', flightsWantScale(expectedFlights.get('cbb003'))],
['cbb001', flightsWantScale('cbb001')],
['cbb003', flightsWantScale('cbb003')],
]) {
const m = flModels.find((x) => x.id === id);
if (!m || Math.abs(m.scale - want) > 1e-9) modelScaleBad.push(`${id}: ${m ? m.scale : 'missing'} != ${want}`);
}
const trackedM = flModels.find((x) => x.id === 'cbb002');
const trackedWant = flightsWantScale(expectedFlights.get('cbb002'));
const trackedWant = flightsWantScale('cbb002');
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
}
record('B3a flights: model scales match the per-class registry (all GLBs meter-scale; shared @1×class)',
record('B3a flights: model scales match each type's life-size fleet model',
modelScaleBad.length === 0,
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact per registry');
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact');
console.log(` world bounding radii (m): ${flModels.filter((m) => m.scale !== undefined).map((m) => `${m.id ?? 'tracked'}=${m.radius?.toFixed(1)}`).join(' ')}`);
await sleep(1500);
@@ -509,7 +508,7 @@ async function main() {
// ========================================================================
// B3 + 3D screenshots — military cluster
// ========================================================================
console.log('\n3D — military cluster (jet.glb, per-class scale)');
console.log('\n3D — military cluster (fleet models, life size)');
await page.evaluate(() => {
const dm = window.__godsEyeView.dataManager;
dm.layers.get('military').module.setParams({ models3d: true });
@@ -526,25 +525,25 @@ async function main() {
const milModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
const modelScaleBad = [];
for (const [id, want] of [
['ddb001', militaryWantScale(expectedMilitary.get('ddb001'))],
['ddb003', militaryWantScale(expectedMilitary.get('ddb003'))],
['ddb004', militaryWantScale(expectedMilitary.get('ddb004'))],
['ddb001', militaryWantScale('ddb001')],
['ddb003', militaryWantScale('ddb003')],
['ddb004', militaryWantScale('ddb004')],
]) {
const m = milModels.find((x) => x.id === id);
if (!m || Math.abs(m.scale - want) > 1e-9) modelScaleBad.push(`${id}: ${m ? m.scale : 'missing'} != ${want}`);
}
const trackedM = milModels.find((x) => x.id === 'ddb002');
const trackedWant = militaryWantScale(expectedMilitary.get('ddb002'));
const trackedWant = militaryWantScale('ddb002');
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
}
record('B3b military: model scales match the weight-class registry (real/heavy/jet assets @1×class)',
record('B3b military: model scales match each type's life-size fleet model',
modelScaleBad.length === 0,
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact per registry');
modelScaleBad.length ? modelScaleBad.join(' ') : 'all exact');
console.log(` world bounding radii (m): ${milModels.filter((m) => m.scale !== undefined).map((m) => `${m.id ?? 'tracked'}=${m.radius?.toFixed(1)}`).join(' ')}`);
await sleep(1500);
// jet.glb at MODEL_SCALE 1 is real-world size — same shot ranges as the
// Fleet models are drawn life size — same shot ranges as the
// flights cluster (close zoom no longer puts the camera inside a model).
await zoomToRange(900);
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-military-far.png') });
+24 -24
View File
@@ -97,9 +97,15 @@ import fs from 'node:fs';
import puppeteer from 'puppeteer';
import { respondToPlanespotters } from './qa-photo-stub.mjs';
import { arrivalAngleViolated, arrivalRotationPassed } from '../src/tooling/trackArrivalObservation.mjs';
import { classifyAircraft, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
import { aircraftModelSpec } from '../src/data/fleetModels.js';
import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js';
/** The fleet model set the aircraft layers draw from, read from disk. */
const FLEET_SET = Object.freeze({
catalog: JSON.parse(fs.readFileSync(new URL('../public/models/fleet/catalog.json', import.meta.url), 'utf8')),
types: JSON.parse(fs.readFileSync(new URL('../public/models/fleet/types.json', import.meta.url), 'utf8')),
});
// ---------------------------------------------------------------------------
// Args
// ---------------------------------------------------------------------------
@@ -547,13 +553,13 @@ async function main() {
// Independent GLB capability control. Product model absence cannot prove
// that the browser backend is incapable: that is the condition this
// harness is meant to catch. Load the already-approved airplane asset
// harness is meant to catch. Load one of the shipped fleet models
// directly through Cesium, outside the Flights/Military model pipelines,
// then remove the control before exercising product behavior.
const glbControlStarted = await evalPage(async () => {
window.__qaGlbControl = null;
window.__qaGlbControlError = null;
const asset = await fetch('/models/airplane.glb');
const asset = await fetch('/models/fleet/aircraft/a320.glb');
const assetBytes = asset.ok ? (await asset.arrayBuffer()).byteLength : 0;
if (!asset.ok || assetBytes === 0) {
throw new Error(`GLB control asset unavailable: HTTP ${asset.status}`);
@@ -564,7 +570,7 @@ async function main() {
Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 9000),
);
void Cesium.Model.fromGltfAsync({
url: '/models/airplane.glb',
url: '/models/fleet/aircraft/a320.glb',
modelMatrix,
scale: 1,
}).then((model) => {
@@ -1454,8 +1460,8 @@ async function main() {
}, trackedIcao);
console.log(' (3D model did not render under SwiftShader; testing the shared display-position contract directly)');
} else {
// Full check: independently transform airplane.glb's measured AABB centre
// through the actual 3D model matrix, then compare it with detection.
// Full check: independently transform the fleet model's visual centre
// (its origin) through the actual 3D model matrix, then compare it with detection.
// glTF [x,y,z] → Cesium-local [x,-z,y]. The effective rendered
// computedScale is separate from modelMatrix and includes any
// minimumPixelSize enlargement, so include it just as the renderer does.
@@ -1469,7 +1475,7 @@ async function main() {
const scale = Number.isFinite(model.computedScale)
? model.computedScale
: (Number.isFinite(model.scale) ? model.scale : 1);
// airplane.glb is now bounding-box centred with transforms applied.
// Fleet models are drawn about their origin, transforms applied.
const x = 0 * scale;
const y = 0 * scale;
const z = 0 * scale;
@@ -2229,18 +2235,12 @@ async function main() {
// ============================================================
console.log('\nGround 3D — grounded planes are model-eligible + snapped to the sampled skin');
const GROUND3D_STUB_H = 187.5; // stubbed tile-skin height (m, ellipsoid)
// Expected belly offsets (m): native origin-above-lowest-vertex × MODEL_SCALE
// × CLASS_SCALE_3D[klass] — constants match the layers' MODEL_BELLY_OFFSET_NATIVE
// / MODEL_SCALE pairs, which modelScale.test.mjs locks against the GLBs.
// Class is computed with the REAL classifier on the synthetic feed fields.
const g3dFlKlass = classifyAircraft({ typeCode: null, category: null });
const g3dMilKlass = classifyAircraft({ typeCode: 'C130', category: undefined });
const g3dFlOffset = 6.719 * 1 * (CLASS_SCALE_3D[g3dFlKlass] || 1); // airplane.glb, flights
// Hangar fleet (2026-08-16): military C130 → turboprop → the real atr72.glb,
// whose belly offset is the registry's measured bellyM (meters, scale 1) —
// NOT the jet.glb 5.631 × class formula. Pinned by modelScale.test.mjs.
const g3dMilOffset = CLASS_MODEL_REAL[g3dMilKlass]?.bellyM
?? 5.631 * 1 * (CLASS_SCALE_3D[g3dMilKlass] || 1); // jet.glb fallback classes
// Expected belly offsets (m): the fleet model's wheel clearance at its
// life-size scale, the model chosen as the layers choose it
// (aircraftModelSpec) from the synthetic feed's fields: no type for the
// flights contact (the default airliner), a C130 for the military one.
const g3dFlOffset = aircraftModelSpec(FLEET_SET, { typeCode: null }).clearanceM;
const g3dMilOffset = aircraftModelSpec(FLEET_SET, { typeCode: 'C130' }).clearanceM;
// Height-datum fix (Task 6): the flights-layer billboard for a grounded synthetic
// (no geo_altitude in this stub feed, no warm terrain-height cache in a headless
// run) now renders at baroM + geoidHeight(lat, lon) rather than the raw baro
@@ -2509,10 +2509,10 @@ async function main() {
const flDelta = g3dState.flModelRadius - g3dIngest.flBBRadius;
const milDelta = g3dState.milModelRadius - g3dIngest.milBBRadius;
const HEIGHT_TOL_M = 1.0;
record(`ground-3d: flights model height = sampled skin + belly offset (${g3dFlKlass})`,
record(`ground-3d: flights model height = sampled skin + belly offset (default airliner)`,
Math.abs(flDelta - flWantDelta) <= HEIGHT_TOL_M,
`Δradial=${flDelta.toFixed(2)} m want=${flWantDelta.toFixed(2)} m (stub ${GROUND3D_STUB_H} + offset ${g3dFlOffset.toFixed(2)} − alt ${(150 + g3dFlGeoidN).toFixed(2)} [150 baro + ${g3dFlGeoidN.toFixed(2)} geoidN])`);
record(`ground-3d: military model height = sampled skin + belly offset (${g3dMilKlass})`,
record(`ground-3d: military model height = sampled skin + belly offset (C130)`,
Math.abs(milDelta - milWantDelta) <= HEIGHT_TOL_M,
`Δradial=${milDelta.toFixed(2)} m want=${milWantDelta.toFixed(2)} m (stub ${GROUND3D_STUB_H} + offset ${g3dMilOffset.toFixed(2)} − alt 0)`);
record('ground-3d: billboard→model handoff holds on the ground (icons hidden once models render)',
@@ -2712,7 +2712,7 @@ async function main() {
const scale = Number.isFinite(m.computedScale)
? m.computedScale
: (Number.isFinite(m.scale) ? m.scale : 1);
// airplane.glb is now bounding-box centred with transforms applied.
// Fleet models are drawn about their origin, transforms applied.
const x = 0 * scale;
const y = 0 * scale;
const z = 0 * scale;
@@ -3722,8 +3722,8 @@ async function main() {
// WARM ground, camera IN: the placement. The model is not merely visible,
// it is standing ON the floor — within one class belly offset above it,
// never under it. MAX_BELLY_M clears the largest offset any class adds
// (widebody b789, 7.81 m in CLASS_MODEL_REAL); the floor itself is the
// never under it. MAX_BELLY_M clears the largest wheel clearance any
// fleet aircraft stands on at life size; the floor itself is the
// hard lower bound, and a model placed at the raw feed altitude is ~240 m
// under it.
const MAX_BELLY_M = 12;