mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +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
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
# Data Sources & Attribution
|
# Data Sources & Attribution
|
||||||
|
|
||||||
God's Eye View's **code** is [MIT](LICENSE)-licensed. **The MIT grant covers the source code only — it does NOT extend to third-party data or visual assets.** Every third-party source keeps its own license and terms. This file documents the live and bundled data sources; bundled 3D-model provenance is recorded in [`public/models/README.md`](public/models/README.md).
|
God's Eye View's **code** is [MIT](LICENSE)-licensed. **The MIT grant covers the source code only — it does NOT extend to third-party data or visual assets.** Every third-party source keeps its own license and terms. This file documents the live and bundled data sources. The bundled 3D models are the project's own procedural fleet models, MIT-licensed with the code ([`public/models/README.md`](public/models/README.md)).
|
||||||
|
|
||||||
How to read this:
|
How to read this:
|
||||||
|
|
||||||
- **The non-permissive datasets are carved out, not omitted.** Some bundled data (e.g. TeleGeography, CC BY-NC-SA) isn't MIT-compatible. Rather than hide it, we **bundle it with a clear license carve-out** so the app works out of the box — but it stays under the provider's terms.
|
- **The non-permissive datasets are carved out, not omitted.** Some bundled data (e.g. TeleGeography, CC BY-NC-SA) isn't MIT-compatible. Rather than hide it, we **bundle it with a clear license carve-out** so the app works out of the box — but it stays under the provider's terms.
|
||||||
- **If your use doesn't fit a dataset's license, remove that dataset.** Most importantly: TeleGeography is **NonCommercial** — commercial users must delete it (or license it from TeleGeography). It's one self-contained folder.
|
- **If your use doesn't fit a dataset's license, remove that dataset.** Most importantly: TeleGeography is **NonCommercial** — commercial users must delete it (or license it from TeleGeography). It's one self-contained folder.
|
||||||
- **Attribution is shown in-app** and listed here. Keep it intact. The required Google/Cesium credit renders on the on-globe credit line (bottom-left, `#cesium-credits`), and every per-layer credit below is registered into the expandable **"Data attribution"** lightbox on that line (`src/data/dataCredits.js` → `viewer.creditDisplay.addStaticCredit`). Both stay visible in clean-view and recording modes.
|
- **Attribution is shown in-app** and listed here. Keep it intact. The required Google/Cesium credit renders on the on-globe credit line (bottom-left, `#cesium-credits`), and every per-layer credit below is registered into the expandable **"Data attribution"** lightbox on that line (`src/data/dataCredits.js` → `viewer.creditDisplay.addStaticCredit`). Both stay visible in clean-view and recording modes.
|
||||||
- **Bundled model attribution lives beside the model files.** [`public/models/README.md`](public/models/README.md) records each shipped model's creator, source, license, and modification status.
|
- **The bundled 3D models are the project's own.** Every model under `public/models/` is a procedural fleet model generated by the project maintainers and covered by the repository's MIT license; [`public/models/README.md`](public/models/README.md) describes the set.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ In short: the MIT grant does not extend to any third-party data. See
|
|||||||
DATA_SOURCES.md for the full per-source license + attribution summary, and
|
DATA_SOURCES.md for the full per-source license + attribution summary, and
|
||||||
remove any dataset whose terms don't fit your use.
|
remove any dataset whose terms don't fit your use.
|
||||||
|
|
||||||
The 3D models under public/models/ are also third-party works and are NOT
|
The 3D models under public/models/ are the project's procedural fleet
|
||||||
licensed under MIT. They remain under their individual licenses. See
|
models, generated from code by the project maintainers, and are covered by
|
||||||
public/models/README.md for the author, source, license, modification notice,
|
the MIT license above like the source code. See public/models/README.md.
|
||||||
and attribution covering each model file.
|
|
||||||
@@ -60,7 +60,7 @@ Start with the included data sources, then add your own. Each layer is a separat
|
|||||||
- **📡 Contacts:** A 250 km roster of everything near your target — step through live aircraft and drop into any cockpit.
|
- **📡 Contacts:** A 250 km roster of everything near your target — step through live aircraft and drop into any cockpit.
|
||||||
- **🎯 Click-to-track anything:** Camera locks on, draws a fading trail, surfaces full metadata — and a tracked fire or vessel hands you off to the nearest live camera in one click.
|
- **🎯 Click-to-track anything:** Camera locks on, draws a fading trail, surfaces full metadata — and a tracked fire or vessel hands you off to the nearest live camera in one click.
|
||||||
- **🖊️ Voice whiteboard:** Speak annotations onto the world — real boundary polygons, marks, and routes.
|
- **🖊️ Voice whiteboard:** Speak annotations onto the world — real boundary polygons, marks, and routes.
|
||||||
- **🛫 3D hangar:** Real per-class aircraft models — 787, ATR-72, Citation, Bell 206, MQ-9 — and a tracked contact swaps from glyph to 3D model as you close in.
|
- **🛫 3D fleet:** 121 procedural models drawn life size by type — airliners, business jets, helicopters, drones, ships, buses, trams and trains — and every contact swaps from glyph to 3D model as you close in.
|
||||||
- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir.
|
- **🎨 Reskin reality:** GLSL sensor looks over the normal globe — CRT, NVG, FLIR/thermal, Noir.
|
||||||
- **🟩 Detection overlay:** Screen-space bounding boxes and IDs on everything in view.
|
- **🟩 Detection overlay:** Screen-space bounding boxes and IDs on everything in view.
|
||||||
- **🎖️ Military HUD:** Tactical heads-up display with intelligence-style telemetry.
|
- **🎖️ Military HUD:** Tactical heads-up display with intelligence-style telemetry.
|
||||||
|
|||||||
+30
-17
@@ -1,20 +1,33 @@
|
|||||||
# Bundled 3D Model Attribution
|
# Bundled 3D Models
|
||||||
|
|
||||||
The model files in this directory are third-party visual assets. They are not
|
Every 3D model shipped in this directory is one of the project's procedural
|
||||||
covered by the repository's MIT source-code license; each remains available
|
fleet models under [`fleet/`](fleet/): aircraft, helicopters, ships, buoys,
|
||||||
under the license listed below.
|
transit and rail vehicles, and a few other airborne and ground objects. They
|
||||||
|
are generated from code by the project maintainers, contain no third-party
|
||||||
|
artwork, and are covered by the repository's [MIT license](../../LICENSE)
|
||||||
|
like the source code.
|
||||||
|
|
||||||
| File | Original work and creator | Source | License | Project modifications |
|
| Path | What it holds |
|
||||||
|---|---|---|---|---|
|
|---|---|
|
||||||
| `airplane.glb` | “boeing 747” by [zairiq-123](https://sketchfab.com/zairiq-123) | [Sketchfab model](https://sketchfab.com/3d-models/boeing-747-9b16672038ba48f98e6d80a159044ed9) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Substantially modified and optimized for God's Eye View, including geometry/material simplification and coordinate/orientation preparation. The former 24× runtime calibration is baked into the mesh; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses glTF +Y-up with its nose toward local −X. Geometry remains uncompressed so the aircraft asset does not compete with photogrammetry for Draco worker capacity. |
|
| `fleet/catalog.json` | Every model by name: its file, category, built length, beam and bounds (metres, glTF axes, Z up, nose toward −X), the clearance below its origin, and its 2D icon class. |
|
||||||
| `jet.glb` | “Private Jet” by [Nick the Name](https://sketchfab.com/Nick_The_Name) | [Sketchfab model](https://sketchfab.com/3d-models/private-jet-cbdd1de6ced9461e950eafaa302cc82b) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Repackaged as a glTF binary for the project's military-flight visualization. Imported hierarchy transforms are baked into the meshes; location, rotation, and scale are applied; the bounding-box centre is at the origin; and the model uses meter scale, glTF +Y-up, and the shared nose −X convention. Existing materials are preserved. |
|
| `fleet/types.json` | Which model draws which vehicle, and at what length: ICAO aircraft and helicopter type codes, ADS-B emitter categories, AIS ship-type and length ranges, GTFS route types, transit modes and rail categories, with the defaults. |
|
||||||
| `bell206.glb` | “Bell 206 JetRanger” by [terran4627](https://sketchfab.com/terran4627) | [Sketchfab model](https://sketchfab.com/3d-models/bell-206-jetranger-d2f7ba1d671549d4b26aaf834139a1dd) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
| `fleet/aircraft/`, `fleet/helicopters/`, `fleet/ships/`, `fleet/buoys/`, `fleet/transit/`, `fleet/other/` | The models, one GLB each (EXT_meshopt_compression and KHR_mesh_quantization, vertex colours, no textures). |
|
||||||
| `c172.glb` | “Cessna 172” by [e737](https://sketchfab.com/e0057537) | [Sketchfab model](https://sketchfab.com/3d-models/cessna-172-64cddaee5aff470682659a8c08525046) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
|
||||||
| `citation2.glb` | “1990 Cessna Citation, Texture Detailed, Exterior” by [BlenderCommunityHead](https://sketchfab.com/aboodgoudagad) | [Sketchfab model](https://sketchfab.com/3d-models/1990-cessna-citation-texture-detailed-exterior-a78839624fe64900a8352cb23462350a) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
|
||||||
| `mq9.glb` | “MQ-9” by [IProZenoN](https://sketchfab.com/IProZenoN) | [Sketchfab model](https://sketchfab.com/3d-models/mq-9-fabe963feb354c5584b51f9c470c3f7e) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
|
||||||
| `b789.glb` | “Boeing 787-9” by [Nobilis 2](https://sketchfab.com/nobilishornet2) | [Sketchfab model](https://sketchfab.com/3d-models/boeing-787-9-b6711e2e698e4e469675c1154a50b7a3) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures resized to 256 px WebP, orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
|
||||||
| `atr72.glb` | “ATR 72 - 600” by [Oyan3D](https://sketchfab.com/oyan3D) | [Sketchfab model](https://sketchfab.com/3d-models/atr-72-600-1e1a7186f7444d288675262fcee44744) | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) | Optimized for God's Eye View: geometry/material simplification, textures removed with dominant material colors baked into PBR factors (flat “abstracted” style), orientation/scale vertex-baked to real-world meters (Y-up, nose −X), origin centered. |
|
|
||||||
|
|
||||||
CC BY 4.0 permits sharing and adaptation, including commercial use, provided
|
How the layers use them:
|
||||||
appropriate credit is retained, the license is linked, and modifications are
|
|
||||||
identified. These credits do not imply endorsement by the original creators.
|
- Each layer draws a vehicle with the model its type selects, scaled to the
|
||||||
|
type's published length (a vessel to its reported AIS length and beam), so
|
||||||
|
every model is drawn life size. Ground objects, balloons and obstacles in
|
||||||
|
the `other` category are drawn at the size they are built.
|
||||||
|
- An aircraft model's origin is its axis; it stands that model's scaled
|
||||||
|
clearance above the ground. A ship's origin is its waterline, and a transit
|
||||||
|
or rail vehicle's the road or rail top (a monorail or gondola is raised by
|
||||||
|
its `transit.elevation` entry).
|
||||||
|
- Transit and rail models carry a `_TINT` vertex attribute that marks where
|
||||||
|
the route or category colour is painted.
|
||||||
|
- The whole set loads once, up front, when a layer that draws it starts; no
|
||||||
|
model is fetched while a session runs.
|
||||||
|
|
||||||
|
`src/data/fleetModels.js` reads the set and chooses each vehicle's model;
|
||||||
|
`src/data/fleetModels.test.mjs` checks that every model `types.json` names is
|
||||||
|
in the catalog and on disk, and that no GLB exceeds 3 MB.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -40,7 +40,7 @@
|
|||||||
* the DISPLAYED (render-delayed) time within 35° — catches
|
* the DISPLAYED (render-delayed) time within 35° — catches
|
||||||
* sign/offset inversion in the whole chain (chord lags the
|
* sign/offset inversion in the whole chain (chord lags the
|
||||||
* tangent by ≤ half a segment ≈ 22.5°)
|
* 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°).
|
* normalized nose -X, offset 180°).
|
||||||
*
|
*
|
||||||
* VISUAL — with the turning flights plane tracked in 3D, capture screenshots
|
* 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
|
// 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) => {
|
await page.evaluate((icao) => {
|
||||||
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
|
window.__godsEyeView.dataManager.layers.get('flights').module.trackById(icao);
|
||||||
}, TURN.flights[0].icao);
|
}, TURN.flights[0].icao);
|
||||||
@@ -807,7 +807,7 @@ async function main() {
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Phase 2 — MILITARY layer: track TRNMIL1 (flights layer auto-clears)
|
// 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) => {
|
await page.evaluate((hex) => {
|
||||||
window.__godsEyeView.dataManager.layers.get('military').module.trackById(hex);
|
window.__godsEyeView.dataManager.layers.get('military').module.trackById(hex);
|
||||||
}, TURN.military[0].hex);
|
}, TURN.military[0].hex);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ try {
|
|||||||
}
|
}
|
||||||
const position = C.Cartesian3.fromDegrees(0, 0, 10000);
|
const position = C.Cartesian3.fromDegrees(0, 0, 10000);
|
||||||
const model = await C.Model.fromGltfAsync({
|
const model = await C.Model.fromGltfAsync({
|
||||||
url: '/models/airplane.glb',
|
url: '/models/fleet/aircraft/a320.glb',
|
||||||
modelMatrix: C.Transforms.eastNorthUpToFixedFrame(position),
|
modelMatrix: C.Transforms.eastNorthUpToFixedFrame(position),
|
||||||
});
|
});
|
||||||
scene.primitives.add(model);
|
scene.primitives.add(model);
|
||||||
|
|||||||
+32
-33
@@ -15,9 +15,9 @@
|
|||||||
* per-class glyph (image data-URI === aircraftIcon(klass),
|
* per-class glyph (image data-URI === aircraftIcon(klass),
|
||||||
* imported from src) and per-class scale (flights:
|
* imported from src) and per-class scale (flights:
|
||||||
* CLASS_SCALE_2D; military: 0.7 × CLASS_SCALE_2D)
|
* CLASS_SCALE_2D; military: 0.7 × CLASS_SCALE_2D)
|
||||||
* B3 models : with 3D on, fleet Cesium.Model.scale === the layer's
|
* B3 models : with 3D on, fleet Cesium.Model.scale === the life-size
|
||||||
* MODEL_SCALE (both 1; legacy scale is baked into the GLB)
|
* scale of the fleet model the aircraft's type or category
|
||||||
* × CLASS_SCALE_3D[klass] per icao; tracked too
|
* selects (aircraftModelSpec) per icao; tracked too
|
||||||
* B4 clean : no console errors
|
* B4 clean : no console errors
|
||||||
*
|
*
|
||||||
* Visual output (qa-shots/b5/, untracked): top-down 2D spreads of both rows,
|
* 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 path from 'node:path';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
import { stubPlanespotters } from './qa-photo-stub.mjs';
|
import { stubPlanespotters } from './qa-photo-stub.mjs';
|
||||||
import { classifyAircraft, CLASS_SCALE_2D, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
|
import { classifyAircraft, CLASS_SCALE_2D } from '../src/data/aircraftClass.js';
|
||||||
// Hangar fleet (2026-08-16): real per-class GLBs render at scale 1; military
|
import { aircraftModelSpec } from '../src/data/fleetModels.js';
|
||||||
// heavies (airliner/quadjet/glider) render airplane.glb at 1 x class; only
|
// Every aircraft draws its fleet model life size: its type's length over the
|
||||||
// fastjet/unknown keep the per-layer jet/airplane MODEL_SCALE formula.
|
// model's built length, chosen by type code (military) or category (flights).
|
||||||
const flightsWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1 : FL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
|
const FLEET_SET = Object.freeze({
|
||||||
const MIL_PLANE_CLASSES = new Set(['airliner', 'quadjet', 'glider']);
|
catalog: JSON.parse(fs.readFileSync(new URL('../public/models/fleet/catalog.json', import.meta.url), 'utf8')),
|
||||||
const militaryWantScale = (klass) => (CLASS_MODEL_REAL[klass] ? 1
|
types: JSON.parse(fs.readFileSync(new URL('../public/models/fleet/types.json', import.meta.url), 'utf8')),
|
||||||
: MIL_PLANE_CLASSES.has(klass) ? MIL_PLANE_MODEL_SCALE * CLASS_SCALE_3D[klass]
|
});
|
||||||
: MIL_MODEL_SCALE * CLASS_SCALE_3D[klass]);
|
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';
|
import { aircraftIcon } from '../src/data/aircraftIcons.js';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -58,9 +61,6 @@ const APP_URL = getOpt('--url', 'http://localhost:4173');
|
|||||||
const HEADFUL = getFlag('--headful');
|
const HEADFUL = getFlag('--headful');
|
||||||
const SHOT_DIR = path.resolve('qa-shots/b5');
|
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 MIL_BILLBOARD_SCALE = 0.7; // militaryFlights BILLBOARD_SCALE
|
||||||
|
|
||||||
const CHROME_EXECUTABLE_CANDIDATES = [
|
const CHROME_EXECUTABLE_CANDIDATES = [
|
||||||
@@ -134,10 +134,9 @@ const SPRITES = {
|
|||||||
// course 0 like the rest — keeps the row a clean size-comparison line (2D
|
// 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).
|
// 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 },
|
{ 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
|
// 3D cluster: C17 vs F16 (tracked) vs B744 vs C130, each drawn life size
|
||||||
// real-world-scale asset (native bounding radius ~29.8) rendered at
|
// (~8–36 m radius) — same 150 m spacing and 450–900 m shot ranges as the
|
||||||
// MODEL_SCALE 1, so models are ~22–43 m radius — same 150 m spacing and
|
// flights cluster.
|
||||||
// 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: '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: '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 },
|
{ 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
|
// 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(() => {
|
await page.evaluate(() => {
|
||||||
const dm = window.__godsEyeView.dataManager;
|
const dm = window.__godsEyeView.dataManager;
|
||||||
dm.layers.get('flights').module.setParams({ models3d: true });
|
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 flModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
|
||||||
const modelScaleBad = [];
|
const modelScaleBad = [];
|
||||||
for (const [id, want] of [
|
for (const [id, want] of [
|
||||||
['cbb001', flightsWantScale(expectedFlights.get('cbb001'))],
|
['cbb001', flightsWantScale('cbb001')],
|
||||||
['cbb003', flightsWantScale(expectedFlights.get('cbb003'))],
|
['cbb003', flightsWantScale('cbb003')],
|
||||||
]) {
|
]) {
|
||||||
const m = flModels.find((x) => x.id === id);
|
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}`);
|
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 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) {
|
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
|
||||||
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
|
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 === 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(' ')}`);
|
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);
|
await sleep(1500);
|
||||||
@@ -509,7 +508,7 @@ async function main() {
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
// B3 + 3D screenshots — military cluster
|
// 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(() => {
|
await page.evaluate(() => {
|
||||||
const dm = window.__godsEyeView.dataManager;
|
const dm = window.__godsEyeView.dataManager;
|
||||||
dm.layers.get('military').module.setParams({ models3d: true });
|
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 milModels = await page.evaluate(() => window.__collectModels().filter((m) => m.ready));
|
||||||
const modelScaleBad = [];
|
const modelScaleBad = [];
|
||||||
for (const [id, want] of [
|
for (const [id, want] of [
|
||||||
['ddb001', militaryWantScale(expectedMilitary.get('ddb001'))],
|
['ddb001', militaryWantScale('ddb001')],
|
||||||
['ddb003', militaryWantScale(expectedMilitary.get('ddb003'))],
|
['ddb003', militaryWantScale('ddb003')],
|
||||||
['ddb004', militaryWantScale(expectedMilitary.get('ddb004'))],
|
['ddb004', militaryWantScale('ddb004')],
|
||||||
]) {
|
]) {
|
||||||
const m = milModels.find((x) => x.id === id);
|
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}`);
|
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 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) {
|
if (!trackedM || Math.abs(trackedM.scale - trackedWant) > 1e-9) {
|
||||||
modelScaleBad.push(`tracked: ${trackedM ? trackedM.scale : 'missing'} != ${trackedWant}`);
|
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 === 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(' ')}`);
|
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);
|
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).
|
// flights cluster (close zoom no longer puts the camera inside a model).
|
||||||
await zoomToRange(900);
|
await zoomToRange(900);
|
||||||
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-military-far.png') });
|
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 puppeteer from 'puppeteer';
|
||||||
import { respondToPlanespotters } from './qa-photo-stub.mjs';
|
import { respondToPlanespotters } from './qa-photo-stub.mjs';
|
||||||
import { arrivalAngleViolated, arrivalRotationPassed } from '../src/tooling/trackArrivalObservation.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';
|
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
|
// Args
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -547,13 +553,13 @@ async function main() {
|
|||||||
|
|
||||||
// Independent GLB capability control. Product model absence cannot prove
|
// Independent GLB capability control. Product model absence cannot prove
|
||||||
// that the browser backend is incapable: that is the condition this
|
// 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,
|
// directly through Cesium, outside the Flights/Military model pipelines,
|
||||||
// then remove the control before exercising product behavior.
|
// then remove the control before exercising product behavior.
|
||||||
const glbControlStarted = await evalPage(async () => {
|
const glbControlStarted = await evalPage(async () => {
|
||||||
window.__qaGlbControl = null;
|
window.__qaGlbControl = null;
|
||||||
window.__qaGlbControlError = 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;
|
const assetBytes = asset.ok ? (await asset.arrayBuffer()).byteLength : 0;
|
||||||
if (!asset.ok || assetBytes === 0) {
|
if (!asset.ok || assetBytes === 0) {
|
||||||
throw new Error(`GLB control asset unavailable: HTTP ${asset.status}`);
|
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),
|
Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 9000),
|
||||||
);
|
);
|
||||||
void Cesium.Model.fromGltfAsync({
|
void Cesium.Model.fromGltfAsync({
|
||||||
url: '/models/airplane.glb',
|
url: '/models/fleet/aircraft/a320.glb',
|
||||||
modelMatrix,
|
modelMatrix,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
}).then((model) => {
|
}).then((model) => {
|
||||||
@@ -1454,8 +1460,8 @@ async function main() {
|
|||||||
}, trackedIcao);
|
}, trackedIcao);
|
||||||
console.log(' (3D model did not render under SwiftShader; testing the shared display-position contract directly)');
|
console.log(' (3D model did not render under SwiftShader; testing the shared display-position contract directly)');
|
||||||
} else {
|
} else {
|
||||||
// Full check: independently transform airplane.glb's measured AABB centre
|
// Full check: independently transform the fleet model's visual centre
|
||||||
// through the actual 3D model matrix, then compare it with detection.
|
// (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
|
// glTF [x,y,z] → Cesium-local [x,-z,y]. The effective rendered
|
||||||
// computedScale is separate from modelMatrix and includes any
|
// computedScale is separate from modelMatrix and includes any
|
||||||
// minimumPixelSize enlargement, so include it just as the renderer does.
|
// minimumPixelSize enlargement, so include it just as the renderer does.
|
||||||
@@ -1469,7 +1475,7 @@ async function main() {
|
|||||||
const scale = Number.isFinite(model.computedScale)
|
const scale = Number.isFinite(model.computedScale)
|
||||||
? model.computedScale
|
? model.computedScale
|
||||||
: (Number.isFinite(model.scale) ? model.scale : 1);
|
: (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 x = 0 * scale;
|
||||||
const y = 0 * scale;
|
const y = 0 * scale;
|
||||||
const z = 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');
|
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)
|
const GROUND3D_STUB_H = 187.5; // stubbed tile-skin height (m, ellipsoid)
|
||||||
// Expected belly offsets (m): native origin-above-lowest-vertex × MODEL_SCALE
|
// Expected belly offsets (m): the fleet model's wheel clearance at its
|
||||||
// × CLASS_SCALE_3D[klass] — constants match the layers' MODEL_BELLY_OFFSET_NATIVE
|
// life-size scale, the model chosen as the layers choose it
|
||||||
// / MODEL_SCALE pairs, which modelScale.test.mjs locks against the GLBs.
|
// (aircraftModelSpec) from the synthetic feed's fields: no type for the
|
||||||
// Class is computed with the REAL classifier on the synthetic feed fields.
|
// flights contact (the default airliner), a C130 for the military one.
|
||||||
const g3dFlKlass = classifyAircraft({ typeCode: null, category: null });
|
const g3dFlOffset = aircraftModelSpec(FLEET_SET, { typeCode: null }).clearanceM;
|
||||||
const g3dMilKlass = classifyAircraft({ typeCode: 'C130', category: undefined });
|
const g3dMilOffset = aircraftModelSpec(FLEET_SET, { typeCode: 'C130' }).clearanceM;
|
||||||
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
|
|
||||||
// Height-datum fix (Task 6): the flights-layer billboard for a grounded synthetic
|
// 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
|
// (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
|
// 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 flDelta = g3dState.flModelRadius - g3dIngest.flBBRadius;
|
||||||
const milDelta = g3dState.milModelRadius - g3dIngest.milBBRadius;
|
const milDelta = g3dState.milModelRadius - g3dIngest.milBBRadius;
|
||||||
const HEIGHT_TOL_M = 1.0;
|
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,
|
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])`);
|
`Δ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,
|
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)`);
|
`Δ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)',
|
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)
|
const scale = Number.isFinite(m.computedScale)
|
||||||
? m.computedScale
|
? m.computedScale
|
||||||
: (Number.isFinite(m.scale) ? m.scale : 1);
|
: (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 x = 0 * scale;
|
||||||
const y = 0 * scale;
|
const y = 0 * scale;
|
||||||
const z = 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,
|
// 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,
|
// 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
|
// never under it. MAX_BELLY_M clears the largest wheel clearance any
|
||||||
// (widebody b789, 7.81 m in CLASS_MODEL_REAL); the floor itself is the
|
// 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
|
// hard lower bound, and a model placed at the raw feed altitude is ~240 m
|
||||||
// under it.
|
// under it.
|
||||||
const MAX_BELLY_M = 12;
|
const MAX_BELLY_M = 12;
|
||||||
|
|||||||
@@ -467,54 +467,3 @@ export const CLASS_SCALE_2D = {
|
|||||||
bizjet: 0.72,
|
bizjet: 0.72,
|
||||||
uav: 0.75,
|
uav: 0.75,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 3D model scale multipliers — clamped [0.75, 1.45] while every class shares
|
|
||||||
* the single airplane.glb (a jet mesh at C172 scale reads wrong). Widen when
|
|
||||||
* real per-class models land in CLASS_MODEL_URL. */
|
|
||||||
export const CLASS_SCALE_3D = {
|
|
||||||
light: 0.75,
|
|
||||||
glider: 0.75,
|
|
||||||
turboprop: 0.85,
|
|
||||||
airliner: 1.0,
|
|
||||||
widebody: 1.3,
|
|
||||||
quadjet: 1.45,
|
|
||||||
helicopter: 0.8,
|
|
||||||
fastjet: 0.8,
|
|
||||||
bizjet: 0.8,
|
|
||||||
uav: 0.8,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Per-class glTF — all the shared airplane today; drop real assets in here.
|
|
||||||
* NOTE for future models: each asset may need its own heading offset (the
|
|
||||||
* shared GLB uses MODEL_HEADING_OFFSET_DEG = 180 in both layers). */
|
|
||||||
export const CLASS_MODEL_URL = {
|
|
||||||
light: '/models/airplane.glb',
|
|
||||||
glider: '/models/airplane.glb',
|
|
||||||
turboprop: '/models/airplane.glb',
|
|
||||||
airliner: '/models/airplane.glb',
|
|
||||||
widebody: '/models/airplane.glb',
|
|
||||||
quadjet: '/models/airplane.glb',
|
|
||||||
helicopter: '/models/airplane.glb',
|
|
||||||
fastjet: '/models/airplane.glb',
|
|
||||||
bizjet: '/models/airplane.glb',
|
|
||||||
uav: '/models/airplane.glb',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Real per-class GLBs (2026-08-15 Hangar fleet, owner picks; CC-BY 4.0 —
|
|
||||||
* provenance in public/models/README.md). Every asset is vertex-baked to the
|
|
||||||
* airplane.glb convention: Y-up, X = length, Z = span, nose −X (so the layers'
|
|
||||||
* MODEL_HEADING_OFFSET_DEG = 180 applies unchanged), origin at bbox centre,
|
|
||||||
* REAL-WORLD METERS (render at scale 1 — no MODEL_SCALE, no CLASS_SCALE_3D).
|
|
||||||
* bellyM = origin→lowest-vertex distance (grounded-model lift, replaces the
|
|
||||||
* airplane.glb MODEL_BELLY_OFFSET_NATIVE formula); radiusM = bounding-sphere
|
|
||||||
* radius in meters (pixel-size math). Values measured from the shipped GLBs —
|
|
||||||
* pinned by modelScale.test.mjs. Classes NOT listed (airliner, quadjet,
|
|
||||||
* glider, fastjet) still render the shared airplane.glb via CLASS_MODEL_URL. */
|
|
||||||
export const CLASS_MODEL_REAL = {
|
|
||||||
helicopter: { url: '/models/bell206.glb', bellyM: 1.66, radiusM: 8.24 },
|
|
||||||
light: { url: '/models/c172.glb', bellyM: 1.36, radiusM: 7.0 },
|
|
||||||
bizjet: { url: '/models/citation2.glb', bellyM: 2.86, radiusM: 11.24 },
|
|
||||||
uav: { url: '/models/mq9.glb', bellyM: 2.02, radiusM: 12.0 },
|
|
||||||
widebody: { url: '/models/b789.glb', bellyM: 7.81, radiusM: 44.08 },
|
|
||||||
turboprop: { url: '/models/atr72.glb', bellyM: 3.81, radiusM: 19.49 },
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/data/aircraftClass.test.mjs
|
// src/data/aircraftClass.test.mjs
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { classifyAircraft, CLASS_SCALE_2D, CLASS_SCALE_3D, CLASS_MODEL_URL, CLASS_MODEL_REAL } from './aircraftClass.js';
|
import { classifyAircraft, CLASS_SCALE_2D } from './aircraftClass.js';
|
||||||
|
|
||||||
test('type-code classification (military layer path)', () => {
|
test('type-code classification (military layer path)', () => {
|
||||||
assert.equal(classifyAircraft({ typeCode: 'F16' }), 'fastjet');
|
assert.equal(classifyAircraft({ typeCode: 'F16' }), 'fastjet');
|
||||||
@@ -48,19 +48,8 @@ test('typeCode outranks category; unknown → airliner', () => {
|
|||||||
assert.equal(classifyAircraft({ typeCode: null, category: 0 }), 'airliner');
|
assert.equal(classifyAircraft({ typeCode: null, category: 0 }), 'airliner');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('scale/url tables cover every class', () => {
|
test('the icon scale table covers every class', () => {
|
||||||
for (const kind of ['light','glider','turboprop','airliner','widebody','quadjet','helicopter','fastjet','bizjet','uav']) {
|
for (const kind of ['light','glider','turboprop','airliner','widebody','quadjet','helicopter','fastjet','bizjet','uav']) {
|
||||||
assert.ok(Number.isFinite(CLASS_SCALE_2D[kind]), kind);
|
assert.ok(Number.isFinite(CLASS_SCALE_2D[kind]), kind);
|
||||||
assert.ok(CLASS_SCALE_3D[kind] >= 0.75 && CLASS_SCALE_3D[kind] <= 1.45, kind);
|
|
||||||
assert.ok(typeof CLASS_MODEL_URL[kind] === 'string', kind);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('CLASS_MODEL_REAL entries carry the fields the layers consume', () => {
|
|
||||||
for (const [kind, spec] of Object.entries(CLASS_MODEL_REAL)) {
|
|
||||||
assert.ok(spec.url.startsWith('/models/') && spec.url.endsWith('.glb'), kind);
|
|
||||||
assert.ok(spec.radiusM > 0 && spec.bellyM > 0, kind);
|
|
||||||
// every real-model class must also exist in the classifier tables
|
|
||||||
assert.ok(Number.isFinite(CLASS_SCALE_2D[kind]), kind);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -160,8 +160,8 @@ const BODIES = {
|
|||||||
L -3,31 L -8,34 L -8,30 L -6,16
|
L -3,31 L -8,34 L -8,30 L -6,16
|
||||||
L -27,26 L -27,20 L -5,-8
|
L -27,26 L -27,20 L -5,-8
|
||||||
C -4.6,-16 -4,-24 -3.5,-30 Z" fill="white" ${STROKE}/>`,
|
C -4.6,-16 -4,-24 -3.5,-30 Z" fill="white" ${STROKE}/>`,
|
||||||
// 2026-08-15 Hangar additions: two NEW classes shipped with the real-model
|
// 2026-08-15 additions: two NEW classes (bizjet, uav). Same contract as
|
||||||
// fleet (CLASS_MODEL_REAL). Same contract as the set above: 96×96 nose-up,
|
// the set above: 96×96 nose-up,
|
||||||
// white fill, hairline stroke, tint-safe.
|
// white fill, hairline stroke, tint-safe.
|
||||||
// Business jet — slender fuselage, modest swept wing, aft engines, T-tail.
|
// Business jet — slender fuselage, modest swept wing, aft engines, T-tail.
|
||||||
bizjet: `
|
bizjet: `
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from './aircraftModelLighting.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Every aircraft model drawn in the scene skips Cesium's dynamic environment
|
* Every aircraft model drawn in the scene skips Cesium's dynamic environment
|
||||||
* map (a sky render and GPU readback per model). The preload calls only warm
|
* map (a sky render and GPU readback per model). Drawn models are the calls
|
||||||
* the glTF cache and are never added to the scene, so they are left out:
|
* that set a minimum pixel size.
|
||||||
* drawn models are the calls that set a minimum pixel size.
|
|
||||||
*/
|
*/
|
||||||
const SOURCES = [
|
const SOURCES = [
|
||||||
{
|
{
|
||||||
@@ -36,7 +35,9 @@ test('the shared option turns the dynamic environment map off', () => {
|
|||||||
for (const { name, source } of SOURCES) {
|
for (const { name, source } of SOURCES) {
|
||||||
test(`${name}: every drawn aircraft model skips the dynamic environment map`, () => {
|
test(`${name}: every drawn aircraft model skips the dynamic environment map`, () => {
|
||||||
const calls = [
|
const calls = [
|
||||||
...source.matchAll(/(?:fromGltfAsync|loadModel)\(\{([\s\S]*?)\}\)/g),
|
...source.matchAll(
|
||||||
|
/(?:fromGltfAsync\(|loadModel\(|(?:_createModel|createFleetModel)\(\s*\w+,\s*)\{([\s\S]*?)\n\s*\}[,)]/g,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.map((match) => match[1])
|
.map((match) => match[1])
|
||||||
.filter((options) => /\bminimumPixelSize:/.test(options));
|
.filter((options) => /\bminimumPixelSize:/.test(options));
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ for (const layer of LAYERS) {
|
|||||||
const source = readLayerSource(layer.path);
|
const source = readLayerSource(layer.path);
|
||||||
|
|
||||||
test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => {
|
test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => {
|
||||||
const calls = [...source.matchAll(/Cesium\.Model\.fromGltfAsync\(\{([\s\S]*?)\}\)/g)];
|
const calls = [...source.matchAll(/_createModel\(\s*\w+,\s*\{([\s\S]*?)\n\s*\}\)/g)];
|
||||||
assert.ok(calls.length >= 3, `expected fleet, tracked, and preload model calls; found ${calls.length}`);
|
assert.ok(calls.length >= 2, `expected fleet and tracked model calls; found ${calls.length}`);
|
||||||
|
assert.doesNotMatch(source, /Cesium\.Model\.fromGltfAsync\(/,
|
||||||
|
'every model is built from the loaded fleet set through _createModel');
|
||||||
for (const [index, call] of calls.entries()) {
|
for (const [index, call] of calls.entries()) {
|
||||||
assert.match(call[1], /\basynchronous:\s*false\b/,
|
assert.match(call[1], /\basynchronous:\s*false\b/,
|
||||||
`Model.fromGltfAsync call ${index + 1} must keep bounded GLB readiness independent of tile jobs`);
|
`_createModel call ${index + 1} must keep bounded GLB readiness independent of tile jobs`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,415 @@
|
|||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { horizonOccluder } from './iconOrientation.js';
|
||||||
|
import { selectModelEligible } from './modelEligibility.js';
|
||||||
|
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from './aircraftModelLighting.js';
|
||||||
|
import { fleetModelSet } from './fleetModels.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cesium models of the procedural fleet set (`fleetModels.js`), and the
|
||||||
|
* icon-to-model handoff the Vessels, Transit and Rail layers share.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Every fleet GLB is nose -X: opposite Cesium's heading 0. */
|
||||||
|
export const FLEET_HEADING_OFFSET_DEG = 180;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Cesium Model of `spec` from the up-front loaded bytes, so no model
|
||||||
|
* is fetched mid-session. Each model keeps its own URL as its base path: the
|
||||||
|
* glTF cache keys by it, and two models sharing one would share geometry.
|
||||||
|
* Rejects when the set is not loaded or the model's GLB failed to load.
|
||||||
|
* @param {object} spec `fleetModelSpec`.
|
||||||
|
* @param {object} options Cesium `Model.fromGltfAsync` options.
|
||||||
|
* @param {{resolveAsset?: (url: string) => string, set?: object|null,
|
||||||
|
* load?: (options: object) => Promise<object>}} [context]
|
||||||
|
* @returns {Promise<object>} Cesium Model.
|
||||||
|
*/
|
||||||
|
export function createFleetModel(
|
||||||
|
spec,
|
||||||
|
options,
|
||||||
|
{
|
||||||
|
resolveAsset = (url) => url,
|
||||||
|
set = fleetModelSet(),
|
||||||
|
// Looked up when called, so a replaced `fromGltfAsync` (tests) is the one used.
|
||||||
|
load = (o) => Cesium.Model.fromGltfAsync(o),
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const bytes = set?.bytes?.get(spec?.model);
|
||||||
|
if (!bytes)
|
||||||
|
return Promise.reject(new Error(`fleet model ${spec?.model} not loaded`));
|
||||||
|
return load({
|
||||||
|
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
||||||
|
...options,
|
||||||
|
gltf: bytes,
|
||||||
|
basePath: resolveAsset(spec.url),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tintShaders = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The custom shader that paints a route colour where a transit model's
|
||||||
|
* `_TINT` vertex attribute marks it: drawn colour = vertex colour ×
|
||||||
|
* mix(1, colour, _TINT). One shader per colour, shared by every model drawn in
|
||||||
|
* it (Cesium compiles the program once; uniforms are per shader).
|
||||||
|
* @param {Cesium.Color} color
|
||||||
|
* @returns {Cesium.CustomShader}
|
||||||
|
*/
|
||||||
|
export function fleetTintShader(color) {
|
||||||
|
const key = color.toCssHexString();
|
||||||
|
let shader = tintShaders.get(key);
|
||||||
|
if (!shader) {
|
||||||
|
shader = new Cesium.CustomShader({
|
||||||
|
mode: Cesium.CustomShaderMode.MODIFY_MATERIAL,
|
||||||
|
uniforms: {
|
||||||
|
u_routeColor: {
|
||||||
|
type: Cesium.UniformType.VEC3,
|
||||||
|
value: new Cesium.Cartesian3(color.red, color.green, color.blue),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fragmentShaderText: `
|
||||||
|
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
|
||||||
|
vec3 route = czm_srgbToLinear(u_routeColor);
|
||||||
|
material.diffuse *= mix(vec3(1.0), route, fsInput.attributes.tint);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
tintShaders.set(key, shader);
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the world matrix of a fleet model standing at `position` heading
|
||||||
|
* `headingDeg` (clockwise from north) into `result`, stretched by the spec's
|
||||||
|
* beam and height scales relative to its length scale (a Model's own `scale`
|
||||||
|
* carries the length scale, so its minimum pixel size still applies).
|
||||||
|
* @param {Cesium.Cartesian3} position
|
||||||
|
* @param {number} headingDeg
|
||||||
|
* @param {object} spec `fleetModelSpec`.
|
||||||
|
* @param {Cesium.Matrix4} result
|
||||||
|
* @param {Cesium.HeadingPitchRoll} scratchHpr
|
||||||
|
* @returns {Cesium.Matrix4}
|
||||||
|
*/
|
||||||
|
export function fleetModelMatrix(
|
||||||
|
position,
|
||||||
|
headingDeg,
|
||||||
|
spec,
|
||||||
|
result,
|
||||||
|
scratchHpr,
|
||||||
|
) {
|
||||||
|
scratchHpr.heading = Cesium.Math.toRadians(
|
||||||
|
(headingDeg || 0) + FLEET_HEADING_OFFSET_DEG,
|
||||||
|
);
|
||||||
|
scratchHpr.pitch = 0;
|
||||||
|
scratchHpr.roll = 0;
|
||||||
|
Cesium.Transforms.headingPitchRollToFixedFrame(
|
||||||
|
position,
|
||||||
|
scratchHpr,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
undefined,
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
const [sx, sy, sz] = spec.scaleXYZ;
|
||||||
|
if (sx > 0 && (sy !== sx || sz !== sx)) {
|
||||||
|
// Cesium's glTF axis correction puts the model's length on local Y, its
|
||||||
|
// beam on local X and its height on local Z.
|
||||||
|
scratchScale.x = sy / sx;
|
||||||
|
scratchScale.y = 1;
|
||||||
|
scratchScale.z = sz / sx;
|
||||||
|
Cesium.Matrix4.multiplyByScale(result, scratchScale, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const scratchScale = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D models for one layer's vehicles under the DISPLAY rail's 3D toggle (the
|
||||||
|
* Flights layer's `models3d`/`models3dMode`), on the same rules the aircraft
|
||||||
|
* layers follow: models only below the camera altitude ceiling, for the
|
||||||
|
* nearest `cap` vehicles in the add/keep band, on-screen first. A vehicle's
|
||||||
|
* icon stays the visual until its model is loaded and placed; the layer reads
|
||||||
|
* `modelOwnsVisual` back from each item and hides that icon.
|
||||||
|
*
|
||||||
|
* Each frame the layer passes its vehicles as items:
|
||||||
|
* `{position, headingDeg, spec, tint, pickId, show}`, `position` the point
|
||||||
|
* the model's origin stands on (the waterline, the road or rail top; the
|
||||||
|
* spec's `elevationM` is added here), `tint` a Cesium.Color for a tinted
|
||||||
|
* model's route colour (or null), `show` false to keep it hidden.
|
||||||
|
* @param {object} options
|
||||||
|
* @param {object} options.viewer Cesium viewer.
|
||||||
|
* @param {() => object|null} [options.display] `{models3d, models3dMode}`.
|
||||||
|
* @param {object} options.policy `{altitudeCeilingM, minimumPixelSize,
|
||||||
|
* cap, capAll, addM, addAllM, keepM, keepAllM, intervalMs}`.
|
||||||
|
* @param {(url: string) => string} [options.resolveAsset]
|
||||||
|
* @param {(spec: object, options: object) => Promise<object>} [options.createModel]
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function createFleetModelLayer({
|
||||||
|
viewer,
|
||||||
|
display = () => null,
|
||||||
|
policy,
|
||||||
|
resolveAsset = (url) => url,
|
||||||
|
createModel = (spec, options) =>
|
||||||
|
createFleetModel(spec, options, { resolveAsset }),
|
||||||
|
}) {
|
||||||
|
const collection = new Cesium.PrimitiveCollection();
|
||||||
|
viewer.scene.primitives.add(collection);
|
||||||
|
/** @type {Map<string, object>} id -> admitted model */
|
||||||
|
const models = new Map();
|
||||||
|
/** @type {Map<string, number>} id -> generation of an in-flight load */
|
||||||
|
const pending = new Map();
|
||||||
|
let generation = 0;
|
||||||
|
let destroyed = false;
|
||||||
|
let lastEligibilityAt = -Infinity;
|
||||||
|
let eligible = new Set();
|
||||||
|
let settling = 0;
|
||||||
|
const scratchHpr = new Cesium.HeadingPitchRoll();
|
||||||
|
const scratchSphere = new Cesium.BoundingSphere(Cesium.Cartesian3.ZERO, 1);
|
||||||
|
const scratchCarto = new Cesium.Cartographic();
|
||||||
|
const scratchPosition = new Cesium.Cartesian3();
|
||||||
|
|
||||||
|
function release(id) {
|
||||||
|
pending.delete(id);
|
||||||
|
const model = models.get(id);
|
||||||
|
if (!model) return;
|
||||||
|
models.delete(id);
|
||||||
|
if (!collection.isDestroyed()) collection.remove(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseAll() {
|
||||||
|
for (const id of [...models.keys()]) release(id);
|
||||||
|
pending.clear();
|
||||||
|
generation += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function preferences() {
|
||||||
|
return display?.() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function regime(prefs) {
|
||||||
|
if (!prefs?.models3d || !fleetModelSet()) return false;
|
||||||
|
const height = viewer.camera?.positionCartographic?.height ?? Infinity;
|
||||||
|
return height < policy.altitudeCeilingM;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shaderFor(item) {
|
||||||
|
return item.spec.tinted && item.tint
|
||||||
|
? fleetTintShader(item.tint)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensure(id, item, cap) {
|
||||||
|
const key = item.spec.key;
|
||||||
|
const current = models.get(id);
|
||||||
|
if (current && current._gevSpecKey === key) return;
|
||||||
|
if (current) release(id);
|
||||||
|
if (pending.has(id) || models.size + pending.size >= cap) return;
|
||||||
|
const loadGeneration = generation;
|
||||||
|
pending.set(id, loadGeneration);
|
||||||
|
let model;
|
||||||
|
try {
|
||||||
|
model = await createModel(item.spec, {
|
||||||
|
asynchronous: false,
|
||||||
|
minimumPixelSize: policy.minimumPixelSize,
|
||||||
|
scale: item.spec.scale,
|
||||||
|
customShader: shaderFor(item),
|
||||||
|
id: item.pickId ?? id,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
if (pending.get(id) === loadGeneration) pending.delete(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stale =
|
||||||
|
destroyed ||
|
||||||
|
pending.get(id) !== loadGeneration ||
|
||||||
|
generation !== loadGeneration ||
|
||||||
|
!eligible.has(id) ||
|
||||||
|
models.has(id);
|
||||||
|
if (pending.get(id) === loadGeneration) pending.delete(id);
|
||||||
|
if (stale) {
|
||||||
|
model.destroy?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.id = item.pickId ?? id;
|
||||||
|
model._gevSpecKey = key;
|
||||||
|
model._gevTint = item.spec.tinted ? item.tint : null;
|
||||||
|
// Admitted, not yet the visual: placement turns it on.
|
||||||
|
model.show = false;
|
||||||
|
collection.add(model);
|
||||||
|
models.set(id, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshEligibility(items, prefs) {
|
||||||
|
const all = prefs.models3dMode === 'all';
|
||||||
|
const cap = all ? policy.capAll : policy.cap;
|
||||||
|
const addM = all ? policy.addAllM : policy.addM;
|
||||||
|
const keepM = all ? policy.keepAllM : policy.keepM;
|
||||||
|
const camera = viewer.camera;
|
||||||
|
const cameraPosition = camera.positionWC;
|
||||||
|
const cull = camera.frustum.computeCullingVolume(
|
||||||
|
cameraPosition,
|
||||||
|
camera.directionWC,
|
||||||
|
camera.upWC,
|
||||||
|
);
|
||||||
|
const candidates = [];
|
||||||
|
for (const [id, item] of items) {
|
||||||
|
if (!item.position || item.show === false || !item.spec) continue;
|
||||||
|
const distanceSq = Cesium.Cartesian3.distanceSquared(
|
||||||
|
cameraPosition,
|
||||||
|
item.position,
|
||||||
|
);
|
||||||
|
if (distanceSq > keepM * keepM) continue;
|
||||||
|
Cesium.Cartesian3.clone(item.position, scratchSphere.center);
|
||||||
|
candidates.push([
|
||||||
|
id,
|
||||||
|
distanceSq,
|
||||||
|
cull.computeVisibility(scratchSphere) !== Cesium.Intersect.OUTSIDE,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
candidates.sort((a, b) => a[1] - b[1]);
|
||||||
|
eligible = selectModelEligible(candidates, {
|
||||||
|
cap,
|
||||||
|
addDistSq: addM * addM,
|
||||||
|
isModeled: (id) => models.has(id),
|
||||||
|
});
|
||||||
|
for (const id of [...models.keys(), ...pending.keys()])
|
||||||
|
if (!eligible.has(id)) release(id);
|
||||||
|
for (const id of eligible) void ensure(id, items.get(id), cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Where the model's origin stands: the item's point raised by the spec's elevation. */
|
||||||
|
function placement(item) {
|
||||||
|
const lift = item.spec.elevationM || 0;
|
||||||
|
if (!lift) return item.position;
|
||||||
|
const carto = Cesium.Cartographic.fromCartesian(
|
||||||
|
item.position,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
scratchCarto,
|
||||||
|
);
|
||||||
|
if (!carto) return item.position;
|
||||||
|
return Cesium.Cartesian3.fromRadians(
|
||||||
|
carto.longitude,
|
||||||
|
carto.latitude,
|
||||||
|
carto.height + lift,
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
scratchPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** Ids whose model admission is current. */
|
||||||
|
get eligible() {
|
||||||
|
return eligible;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-frame pass: re-derive eligibility at the policy's cadence, then
|
||||||
|
* place every admitted model and hand each item's visual to its model
|
||||||
|
* only once the model is ready and placed (`item.modelOwnsVisual`).
|
||||||
|
* @param {Map<string, object>} items
|
||||||
|
* @param {number} nowMs
|
||||||
|
* @returns {boolean} Whether any model is drawn.
|
||||||
|
*/
|
||||||
|
frame(items, nowMs) {
|
||||||
|
if (destroyed) return false;
|
||||||
|
const prefs = preferences();
|
||||||
|
if (!regime(prefs)) {
|
||||||
|
settling = 0;
|
||||||
|
if (models.size || pending.size) releaseAll();
|
||||||
|
eligible = new Set();
|
||||||
|
for (const item of items.values()) item.modelOwnsVisual = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (nowMs - lastEligibilityAt >= policy.intervalMs) {
|
||||||
|
lastEligibilityAt = nowMs;
|
||||||
|
refreshEligibility(items, prefs);
|
||||||
|
}
|
||||||
|
const occluder = horizonOccluder(viewer.camera);
|
||||||
|
let drawn = false;
|
||||||
|
settling = pending.size;
|
||||||
|
for (const [id, item] of items) {
|
||||||
|
const model = models.get(id);
|
||||||
|
item.modelOwnsVisual = false;
|
||||||
|
if (!model) continue;
|
||||||
|
if (
|
||||||
|
!item.position ||
|
||||||
|
item.show === false ||
|
||||||
|
!occluder.isPointVisible(item.position)
|
||||||
|
) {
|
||||||
|
model.show = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.spec.key !== model._gevSpecKey) {
|
||||||
|
// A new type or size: the next eligibility pass loads it.
|
||||||
|
model.show = false;
|
||||||
|
lastEligibilityAt = -Infinity;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tint = item.spec.tinted ? item.tint : null;
|
||||||
|
if (tint && !Cesium.Color.equals(tint, model._gevTint)) {
|
||||||
|
model.customShader = fleetTintShader(tint);
|
||||||
|
model._gevTint = tint;
|
||||||
|
}
|
||||||
|
fleetModelMatrix(
|
||||||
|
placement(item),
|
||||||
|
item.headingDeg,
|
||||||
|
item.spec,
|
||||||
|
model.modelMatrix,
|
||||||
|
scratchHpr,
|
||||||
|
);
|
||||||
|
if (!model.ready) {
|
||||||
|
model.show = false;
|
||||||
|
settling += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
model.show = true;
|
||||||
|
item.modelOwnsVisual = true;
|
||||||
|
drawn = true;
|
||||||
|
}
|
||||||
|
for (const id of [...models.keys()]) if (!items.has(id)) release(id);
|
||||||
|
return drawn;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models loading or not yet ready after the last frame: the layer keeps
|
||||||
|
* frames coming while any are, so each one takes over as soon as it can.
|
||||||
|
*/
|
||||||
|
get settling() {
|
||||||
|
return settling;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether models are drawn now: 3D on, the set loaded and the camera
|
||||||
|
* below the altitude ceiling. A layer skips building items while not.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
active() {
|
||||||
|
return !destroyed && regime(preferences());
|
||||||
|
},
|
||||||
|
|
||||||
|
/** The model drawing `id`, if one is admitted. */
|
||||||
|
modelFor(id) {
|
||||||
|
return models.get(id) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Show or hide every model (the layer's own visibility). */
|
||||||
|
setVisible(show) {
|
||||||
|
if (!collection.isDestroyed()) collection.show = show;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Drop every model (3D off, layer disabled). */
|
||||||
|
clear: releaseAll,
|
||||||
|
|
||||||
|
/** Number of admitted models (tests and diagnostics). */
|
||||||
|
get size() {
|
||||||
|
return models.size;
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
destroyed = true;
|
||||||
|
releaseAll();
|
||||||
|
if (!viewer.isDestroyed?.() && !collection.isDestroyed())
|
||||||
|
viewer.scene.primitives.remove(collection);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+135
-773
File diff suppressed because it is too large.
Load diff
@@ -1,74 +1,15 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model-space visual centres measured from the shipped GLBs' scene-space AABBs.
|
* Model-space anchors on the aircraft layers' fleet models: where brackets
|
||||||
|
* and readouts sit (a model's visual centre) and where a tracked trail
|
||||||
|
* attaches (its aft-belly point, measured per model in fleetTrailAnchors.js
|
||||||
|
* and handed to the layers with each model's spec by fleetModels.js).
|
||||||
*
|
*
|
||||||
* glTF stores these assets Y-up. Cesium's model frame is Z-up, so a glTF
|
* Values are native model units in raw glTF axes; callers supply the Model's
|
||||||
* centre `[x, y, z]` becomes `[x, -z, y]` before the layer's ENU modelMatrix
|
|
||||||
* is applied. Values remain in native model units; callers supply the Model's
|
|
||||||
* effective rendered `computedScale` because Cesium does not bake it into
|
* effective rendered `computedScale` because Cesium does not bake it into
|
||||||
* modelMatrix and may raise it above `scale` to satisfy minimumPixelSize.
|
* modelMatrix and may raise it above `scale` to satisfy minimumPixelSize.
|
||||||
*/
|
*/
|
||||||
const CENTERED_MODEL = Object.freeze([0, 0, 0]);
|
|
||||||
|
|
||||||
export const MODEL_VISUAL_CENTER_NATIVE = Object.freeze({
|
|
||||||
'/models/airplane.glb': CENTERED_MODEL,
|
|
||||||
'/models/jet.glb': CENTERED_MODEL,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aft-belly trail attachment points, measured on each approved model's actual
|
|
||||||
* MESH SURFACE. Every bundled asset is baked to one convention (see
|
|
||||||
* aircraftClass.js): origin at the bbox centre, X = length with the NOSE at
|
|
||||||
* local −X, glTF Y = up (Cesium-local Z here).
|
|
||||||
*
|
|
||||||
* Each value is the point of the hull's CENTRELINE PROFILE (the y = 0 slice of
|
|
||||||
* the mesh) closest to that model's aft-belly AABB corner. Three things follow
|
|
||||||
* from that construction, and each is asserted rather than described in
|
|
||||||
* modelScale.test.mjs:
|
|
||||||
*
|
|
||||||
* ON THE HULL. This is the correction that matters. The previous round put
|
|
||||||
* the anchor ON the AABB corner itself, reasoning that an anchor aft of
|
|
||||||
* every vertex makes non-intersection geometric. It does — but an AABB
|
|
||||||
* corner is EMPTY SPACE, and on these airframes it is a long way from
|
|
||||||
* any of them: 4.80 m out for airplane.glb and 6.14 m for jet.glb in
|
|
||||||
* native units, which at the 450 m framing is a visible gap between the
|
|
||||||
* aircraft and the end of its own trail. The trail must terminate on the
|
|
||||||
* aeroplane, so the anchor is a surface point.
|
|
||||||
*
|
|
||||||
* AFT. The head segment is drawn from a point behind the aircraft to this
|
|
||||||
* anchor, so an anchor near the middle makes the segment enter at the
|
|
||||||
* tail and stop mid-fuselage — a line drawn through the airframe. These
|
|
||||||
* sit at 83–96 % of each model's aft extreme, on the aft-most hull the
|
|
||||||
* centreline has.
|
|
||||||
*
|
|
||||||
* BELOW. Nothing of the aircraft is lower than the anchor at or behind its
|
|
||||||
* own station, so the trail leaves from underneath the way a wake does
|
|
||||||
* rather than out of the roof. On most assets that is a frank belly
|
|
||||||
* point; on jet.glb the aft centreline profile has swept up into the
|
|
||||||
* tail cone by then, so the anchor sits just above the model's vertical
|
|
||||||
* centre (+0.13 native on a 5.63 m half-height) at the exhaust — which
|
|
||||||
* is where a fast jet's wake leaves it anyway.
|
|
||||||
*
|
|
||||||
* Y stays on the centreline — the profile is sliced at y = 0 rather than taking
|
|
||||||
* the nearest surface point in the round — so the attachment cannot drift
|
|
||||||
* screen-left or screen-right as the aircraft turns. (The unconstrained nearest
|
|
||||||
* point would put jet.glb's anchor 1.29 m off-axis.) Values are native model
|
|
||||||
* units; callers scale them by Cesium's effective `computedScale`.
|
|
||||||
*
|
|
||||||
* Locked against the shipped GLBs by modelScale.test.mjs, which re-reads the
|
|
||||||
* POSITION buffers and re-derives every property above — re-measure, never
|
|
||||||
* re-guess, when an asset changes.
|
|
||||||
*/
|
|
||||||
export const MODEL_TRAIL_ANCHOR_NATIVE = Object.freeze({
|
|
||||||
'/models/airplane.glb': Object.freeze([24.0879, -2.2163, 0]),
|
|
||||||
'/models/jet.glb': Object.freeze([18.9103, 0.1266, 0]),
|
|
||||||
'/models/bell206.glb': Object.freeze([5.2077, -0.0005, 0]),
|
|
||||||
'/models/c172.glb': Object.freeze([3.6816, -0.2425, 0]),
|
|
||||||
'/models/citation2.glb': Object.freeze([6.8804, -0.6136, 0]),
|
|
||||||
'/models/mq9.glb': Object.freeze([5.1951, -1.1516, 0]),
|
|
||||||
'/models/b789.glb': Object.freeze([30.1007, -3.4814, 0]),
|
|
||||||
'/models/atr72.glb': Object.freeze([12.9785, -1.2935, 0]),
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @constant Cesium's OWN axis correction for a glTF loaded with the
|
/** @constant Cesium's OWN axis correction for a glTF loaded with the
|
||||||
* `Model.fromGltfAsync` defaults (upAxis Y, forwardAxis Z), assembled from its
|
* `Model.fromGltfAsync` defaults (upAxis Y, forwardAxis Z), assembled from its
|
||||||
@@ -134,27 +75,6 @@ export function modelAnchorWorld(model, nativeAnchor, result) {
|
|||||||
return Cesium.Matrix4.multiplyByPoint(_chainScratch, _anchorScratch, result);
|
return Cesium.Matrix4.multiplyByPoint(_chainScratch, _anchorScratch, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the model-local visual centre for an approved bundled GLB.
|
|
||||||
* Assets absent from the table were audited as origin-centred.
|
|
||||||
* @param {string} url Bundled model URL.
|
|
||||||
* @returns {readonly number[]} Cesium-local `[x, y, z]` native-unit offset.
|
|
||||||
*/
|
|
||||||
export function visualCenterForModel(url) {
|
|
||||||
return MODEL_VISUAL_CENTER_NATIVE[url] || CENTERED_MODEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the model-local aft-belly attachment point for a tracked trail.
|
|
||||||
* Unknown future assets safely fall back to their visual centre until their
|
|
||||||
* geometry is measured and added to the table.
|
|
||||||
* @param {string} url Bundled model URL.
|
|
||||||
* @returns {readonly number[]} Cesium-local `[x, y, z]` native-unit offset.
|
|
||||||
*/
|
|
||||||
export function trailAnchorForModel(url) {
|
|
||||||
return MODEL_TRAIL_ANCHOR_NATIVE[url] || visualCenterForModel(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where a trail head segment should START being drawn: the whole segment, a
|
* Where a trail head segment should START being drawn: the whole segment, a
|
||||||
* shortened piece of it, or nothing at all.
|
* shortened piece of it, or nothing at all.
|
||||||
@@ -171,7 +91,7 @@ export function trailAnchorForModel(url) {
|
|||||||
* mistake matters:
|
* mistake matters:
|
||||||
*
|
*
|
||||||
* - Testing segment LENGTH against the radius hid real trail. With
|
* - Testing segment LENGTH against the radius hid real trail. With
|
||||||
* `airplane.glb`'s 34.41 m radius and 24.09 m tail anchor, a prior fix 58 m
|
* an airliner model's 34.41 m radius and 24.09 m tail anchor, a prior fix 58 m
|
||||||
* aft gives a 33.98 m segment — under the radius, so suppressed — even
|
* aft gives a 33.98 m segment — under the radius, so suppressed — even
|
||||||
* though ~23.6 m of it is in open air behind the aircraft. It also made the
|
* though ~23.6 m of it is in open air behind the aircraft. It also made the
|
||||||
* whole segment pop in at once on crossing ~58.4 m.
|
* whole segment pop in at once on crossing ~58.4 m.
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ import militaryFlightsLayer, {
|
|||||||
} from './militaryFlights.js';
|
} from './militaryFlights.js';
|
||||||
import { clearTr3bRegistry, setTr3b } from './tr3bRegistry.js';
|
import { clearTr3bRegistry, setTr3b } from './tr3bRegistry.js';
|
||||||
import { reportMeshFloorCell, _clearMeshFloorCellsForTest } from './groundFloor.js';
|
import { reportMeshFloorCell, _clearMeshFloorCellsForTest } from './groundFloor.js';
|
||||||
|
import { loadFleetModelSet } from './fleetModels.js';
|
||||||
|
import { aircraftLayerModelSpec } from '../layers/flights/modelSpec.js';
|
||||||
|
import { fetchPublicFile } from '../testSupport/fleetGlb.mjs';
|
||||||
|
|
||||||
|
// The layers draw only once the fleet model set has loaded; load it from disk.
|
||||||
|
// Model creation itself is stubbed per test through Cesium.Model.fromGltfAsync.
|
||||||
|
const _silentInfo = console.info;
|
||||||
|
console.info = () => {};
|
||||||
|
await loadFleetModelSet({ fetchImpl: fetchPublicFile });
|
||||||
|
console.info = _silentInfo;
|
||||||
|
const FLIGHTS_META = Object.freeze({ typeCode: 'A320', klass: 'airliner' });
|
||||||
|
const MILITARY_META = Object.freeze({ typeCode: 'F16', klass: 'fastjet' });
|
||||||
|
|
||||||
// Sample altitudes are expressed RELATIVE to the band rather than as absolute
|
// Sample altitudes are expressed RELATIVE to the band rather than as absolute
|
||||||
// offsets. The thresholds have already been retuned once (1_000_000 → 150_000
|
// offsets. The thresholds have already been retuned once (1_000_000 → 150_000
|
||||||
@@ -201,7 +213,8 @@ const LAYERS = [
|
|||||||
driveFleetModel: flightsDriveFleetModel,
|
driveFleetModel: flightsDriveFleetModel,
|
||||||
ensureFleetModel: flightsEnsureFleetModel,
|
ensureFleetModel: flightsEnsureFleetModel,
|
||||||
trackedBillboardColor: flightsTrackedBillboardColor,
|
trackedBillboardColor: flightsTrackedBillboardColor,
|
||||||
bellyM: 6.719,
|
/** How far a grounded A320 stands its origin above the ground (scaled wheel clearance). */
|
||||||
|
bellyM: aircraftLayerModelSpec(FLIGHTS_META).bellyM,
|
||||||
seed({ viewer, tracked = true, icao24 = ICAO, modelCollection = null, onGround = false, models = [] }) {
|
seed({ viewer, tracked = true, icao24 = ICAO, modelCollection = null, onGround = false, models = [] }) {
|
||||||
const billboard = contactBillboard();
|
const billboard = contactBillboard();
|
||||||
_setTrackedFlightRefreshStateForTest({
|
_setTrackedFlightRefreshStateForTest({
|
||||||
@@ -213,7 +226,7 @@ const LAYERS = [
|
|||||||
tracked,
|
tracked,
|
||||||
models,
|
models,
|
||||||
modelCollection,
|
modelCollection,
|
||||||
meta: { callsign: 'TEST123 ', altitude: 10_668, klass: 'airliner', onGround },
|
meta: { callsign: 'TEST123 ', altitude: 10_668, ...FLIGHTS_META, onGround },
|
||||||
});
|
});
|
||||||
return billboard;
|
return billboard;
|
||||||
},
|
},
|
||||||
@@ -226,7 +239,7 @@ const LAYERS = [
|
|||||||
driveFleetModel: militaryDriveFleetModel,
|
driveFleetModel: militaryDriveFleetModel,
|
||||||
ensureFleetModel: militaryEnsureFleetModel,
|
ensureFleetModel: militaryEnsureFleetModel,
|
||||||
trackedBillboardColor: militaryTrackedBillboardColor,
|
trackedBillboardColor: militaryTrackedBillboardColor,
|
||||||
bellyM: 5.631 * 0.8,
|
bellyM: aircraftLayerModelSpec(MILITARY_META).bellyM,
|
||||||
seed({ viewer, tracked = true, icao24 = ICAO, modelCollection = null, onGround = false, models = [] }) {
|
seed({ viewer, tracked = true, icao24 = ICAO, modelCollection = null, onGround = false, models = [] }) {
|
||||||
const billboard = contactBillboard();
|
const billboard = contactBillboard();
|
||||||
_setTrackedMilitaryRefreshStateForTest({
|
_setTrackedMilitaryRefreshStateForTest({
|
||||||
@@ -238,7 +251,7 @@ const LAYERS = [
|
|||||||
tracked,
|
tracked,
|
||||||
models,
|
models,
|
||||||
modelCollection,
|
modelCollection,
|
||||||
meta: { callsign: 'TEST123 ', altitudeFt: 35_000, klass: 'fastjet', onGround },
|
meta: { callsign: 'TEST123 ', altitudeFt: 35_000, ...MILITARY_META, onGround },
|
||||||
});
|
});
|
||||||
return billboard;
|
return billboard;
|
||||||
},
|
},
|
||||||
@@ -841,8 +854,8 @@ for (const fixture of LAYERS) {
|
|||||||
Date.now = () => now;
|
Date.now = () => now;
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const requested = [];
|
const requested = [];
|
||||||
Cesium.Model.fromGltfAsync = ({ url }) => {
|
Cesium.Model.fromGltfAsync = ({ basePath }) => {
|
||||||
attempts++; requested.push(url);
|
attempts++; requested.push(basePath);
|
||||||
return Promise.reject(new Error('simulated missing GLB'));
|
return Promise.reject(new Error('simulated missing GLB'));
|
||||||
};
|
};
|
||||||
// The give-up path warns once by design; keep the run's output clean.
|
// The give-up path warns once by design; keep the run's output clean.
|
||||||
|
|||||||
@@ -164,10 +164,10 @@ export function createEnrichment({
|
|||||||
const bb = flightState._billboards.get(icao24);
|
const bb = flightState._billboards.get(icao24);
|
||||||
if (bb)
|
if (bb)
|
||||||
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
parts.rendering._applyFleetBillboardPresentation(icao24, bb);
|
||||||
// Hangar fleet: the class's GLB/scale may have changed — resync the
|
|
||||||
// live model, any in-flight load, and the tracked standalone model.
|
|
||||||
parts.rendering._syncModelToClass(icao24);
|
|
||||||
}
|
}
|
||||||
|
// The type picks the fleet model and its length — resync the live
|
||||||
|
// model, any in-flight load, and the tracked standalone model.
|
||||||
|
parts.rendering._syncModelToClass(icao24);
|
||||||
}
|
}
|
||||||
if (icao24 === flightState._trackedIcao && flightState._trackedEntity)
|
if (icao24 === flightState._trackedIcao && flightState._trackedEntity)
|
||||||
parts.tracking._updateTrackedLabelModel(icao24);
|
parts.tracking._updateTrackedLabelModel(icao24);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { PLANE_MODEL_URL } from './policy.js';
|
import { loadFleetModelSet } from '../../data/fleetModels.js';
|
||||||
|
|
||||||
export function createLifecycle({
|
export function createLifecycle({
|
||||||
flightState,
|
flightState,
|
||||||
@@ -53,29 +53,19 @@ export function createLifecycle({
|
|||||||
registerSpriteCollection('flights', flightState._billboardCollection);
|
registerSpriteCollection('flights', flightState._billboardCollection);
|
||||||
flightState._modelCollection = new Cesium.PrimitiveCollection();
|
flightState._modelCollection = new Cesium.PrimitiveCollection();
|
||||||
viewer.scene.primitives.add(flightState._modelCollection);
|
viewer.scene.primitives.add(flightState._modelCollection);
|
||||||
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
// Every fleet model loads once, up front, for every layer that draws
|
||||||
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
// them (fleetModels.js), so no model is fetched while the session runs.
|
||||||
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
// Captured against this epoch so a destroy/re-init mid-load doesn't flip
|
||||||
if (!flightState._preloadModel) {
|
// the flag for a torn-down lifecycle.
|
||||||
|
{
|
||||||
const epoch = flightState._modelEpoch;
|
const epoch = flightState._modelEpoch;
|
||||||
Cesium.Model.fromGltfAsync({
|
loadFleetModelSet({ resolveAsset })
|
||||||
url: resolveAsset(PLANE_MODEL_URL),
|
.then(() => {
|
||||||
asynchronous: false,
|
if (epoch === flightState._modelEpoch)
|
||||||
})
|
|
||||||
.then((m) => {
|
|
||||||
if (epoch === flightState._modelEpoch) {
|
|
||||||
flightState._preloadModel = m;
|
|
||||||
flightState._planeModelLoaded = true;
|
flightState._planeModelLoaded = true;
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
m.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
/* tracked plane just stays a billboard a beat longer */
|
/* aircraft stay billboards */
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
flightState._billboards = new Map();
|
flightState._billboards = new Map();
|
||||||
@@ -296,14 +286,6 @@ export function createLifecycle({
|
|||||||
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
||||||
flightState._modelPending.clear();
|
flightState._modelPending.clear();
|
||||||
flightState._modelGen.clear();
|
flightState._modelGen.clear();
|
||||||
if (flightState._preloadModel) {
|
|
||||||
try {
|
|
||||||
flightState._preloadModel.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
flightState._preloadModel = null;
|
|
||||||
}
|
|
||||||
flightState._planeModelLoaded = false;
|
flightState._planeModelLoaded = false;
|
||||||
flightState._billboards.clear();
|
flightState._billboards.clear();
|
||||||
flightState._cullPositions.clear();
|
flightState._cullPositions.clear();
|
||||||
|
|||||||
@@ -1,52 +1,58 @@
|
|||||||
import {
|
import { aircraftModelSpec, fleetModelSet } from '../../data/fleetModels.js';
|
||||||
CLASS_MODEL_REAL,
|
import { MODEL_COLOR_BLEND_AMOUNT } from './policy.js';
|
||||||
CLASS_MODEL_URL,
|
|
||||||
CLASS_SCALE_3D,
|
|
||||||
} from '../../data/aircraftClass.js';
|
|
||||||
import {
|
|
||||||
trailAnchorForModel,
|
|
||||||
visualCenterForModel,
|
|
||||||
} from '../../data/modelVisualAnchor.js';
|
|
||||||
import {
|
|
||||||
MODEL_BELLY_OFFSET_NATIVE,
|
|
||||||
MODEL_COLOR_BLEND_AMOUNT,
|
|
||||||
MODEL_NATIVE_RADIUS_M,
|
|
||||||
MODEL_SCALE,
|
|
||||||
PLANE_MODEL_URL,
|
|
||||||
} from './policy.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The glTF asset, scale and measured geometry a civil aircraft class renders
|
* What an aircraft draws with before the fleet model set has loaded: no
|
||||||
* with. Real per-class GLBs are baked to real-world meters (scale 1, measured
|
* model (its billboard stays the visual), with neutral geometry for any
|
||||||
* belly and radius); classes without one share airplane.glb at the class's
|
* reader that asks.
|
||||||
* 3D scale. Shared by the Flights layer and the Local ADS-B layer so both draw
|
|
||||||
* the same model for the same class.
|
|
||||||
* @param {string} klass Aircraft class from `classifyAircraft`.
|
|
||||||
* @returns {{url:string, scale:number, nativeRadiusM:number, bellyM:number,
|
|
||||||
* blendAmount:number, visualCenterNative:object, trailAnchorNative:object}}
|
|
||||||
*/
|
*/
|
||||||
export function civilAircraftModelSpec(klass) {
|
export const UNLOADED_AIRCRAFT_MODEL_SPEC = Object.freeze({
|
||||||
const real = CLASS_MODEL_REAL[klass];
|
model: null,
|
||||||
if (real) {
|
url: null,
|
||||||
return {
|
scale: 1,
|
||||||
url: real.url,
|
scaleXYZ: Object.freeze([1, 1, 1]),
|
||||||
scale: 1,
|
length: 0,
|
||||||
nativeRadiusM: real.radiusM,
|
nativeRadiusM: 20,
|
||||||
bellyM: real.bellyM,
|
clearanceM: 0,
|
||||||
blendAmount: MODEL_COLOR_BLEND_AMOUNT,
|
bellyM: 0,
|
||||||
visualCenterNative: visualCenterForModel(real.url),
|
elevationM: 0,
|
||||||
trailAnchorNative: trailAnchorForModel(real.url),
|
tinted: false,
|
||||||
};
|
blendAmount: MODEL_COLOR_BLEND_AMOUNT,
|
||||||
}
|
visualCenterNative: Object.freeze([0, 0, 0]),
|
||||||
const scale = MODEL_SCALE * (CLASS_SCALE_3D[klass] || 1);
|
trailAnchorNative: Object.freeze([0, 0, 0]),
|
||||||
const url = CLASS_MODEL_URL[klass] || PLANE_MODEL_URL;
|
key: 'unloaded',
|
||||||
return {
|
});
|
||||||
url,
|
|
||||||
scale,
|
/**
|
||||||
nativeRadiusM: MODEL_NATIVE_RADIUS_M,
|
* The fleet model, life-size scale and measured geometry an aircraft draws
|
||||||
bellyM: MODEL_BELLY_OFFSET_NATIVE * scale,
|
* with, chosen by its ICAO type and emitter category (`fleetModels.js`).
|
||||||
|
* Shared by the Flights, Military Flights and Local ADS-B layers so all three
|
||||||
|
* draw the same model for the same aircraft. `bellyM` is the model's scaled
|
||||||
|
* clearance: how far a grounded aircraft's origin stands above the ground.
|
||||||
|
* @param {{typeCode?: string|null, type?: string|null,
|
||||||
|
* category?: string|number|null}|null|undefined} aircraft
|
||||||
|
* @param {object|null} [set] The loaded fleet model set.
|
||||||
|
* @returns {object} The spec, or {@link UNLOADED_AIRCRAFT_MODEL_SPEC} before
|
||||||
|
* the set has loaded.
|
||||||
|
*/
|
||||||
|
export function aircraftLayerModelSpec(aircraft, set = fleetModelSet()) {
|
||||||
|
if (!set) return UNLOADED_AIRCRAFT_MODEL_SPEC;
|
||||||
|
const spec = aircraftModelSpec(set, {
|
||||||
|
typeCode: aircraft?.typeCode ?? aircraft?.type ?? null,
|
||||||
|
category: aircraft?.category ?? null,
|
||||||
|
});
|
||||||
|
return Object.freeze({
|
||||||
|
...spec,
|
||||||
|
bellyM: spec.clearanceM,
|
||||||
blendAmount: MODEL_COLOR_BLEND_AMOUNT,
|
blendAmount: MODEL_COLOR_BLEND_AMOUNT,
|
||||||
visualCenterNative: visualCenterForModel(url),
|
});
|
||||||
trailAnchorNative: trailAnchorForModel(url),
|
}
|
||||||
};
|
|
||||||
|
/**
|
||||||
|
* Cache key of an aircraft's model spec: what selects its model.
|
||||||
|
* @param {object|null|undefined} aircraft
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function aircraftModelSpecCacheKey(aircraft) {
|
||||||
|
return `${aircraft?.typeCode ?? aircraft?.type ?? ''}|${aircraft?.category ?? ''}`;
|
||||||
}
|
}
|
||||||
@@ -431,7 +431,7 @@ export function createMotion({
|
|||||||
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
|
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
|
||||||
if (parts.rendering._modelOwnsVisual(icao24)) {
|
if (parts.rendering._modelOwnsVisual(icao24)) {
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(icao24)?.klass,
|
flightState.records.data.get(icao24),
|
||||||
);
|
);
|
||||||
const scale = trackedModelScaleForPixelCap({
|
const scale = trackedModelScaleForPixelCap({
|
||||||
baseScale: spec.scale,
|
baseScale: spec.scale,
|
||||||
@@ -614,7 +614,7 @@ export function createMotion({
|
|||||||
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
) {
|
) {
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
return modelVisualAnchor(
|
return modelVisualAnchor(
|
||||||
flightState._trackedModel.modelMatrix,
|
flightState._trackedModel.modelMatrix,
|
||||||
@@ -659,7 +659,7 @@ export function createMotion({
|
|||||||
)
|
)
|
||||||
return 0;
|
return 0;
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
const scale = Number.isFinite(flightState._trackedModel.computedScale)
|
const scale = Number.isFinite(flightState._trackedModel.computedScale)
|
||||||
? flightState._trackedModel.computedScale
|
? flightState._trackedModel.computedScale
|
||||||
@@ -673,7 +673,7 @@ export function createMotion({
|
|||||||
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
) {
|
) {
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled
|
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled
|
||||||
// half-correction put this offset on the lateral axis — see
|
// half-correction put this offset on the lateral axis — see
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ export const GROUND_SCALE = 0.8;
|
|||||||
// the slots go to either the nearest planes ('proximity') or every in-view plane ('all'), each
|
// the slots go to either the nearest planes ('proximity') or every in-view plane ('all'), each
|
||||||
// backed by a hard cap so a draw-call explosion can't tank the frame (no instancing yet).
|
// backed by a hard cap so a draw-call explosion can't tank the frame (no instancing yet).
|
||||||
|
|
||||||
export const PLANE_MODEL_URL = '/models/airplane.glb';
|
|
||||||
|
|
||||||
export const MODEL_ALT_CEIL_M = 800000;
|
export const MODEL_ALT_CEIL_M = 800000;
|
||||||
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
|
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
|
||||||
|
|
||||||
@@ -55,10 +53,6 @@ export const TRACKED_MODEL_MIN_PX = 40;
|
|||||||
export const TRACKED_MODEL_MAX_PX = 200;
|
export const TRACKED_MODEL_MAX_PX = 200;
|
||||||
// owner-selected close-range tracked-target feel
|
// owner-selected close-range tracked-target feel
|
||||||
|
|
||||||
export const MODEL_NATIVE_RADIUS_M = 34.41;
|
|
||||||
|
|
||||||
export const MODEL_SCALE = 1;
|
|
||||||
// airplane.glb is transform-applied and baked to real-world meters
|
|
||||||
// Per-mode caps. Each model is its own draw call (no instancing yet), so these bound the frame cost.
|
// Per-mode caps. Each model is its own draw call (no instancing yet), so these bound the frame cost.
|
||||||
|
|
||||||
export const MODEL_MAX = 150;
|
export const MODEL_MAX = 150;
|
||||||
@@ -90,21 +84,12 @@ export const MODEL_ALL_KEEP_M = 450000;
|
|||||||
// all: KEEP modeled planes out to 450 km
|
// all: KEEP modeled planes out to 450 km
|
||||||
|
|
||||||
export const MODEL_HEADING_OFFSET_DEG = 180;
|
export const MODEL_HEADING_OFFSET_DEG = 180;
|
||||||
// airplane.glb nose is opposite Cesium heading-0
|
// every fleet model is nose -X: opposite Cesium heading 0
|
||||||
// Owner launch-polish direction: models should read as clean light silhouettes,
|
// Owner launch-polish direction: models should read as clean light silhouettes,
|
||||||
// with only a weak diffuse contribution from the existing approved textures.
|
// with only a weak diffuse contribution from the models' own colours.
|
||||||
|
|
||||||
export const MODEL_COLOR_BLEND_AMOUNT = 0.94;
|
export const MODEL_COLOR_BLEND_AMOUNT = 0.94;
|
||||||
|
|
||||||
// Grounded-model belly offset: airplane.glb's centred origin sits 6.719 m ABOVE its
|
|
||||||
// lowest vertex (glTF Y-up scene AABB with node transforms applied — same reader as
|
|
||||||
// modelScale.test.mjs, measured after its 24× transform bake). × class multiplier ≈ 5.0–9.7 m
|
|
||||||
// of lift, so a ground-snapped model rests its lowest geometry (gear/belly) ON the sampled
|
|
||||||
// tile skin instead of sinking to the fuselage-centerline origin. Locked against the GLB
|
|
||||||
// by modelScale.test.mjs.
|
|
||||||
|
|
||||||
export const MODEL_BELLY_OFFSET_NATIVE = 6.719;
|
|
||||||
|
|
||||||
export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0);
|
export const CYAN_TRANSPARENT = Cesium.Color.CYAN.withAlpha(0);
|
||||||
|
|
||||||
export const COCKPIT_CONTACT_SIZE_PX = 6;
|
export const COCKPIT_CONTACT_SIZE_PX = 6;
|
||||||
@@ -203,6 +188,24 @@ export const LANDED_SPEED_MAX_MPS = 23;
|
|||||||
|
|
||||||
export const FLEET_DR_INTERVAL_MS = 80;
|
export const FLEET_DR_INTERVAL_MS = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The icon-to-model rules the aircraft layers follow, as the Vessels, Transit
|
||||||
|
* and Rail layers take them (`createFleetModelLayer`): the same altitude
|
||||||
|
* ceiling, minimum pixel size, proximity/all caps, add/keep radii and
|
||||||
|
* eligibility cadence, so every layer turns icons into models at one distance.
|
||||||
|
*/
|
||||||
|
export const FLEET_MODEL_POLICY = Object.freeze({
|
||||||
|
altitudeCeilingM: MODEL_ALT_CEIL_M,
|
||||||
|
minimumPixelSize: MODEL_MIN_PX,
|
||||||
|
cap: MODEL_MAX,
|
||||||
|
capAll: MODEL_MAX_ALL,
|
||||||
|
addM: MODEL_PROX_ADD_M,
|
||||||
|
addAllM: MODEL_ALL_ADD_M,
|
||||||
|
keepM: MODEL_PROX_KEEP_M,
|
||||||
|
keepAllM: MODEL_ALL_KEEP_M,
|
||||||
|
intervalMs: FLEET_DR_INTERVAL_MS,
|
||||||
|
});
|
||||||
|
|
||||||
/** @constant {number} Max ms between rotation passes while the camera is idle. */
|
/** @constant {number} Max ms between rotation passes while the camera is idle. */
|
||||||
|
|
||||||
export const ROTATION_REFRESH_MS = 1000;
|
export const ROTATION_REFRESH_MS = 1000;
|
||||||
|
|||||||
@@ -480,9 +480,7 @@ export function createQueries({
|
|||||||
// and rises only as the coarse ground-floor cell warms. Zero extra sampling and
|
// and rises only as the coarse ground-floor cell warms. Zero extra sampling and
|
||||||
// no `_modelDisplayPosition` call from postRender. Sprite-owned contacts keep
|
// no `_modelDisplayPosition` call from postRender. Sprite-owned contacts keep
|
||||||
// `bb.position` — sprite and bracket are co-located there, so association holds.
|
// `bb.position` — sprite and bracket are co-located there, so association holds.
|
||||||
const spec = modelOwnsVisual
|
const spec = modelOwnsVisual ? parts.rendering._modelSpec(info) : null;
|
||||||
? parts.rendering._modelSpec(info?.klass)
|
|
||||||
: null;
|
|
||||||
const pos = isTracked
|
const pos = isTracked
|
||||||
? parts.motion._trackedVisualCached() || bb.position
|
? parts.motion._trackedVisualCached() || bb.position
|
||||||
: modelOwnsVisual
|
: modelOwnsVisual
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import * as Cesium from 'cesium';
|
|||||||
import { julianAt } from '../../time/julian.js';
|
import { julianAt } from '../../time/julian.js';
|
||||||
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
|
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
|
||||||
import { selectModelEligible } from '../../data/modelEligibility.js';
|
import { selectModelEligible } from '../../data/modelEligibility.js';
|
||||||
import { civilAircraftModelSpec } from './modelSpec.js';
|
import {
|
||||||
|
aircraftLayerModelSpec,
|
||||||
|
aircraftModelSpecCacheKey,
|
||||||
|
} from './modelSpec.js';
|
||||||
|
import { createFleetModel } from '../../data/fleetModelInstances.js';
|
||||||
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
||||||
import { isEmergency } from '../../data/aircraftEmergency.js';
|
import { isEmergency } from '../../data/aircraftEmergency.js';
|
||||||
import { cockpitContactDotImage } from '../../data/cockpitContactDot.js';
|
import { cockpitContactDotImage } from '../../data/cockpitContactDot.js';
|
||||||
@@ -115,11 +119,14 @@ export function createRendering({
|
|||||||
return Number.POSITIVE_INFINITY;
|
return Number.POSITIVE_INFINITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _modelSpec(klass) {
|
/** The fleet model an aircraft draws with, by its type and emitter category. */
|
||||||
const cached = flightState._specCache.get(klass);
|
function _modelSpec(meta) {
|
||||||
|
const key = aircraftModelSpecCacheKey(meta);
|
||||||
|
const cached = flightState._specCache.get(key);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
const spec = civilAircraftModelSpec(klass);
|
const spec = aircraftLayerModelSpec(meta);
|
||||||
flightState._specCache.set(klass, spec);
|
// Not cached before the model set loads: the answer changes once it has.
|
||||||
|
if (spec.model) flightState._specCache.set(key, spec);
|
||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +316,7 @@ export function createRendering({
|
|||||||
Cesium.Ellipsoid.WGS84,
|
Cesium.Ellipsoid.WGS84,
|
||||||
flightState._scratchGroundCarto,
|
flightState._scratchGroundCarto,
|
||||||
);
|
);
|
||||||
carto.height = h + _modelSpec(meta.klass).bellyM;
|
carto.height = h + _modelSpec(meta).bellyM;
|
||||||
return Cesium.Cartesian3.fromRadians(
|
return Cesium.Cartesian3.fromRadians(
|
||||||
carto.longitude,
|
carto.longitude,
|
||||||
carto.latitude,
|
carto.latitude,
|
||||||
@@ -416,16 +423,13 @@ export function createRendering({
|
|||||||
return !!model && model.ready === true && model.show === true;
|
return !!model && model.ready === true && model.show === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Spec identity for a LOADED model: URL and scale together (same-URL classes
|
/** Spec identity for a LOADED model: the model and its scale together (one
|
||||||
* differ by scale — airliner vs quadjet both ship airplane.glb). */
|
* model draws several types, each at its own length). */
|
||||||
|
|
||||||
const _specKeyFor = (klass) => {
|
const _specKeyFor = (meta) => _modelSpec(meta).key;
|
||||||
const spec = _modelSpec(klass);
|
|
||||||
return `${spec.url}@${spec.scale}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Class-change model sync (enrichment AND poll-path klass updates): when the
|
/** Type-change model sync (enrichment AND poll-path type/category updates): when the
|
||||||
* aircraft's live model or in-flight load no longer matches its class's spec,
|
* aircraft's live model or in-flight load no longer matches its type's spec,
|
||||||
* drop it so the eligibility pass reloads the right asset at the right scale.
|
* drop it so the eligibility pass reloads the right asset at the right scale.
|
||||||
* Gap-proof: the fleet billboard is re-shown BEFORE the release so the
|
* Gap-proof: the fleet billboard is re-shown BEFORE the release so the
|
||||||
* contact never goes invisible for the tick gap; _releaseModel's generation
|
* contact never goes invisible for the tick gap; _releaseModel's generation
|
||||||
@@ -433,7 +437,7 @@ export function createRendering({
|
|||||||
* the same rule (its billboard entity is always the fallback visual). */
|
* the same rule (its billboard entity is always the fallback visual). */
|
||||||
|
|
||||||
function _syncModelToClass(icao24) {
|
function _syncModelToClass(icao24) {
|
||||||
const key = _specKeyFor(flightState.records.data.get(icao24)?.klass);
|
const key = _specKeyFor(flightState.records.data.get(icao24));
|
||||||
const current = flightState._models.get(icao24);
|
const current = flightState._models.get(icao24);
|
||||||
if (
|
if (
|
||||||
(current && current._gevSpecKey !== key) ||
|
(current && current._gevSpecKey !== key) ||
|
||||||
@@ -478,6 +482,11 @@ export function createRendering({
|
|||||||
flightState._irReloadQueue = null;
|
flightState._irReloadQueue = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A Cesium Model of `spec` from the fleet set loaded up front (no fetch mid-session). */
|
||||||
|
function _createModel(spec, options) {
|
||||||
|
return createFleetModel(spec, options, { resolveAsset });
|
||||||
|
}
|
||||||
|
|
||||||
/** Lazily create the glTF model for an aircraft (fire-and-forget; billboard shows until ready). */
|
/** Lazily create the glTF model for an aircraft (fire-and-forget; billboard shows until ready). */
|
||||||
|
|
||||||
async function _ensureModel(icao24) {
|
async function _ensureModel(icao24) {
|
||||||
@@ -497,6 +506,8 @@ export function createRendering({
|
|||||||
_modelCap()
|
_modelCap()
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
// Until the fleet model set has loaded the aircraft stays a billboard.
|
||||||
|
if (!_modelSpec(flightState.records.data.get(icao24)).model) return;
|
||||||
const epoch = flightState._modelEpoch; // lifecycle token: if destroy() bumps it, this load is dead
|
const epoch = flightState._modelEpoch; // lifecycle token: if destroy() bumps it, this load is dead
|
||||||
const gen = flightState._modelGen.get(icao24) || 0; // capture; if it changes during the load, we're stale
|
const gen = flightState._modelGen.get(icao24) || 0; // capture; if it changes during the load, we're stale
|
||||||
flightState._modelPending.add(icao24);
|
flightState._modelPending.add(icao24);
|
||||||
@@ -505,12 +516,11 @@ export function createRendering({
|
|||||||
// aircraft mid-load, the post-await admission below rejects the stale asset.
|
// aircraft mid-load, the post-await admission below rejects the stale asset.
|
||||||
// Boost state likewise: the creation options bake it in, so a mid-load
|
// Boost state likewise: the creation options bake it in, so a mid-load
|
||||||
// toggle must reject too (the reload queue only covers ADMITTED models).
|
// toggle must reject too (the reload queue only covers ADMITTED models).
|
||||||
const specKey = _specKeyFor(flightState.records.data.get(icao24)?.klass);
|
const specKey = _specKeyFor(flightState.records.data.get(icao24));
|
||||||
const loadIrBoost = flightState._irBoost;
|
const loadIrBoost = flightState._irBoost;
|
||||||
try {
|
try {
|
||||||
const spec = _modelSpec(flightState.records.data.get(icao24)?.klass);
|
const spec = _modelSpec(flightState.records.data.get(icao24));
|
||||||
model = await Cesium.Model.fromGltfAsync({
|
model = await _createModel(spec, {
|
||||||
url: resolveAsset(spec.url),
|
|
||||||
asynchronous: false,
|
asynchronous: false,
|
||||||
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
||||||
minimumPixelSize: MODEL_MIN_PX,
|
minimumPixelSize: MODEL_MIN_PX,
|
||||||
@@ -560,7 +570,7 @@ export function createRendering({
|
|||||||
flightState._models.has(icao24) ||
|
flightState._models.has(icao24) ||
|
||||||
flightState._models.size >= _modelCap() ||
|
flightState._models.size >= _modelCap() ||
|
||||||
// Class reclassified mid-load → this GLB/scale is for the OLD class.
|
// Class reclassified mid-load → this GLB/scale is for the OLD class.
|
||||||
_specKeyFor(flightState.records.data.get(icao24)?.klass) !== specKey ||
|
_specKeyFor(flightState.records.data.get(icao24)) !== specKey ||
|
||||||
// IR boost flipped mid-load → this model baked the wrong shader/tint.
|
// IR boost flipped mid-load → this model baked the wrong shader/tint.
|
||||||
flightState._irBoost !== loadIrBoost;
|
flightState._irBoost !== loadIrBoost;
|
||||||
if (stale) {
|
if (stale) {
|
||||||
@@ -716,6 +726,9 @@ export function createRendering({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
// Until the fleet model set has loaded the billboard carries it.
|
||||||
|
_modelSpec(flightState.records.data.get(flightState._trackedIcao))
|
||||||
|
.model &&
|
||||||
!flightState._trackedModel &&
|
!flightState._trackedModel &&
|
||||||
!flightState._trackedModelLoading &&
|
!flightState._trackedModelLoading &&
|
||||||
parts.tracking._trackedModelLoadAllowed()
|
parts.tracking._trackedModelLoadAllowed()
|
||||||
@@ -723,14 +736,13 @@ export function createRendering({
|
|||||||
flightState._trackedModelLoading = true;
|
flightState._trackedModelLoading = true;
|
||||||
const gen = flightState._trackedModelGen;
|
const gen = flightState._trackedModelGen;
|
||||||
const trackedSpec = _modelSpec(
|
const trackedSpec = _modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
const trackedKey = _specKeyFor(
|
const trackedKey = _specKeyFor(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
const trackedIrBoost = flightState._irBoost;
|
const trackedIrBoost = flightState._irBoost;
|
||||||
Cesium.Model.fromGltfAsync({
|
_createModel(trackedSpec, {
|
||||||
url: resolveAsset(trackedSpec.url),
|
|
||||||
asynchronous: false,
|
asynchronous: false,
|
||||||
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
||||||
minimumPixelSize: TRACKED_MODEL_MIN_PX,
|
minimumPixelSize: TRACKED_MODEL_MIN_PX,
|
||||||
@@ -766,7 +778,7 @@ export function createRendering({
|
|||||||
// _trackedModel was still null): drop the stale asset; driver reloads.
|
// _trackedModel was still null): drop the stale asset; driver reloads.
|
||||||
if (
|
if (
|
||||||
_specKeyFor(
|
_specKeyFor(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
) !== trackedKey ||
|
) !== trackedKey ||
|
||||||
flightState._irBoost !== trackedIrBoost
|
flightState._irBoost !== trackedIrBoost
|
||||||
) {
|
) {
|
||||||
@@ -840,7 +852,7 @@ export function createRendering({
|
|||||||
_applyTrackedModelFade(flightState._trackedModel);
|
_applyTrackedModelFade(flightState._trackedModel);
|
||||||
if (!flightState._trackedModel.ready) return;
|
if (!flightState._trackedModel.ready) return;
|
||||||
const spec = _modelSpec(
|
const spec = _modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
flightState._trackedModel.scale = trackedModelScaleForPixelCap({
|
flightState._trackedModel.scale = trackedModelScaleForPixelCap({
|
||||||
baseScale: spec.scale,
|
baseScale: spec.scale,
|
||||||
|
|||||||
@@ -198,9 +198,14 @@ export function createFlightSnapshotRenderer({
|
|||||||
) {
|
) {
|
||||||
rendering._applyFleetBillboardPresentation(icao24, bb);
|
rendering._applyFleetBillboardPresentation(icao24, bb);
|
||||||
}
|
}
|
||||||
// Poll-path class change (category updates): same model resync rule
|
// Poll-path type or category change: same model resync rule as the
|
||||||
// as the enrichment path — the class's GLB/scale may have changed.
|
// enrichment path — the fleet model or its scale may have changed.
|
||||||
if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24);
|
if (
|
||||||
|
prevMeta?.klass !== meta.klass ||
|
||||||
|
prevMeta?.category !== meta.category ||
|
||||||
|
prevMeta?.typeCode !== meta.typeCode
|
||||||
|
)
|
||||||
|
rendering._syncModelToClass(icao24);
|
||||||
} else {
|
} else {
|
||||||
// Start where the fleet pass will draw it: the fleet renders behind
|
// Start where the fleet pass will draw it: the fleet renders behind
|
||||||
// real time, so a fresh contact's displayed position is its fix
|
// real time, so a fresh contact's displayed position is its fix
|
||||||
|
|||||||
@@ -20,17 +20,13 @@ export function createFlightState({ source, services }) {
|
|||||||
* layer); see followsClock and displayDelayMs in policy.js. */
|
* layer); see followsClock and displayDelayMs in policy.js. */
|
||||||
flightState.timeline = null;
|
flightState.timeline = null;
|
||||||
|
|
||||||
/** Per-class model spec. Hangar-fleet classes (CLASS_MODEL_REAL) ship GLBs
|
/** Model spec per aircraft type and emitter category (aircraftLayerModelSpec in
|
||||||
* vertex-baked to real-world METERS in the airplane.glb axis convention, so
|
* flights/modelSpec.js): the procedural fleet model that draws the type, life size,
|
||||||
* they render at scale 1 with their own measured belly lift and bounding
|
* with its measured clearance, bounding radius and trail anchor. nativeRadiusM is PER
|
||||||
* radius. Every other class keeps the shared-airplane.glb formula
|
* SCALE UNIT — pixel-cap math multiplies it by `scale`. The code-side MIX tint dominates
|
||||||
* (MODEL_SCALE × CLASS_SCALE_3D). nativeRadiusM is PER SCALE UNIT — pixel-cap
|
* every model so silhouettes stay light (and this layer's own colour). Memoized: the
|
||||||
* math multiplies it by `scale`, so world radius = nativeRadiusM × scale in
|
* detection weld asks for one per MODELED contact per frame on top of the 12 Hz fleet
|
||||||
* both branches. The code-side MIX tint dominates every existing asset so
|
* pass. */
|
||||||
* class silhouettes stay light without modifying third-party GLBs/textures. */
|
|
||||||
/* Specs are static per class, and the detection weld now asks for one per
|
|
||||||
* MODELED contact per frame (up to the fleet cap) on top of the 12 Hz fleet
|
|
||||||
* pass — so this is memoized, as militaryFlights.js already does. */
|
|
||||||
|
|
||||||
flightState._specCache = new Map();
|
flightState._specCache = new Map();
|
||||||
|
|
||||||
@@ -79,16 +75,11 @@ export function createFlightState({ source, services }) {
|
|||||||
// throttle the "more planes in view than the cap" console notice
|
// throttle the "more planes in view than the cap" console notice
|
||||||
// The tracked entity's billboard goes transparent (rather than hidden) once the model takes
|
// The tracked entity's billboard goes transparent (rather than hidden) once the model takes
|
||||||
// over, so it keeps supplying a bounding sphere for follow-camera framing. We only drop its
|
// over, so it keeps supplying a bounding sphere for follow-camera framing. We only drop its
|
||||||
// alpha after the GLB is preloaded so the model is ready to render the instant the billboard
|
// alpha after the fleet model set is loaded so the model is ready to render the instant the
|
||||||
// fades — no gap, no double-image. Preloaded once at init; the instance is retained (not
|
// billboard fades — no gap, no double-image. The set loads once at init (fleetModels.js).
|
||||||
// destroyed) purely to keep Cesium's glTF cache warm for fast tracked-model instantiation.
|
|
||||||
|
|
||||||
flightState._planeModelLoaded = false;
|
flightState._planeModelLoaded = false;
|
||||||
|
|
||||||
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
|
|
||||||
|
|
||||||
flightState._preloadModel = null;
|
|
||||||
|
|
||||||
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
||||||
|
|
||||||
flightState._scratchModelMtx = new Cesium.Matrix4();
|
flightState._scratchModelMtx = new Cesium.Matrix4();
|
||||||
|
|||||||
@@ -254,6 +254,9 @@ export function createLocalAdsbLayer({
|
|||||||
);
|
);
|
||||||
marker.evidence = evidence;
|
marker.evidence = evidence;
|
||||||
marker.klass = klass;
|
marker.klass = klass;
|
||||||
|
// What picks its fleet model: the adsbdb type, else the emitter category.
|
||||||
|
marker.typeCode = enrichment.get(marker.record.icao)?.typeCode || null;
|
||||||
|
marker.category = marker.record.category || null;
|
||||||
const billboard = marker.entity?.billboard;
|
const billboard = marker.entity?.billboard;
|
||||||
if (!billboard) return;
|
if (!billboard) return;
|
||||||
if (marker.iconKind !== klass) {
|
if (marker.iconKind !== klass) {
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import {
|
|||||||
localAdsbReceiverLine,
|
localAdsbReceiverLine,
|
||||||
localAdsbStatus,
|
localAdsbStatus,
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
|
import { loadFleetModelSet } from '../../data/fleetModels.js';
|
||||||
|
import { aircraftLayerModelSpec } from '../flights/modelSpec.js';
|
||||||
|
import { fetchPublicFile } from '../../testSupport/fleetGlb.mjs';
|
||||||
|
|
||||||
|
// The layer draws models only once the fleet model set has loaded; load it from disk.
|
||||||
|
const realInfo = console.info;
|
||||||
|
console.info = () => {};
|
||||||
|
await loadFleetModelSet({ fetchImpl: fetchPublicFile });
|
||||||
|
console.info = realInfo;
|
||||||
|
|
||||||
function fakeReceiver(initial = {}) {
|
function fakeReceiver(initial = {}) {
|
||||||
const state = {
|
const state = {
|
||||||
@@ -529,12 +538,14 @@ test('the DISPLAY 3D toggle gives local aircraft magenta class models', async (t
|
|||||||
await layer.update();
|
await layer.update();
|
||||||
assert.equal(loads.length, 1);
|
assert.equal(loads.length, 1);
|
||||||
const [model] = loads;
|
const [model] = loads;
|
||||||
|
const spec = aircraftLayerModelSpec({ category: 'A7' });
|
||||||
assert.equal(
|
assert.equal(
|
||||||
model.url,
|
model.basePath,
|
||||||
'/models/bell206.glb',
|
'/models/fleet/helicopters/generic_heli.glb',
|
||||||
'the Flights helicopter model',
|
'the Flights rotorcraft model',
|
||||||
);
|
);
|
||||||
assert.equal(model.scale, 1);
|
assert.ok(model.gltf instanceof Uint8Array, 'built from the loaded set');
|
||||||
|
assert.equal(model.scale, spec.scale, 'drawn life size');
|
||||||
assert.equal(model.id, 'local-adsb:a0b702');
|
assert.equal(model.id, 'local-adsb:a0b702');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
Cesium.Color.equals(
|
Cesium.Color.equals(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { horizonOccluder } from '../../data/iconOrientation.js';
|
import { horizonOccluder } from '../../data/iconOrientation.js';
|
||||||
import { selectModelEligible } from '../../data/modelEligibility.js';
|
import { selectModelEligible } from '../../data/modelEligibility.js';
|
||||||
import { civilAircraftModelSpec } from '../flights/modelSpec.js';
|
import { aircraftLayerModelSpec } from '../flights/modelSpec.js';
|
||||||
|
import { createFleetModel } from '../../data/fleetModelInstances.js';
|
||||||
|
import { loadFleetModelSet } from '../../data/fleetModels.js';
|
||||||
import {
|
import {
|
||||||
FLEET_DR_INTERVAL_MS,
|
FLEET_DR_INTERVAL_MS,
|
||||||
MODEL_ALL_ADD_M,
|
MODEL_ALL_ADD_M,
|
||||||
@@ -16,19 +18,24 @@ import {
|
|||||||
} from '../flights/policy.js';
|
} from '../flights/policy.js';
|
||||||
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
|
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
|
||||||
|
|
||||||
const specKey = (spec) => `${spec.url}@${spec.scale}`;
|
const specOf = (marker) =>
|
||||||
|
aircraftLayerModelSpec({
|
||||||
|
typeCode: marker?.typeCode,
|
||||||
|
category: marker?.category,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3D models for Local ADS-B aircraft under the DISPLAY rail's 3D toggle.
|
* 3D models for Local ADS-B aircraft under the DISPLAY rail's 3D toggle.
|
||||||
*
|
*
|
||||||
* Mirrors the public Flights fleet: models only below the altitude ceiling,
|
* Mirrors the public Flights fleet: models only below the altitude ceiling,
|
||||||
* the same proximity/all caps and add/keep radii, the same visible-first
|
* the same proximity/all caps and add/keep radii, the same visible-first
|
||||||
* eligibility (`selectModelEligible`), the same per-class asset and scale
|
* eligibility (`selectModelEligible`), the same fleet model per type, life
|
||||||
* (`civilAircraftModelSpec`) and the same tint path (a MIX colour blend, here
|
* size (`aircraftLayerModelSpec`) and the same tint path (a MIX colour blend, here
|
||||||
* magenta, as Flights tints military amber). Each model owns its matrix. A
|
* magenta, as Flights tints military amber). Each model owns its matrix. A
|
||||||
* billboard stays the visual until its model is loaded AND placed; a grounded
|
* billboard stays the visual until its model is loaded AND placed; a grounded
|
||||||
* aircraft's model rides the one-shot ground snap plus the class belly
|
* aircraft's model rides the one-shot ground snap plus the class belly
|
||||||
* offset, and without a resolved ground it stays a billboard.
|
* offset, and without a resolved ground it stays a billboard. Every fleet
|
||||||
|
* model loads once, up front, when the layer starts (`loadFleetModelSet`).
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {object} options.viewer Cesium viewer.
|
* @param {object} options.viewer Cesium viewer.
|
||||||
* @param {Cesium.Color} options.color Model tint.
|
* @param {Cesium.Color} options.color Model tint.
|
||||||
@@ -44,6 +51,9 @@ export function createLocalAdsbModels({
|
|||||||
groundSnap = null,
|
groundSnap = null,
|
||||||
loadModel = (options) => Cesium.Model.fromGltfAsync(options),
|
loadModel = (options) => Cesium.Model.fromGltfAsync(options),
|
||||||
}) {
|
}) {
|
||||||
|
void loadFleetModelSet({ resolveAsset }).catch(() => {
|
||||||
|
/* aircraft stay billboards */
|
||||||
|
});
|
||||||
const collection = new Cesium.PrimitiveCollection();
|
const collection = new Cesium.PrimitiveCollection();
|
||||||
viewer.scene.primitives.add(collection);
|
viewer.scene.primitives.add(collection);
|
||||||
/** @type {Map<string, object>} id -> admitted model */
|
/** @type {Map<string, object>} id -> admitted model */
|
||||||
@@ -75,8 +85,9 @@ export function createLocalAdsbModels({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensure(id, marker, cap) {
|
async function ensure(id, marker, cap) {
|
||||||
const spec = civilAircraftModelSpec(marker.klass);
|
const spec = specOf(marker);
|
||||||
const key = specKey(spec);
|
if (!spec.model) return;
|
||||||
|
const key = spec.key;
|
||||||
const current = models.get(id);
|
const current = models.get(id);
|
||||||
if (current && current._gevSpecKey === key) return;
|
if (current && current._gevSpecKey === key) return;
|
||||||
if (current) release(id);
|
if (current) release(id);
|
||||||
@@ -85,17 +96,20 @@ export function createLocalAdsbModels({
|
|||||||
pending.set(id, loadGeneration);
|
pending.set(id, loadGeneration);
|
||||||
let model;
|
let model;
|
||||||
try {
|
try {
|
||||||
model = await loadModel({
|
model = await createFleetModel(
|
||||||
url: resolveAsset(spec.url),
|
spec,
|
||||||
asynchronous: false,
|
{
|
||||||
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
asynchronous: false,
|
||||||
minimumPixelSize: MODEL_MIN_PX,
|
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
||||||
scale: spec.scale,
|
minimumPixelSize: MODEL_MIN_PX,
|
||||||
color,
|
scale: spec.scale,
|
||||||
colorBlendMode: Cesium.ColorBlendMode.MIX,
|
color,
|
||||||
colorBlendAmount: spec.blendAmount,
|
colorBlendMode: Cesium.ColorBlendMode.MIX,
|
||||||
id,
|
colorBlendAmount: spec.blendAmount,
|
||||||
});
|
id,
|
||||||
|
},
|
||||||
|
{ resolveAsset, load: loadModel },
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
if (pending.get(id) === loadGeneration) pending.delete(id);
|
if (pending.get(id) === loadGeneration) pending.delete(id);
|
||||||
return;
|
return;
|
||||||
@@ -106,7 +120,7 @@ export function createLocalAdsbModels({
|
|||||||
generation !== loadGeneration ||
|
generation !== loadGeneration ||
|
||||||
!eligible.has(id) ||
|
!eligible.has(id) ||
|
||||||
models.has(id) ||
|
models.has(id) ||
|
||||||
specKey(civilAircraftModelSpec(marker.klass)) !== key;
|
specOf(marker).key !== key;
|
||||||
if (pending.get(id) === loadGeneration) pending.delete(id);
|
if (pending.get(id) === loadGeneration) pending.delete(id);
|
||||||
if (stale) {
|
if (stale) {
|
||||||
model.destroy?.();
|
model.destroy?.();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { JET_MODEL_URL } from './policy.js';
|
import { loadFleetModelSet } from '../../data/fleetModels.js';
|
||||||
|
|
||||||
export function createLifecycle({
|
export function createLifecycle({
|
||||||
flightState,
|
flightState,
|
||||||
@@ -47,29 +47,19 @@ export function createLifecycle({
|
|||||||
registerSpriteCollection('military', flightState._billboardCollection);
|
registerSpriteCollection('military', flightState._billboardCollection);
|
||||||
flightState._modelCollection = new Cesium.PrimitiveCollection();
|
flightState._modelCollection = new Cesium.PrimitiveCollection();
|
||||||
viewer.scene.primitives.add(flightState._modelCollection);
|
viewer.scene.primitives.add(flightState._modelCollection);
|
||||||
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
// Every fleet model loads once, up front, for every layer that draws
|
||||||
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
// them (fleetModels.js), so no model is fetched while the session runs.
|
||||||
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
// Captured against this epoch so a destroy/re-init mid-load doesn't flip
|
||||||
if (!flightState._preloadModel) {
|
// the flag for a torn-down lifecycle.
|
||||||
|
{
|
||||||
const epoch = flightState._modelEpoch;
|
const epoch = flightState._modelEpoch;
|
||||||
Cesium.Model.fromGltfAsync({
|
loadFleetModelSet({ resolveAsset })
|
||||||
url: resolveAsset(JET_MODEL_URL),
|
.then(() => {
|
||||||
asynchronous: false,
|
if (epoch === flightState._modelEpoch)
|
||||||
})
|
|
||||||
.then((m) => {
|
|
||||||
if (epoch === flightState._modelEpoch) {
|
|
||||||
flightState._preloadModel = m;
|
|
||||||
flightState._planeModelLoaded = true;
|
flightState._planeModelLoaded = true;
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
m.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
/* tracked plane just stays a billboard a beat longer */
|
/* aircraft stay billboards */
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
flightState._billboards = new Map();
|
flightState._billboards = new Map();
|
||||||
@@ -269,14 +259,6 @@ export function createLifecycle({
|
|||||||
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
||||||
flightState._modelPending.clear();
|
flightState._modelPending.clear();
|
||||||
flightState._modelGen.clear();
|
flightState._modelGen.clear();
|
||||||
if (flightState._preloadModel) {
|
|
||||||
try {
|
|
||||||
flightState._preloadModel.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
flightState._preloadModel = null;
|
|
||||||
}
|
|
||||||
flightState._planeModelLoaded = false;
|
flightState._planeModelLoaded = false;
|
||||||
flightState._billboards.clear();
|
flightState._billboards.clear();
|
||||||
flightState._cullPositions.clear();
|
flightState._cullPositions.clear();
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export function createMotion({
|
|||||||
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
|
const rangeM = Cesium.Cartesian3.distance(camera.positionWC, position);
|
||||||
if (parts.rendering._modelOwnsVisual(icao24)) {
|
if (parts.rendering._modelOwnsVisual(icao24)) {
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(icao24)?.klass,
|
flightState.records.data.get(icao24),
|
||||||
);
|
);
|
||||||
const scale = trackedModelScaleForPixelCap({
|
const scale = trackedModelScaleForPixelCap({
|
||||||
baseScale: spec.scale,
|
baseScale: spec.scale,
|
||||||
@@ -459,7 +459,7 @@ export function createMotion({
|
|||||||
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
) {
|
) {
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
return modelVisualAnchor(
|
return modelVisualAnchor(
|
||||||
flightState._trackedModel.modelMatrix,
|
flightState._trackedModel.modelMatrix,
|
||||||
@@ -502,7 +502,7 @@ export function createMotion({
|
|||||||
)
|
)
|
||||||
return 0;
|
return 0;
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
const scale = Number.isFinite(flightState._trackedModel.computedScale)
|
const scale = Number.isFinite(flightState._trackedModel.computedScale)
|
||||||
? flightState._trackedModel.computedScale
|
? flightState._trackedModel.computedScale
|
||||||
@@ -519,7 +519,7 @@ export function createMotion({
|
|||||||
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
) {
|
) {
|
||||||
const spec = parts.rendering._modelSpec(
|
const spec = parts.rendering._modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled
|
// Through the model's OWN render chain (modelVisualAnchor's hand-rolled
|
||||||
// half-correction put this offset on the lateral axis — see
|
// half-correction put this offset on the lateral axis — see
|
||||||
|
|||||||
@@ -58,20 +58,18 @@ export const EMERGENCY_ICON_COLOR =
|
|||||||
export const GROUND_SCALE = 0.8;
|
export const GROUND_SCALE = 0.8;
|
||||||
|
|
||||||
// --- 3D model rendering (mirrors flights.js) -----------------------------------------
|
// --- 3D model rendering (mirrors flights.js) -----------------------------------------
|
||||||
// When enabled, military aircraft render as 3D glTF jet models once the camera is below
|
// When enabled, military aircraft render as 3D glTF models once the camera is below
|
||||||
// MODEL_ALT_CEIL_M (zoomed in); higher up they stay flat billboards. Eligibility is
|
// MODEL_ALT_CEIL_M (zoomed in); higher up they stay flat billboards. Eligibility is
|
||||||
// FRUSTUM-based (on-screen), and the slots go to either the nearest planes ('proximity')
|
// FRUSTUM-based (on-screen), and the slots go to either the nearest planes ('proximity')
|
||||||
// or every in-view plane ('all'), each backed by a hard cap so a draw-call explosion
|
// or every in-view plane ('all'), each backed by a hard cap so a draw-call explosion
|
||||||
// can't tank the frame (no instancing yet). Distinct asset + amber tint set this layer
|
// can't tank the frame (no instancing yet). Each aircraft draws the fleet model of its
|
||||||
// apart from the commercial flights layer.
|
// type, as the flights layer does; the amber tint sets this layer apart.
|
||||||
|
|
||||||
export const JET_MODEL_URL = '/models/jet.glb';
|
|
||||||
|
|
||||||
export const MODEL_ALT_CEIL_M = 800000;
|
export const MODEL_ALT_CEIL_M = 800000;
|
||||||
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
|
// m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
|
||||||
|
|
||||||
export const MODEL_MIN_PX = 24;
|
export const MODEL_MIN_PX = 24;
|
||||||
// floor so distant models stay visible without ballooning into a min-pixel blob (mirror of flights.js, whose models now share this layer's ~30 m world size)
|
// floor so distant models stay visible without ballooning into a min-pixel blob (mirror of flights.js)
|
||||||
|
|
||||||
export const TRACKED_MODEL_MIN_PX = 40;
|
export const TRACKED_MODEL_MIN_PX = 40;
|
||||||
// keep the glTF silhouette comparable to the selected 2D glyph at handoff
|
// keep the glTF silhouette comparable to the selected 2D glyph at handoff
|
||||||
@@ -79,17 +77,6 @@ export const TRACKED_MODEL_MIN_PX = 40;
|
|||||||
export const TRACKED_MODEL_MAX_PX = 200;
|
export const TRACKED_MODEL_MAX_PX = 200;
|
||||||
// owner-selected close-range tracked-target feel
|
// owner-selected close-range tracked-target feel
|
||||||
|
|
||||||
export const MODEL_NATIVE_RADIUS_M = 29.83;
|
|
||||||
|
|
||||||
// jet.glb is transform-applied at real-world scale — native bounding radius
|
|
||||||
// 29.83 m at scale 1. ×1 → ~22–43 m aircraft across CLASS_SCALE_3D, matching
|
|
||||||
// flights.js world sizes. At the old copied ×24, models rendered
|
|
||||||
// 600–1000 m across (invisible at the ~6 km follow range where minimumPixelSize
|
|
||||||
// dominates, but zoom under ~1 km put the camera INSIDE the plane). Locked by
|
|
||||||
// modelScale.test.mjs.
|
|
||||||
|
|
||||||
export const MODEL_SCALE = 1;
|
|
||||||
|
|
||||||
// Per-mode caps + radii — mirror of flights.js. Modes differ by RADIUS (not just cap) so Proximity
|
// Per-mode caps + radii — mirror of flights.js. Modes differ by RADIUS (not just cap) so Proximity
|
||||||
// and All aren't identical when few planes are in range; on-screen planes win the cap (see flights.js).
|
// and All aren't identical when few planes are in range; on-screen planes win the cap (see flights.js).
|
||||||
|
|
||||||
@@ -119,33 +106,12 @@ export const MODEL_ALL_KEEP_M = 450000;
|
|||||||
// all: KEEP modeled planes out to 450 km
|
// all: KEEP modeled planes out to 450 km
|
||||||
|
|
||||||
export const MODEL_HEADING_OFFSET_DEG = 180;
|
export const MODEL_HEADING_OFFSET_DEG = 180;
|
||||||
// every aircraft GLB is exported nose -X in the shared transform-applied convention
|
// every fleet model is nose -X: opposite Cesium heading 0
|
||||||
// Preserve the layer's amber identity while reducing approved texture/livery
|
// Preserve the layer's amber identity while reducing the models' own colours
|
||||||
// contribution to a weak diffuse hint, matching civilian launch presentation.
|
// to a weak diffuse hint, matching civilian launch presentation.
|
||||||
|
|
||||||
export const MODEL_COLOR_BLEND_AMOUNT = 0.94;
|
export const MODEL_COLOR_BLEND_AMOUNT = 0.94;
|
||||||
|
|
||||||
// airplane.glb (the shared 747) constants for this layer's heavy classes. The
|
|
||||||
// asset has its former 24× runtime calibration baked into transform-applied
|
|
||||||
// meter-scale geometry; these values mirror flights.js and are regression-pinned.
|
|
||||||
|
|
||||||
export const PLANE_MODEL_URL = '/models/airplane.glb';
|
|
||||||
|
|
||||||
export const PLANE_MODEL_SCALE = 1;
|
|
||||||
|
|
||||||
export const PLANE_NATIVE_RADIUS_M = 34.41;
|
|
||||||
|
|
||||||
export const PLANE_BELLY_OFFSET_NATIVE = 6.719;
|
|
||||||
|
|
||||||
// Grounded-model belly offset: jet.glb's centred origin sits 5.631 native units (= meters — this
|
|
||||||
// asset is real-world scale) ABOVE its lowest vertex (glTF Y-up scene AABB with node
|
|
||||||
// transforms applied — same reader as modelScale.test.mjs, measured 2026-07-03).
|
|
||||||
// × MODEL_SCALE(1) × class multiplier ≈ 4.4–8.6 m of lift, so a ground-snapped model
|
|
||||||
// rests its lowest geometry (gear/belly) ON the sampled tile skin instead of sinking to
|
|
||||||
// the fuselage-centerline origin. Locked against the GLB by modelScale.test.mjs.
|
|
||||||
|
|
||||||
export const MODEL_BELLY_OFFSET_NATIVE = 5.631;
|
|
||||||
|
|
||||||
/** Amber fade target for the tracked billboard once the model takes over (mirrors flights.js CYAN_TRANSPARENT). */
|
/** Amber fade target for the tracked billboard once the model takes over (mirrors flights.js CYAN_TRANSPARENT). */
|
||||||
|
|
||||||
export const AMBER_TRANSPARENT = MIL_ICON_COLOR.withAlpha(0);
|
export const AMBER_TRANSPARENT = MIL_ICON_COLOR.withAlpha(0);
|
||||||
|
|||||||
@@ -484,9 +484,7 @@ export function createQueries({
|
|||||||
// contacts read the translation the fleet tick already wrote into their
|
// contacts read the translation the fleet tick already wrote into their
|
||||||
// modelMatrix; sprite-owned contacts keep `bb.position`, where sprite and bracket
|
// modelMatrix; sprite-owned contacts keep `bb.position`, where sprite and bracket
|
||||||
// are co-located anyway.
|
// are co-located anyway.
|
||||||
const spec = modelOwnsVisual
|
const spec = modelOwnsVisual ? parts.rendering._modelSpec(info) : null;
|
||||||
? parts.rendering._modelSpec(info?.klass)
|
|
||||||
: null;
|
|
||||||
const pos = isTracked
|
const pos = isTracked
|
||||||
? parts.motion._trackedVisualCached() || bb.position
|
? parts.motion._trackedVisualCached() || bb.position
|
||||||
: modelOwnsVisual
|
: modelOwnsVisual
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ export class MilitaryFlightRecords {
|
|||||||
newestFixMs: this._noteFix(icao24, prevMeta, fixEpochMs),
|
newestFixMs: this._noteFix(icao24, prevMeta, fixEpochMs),
|
||||||
callsign: stickyText(callsign, prevMeta?.callsign),
|
callsign: stickyText(callsign, prevMeta?.callsign),
|
||||||
type: stickyType,
|
type: stickyType,
|
||||||
|
// The emitter category, kept for the fleet model a type-less row draws.
|
||||||
|
category: aircraft?.category ?? prevMeta?.category ?? null,
|
||||||
// Type outranks category automatically inside classifyAircraft.
|
// Type outranks category automatically inside classifyAircraft.
|
||||||
klass: classifyAircraft({
|
klass: classifyAircraft({
|
||||||
typeCode: stickyType,
|
typeCode: stickyType,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
|
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
||||||
import {
|
import {
|
||||||
CLASS_MODEL_REAL,
|
aircraftLayerModelSpec,
|
||||||
CLASS_SCALE_3D,
|
aircraftModelSpecCacheKey,
|
||||||
CLASS_SCALE_2D,
|
} from '../flights/modelSpec.js';
|
||||||
} from '../../data/aircraftClass.js';
|
import { createFleetModel } from '../../data/fleetModelInstances.js';
|
||||||
import {
|
|
||||||
visualCenterForModel,
|
|
||||||
trailAnchorForModel,
|
|
||||||
} from '../../data/modelVisualAnchor.js';
|
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { selectModelEligible } from '../../data/modelEligibility.js';
|
import { selectModelEligible } from '../../data/modelEligibility.js';
|
||||||
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
|
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
|
||||||
@@ -20,14 +17,6 @@ import {
|
|||||||
import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js';
|
import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js';
|
||||||
import { isEmergency } from '../../data/aircraftEmergency.js';
|
import { isEmergency } from '../../data/aircraftEmergency.js';
|
||||||
import {
|
import {
|
||||||
PLANE_MODEL_SCALE,
|
|
||||||
PLANE_MODEL_URL,
|
|
||||||
PLANE_NATIVE_RADIUS_M,
|
|
||||||
PLANE_BELLY_OFFSET_NATIVE,
|
|
||||||
MODEL_SCALE,
|
|
||||||
JET_MODEL_URL,
|
|
||||||
MODEL_NATIVE_RADIUS_M,
|
|
||||||
MODEL_BELLY_OFFSET_NATIVE,
|
|
||||||
MODEL_HEADING_OFFSET_DEG,
|
MODEL_HEADING_OFFSET_DEG,
|
||||||
BILLBOARD_SCALE,
|
BILLBOARD_SCALE,
|
||||||
GROUND_SCALE,
|
GROUND_SCALE,
|
||||||
@@ -83,48 +72,20 @@ export function createRendering({
|
|||||||
return Number.POSITIVE_INFINITY;
|
return Number.POSITIVE_INFINITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _modelSpec(klass) {
|
/** The fleet model an aircraft draws with, by its type and emitter category
|
||||||
let spec = flightState._specCache.get(klass);
|
* (shared with flights.js), in this layer's own tint. */
|
||||||
|
function _modelSpec(meta) {
|
||||||
|
const key = aircraftModelSpecCacheKey(meta);
|
||||||
|
let spec = flightState._specCache.get(key);
|
||||||
if (spec) return spec;
|
if (spec) return spec;
|
||||||
const real = CLASS_MODEL_REAL[klass];
|
const base = aircraftLayerModelSpec(meta);
|
||||||
if (real) {
|
spec = Object.freeze({
|
||||||
spec = {
|
...base,
|
||||||
url: real.url,
|
blendAmount: MODEL_COLOR_BLEND_AMOUNT,
|
||||||
scale: 1,
|
headingOffsetDeg: MODEL_HEADING_OFFSET_DEG,
|
||||||
nativeRadiusM: real.radiusM,
|
});
|
||||||
bellyM: real.bellyM,
|
// Not cached before the model set loads: the answer changes once it has.
|
||||||
headingOffsetDeg: 180,
|
if (spec.model) flightState._specCache.set(key, spec);
|
||||||
visualCenterNative: visualCenterForModel(real.url),
|
|
||||||
trailAnchorNative: trailAnchorForModel(real.url),
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
klass === 'airliner' ||
|
|
||||||
klass === 'quadjet' ||
|
|
||||||
klass === 'glider'
|
|
||||||
) {
|
|
||||||
const scale = PLANE_MODEL_SCALE * (CLASS_SCALE_3D[klass] || 1);
|
|
||||||
spec = {
|
|
||||||
url: PLANE_MODEL_URL,
|
|
||||||
scale,
|
|
||||||
nativeRadiusM: PLANE_NATIVE_RADIUS_M,
|
|
||||||
bellyM: PLANE_BELLY_OFFSET_NATIVE * scale,
|
|
||||||
headingOffsetDeg: 180,
|
|
||||||
visualCenterNative: visualCenterForModel(PLANE_MODEL_URL),
|
|
||||||
trailAnchorNative: trailAnchorForModel(PLANE_MODEL_URL),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const scale = MODEL_SCALE * (CLASS_SCALE_3D[klass] || 1);
|
|
||||||
spec = {
|
|
||||||
url: JET_MODEL_URL,
|
|
||||||
scale,
|
|
||||||
nativeRadiusM: MODEL_NATIVE_RADIUS_M,
|
|
||||||
bellyM: MODEL_BELLY_OFFSET_NATIVE * scale,
|
|
||||||
headingOffsetDeg: MODEL_HEADING_OFFSET_DEG,
|
|
||||||
visualCenterNative: visualCenterForModel(JET_MODEL_URL),
|
|
||||||
trailAnchorNative: trailAnchorForModel(JET_MODEL_URL),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
flightState._specCache.set(klass, spec);
|
|
||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +273,7 @@ export function createRendering({
|
|||||||
Cesium.Ellipsoid.WGS84,
|
Cesium.Ellipsoid.WGS84,
|
||||||
flightState._scratchGroundCarto,
|
flightState._scratchGroundCarto,
|
||||||
);
|
);
|
||||||
carto.height = h + _modelSpec(meta.klass).bellyM;
|
carto.height = h + _modelSpec(meta).bellyM;
|
||||||
return Cesium.Cartesian3.fromRadians(
|
return Cesium.Cartesian3.fromRadians(
|
||||||
carto.longitude,
|
carto.longitude,
|
||||||
carto.latitude,
|
carto.latitude,
|
||||||
@@ -350,7 +311,7 @@ export function createRendering({
|
|||||||
displayPos,
|
displayPos,
|
||||||
course,
|
course,
|
||||||
model.modelMatrix,
|
model.modelMatrix,
|
||||||
_modelSpec(flightState.records.data.get(icao24)?.klass).headingOffsetDeg,
|
_modelSpec(flightState.records.data.get(icao24)).headingOffsetDeg,
|
||||||
);
|
);
|
||||||
if (!model.ready) {
|
if (!model.ready) {
|
||||||
model.show = false; // not loaded yet → keep the 2D icon, no half-model flash
|
model.show = false; // not loaded yet → keep the 2D icon, no half-model flash
|
||||||
@@ -385,20 +346,17 @@ export function createRendering({
|
|||||||
return !!model && model.ready === true && model.show === true;
|
return !!model && model.ready === true && model.show === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Spec identity for a LOADED model — mirror of flights.js: URL and scale
|
/** Spec identity for a LOADED model — mirror of flights.js: the model and
|
||||||
* together (same-URL classes differ by scale). */
|
* its scale together. */
|
||||||
|
|
||||||
const _specKeyFor = (klass) => {
|
const _specKeyFor = (meta) => _modelSpec(meta).key;
|
||||||
const spec = _modelSpec(klass);
|
|
||||||
return `${spec.url}@${spec.scale}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Class-change model sync — mirror of flights.js: drop a live model or
|
/** Type-change model sync — mirror of flights.js: drop a live model or
|
||||||
* in-flight load whose spec no longer matches the class, re-showing the
|
* in-flight load whose spec no longer matches the type, re-showing the
|
||||||
* fleet billboard FIRST (gap-proof) and covering the tracked standalone. */
|
* fleet billboard FIRST (gap-proof) and covering the tracked standalone. */
|
||||||
|
|
||||||
function _syncModelToClass(icao24) {
|
function _syncModelToClass(icao24) {
|
||||||
const key = _specKeyFor(flightState.records.data.get(icao24)?.klass);
|
const key = _specKeyFor(flightState.records.data.get(icao24));
|
||||||
const current = flightState._models.get(icao24);
|
const current = flightState._models.get(icao24);
|
||||||
if (
|
if (
|
||||||
(current && current._gevSpecKey !== key) ||
|
(current && current._gevSpecKey !== key) ||
|
||||||
@@ -445,6 +403,11 @@ export function createRendering({
|
|||||||
|
|
||||||
/** Lazily create the glTF model for an aircraft (fire-and-forget; billboard shows until ready). */
|
/** Lazily create the glTF model for an aircraft (fire-and-forget; billboard shows until ready). */
|
||||||
|
|
||||||
|
/** A Cesium Model of `spec` from the fleet set loaded up front (no fetch mid-session). */
|
||||||
|
function _createModel(spec, options) {
|
||||||
|
return createFleetModel(spec, options, { resolveAsset });
|
||||||
|
}
|
||||||
|
|
||||||
async function _ensureModel(icao24) {
|
async function _ensureModel(icao24) {
|
||||||
// Never model the TRACKED aircraft — it owns a separate entity billboard, and the fleet
|
// Never model the TRACKED aircraft — it owns a separate entity billboard, and the fleet
|
||||||
// tick skips it, so a model here would be orphaned + double-rendered.
|
// tick skips it, so a model here would be orphaned + double-rendered.
|
||||||
@@ -461,6 +424,8 @@ export function createRendering({
|
|||||||
_modelCap()
|
_modelCap()
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
// Until the fleet model set has loaded the aircraft stays a billboard.
|
||||||
|
if (!_modelSpec(flightState.records.data.get(icao24)).model) return;
|
||||||
const epoch = flightState._modelEpoch; // lifecycle token: if destroy() bumps it, this load is dead
|
const epoch = flightState._modelEpoch; // lifecycle token: if destroy() bumps it, this load is dead
|
||||||
const gen = flightState._modelGen.get(icao24) || 0; // capture; if it changes during the load, we're stale
|
const gen = flightState._modelGen.get(icao24) || 0; // capture; if it changes during the load, we're stale
|
||||||
flightState._modelPending.add(icao24);
|
flightState._modelPending.add(icao24);
|
||||||
@@ -468,12 +433,11 @@ export function createRendering({
|
|||||||
// Spec identity captured at load START (mirror of flights.js): a mid-load
|
// Spec identity captured at load START (mirror of flights.js): a mid-load
|
||||||
// reclassification makes the post-await admission reject the stale asset.
|
// reclassification makes the post-await admission reject the stale asset.
|
||||||
// Boost state likewise — creation options bake it in.
|
// Boost state likewise — creation options bake it in.
|
||||||
const specKey = _specKeyFor(flightState.records.data.get(icao24)?.klass);
|
const specKey = _specKeyFor(flightState.records.data.get(icao24));
|
||||||
const loadIrBoost = flightState._irBoost;
|
const loadIrBoost = flightState._irBoost;
|
||||||
try {
|
try {
|
||||||
const spec = _modelSpec(flightState.records.data.get(icao24)?.klass);
|
const spec = _modelSpec(flightState.records.data.get(icao24));
|
||||||
model = await Cesium.Model.fromGltfAsync({
|
model = await _createModel(spec, {
|
||||||
url: resolveAsset(spec.url),
|
|
||||||
asynchronous: false,
|
asynchronous: false,
|
||||||
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
||||||
minimumPixelSize: MODEL_MIN_PX,
|
minimumPixelSize: MODEL_MIN_PX,
|
||||||
@@ -522,7 +486,7 @@ export function createRendering({
|
|||||||
flightState._models.has(icao24) ||
|
flightState._models.has(icao24) ||
|
||||||
flightState._models.size >= _modelCap() ||
|
flightState._models.size >= _modelCap() ||
|
||||||
// Class reclassified mid-load → this GLB/scale is for the OLD class.
|
// Class reclassified mid-load → this GLB/scale is for the OLD class.
|
||||||
_specKeyFor(flightState.records.data.get(icao24)?.klass) !== specKey ||
|
_specKeyFor(flightState.records.data.get(icao24)) !== specKey ||
|
||||||
// IR boost flipped mid-load → this model baked the wrong shader/tint.
|
// IR boost flipped mid-load → this model baked the wrong shader/tint.
|
||||||
flightState._irBoost !== loadIrBoost;
|
flightState._irBoost !== loadIrBoost;
|
||||||
if (stale) {
|
if (stale) {
|
||||||
@@ -676,6 +640,9 @@ export function createRendering({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
// Until the fleet model set has loaded the billboard carries it.
|
||||||
|
_modelSpec(flightState.records.data.get(flightState._trackedIcao))
|
||||||
|
.model &&
|
||||||
!flightState._trackedModel &&
|
!flightState._trackedModel &&
|
||||||
!flightState._trackedModelLoading &&
|
!flightState._trackedModelLoading &&
|
||||||
parts.tracking._trackedModelLoadAllowed()
|
parts.tracking._trackedModelLoadAllowed()
|
||||||
@@ -683,14 +650,13 @@ export function createRendering({
|
|||||||
flightState._trackedModelLoading = true;
|
flightState._trackedModelLoading = true;
|
||||||
const gen = flightState._trackedModelGen;
|
const gen = flightState._trackedModelGen;
|
||||||
const trackedSpec = _modelSpec(
|
const trackedSpec = _modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
const trackedKey = _specKeyFor(
|
const trackedKey = _specKeyFor(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
const trackedIrBoost = flightState._irBoost;
|
const trackedIrBoost = flightState._irBoost;
|
||||||
Cesium.Model.fromGltfAsync({
|
_createModel(trackedSpec, {
|
||||||
url: resolveAsset(trackedSpec.url),
|
|
||||||
asynchronous: false,
|
asynchronous: false,
|
||||||
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
environmentMapOptions: AIRCRAFT_ENVIRONMENT_MAP_OPTIONS,
|
||||||
minimumPixelSize: TRACKED_MODEL_MIN_PX,
|
minimumPixelSize: TRACKED_MODEL_MIN_PX,
|
||||||
@@ -724,7 +690,7 @@ export function createRendering({
|
|||||||
// _trackedModel was still null): drop the stale asset; driver reloads.
|
// _trackedModel was still null): drop the stale asset; driver reloads.
|
||||||
if (
|
if (
|
||||||
_specKeyFor(
|
_specKeyFor(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
) !== trackedKey ||
|
) !== trackedKey ||
|
||||||
flightState._irBoost !== trackedIrBoost
|
flightState._irBoost !== trackedIrBoost
|
||||||
) {
|
) {
|
||||||
@@ -793,7 +759,7 @@ export function createRendering({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const spec = _modelSpec(
|
const spec = _modelSpec(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao),
|
||||||
);
|
);
|
||||||
_modelMatrix(
|
_modelMatrix(
|
||||||
displayPos,
|
displayPos,
|
||||||
|
|||||||
@@ -164,10 +164,15 @@ export function createMilitarySnapshotRenderer({
|
|||||||
) {
|
) {
|
||||||
rendering._applyFleetBillboardPresentation(icao24, bb);
|
rendering._applyFleetBillboardPresentation(icao24, bb);
|
||||||
}
|
}
|
||||||
// Per-class GLBs: a class change can mean a different asset OR scale —
|
// Per-type fleet models: a type or category change can mean a
|
||||||
// resync the live model, any in-flight load, and the tracked
|
// different model OR scale — resync the live model, any in-flight
|
||||||
// standalone (mirror of flights.js).
|
// load, and the tracked standalone (mirror of flights.js).
|
||||||
if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24);
|
if (
|
||||||
|
prevMeta?.klass !== meta.klass ||
|
||||||
|
prevMeta?.type !== meta.type ||
|
||||||
|
prevMeta?.category !== meta.category
|
||||||
|
)
|
||||||
|
rendering._syncModelToClass(icao24);
|
||||||
// Round 5: depth policy is uniform (always depth-test-free, see
|
// Round 5: depth policy is uniform (always depth-test-free, see
|
||||||
// _groundDepthDistance) — nothing to flip on landing/takeoff.
|
// _groundDepthDistance) — nothing to flip on landing/takeoff.
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,15 +19,13 @@ export function createFlightState({ source, services }) {
|
|||||||
* layer); see followsClock and displayDelayMs in policy.js. */
|
* layer); see followsClock and displayDelayMs in policy.js. */
|
||||||
flightState.timeline = null;
|
flightState.timeline = null;
|
||||||
|
|
||||||
/** Per-class model spec for THIS layer (2026-08-16, owner playtest ask:
|
/** Model spec per aircraft type and emitter category (aircraftLayerModelSpec in
|
||||||
* military contacts should read as their WEIGHT CLASS, always in this layer's
|
* flights/modelSpec.js): the procedural fleet model that draws the type, life size,
|
||||||
* amber). Real Hangar GLBs serve the classes they cover (meters, nose −X →
|
* with its measured clearance, bounding radius and trail anchor. nativeRadiusM is PER
|
||||||
* 180° offset); airliner/quadjet/glider get the shared 747 silhouette
|
* SCALE UNIT — pixel-cap math multiplies it by `scale`. The code-side MIX tint dominates
|
||||||
* (airplane.glb — C-5M/RC-135-style heavies stop rendering as bizjets);
|
* every model so silhouettes stay light (and in this layer's own colour). Memoized: the
|
||||||
* fastjet and unknown keep jet.glb with the same 180° offset. The MIX tint stays
|
* detection weld asks for one per MODELED contact per frame on top of the 12 Hz fleet
|
||||||
* dominant everywhere — military is amber, tracked is TRACKED_ICON_COLOR,
|
* pass. */
|
||||||
* and the tint must dominate any livery (owner: "stays yellow"). Specs are
|
|
||||||
* static per class — memoized (the fleet pass asks at 12 Hz per model). */
|
|
||||||
|
|
||||||
flightState._specCache = new Map();
|
flightState._specCache = new Map();
|
||||||
|
|
||||||
@@ -76,16 +74,11 @@ export function createFlightState({ source, services }) {
|
|||||||
// throttle the "more planes in view than the cap" console notice
|
// throttle the "more planes in view than the cap" console notice
|
||||||
// The tracked entity's billboard goes transparent (rather than hidden) once the model takes
|
// The tracked entity's billboard goes transparent (rather than hidden) once the model takes
|
||||||
// over, so it keeps supplying a bounding sphere for follow-camera framing. We only drop its
|
// over, so it keeps supplying a bounding sphere for follow-camera framing. We only drop its
|
||||||
// alpha after the GLB is preloaded so the model is ready to render the instant the billboard
|
// alpha after the fleet model set is loaded so the model is ready to render the instant the
|
||||||
// fades — no gap, no double-image. Preloaded once at init; the instance is retained (not
|
// billboard fades — no gap, no double-image. The set loads once at init (fleetModels.js).
|
||||||
// destroyed) purely to keep Cesium's glTF cache warm for fast tracked-model instantiation.
|
|
||||||
|
|
||||||
flightState._planeModelLoaded = false;
|
flightState._planeModelLoaded = false;
|
||||||
|
|
||||||
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
|
|
||||||
|
|
||||||
flightState._preloadModel = null;
|
|
||||||
|
|
||||||
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
||||||
|
|
||||||
flightState._scratchModelMtx = new Cesium.Matrix4();
|
flightState._scratchModelMtx = new Cesium.Matrix4();
|
||||||
|
|||||||
Reference in new issue
Block a user