Preserve military aircraft and classification across renderer handoffs

This commit is contained in:
German Khokhlov committed 2026-11-03 02:47:32 +04:00
1 parent 1bda760763
commit e08ddd787b
15 files changed
+363 -305

No files matched your search

+4
View File
@@ -1353,6 +1353,7 @@
"src/layers/flights/index.js", "src/layers/flights/index.js",
"src/layers/flights/ingestion.js", "src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js", "src/layers/flights/lifecycle.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/flights/motion.js", "src/layers/flights/motion.js",
"src/layers/flights/policy.js", "src/layers/flights/policy.js",
"src/layers/flights/queries.js", "src/layers/flights/queries.js",
@@ -1433,6 +1434,7 @@
"src/layers/military/controller.js", "src/layers/military/controller.js",
"src/layers/military/index.js", "src/layers/military/index.js",
"src/layers/military/ingestion.js", "src/layers/military/ingestion.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/military/lifecycle.js", "src/layers/military/lifecycle.js",
"src/layers/military/motion.js", "src/layers/military/motion.js",
"src/layers/military/policy.js", "src/layers/military/policy.js",
@@ -2868,6 +2870,7 @@
"src/layers/flights/index.js", "src/layers/flights/index.js",
"src/layers/flights/ingestion.js", "src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js", "src/layers/flights/lifecycle.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/flights/motion.js", "src/layers/flights/motion.js",
"src/layers/flights/policy.js", "src/layers/flights/policy.js",
"src/layers/flights/queries.js", "src/layers/flights/queries.js",
@@ -3927,6 +3930,7 @@
"src/layers/flights/index.js", "src/layers/flights/index.js",
"src/layers/flights/ingestion.js", "src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js", "src/layers/flights/lifecycle.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/flights/motion.js", "src/layers/flights/motion.js",
"src/layers/flights/policy.js", "src/layers/flights/policy.js",
"src/layers/flights/queries.js", "src/layers/flights/queries.js",
+38 -18
View File
@@ -10,9 +10,13 @@ if (
) )
) )
throw new Error('Supply an unreserved local keyless preview URL'); throw new Error('Supply an unreserved local keyless preview URL');
const military = process.argv.includes('--military');
const layerId = military ? 'military' : 'flights';
const aircraftId = military ? 'aaa052' : '71be26';
url.search = '?engine=navara&welcome=0'; url.search = '?engine=navara&welcome=0';
url.hash = url.hash =
'v=2&lat=40.2539&lon=-125.5779&alt=21130&heading=360&pitch=-35&map=sentinel2&l=f&lo=f.e.1_f.t.71be26'; 'v=2&lat=40.2539&lon=-125.5779&alt=21130&heading=360&pitch=-35&map=sentinel2' +
(military ? '&l=m&lo=f.e.1_f.u.aaa052' : '&l=f&lo=f.e.1_f.t.71be26');
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
headless: true, headless: true,
args: ['--use-angle=metal', '--enable-gpu'], args: ['--use-angle=metal', '--enable-gpu'],
@@ -23,14 +27,24 @@ try {
const errors = []; const errors = [];
page.on('pageerror', (e) => errors.push(e.message)); page.on('pageerror', (e) => errors.push(e.message));
await page.setViewport({ width: 1280, height: 720 }); await page.setViewport({ width: 1280, height: 720 });
await page.evaluateOnNewDocument(() => { await page.evaluateOnNewDocument((layerId, aircraftId) => {
window.__qaAircraft = {layerId, aircraftId};
const fetch = window.fetch.bind(window); const fetch = window.fetch.bind(window);
window.__flightPolls = 0; window.__flightPolls = 0;
const interval = window.setInterval.bind(window);
window.setInterval = function (callback, delay, ...args) {
if (typeof callback !== 'function') return interval(callback, delay, ...args);
const id = interval(function (...values) {
if (window.__handoff?.timer === id) window.__handoff.ticks++;
return callback.apply(this, values);
}, delay, ...args);
return id;
};
window.fetch = (input, options) => { window.fetch = (input, options) => {
const path = new URL(input.url ?? input, document.baseURI).pathname; const path = new URL(input.url ?? input, document.baseURI).pathname;
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
if (path === '/api/flights') { if (path === '/api/flights') {
window.__flightPolls++; if (layerId === 'flights') window.__flightPolls++;
return Promise.resolve( return Promise.resolve(
Response.json({ Response.json({
time: now, time: now,
@@ -58,15 +72,21 @@ try {
}), }),
); );
} }
if (path === '/api/military') if (path === '/api/military') {
return Promise.resolve(Response.json({ now: Date.now(), ac: [] })); if (layerId === 'military') window.__flightPolls++;
return Promise.resolve(Response.json({now: Date.now(), ac: [{
hex: 'aaa052', flight: 'GEVMIL', lon: -125.5779, lat: 40.2539,
alt_geom: 31825, alt_baro: 31825, gs: 389, track: 45,
seen: 0, seen_pos: 0, dbFlags: 1, type: 'adsb_icao',
}]}));
}
if (path === '/api/flights/track') if (path === '/api/flights/track')
return Promise.resolve(Response.json({ path: [] })); return Promise.resolve(Response.json({ path: [] }));
if (path.startsWith('/api/flights/') || path.startsWith('/api/openai/')) if (path.startsWith('/api/flights/') || path.startsWith('/api/openai/'))
return Promise.resolve(Response.json({})); return Promise.resolve(Response.json({}));
return fetch(input, options); return fetch(input, options);
}; };
}); }, layerId, aircraftId);
await page.goto(url.href, { waitUntil: 'domcontentloaded' }); await page.goto(url.href, { waitUntil: 'domcontentloaded' });
const ready = (kind) => const ready = (kind) =>
page.waitForFunction( page.waitForFunction(
@@ -92,16 +112,16 @@ try {
if (window.__GOOGLE_MAPS_API_KEY__ || tileset) if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required'); throw Error('Keyless preview required');
styleManager.cockpitView.setVisionMode('thermal'); styleManager.cockpitView.setVisionMode('thermal');
const entry = dataManager.layers.get('flights'); const entry = dataManager.layers.get(window.__qaAircraft.layerId);
window.__handoff = { window.__handoff = {
dataManager, dataManager,
entry, entry,
row: document.querySelector('[data-layer-id="flights"] .data-toggle-btn'), row: document.querySelector(`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`),
timer: entry.intervalId, timer: entry.intervalId,
ticks: 0,
model: entry.module, model: entry.module,
pollCount: window.__flightPolls, pollCount: window.__flightPolls,
timestamp: entry.module.getStats().lastUpdate, timestamp: entry.module.getStats().lastUpdate,
started: Date.now(),
}; };
}); });
// Refuse the first Cesium canvas. The application must roll back with the // Refuse the first Cesium canvas. The application must roll back with the
@@ -127,9 +147,9 @@ try {
const {dataManager, styleManager} = window.__godsEyeView; const {dataManager, styleManager} = window.__godsEyeView;
return new URL(location.href).searchParams.get('engine') === 'navara' && return new URL(location.href).searchParams.get('engine') === 'navara' &&
dataManager === window.__handoff.dataManager && dataManager === window.__handoff.dataManager &&
dataManager.layers.get('flights') === window.__handoff.entry && dataManager.layers.get(window.__qaAircraft.layerId) === window.__handoff.entry &&
styleManager.getCockpitState().active && styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id === '71be26'; styleManager.getAircraftTrackingTarget()?.id === window.__qaAircraft.aircraftId;
}), 'a failed scene must leave the live session usable'); }), 'a failed scene must leave the live session usable');
console.log('PASS failed renderer rolled back to the same flight and cockpit'); console.log('PASS failed renderer rolled back to the same flight and cockpit');
@@ -139,7 +159,7 @@ try {
const result = await page.evaluate(() => { const result = await page.evaluate(() => {
const { dataManager, engine, styleManager } = window.__godsEyeView; const { dataManager, engine, styleManager } = window.__godsEyeView;
const before = window.__handoff; const before = window.__handoff;
const entry = dataManager.layers.get('flights'); const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const cockpit = styleManager.getCockpitState(); const cockpit = styleManager.getCockpitState();
return { return {
sameOwner: sameOwner:
@@ -147,18 +167,18 @@ try {
before.entry === entry && before.entry === entry &&
before.model === entry.module, before.model === entry.module,
sameRow: Boolean(before.row) && sameRow: Boolean(before.row) &&
before.row === document.querySelector('[data-layer-id="flights"] .data-toggle-btn'), before.row === document.querySelector(`[data-layer-id="${window.__qaAircraft.layerId}"] .data-toggle-btn`),
sameTimer: sameTimer:
before.timer === entry.intervalId && entry.intervalId != null, before.timer === entry.intervalId && entry.intervalId != null,
sourcePolls: window.__flightPolls - before.pollCount, sourcePolls: window.__flightPolls - before.pollCount,
elapsed: Date.now() - before.started, timerTicks: before.ticks,
stamp: entry.module.getStats().lastUpdate, stamp: entry.module.getStats().lastUpdate,
initialStamp: before.timestamp, initialStamp: before.timestamp,
id: cockpit.subject?.id, id: cockpit.subject?.id,
active: cockpit.active, active: cockpit.active,
mode: cockpit.visionMode, mode: cockpit.visionMode,
hud: !document.querySelector('#cockpit-hud').hidden, hud: !document.querySelector('#cockpit-hud').hidden,
drawn: entry.module.qaDrawn().some((row) => row.id === '71be26'), drawn: entry.module.qaDrawn().some((row) => row.id === window.__qaAircraft.aircraftId),
height: engine.camera.height(), height: engine.camera.height(),
}; };
}); });
@@ -171,10 +191,10 @@ try {
result.drawn, result.drawn,
JSON.stringify(result), JSON.stringify(result),
); );
assert.equal(result.id, '71be26'); assert.equal(result.id, aircraftId);
assert.equal(result.mode, 'thermal'); assert.equal(result.mode, 'thermal');
if (result.elapsed < 30000) { assert.ok(result.sourcePolls <= result.timerTicks, 'switch must not add a source request');
assert.equal(result.sourcePolls, 0); if (result.timerTicks === 0) {
assert.equal(result.stamp, result.initialStamp); assert.equal(result.stamp, result.initialStamp);
} }
await page.waitForFunction( await page.waitForFunction(
+2 -2
View File
@@ -124,12 +124,12 @@ export function createApplicationCatalog({
), ),
); );
const military = once(() => const military = once(() =>
createApplicationMilitary({ sessionModel('military', () => createApplicationMilitary({
surface, surface,
source: sources.military, source: sources.military,
militaryRegistry, militaryRegistry,
resolveAsset, resolveAsset,
}), })),
); );
const vessels = once(() => const vessels = once(() =>
createApplicationVessels({ createApplicationVessels({
+2 -1
View File
@@ -482,7 +482,8 @@ test('GERMAN: an existing untracked military billboard repaints each poll withou
floorAltitudeM: (value) => value, floorAltitudeM: (value) => value,
}); });
flightState.records = records; flightState.records = records;
const apply = createMilitarySnapshotRenderer({ flightState._renderer = { view: {}, surfaceExclusions: () => [] };
const {applySnapshot: apply} = createMilitarySnapshotRenderer({
flightState, flightState,
records, records,
groundFloor: { warmGroundFloor() {}, cachedGroundFloor: () => null }, groundFloor: { warmGroundFloor() {}, cachedGroundFloor: () => null },
+2 -3
View File
@@ -1658,12 +1658,11 @@ test('hasContact declines while a layer is disabled, whatever its maps still hol
// disable() hides the collection but keeps the records, so a map lookup // disable() hides the collection but keeps the records, so a map lookup
// alone would report a preserved subject as FRESH from hidden stale data. // alone would report a preserved subject as FRESH from hidden stale data.
for (const [name, source, guard] of [ for (const [name, source, guard] of [
// Flights asks its renderer whether the fleet is shown (phase 3). // Both aircraft layers answer from enabled observation models.
['flights', readLayerSource(new URL('./flights.js', import.meta.url)), ['flights', readLayerSource(new URL('./flights.js', import.meta.url)),
/hasContact\(\s*icao24,?\s*\)\s*\{\s*if\s*\(\s*!flightState\.enabled\s*\|\|\s*flightState\.records\.data\.size\s*===\s*0,?\s*\)\s*return\s*null;/], /hasContact\(\s*icao24,?\s*\)\s*\{\s*if\s*\(\s*!flightState\.enabled\s*\|\|\s*flightState\.records\.data\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
// Military asks its renderer too (phase 3).
['militaryFlights', readLayerSource(new URL('./militaryFlights.js', import.meta.url)), ['militaryFlights', readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_renderer\?\.fleet\.visible\(\)\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/], /hasContact\(\s*icao24,?\s*\)\s*\{\s*if\s*\(\s*!flightState\.enabled\s*\|\|\s*flightState\.records\.data\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
['aisLiveVessels', readLayerSource(new URL('./aisLiveVessels.js', import.meta.url)), ['aisLiveVessels', readLayerSource(new URL('./aisLiveVessels.js', import.meta.url)),
/hasContact\(\s*mmsi,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!state\.feed\.enabled\s*\|\|\s*!state\.records\.byMmsi\s*\|\|\s*state\.records\.byMmsi\.size\s*===\s*0,?\s*\)\s*return\s*null;/], /hasContact\(\s*mmsi,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!state\.feed\.enabled\s*\|\|\s*!state\.records\.byMmsi\s*\|\|\s*state\.records\.byMmsi\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
]) { ]) {
+117
View File
@@ -0,0 +1,117 @@
/** Scene-only ownership shared by the observed aircraft layers. Source
* requests, classification, selection and track history outlive this binding. */
export function createAircraftSceneBinding({
id,
flightState,
services,
parts,
resolveModel,
enable,
}) {
let resumeFollow = false;
function removeSceneListeners() {
parts.tracking._removeClickHandler();
services.picking.unregisterPickOwner(id);
if (flightState._preRenderRemove) {
flightState._preRenderRemove();
flightState._preRenderRemove = null;
}
if (flightState._trackedModelPreUpdateRemove) {
flightState._trackedModelPreUpdateRemove();
flightState._trackedModelPreUpdateRemove = null;
}
if (flightState._moveEndRemove) {
flightState._moveEndRemove();
flightState._moveEndRemove = null;
}
}
const methods = {
attachRenderer(viewer, { engine = null } = {}) {
if (!flightState.initialized)
throw new Error('Initialize the flight layer before attaching');
if (flightState._renderer)
throw new Error('Flight renderer already attached');
const renderer = services.renderer.create({ viewer, engine });
flightState._renderer = renderer;
try {
renderer.fleet.attach();
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
if (!flightState._preloadModel) {
const epoch = flightState._modelEpoch;
renderer.models
.preload(resolveModel())
.then((m) => {
if (epoch === flightState._modelEpoch) {
flightState._preloadModel = m;
flightState._planeModelLoaded = true;
} else {
renderer.models.destroy(m);
}
})
.catch(() => {
/* tracked plane just stays a billboard a beat longer */
});
}
parts.snapshots.presentRecords();
if (flightState._trackedIcao) {
parts.tracking._trackFlight(flightState._trackedIcao, {
resume: true,
follow: resumeFollow,
});
}
renderer.fleet.setVisible(flightState.enabled);
renderer.models.setVisible(flightState.enabled);
renderer.fleet.restoreOrder();
if (flightState.enabled) {
enable();
parts.tracking._applyPendingTrackingRestore();
}
} catch (error) {
methods.detachRenderer(viewer);
throw error;
}
},
detachRenderer(viewer) {
const renderer = flightState._renderer;
if (!renderer) return;
removeSceneListeners();
services.render.releaseContinuousRender(id);
flightState._trackedCameraFrameStop?.();
flightState._trackedCameraFrameStop = null;
if (flightState._trackedEntity) {
resumeFollow = renderer.follow.isFollowing(flightState._trackedEntity);
if (resumeFollow) renderer.follow.release();
renderer.tracked.remove(flightState._trackedEntity);
flightState._trackedEntity = null;
}
parts.rendering._releaseTrackedModel();
parts.tracking._detachTrail();
parts.rendering._releaseModels();
// Removing the model collection destroys it and its models.
renderer.fleet.detach(viewer);
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
flightState._modelPending.clear();
flightState._modelGen.clear();
if (flightState._preloadModel) {
renderer.models.destroy(flightState._preloadModel);
flightState._preloadModel = null;
}
flightState._planeModelLoaded = false;
flightState._billboards.clear();
flightState._cullPositions.clear();
flightState._detectionObjects.clear();
flightState._displayFloorState?.clear();
flightState._groundSnap.clear();
parts.motion._resetTrackedDisplay();
renderer.destroy();
flightState._renderer = null;
},
removeSceneListeners,
};
return methods;
}
+14 -109
View File
@@ -1,3 +1,4 @@
import { createAircraftSceneBinding } from '../aircraft/sceneBinding.js';
import { PLANE_MODEL_URL } from './policy.js'; import { PLANE_MODEL_URL } from './policy.js';
export function createLifecycle({ export function createLifecycle({
@@ -11,32 +12,20 @@ export function createLifecycle({
const { onMilitaryLayerActiveChange } = services.militaryRegistry; const { onMilitaryLayerActiveChange } = services.militaryRegistry;
const { holdContinuousRender, releaseContinuousRender } = services.render; const { holdContinuousRender, releaseContinuousRender } = services.render;
const { ensureGeoidReady } = services.geoid; const { ensureGeoidReady } = services.geoid;
const { registerPickOwner, unregisterPickOwner } = services.picking; const { registerPickOwner } = services.picking;
let initialized = false; const binding = createAircraftSceneBinding({
let resumeFollow = false; id: 'flights', flightState, services, parts,
resolveModel: () => resolveAsset(PLANE_MODEL_URL),
function removeSceneListeners() { enable: () => methods.enable(),
parts.tracking._removeClickHandler(); });
unregisterPickOwner('flights');
if (flightState._preRenderRemove) {
flightState._preRenderRemove();
flightState._preRenderRemove = null;
}
if (flightState._trackedModelPreUpdateRemove) {
flightState._trackedModelPreUpdateRemove();
flightState._trackedModelPreUpdateRemove = null;
}
if (flightState._moveEndRemove) {
flightState._moveEndRemove();
flightState._moveEndRemove = null;
}
}
const methods = { const methods = {
attachRenderer: binding.attachRenderer,
detachRenderer: binding.detachRenderer,
/** Configure the source before initialization; an active layer keeps its owner. */ /** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) { setSource(source) {
if (initialized) if (flightState.initialized)
throw new Error('Configure the source before layer initialization'); throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
@@ -55,7 +44,7 @@ export function createLifecycle({
* @param {{ engine?: object }} [context] - LayerLifecycle's context. * @param {{ engine?: object }} [context] - LayerLifecycle's context.
*/ */
init(viewer, { engine = null } = {}) { init(viewer, { engine = null } = {}) {
if (initialized) throw new Error('Flight layer is already initialized'); if (flightState.initialized) throw new Error('Flight layer is already initialized');
if (typeof flightState.feed._source?.getSnapshot !== 'function') if (typeof flightState.feed._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted) if (flightState.lifetime.signal.aborted)
@@ -106,7 +95,7 @@ export function createLifecycle({
); );
} }
initialized = true; flightState.initialized = true;
try { try {
if (viewer || engine) methods.attachRenderer(viewer, { engine }); if (viewer || engine) methods.attachRenderer(viewer, { engine });
} catch (error) { } catch (error) {
@@ -115,90 +104,6 @@ export function createLifecycle({
} }
}, },
attachRenderer(viewer, { engine = null } = {}) {
if (!initialized)
throw new Error('Initialize the flight layer before attaching');
if (flightState._renderer)
throw new Error('Flight renderer already attached');
const renderer = services.renderer.create({ viewer, engine });
flightState._renderer = renderer;
try {
renderer.fleet.attach();
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
if (!flightState._preloadModel) {
const epoch = flightState._modelEpoch;
renderer.models
.preload(resolveAsset(PLANE_MODEL_URL))
.then((m) => {
if (epoch === flightState._modelEpoch) {
flightState._preloadModel = m;
flightState._planeModelLoaded = true;
} else {
renderer.models.destroy(m);
}
})
.catch(() => {
/* tracked plane just stays a billboard a beat longer */
});
}
parts.snapshots.presentRecords();
if (flightState._trackedIcao) {
parts.tracking._trackFlight(flightState._trackedIcao, {
resume: true,
follow: resumeFollow,
});
}
renderer.fleet.setVisible(flightState.enabled);
renderer.models.setVisible(flightState.enabled);
renderer.fleet.restoreOrder();
if (flightState.enabled) {
methods.enable();
parts.tracking._applyPendingTrackingRestore();
}
} catch (error) {
methods.detachRenderer(viewer);
throw error;
}
},
detachRenderer(viewer) {
const renderer = flightState._renderer;
if (!renderer) return;
removeSceneListeners();
releaseContinuousRender('flights');
flightState._trackedCameraFrameStop?.();
flightState._trackedCameraFrameStop = null;
if (flightState._trackedEntity) {
resumeFollow = renderer.follow.isFollowing(flightState._trackedEntity);
if (resumeFollow) renderer.follow.release();
renderer.tracked.remove(flightState._trackedEntity);
flightState._trackedEntity = null;
}
parts.rendering._releaseTrackedModel();
parts.tracking._detachTrail();
parts.rendering._releaseModels();
// Removing the model collection destroys it and its models.
renderer.fleet.detach(viewer);
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
flightState._modelPending.clear();
flightState._modelGen.clear();
if (flightState._preloadModel) {
renderer.models.destroy(flightState._preloadModel);
flightState._preloadModel = null;
}
flightState._planeModelLoaded = false;
flightState._billboards.clear();
flightState._cullPositions.clear();
flightState._detectionObjects.clear();
flightState._displayFloorState.clear();
flightState._groundSnap.clear();
parts.motion._resetTrackedDisplay();
renderer.destroy();
flightState._renderer = null;
},
/** /**
* Show the fleet and re-join the selection dispatcher. * Show the fleet and re-join the selection dispatcher.
*/ */
@@ -275,7 +180,7 @@ export function createLifecycle({
flightState._renderer?.models.setVisible(false); flightState._renderer?.models.setVisible(false);
parts.tracking._clearTracking(); parts.tracking._clearTracking();
parts.tracking._destroyTrail(); parts.tracking._destroyTrail();
removeSceneListeners(); binding.removeSceneListeners();
}, },
/** /**
@@ -284,7 +189,7 @@ export function createLifecycle({
* @param {object|null} viewer - The viewer the layer was drawn on. * @param {object|null} viewer - The viewer the layer was drawn on.
*/ */
destroy(viewer) { destroy(viewer) {
initialized = false; flightState.initialized = false;
flightState.enabled = false; flightState.enabled = false;
flightState.lifetime.abort(); flightState.lifetime.abort();
flightState._enrichActive = 0; flightState._enrichActive = 0;
+2 -2
View File
@@ -25,7 +25,7 @@ export function createMilitaryFlightLayer({
parts.lifecycle = createLifecycle(context); parts.lifecycle = createLifecycle(context);
parts.testing = createTesting(context); parts.testing = createTesting(context);
parts.queries = createQueries(context); parts.queries = createQueries(context);
const applySnapshot = createMilitarySnapshotRenderer({ parts.snapshots = createMilitarySnapshotRenderer({
flightState, flightState,
records: flightState.records, records: flightState.records,
groundFloor: services.groundFloor, groundFloor: services.groundFloor,
@@ -38,7 +38,7 @@ export function createMilitaryFlightLayer({
}); });
parts.ingestion = createIngestion({ parts.ingestion = createIngestion({
feed: flightState.feed, feed: flightState.feed,
applySnapshot, applySnapshot: parts.snapshots.applySnapshot,
setSourceLabel: (source) => { setSourceLabel: (source) => {
layer.source = source; layer.source = source;
}, },
+32 -82
View File
@@ -1,3 +1,4 @@
import { createAircraftSceneBinding } from '../aircraft/sceneBinding.js';
import { JET_MODEL_URL } from './policy.js'; import { JET_MODEL_URL } from './policy.js';
export function createLifecycle({ export function createLifecycle({
@@ -10,13 +11,20 @@ export function createLifecycle({
const { clearFocusTarget } = services.focus; const { clearFocusTarget } = services.focus;
const { holdContinuousRender, releaseContinuousRender } = services.render; const { holdContinuousRender, releaseContinuousRender } = services.render;
const { ensureGeoidReady } = services.geoid; const { ensureGeoidReady } = services.geoid;
const { registerPickOwner, unregisterPickOwner } = services.picking; const { registerPickOwner } = services.picking;
const { setMilitaryLayerActive } = services.militaryRegistry; const { setMilitaryLayerActive } = services.militaryRegistry;
const binding = createAircraftSceneBinding({
id: 'military', flightState, services, parts,
resolveModel: () => resolveAsset(JET_MODEL_URL),
enable: () => methods.enable(),
});
const methods = { const methods = {
attachRenderer: binding.attachRenderer,
detachRenderer: binding.detachRenderer,
/** Configure the source before initialization; an active layer keeps its owner. */ /** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) { setSource(source) {
if (flightState._renderer) if (flightState.initialized)
throw new Error('Configure the source before layer initialization'); throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
@@ -35,35 +43,13 @@ export function createLifecycle({
* @param {{ engine?: object }} [context] - LayerLifecycle's context. * @param {{ engine?: object }} [context] - LayerLifecycle's context.
*/ */
init(viewer, { engine = null } = {}) { init(viewer, { engine = null } = {}) {
if (flightState._renderer) if (flightState.initialized)
throw new Error('Flight layer is already initialized'); throw new Error('Flight layer is already initialized');
if (typeof flightState.feed._source?.getSnapshot !== 'function') if (typeof flightState.feed._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required'); throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted) if (flightState.lifetime.signal.aborted)
flightState.lifetime = new AbortController(); flightState.lifetime = new AbortController();
clearFocusTarget('militaryFlights'); clearFocusTarget('militaryFlights');
const renderer = services.renderer.create({ viewer, engine });
flightState._renderer = renderer;
renderer.fleet.attach();
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
if (!flightState._preloadModel) {
const epoch = flightState._modelEpoch;
renderer.models
.preload(resolveAsset(JET_MODEL_URL))
.then((m) => {
if (epoch === flightState._modelEpoch) {
flightState._preloadModel = m;
flightState._planeModelLoaded = true;
} else {
renderer.models.destroy(m);
}
})
.catch(() => {
/* tracked plane just stays a billboard a beat longer */
});
}
flightState._billboards = new Map(); flightState._billboards = new Map();
flightState._cullPositions.clear(); flightState._cullPositions.clear();
flightState._detectionObjects = new Map(); flightState._detectionObjects = new Map();
@@ -82,9 +68,9 @@ export function createLifecycle({
flightState._trackedEntity = null; flightState._trackedEntity = null;
flightState._cockpitSubjectId = null; flightState._cockpitSubjectId = null;
flightState._cockpitContactMode = flightState._cockpitContactMode =
document.body.classList.contains('cockpit-mode'); globalThis.document?.body.classList.contains('cockpit-mode') === true;
flightState._cockpitNearContacts = new Set(); flightState._cockpitNearContacts = new Set();
if (!flightState._cockpitModeListener) { if (globalThis.window && !flightState._cockpitModeListener) {
flightState._cockpitModeListener = (event) => flightState._cockpitModeListener = (event) =>
parts.tracking._applyCockpitState(event?.detail); parts.tracking._applyCockpitState(event?.detail);
window.addEventListener( window.addEventListener(
@@ -93,11 +79,13 @@ export function createLifecycle({
); );
} }
parts.tracking._installClickHandler(); flightState.initialized = true;
try {
renderer.fleet.restoreOrder(); if (viewer || engine) binding.attachRenderer(viewer, { engine });
} catch (error) {
console.log('[Data:Military] Initialized with billboard icons'); methods.destroy(viewer);
throw error;
}
}, },
/** /**
@@ -105,11 +93,12 @@ export function createLifecycle({
*/ */
enable() { enable() {
const renderer = flightState._renderer; const renderer = flightState._renderer;
flightState.enabled = true;
renderer?.fleet.setVisible(true); renderer?.fleet.setVisible(true);
holdContinuousRender('military'); // per-frame animator (perf wave 2) if (renderer) holdContinuousRender('military'); // per-frame animator (perf wave 2)
renderer?.models.setVisible(true); renderer?.models.setVisible(true);
parts.tracking._setCockpitContactMode( parts.tracking._setCockpitContactMode(
document.body.classList.contains('cockpit-mode'), globalThis.document?.body.classList.contains('cockpit-mode') === true,
); );
// Height-datum fix: warm the geoid grid once per layer-enable. The poll loop // Height-datum fix: warm the geoid grid once per layer-enable. The poll loop
// only reads geoidHeight() synchronously after this resolves (guarded by // only reads geoidHeight() synchronously after this resolves (guarded by
@@ -124,12 +113,12 @@ export function createLifecycle({
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */ /* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
}); });
} }
setMilitaryLayerActive(true);
if (!renderer) return;
parts.tracking._installClickHandler(); parts.tracking._installClickHandler();
registerPickOwner('military', (pickedId) => registerPickOwner('military', (pickedId) =>
flightState._billboards.has(pickedId), flightState._billboards.has(pickedId),
); );
// The flights layer suppresses its military duplicates while we render them
setMilitaryLayerActive(true);
// Force a fresh rotation pass on the first tick after re-enable // Force a fresh rotation pass on the first tick after re-enable
flightState._lastCamPoseSig = ''; flightState._lastCamPoseSig = '';
if (!flightState._preRenderRemove && renderer) { if (!flightState._preRenderRemove && renderer) {
@@ -163,6 +152,7 @@ export function createLifecycle({
* dispatcher so clicks do not get intercepted while the layer is off. * dispatcher so clicks do not get intercepted while the layer is off.
*/ */
disable() { disable() {
flightState.enabled = false;
parts.controller._abortActiveUpdates(); parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore(); parts.tracking._cancelPendingTrackingRestore();
flightState._renderer?.fleet.setVisible(false); flightState._renderer?.fleet.setVisible(false);
@@ -171,24 +161,8 @@ export function createLifecycle({
flightState._renderer?.models.setVisible(false); flightState._renderer?.models.setVisible(false);
parts.tracking._clearTracking(); parts.tracking._clearTracking();
parts.tracking._destroyTrail(); parts.tracking._destroyTrail();
// Leave the selection dispatcher (clicks, Escape) while disabled to binding.removeSceneListeners();
// avoid intercepting input when the layer is off
parts.tracking._removeClickHandler();
unregisterPickOwner('military');
// Flights layer takes over rendering known-military aircraft (amber)
setMilitaryLayerActive(false); setMilitaryLayerActive(false);
if (flightState._preRenderRemove) {
flightState._preRenderRemove();
flightState._preRenderRemove = null;
}
if (flightState._trackedModelPreUpdateRemove) {
flightState._trackedModelPreUpdateRemove();
flightState._trackedModelPreUpdateRemove = null;
}
if (flightState._moveEndRemove) {
flightState._moveEndRemove();
flightState._moveEndRemove = null;
}
}, },
/** /**
@@ -197,6 +171,9 @@ export function createLifecycle({
* @param {object|null} viewer - The viewer the layer was drawn on. * @param {object|null} viewer - The viewer the layer was drawn on.
*/ */
destroy(viewer) { destroy(viewer) {
flightState.initialized = false;
flightState.enabled = false;
setMilitaryLayerActive(false);
flightState.lifetime.abort(); flightState.lifetime.abort();
parts.controller._abortActiveUpdates(); parts.controller._abortActiveUpdates();
releaseContinuousRender('military'); // direct-destroy path (perf wave 2 fix) releaseContinuousRender('military'); // direct-destroy path (perf wave 2 fix)
@@ -211,38 +188,13 @@ export function createLifecycle({
); );
flightState._cockpitModeListener = null; flightState._cockpitModeListener = null;
} }
unregisterPickOwner('military'); binding.detachRenderer(viewer);
if (flightState._preRenderRemove) {
flightState._preRenderRemove();
flightState._preRenderRemove = null;
}
if (flightState._trackedModelPreUpdateRemove) {
flightState._trackedModelPreUpdateRemove();
flightState._trackedModelPreUpdateRemove = null;
}
if (flightState._moveEndRemove) {
flightState._moveEndRemove();
flightState._moveEndRemove = null;
}
parts.rendering._releaseModels();
// Removing the model collection destroys it and its models.
flightState._renderer?.fleet.detach(viewer);
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
flightState._modelPending.clear();
flightState._modelGen.clear();
if (flightState._preloadModel) {
flightState._renderer?.models.destroy(flightState._preloadModel);
flightState._preloadModel = null;
}
flightState._planeModelLoaded = false;
flightState._billboards.clear();
flightState._cullPositions.clear();
flightState._detectionObjects.clear();
flightState.records.data.clear(); flightState.records.data.clear();
flightState._positionHistory.clear(); flightState._positionHistory.clear();
flightState._displayCourse.clear(); flightState._displayCourse.clear();
flightState._groundSnap.clear(); flightState._groundSnap.clear();
flightState.records.missingPolls.clear(); flightState.records.missingPolls.clear();
flightState.records.geoidNCache.clear();
flightState.feed._count = 0; flightState.feed._count = 0;
flightState.feed._lastUpdate = null; flightState.feed._lastUpdate = null;
flightState._cockpitContactMode = false; flightState._cockpitContactMode = false;
@@ -256,8 +208,6 @@ export function createLifecycle({
source: flightState.feed._lastSource, source: flightState.feed._lastSource,
}; };
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
flightState._renderer?.destroy();
flightState._renderer = null;
}, },
}; };
+10 -14
View File
@@ -353,7 +353,7 @@ export function createQueries({
* @returns {Array<Object>} Sorted array of nearby aircraft descriptors * @returns {Array<Object>} Sorted array of nearby aircraft descriptors
*/ */
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) { getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
if (!center || !flightState._renderer?.fleet.visible()) return []; if (!center || !flightState.enabled) return [];
const limit = Number.isFinite(maxCount) const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount)) ? Math.max(1, Math.floor(maxCount))
@@ -363,11 +363,14 @@ export function createQueries({
const nearby = []; const nearby = [];
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, info] of flightState.records.data) {
const bb = flightState._billboards.get(icao24);
if (!bb && flightState._renderer) continue;
const isTracked = icao24 === flightState._trackedIcao; const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (billboard hidden) so proximity counts // Keep planes rendered as a 3D model (billboard hidden) so proximity counts
// don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden. // don't drop to zero on the 2D→3D handoff; bb.position stays current while hidden.
if ( if (
bb &&
!aircraftIncludedInNearby({ !aircraftIncludedInNearby({
isTracked, isTracked,
billboardShown: bb.show, billboardShown: bb.show,
@@ -380,13 +383,12 @@ export function createQueries({
const trackedPos = isTracked const trackedPos = isTracked
? parts.motion._trackedDisplayCached() ? parts.motion._trackedDisplayCached()
: null; // cached, no recompute (anti-jitter) : null; // cached, no recompute (anti-jitter)
const pos = trackedPos || bb.position; const pos = trackedPos || bb?.position || parts.motion._deadReckon(icao24);
if (!pos) continue; if (!pos) continue;
const distance = geo.cartesian3.distance(center, pos); const distance = geo.cartesian3.distance(center, pos);
if (distance > maxRange) continue; if (distance > maxRange) continue;
const info = flightState.records.data.get(icao24);
nearby.push({ nearby.push({
id: info?.callsign?.trim() || info?.registration?.trim() || icao24, id: info?.callsign?.trim() || info?.registration?.trim() || icao24,
icao24, icao24,
@@ -581,16 +583,13 @@ export function createQueries({
* holds no data and therefore cannot answer. * holds no data and therefore cannot answer.
*/ */
hasContact(icao24) { hasContact(icao24) {
if ( if (!flightState.enabled || flightState.records.data.size === 0)
!flightState._renderer?.fleet.visible() ||
flightState._billboards.size === 0
)
return null; return null;
if (!icao24) return false; if (!icao24) return false;
const id = String(icao24).trim(); const id = String(icao24).trim();
return ( return (
flightState._billboards.has(id) || flightState.records.data.has(id) ||
flightState._billboards.has(id.toLowerCase()) flightState.records.data.has(id.toLowerCase())
); );
}, },
@@ -642,10 +641,7 @@ export function createQueries({
* @returns {Array<Object>} See mapAnalystRecord for the record shape. * @returns {Array<Object>} See mapAnalystRecord for the record shape.
*/ */
getAnalystRecords(maxCount = 2000) { getAnalystRecords(maxCount = 2000) {
if ( if (!flightState.enabled || flightState.records.data.size === 0)
!flightState._renderer?.fleet.visible() ||
flightState.records.data.size === 0
)
return []; return [];
const limit = Number.isFinite(maxCount) const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount)) ? Math.max(1, Math.floor(maxCount))
+34
View File
@@ -292,3 +292,37 @@ test('the military logic draws and follows on the Navara renderer; the follow pa
assert.equal(engine.movers.list().length, 0, 'no mover set left behind'); assert.equal(engine.movers.list().length, 0, 'no mover set left behind');
engine.destroy(); engine.destroy();
}); });
test('military classification and the selected aircraft survive an update between scenes', async () => {
const first = createFakeEngine(), second = createFakeEngine();
let polls = 0, binding;
const layer = createApplicationMilitary({
surface: defaultSurface, militaryRegistry,
source: { getSnapshot: async () => militarySnapshotOf(['ae0001'], 1700000000000 + (++polls)*1000) },
renderer: {create: ({engine}) => (binding = recordingAircraftRenderer(engine))},
});
try {
layer.init(null);
layer.enable();
await layer.update(null);
assert.equal(layer.hasContact('AE0001'), true);
assert.equal(layer.getAnalystRecords().length, 1);
layer.attachRenderer(null, { engine: first });
assert.equal(layer.trackById('ae0001'), true);
layer.detachRenderer();
assert.equal(binding.follow.current(), undefined);
assert.equal(militaryRegistry.isMilitaryLayerActive(), true);
assert.equal(layer.getTrackedInfo().icao24, 'ae0001');
await layer.update(null);
layer.attachRenderer(null, {engine: second});
assert.ok(binding.follow.current());
assert.equal(layer.getStats().lastUpdate, 1700000002000);
assert.equal(polls, 2);
layer.disable();
assert.equal(militaryRegistry.isMilitaryLayerActive(), false);
assert.equal(layer.hasContact('ae0001'), null);
} finally {
layer.destroy(null);
first.destroy(); second.destroy();
}
});
+76 -63
View File
@@ -31,7 +31,74 @@ export function createMilitarySnapshotRenderer({
groundFloor; groundFloor;
const { sampleMeshFloorCells } = meshFloor; const { sampleMeshFloorCells } = meshFloor;
const { registerMilitaryIcaos } = militaryRegistry; const { registerMilitaryIcaos } = militaryRegistry;
return function applySnapshot(snapshot, viewer) { function presentContact(icao24, meta, position, prevMeta, groundFlipped) {
const isTracked = icao24 === flightState._trackedIcao;
if (flightState._billboards.has(icao24)) {
const bb = flightState._billboards.get(icao24);
if (!isTracked) {
bb.color = rendering
._fleetBillboardColor(icao24, meta)
.withAlpha(cyberSonarBaseAlpha(bb));
}
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
// Ground flips (landing/takeoff) restyle this SAME billboard in
// place — the transition is never a removal.
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Per-class GLBs: a class change can mean a different asset OR scale —
// resync the live model, any in-flight load, and the tracked
// standalone (mirror of flights.js).
if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24);
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
} else {
// Start where the fleet pass will draw it: the fleet renders behind
// real time, so a fresh contact's displayed position is its fix
// projected back to that delayed time. Creating it at the raw fix drew
// it ahead until the next fleet tick snapped it back.
const displayed =
motion._deadReckon(icao24, geo.cartesian3.create()) || position;
const bb = flightState._renderer.fleet.add({
position: displayed,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta.klass] || 1) *
(meta.onGround ? GROUND_SCALE : 1),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: geo.cartesian3.ZERO,
color: isTracked ? TRACKED_ICON_COLOR : MIL_ICON_COLOR,
sizeInMeters: false,
scaleByDistance: rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance).
disableDepthTestDistance: rendering._groundDepthDistance(),
id: icao24,
show: !isTracked, // hidden if currently tracked (entity replaces it)
});
flightState._billboards.set(icao24, bb);
rendering._applyFleetBillboardPresentation(icao24, bb);
}
}
function presentRecords() {
for (const [id, meta] of records.data) {
const position = geo.cartesian3.fromDegrees(meta.rawLon, meta.rawLat, meta.renderAltitudeM);
presentContact(id, meta, position, null, false);
}
}
function applySnapshot(snapshot) {
const currentIcaos = new Set(); const currentIcaos = new Set();
const receiptNowMs = snapshot.observedAtMs; const receiptNowMs = snapshot.observedAtMs;
// Field-test fix (RS46): coarse floor cells to warm for the below-ground // Field-test fix (RS46): coarse floor cells to warm for the below-ground
@@ -128,63 +195,7 @@ export function createMilitarySnapshotRenderer({
} }
} }
if (flightState._billboards.has(icao24)) { if (flightState._renderer) presentContact(icao24, meta, position, prevMeta, groundFlipped);
const bb = flightState._billboards.get(icao24);
if (!isTracked) {
bb.color = rendering
._fleetBillboardColor(icao24, meta)
.withAlpha(cyberSonarBaseAlpha(bb));
}
// Position AND rotation are owned by the fleet pass (_fleetTick);
// course changes land on the next rotation pass (forced below).
// Ground flips (landing/takeoff) restyle this SAME billboard in
// place — the transition is never a removal.
if (
prevMeta?.klass !== meta.klass ||
groundFlipped ||
flightState._cockpitContactMode
) {
rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Per-class GLBs: a class change can mean a different asset OR scale —
// resync the live model, any in-flight load, and the tracked
// standalone (mirror of flights.js).
if (prevMeta?.klass !== meta.klass) rendering._syncModelToClass(icao24);
// Round 5: depth policy is uniform (always depth-test-free, see
// _groundDepthDistance) — nothing to flip on landing/takeoff.
} else {
// Start where the fleet pass will draw it: the fleet renders behind
// real time, so a fresh contact's displayed position is its fix
// projected back to that delayed time. Creating it at the raw fix drew
// it ahead until the next fleet tick snapped it back.
const displayed =
motion._deadReckon(icao24, geo.cartesian3.create()) || position;
const bb = flightState._renderer.fleet.add({
position: displayed,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20,
height: isTracked ? 24 : 20,
scale:
BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta.klass] || 1) *
(meta.onGround ? GROUND_SCALE : 1),
// Screen-projected rotation lands on the next fleet tick.
rotation: 0,
alignedAxis: geo.cartesian3.ZERO,
color: isTracked ? TRACKED_ICON_COLOR : MIL_ICON_COLOR,
sizeInMeters: false,
scaleByDistance: rendering._normalBillboardScaleByDistance(),
// Grounded/near-surface planes sit at/below the photoreal tile
// surface — render them depth-test-free so they never vanish up
// close (_groundDepthDistance).
disableDepthTestDistance: rendering._groundDepthDistance(),
id: icao24,
show: !isTracked, // hidden if currently tracked (entity replaces it)
});
flightState._billboards.set(icao24, bb);
rendering._applyFleetBillboardPresentation(icao24, bb);
}
// Takeoff while TRACKED: ground traffic drew no trail, so start one // Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the // from the fresh airborne history (touchdown needs no action — the
// append gate above simply freezes the existing trail). // append gate above simply freezes the existing trail).
@@ -238,7 +249,7 @@ export function createMilitarySnapshotRenderer({
// Round 4: sample the RENDERED mesh for those cells (one-shot per cell, // Round 4: sample the RENDERED mesh for those cells (one-shot per cell,
// budget-capped, viewer-proximate, google-3d regime only), excluding // budget-capped, viewer-proximate, google-3d regime only), excluding
// this layer's own billboards/models from the probe. // this layer's own billboards/models from the probe.
{ if (flightState._renderer) {
const viewerCarto = const viewerCarto =
flightState._renderer?.view.camera?.positionCartographic || null; flightState._renderer?.view.camera?.positionCartographic || null;
sampleMeshFloorCells( sampleMeshFloorCells(
@@ -269,7 +280,7 @@ export function createMilitarySnapshotRenderer({
// EXCEPTION: likely-landed planes (last fix low + slow) get only // EXCEPTION: likely-landed planes (last fix low + slow) get only
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a // LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// feed gap, and the full grace left phantom planes parked at airports. // feed gap, and the full grace left phantom planes parked at airports.
for (const [icao24, bb] of flightState._billboards) { for (const icao24 of records.data.keys()) {
if (currentIcaos.has(icao24)) continue; if (currentIcaos.has(icao24)) continue;
const absence = records.absence(icao24, { const absence = records.absence(icao24, {
complete: snapshot.complete, complete: snapshot.complete,
@@ -297,7 +308,8 @@ export function createMilitarySnapshotRenderer({
tracking._clearTracking(false, { evicted: true }); tracking._clearTracking(false, { evicted: true });
} }
flightState._renderer.fleet.remove(bb); const bb = flightState._billboards.get(icao24);
if (bb) flightState._renderer.fleet.remove(bb);
flightState._billboards.delete(icao24); flightState._billboards.delete(icao24);
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak) rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
records.forget(icao24); records.forget(icao24);
@@ -313,6 +325,7 @@ export function createMilitarySnapshotRenderer({
// Feed the shared registry so the flights layer can classify/suppress // Feed the shared registry so the flights layer can classify/suppress
registerMilitaryIcaos(currentIcaos); registerMilitaryIcaos(currentIcaos);
return { count: flightState._billboards.size, ids: currentIcaos }; return { count: records.data.size, ids: currentIcaos };
}; }
return { applySnapshot, presentRecords };
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import * as geo from '../../geo/index.js';
export function createFlightState({ source, services }) { export function createFlightState({ source, services }) {
const { createGroundSnap } = services.groundSnap; const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() }; const flightState = { lifetime: new AbortController(), enabled: false };
flightState.feed = createMilitaryFeed(source); flightState.feed = createMilitaryFeed(source);
flightState.records = new MilitaryFlightRecords({ flightState.records = new MilitaryFlightRecords({
...services.geoid, ...services.geoid,
+1
View File
@@ -36,6 +36,7 @@ export function createTesting({
models = [], models = [],
modelCollection = null, modelCollection = null,
}) { }) {
flightState.enabled = billboardCollection?.show !== false;
// The production renderer over the test's viewer and (fake) collections. // The production renderer over the test's viewer and (fake) collections.
flightState._renderer = services.renderer.create({ flightState._renderer = services.renderer.create({
viewer, viewer,
+28 -10
View File
@@ -502,6 +502,14 @@ export function createTracking({
); );
} }
} }
_attachTrail();
const oldestFixEpochSec = history.length
? history[0].timeMs / 1000
: Infinity;
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
}
function _attachTrail() {
if (!flightState._trail && flightState._renderer) { if (!flightState._trail && flightState._renderer) {
flightState._trail = flightState._renderer.trails.create({ flightState._trail = flightState._renderer.trails.create({
color: TRAIL_COLOR, color: TRAIL_COLOR,
@@ -568,11 +576,6 @@ export function createTracking({
}); });
} }
_refreshTrailDisplay(); _refreshTrailDisplay();
const oldestFixEpochSec = history.length
? history[0].timeMs / 1000
: Infinity;
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
} }
/** /**
@@ -703,6 +706,13 @@ export function createTracking({
function _destroyTrail() { function _destroyTrail() {
_clearTrail(); _clearTrail();
_detachTrail();
}
function _detachTrail() {
if (flightState._trailHeadEntity)
flightState._renderer?.trails.removeHead(flightState._trailHeadEntity);
flightState._trailHeadEntity = null;
if (flightState._trail) { if (flightState._trail) {
flightState._trail.destroy(); flightState._trail.destroy();
flightState._trail = null; flightState._trail = null;
@@ -830,8 +840,8 @@ export function createTracking({
* @param {string} icao24 - ICAO hex identifier of the aircraft to track * @param {string} icao24 - ICAO hex identifier of the aircraft to track
*/ */
function _trackFlight(icao24, { origin = 'programmatic' } = {}) { function _trackFlight(icao24, { origin = 'programmatic', resume = false, follow = true } = {}) {
_clearTracking(false, { origin }); // switching planes — the new follow-camera takes over if (!resume) _clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
@@ -958,9 +968,15 @@ export function createTracking({
refreshTrackedReadout(flightState._trackedEntity); refreshTrackedReadout(flightState._trackedEntity);
// Camera follows the tracked entity (the renderer cancels any flight // Camera follows the tracked entity (the renderer cancels any flight
// first and applies the one exact tracked frame). // first and applies the one exact tracked frame).
flightState._trackedCameraFrameStop = flightState._renderer.follow.start( if (follow) {
flightState._trackedEntity, flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
); flightState._trackedEntity,
);
}
if (resume) {
_attachTrail();
return;
}
// Track-history trail (PRD F2): seed from local history + async backfill. // Track-history trail (PRD F2): seed from local history + async backfill.
// Ground traffic draws NO trail — if the plane takes off while tracked, the // Ground traffic draws NO trail — if the plane takes off while tracked, the
@@ -1108,6 +1124,8 @@ export function createTracking({
_appendTrailFix, _appendTrailFix,
_refreshTrailDisplay, _refreshTrailDisplay,
_startTrail, _startTrail,
_attachTrail,
_detachTrail,
_backfillTrail, _backfillTrail,
_clearTrail, _clearTrail,
_destroyTrail, _destroyTrail,