mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 07:12:21 +00:00
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:
1 parent
f6760ad96c
commit
fcc44d0eaf
45 files changed
+970
-1371
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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
@@ -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') });
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user