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
+360 -302

No files matched your search

+4
View File
@@ -1353,6 +1353,7 @@
"src/layers/flights/index.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/flights/motion.js",
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
@@ -1433,6 +1434,7 @@
"src/layers/military/controller.js",
"src/layers/military/index.js",
"src/layers/military/ingestion.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/military/lifecycle.js",
"src/layers/military/motion.js",
"src/layers/military/policy.js",
@@ -2868,6 +2870,7 @@
"src/layers/flights/index.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/flights/motion.js",
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
@@ -3927,6 +3930,7 @@
"src/layers/flights/index.js",
"src/layers/flights/ingestion.js",
"src/layers/flights/lifecycle.js",
"src/layers/aircraft/sceneBinding.js",
"src/layers/flights/motion.js",
"src/layers/flights/policy.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');
const military = process.argv.includes('--military');
const layerId = military ? 'military' : 'flights';
const aircraftId = military ? 'aaa052' : '71be26';
url.search = '?engine=navara&welcome=0';
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({
headless: true,
args: ['--use-angle=metal', '--enable-gpu'],
@@ -23,14 +27,24 @@ try {
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
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);
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) => {
const path = new URL(input.url ?? input, document.baseURI).pathname;
const now = Math.floor(Date.now() / 1000);
if (path === '/api/flights') {
window.__flightPolls++;
if (layerId === 'flights') window.__flightPolls++;
return Promise.resolve(
Response.json({
time: now,
@@ -58,15 +72,21 @@ try {
}),
);
}
if (path === '/api/military')
return Promise.resolve(Response.json({ now: Date.now(), ac: [] }));
if (path === '/api/military') {
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')
return Promise.resolve(Response.json({ path: [] }));
if (path.startsWith('/api/flights/') || path.startsWith('/api/openai/'))
return Promise.resolve(Response.json({}));
return fetch(input, options);
};
});
}, layerId, aircraftId);
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
const ready = (kind) =>
page.waitForFunction(
@@ -92,16 +112,16 @@ try {
if (window.__GOOGLE_MAPS_API_KEY__ || tileset)
throw Error('Keyless preview required');
styleManager.cockpitView.setVisionMode('thermal');
const entry = dataManager.layers.get('flights');
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
window.__handoff = {
dataManager,
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,
ticks: 0,
model: entry.module,
pollCount: window.__flightPolls,
timestamp: entry.module.getStats().lastUpdate,
started: Date.now(),
};
});
// Refuse the first Cesium canvas. The application must roll back with the
@@ -127,9 +147,9 @@ try {
const {dataManager, styleManager} = window.__godsEyeView;
return new URL(location.href).searchParams.get('engine') === 'navara' &&
dataManager === window.__handoff.dataManager &&
dataManager.layers.get('flights') === window.__handoff.entry &&
dataManager.layers.get(window.__qaAircraft.layerId) === window.__handoff.entry &&
styleManager.getCockpitState().active &&
styleManager.getAircraftTrackingTarget()?.id === '71be26';
styleManager.getAircraftTrackingTarget()?.id === window.__qaAircraft.aircraftId;
}), 'a failed scene must leave the live session usable');
console.log('PASS failed renderer rolled back to the same flight and cockpit');
@@ -139,7 +159,7 @@ try {
const result = await page.evaluate(() => {
const { dataManager, engine, styleManager } = window.__godsEyeView;
const before = window.__handoff;
const entry = dataManager.layers.get('flights');
const entry = dataManager.layers.get(window.__qaAircraft.layerId);
const cockpit = styleManager.getCockpitState();
return {
sameOwner:
@@ -147,18 +167,18 @@ try {
before.entry === entry &&
before.model === entry.module,
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:
before.timer === entry.intervalId && entry.intervalId != null,
sourcePolls: window.__flightPolls - before.pollCount,
elapsed: Date.now() - before.started,
timerTicks: before.ticks,
stamp: entry.module.getStats().lastUpdate,
initialStamp: before.timestamp,
id: cockpit.subject?.id,
active: cockpit.active,
mode: cockpit.visionMode,
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(),
};
});
@@ -171,10 +191,10 @@ try {
result.drawn,
JSON.stringify(result),
);
assert.equal(result.id, '71be26');
assert.equal(result.id, aircraftId);
assert.equal(result.mode, 'thermal');
if (result.elapsed < 30000) {
assert.equal(result.sourcePolls, 0);
assert.ok(result.sourcePolls <= result.timerTicks, 'switch must not add a source request');
if (result.timerTicks === 0) {
assert.equal(result.stamp, result.initialStamp);
}
await page.waitForFunction(
+2 -2
View File
@@ -124,12 +124,12 @@ export function createApplicationCatalog({
),
);
const military = once(() =>
createApplicationMilitary({
sessionModel('military', () => createApplicationMilitary({
surface,
source: sources.military,
militaryRegistry,
resolveAsset,
}),
})),
);
const vessels = once(() =>
createApplicationVessels({
+2 -1
View File
@@ -482,7 +482,8 @@ test('GERMAN: an existing untracked military billboard repaints each poll withou
floorAltitudeM: (value) => value,
});
flightState.records = records;
const apply = createMilitarySnapshotRenderer({
flightState._renderer = { view: {}, surfaceExclusions: () => [] };
const {applySnapshot: apply} = createMilitarySnapshotRenderer({
flightState,
records,
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
// alone would report a preserved subject as FRESH from hidden stale data.
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)),
/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)),
/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)),
/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';
export function createLifecycle({
@@ -11,32 +12,20 @@ export function createLifecycle({
const { onMilitaryLayerActiveChange } = services.militaryRegistry;
const { holdContinuousRender, releaseContinuousRender } = services.render;
const { ensureGeoidReady } = services.geoid;
const { registerPickOwner, unregisterPickOwner } = services.picking;
const { registerPickOwner } = services.picking;
let initialized = false;
let resumeFollow = false;
function removeSceneListeners() {
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 binding = createAircraftSceneBinding({
id: 'flights', flightState, services, parts,
resolveModel: () => resolveAsset(PLANE_MODEL_URL),
enable: () => methods.enable(),
});
const methods = {
attachRenderer: binding.attachRenderer,
detachRenderer: binding.detachRenderer,
/** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) {
if (initialized)
if (flightState.initialized)
throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
@@ -55,7 +44,7 @@ export function createLifecycle({
* @param {{ engine?: object }} [context] - LayerLifecycle's context.
*/
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')
throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted)
@@ -106,7 +95,7 @@ export function createLifecycle({
);
}
initialized = true;
flightState.initialized = true;
try {
if (viewer || engine) methods.attachRenderer(viewer, { engine });
} 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.
*/
@@ -275,7 +180,7 @@ export function createLifecycle({
flightState._renderer?.models.setVisible(false);
parts.tracking._clearTracking();
parts.tracking._destroyTrail();
removeSceneListeners();
binding.removeSceneListeners();
},
/**
@@ -284,7 +189,7 @@ export function createLifecycle({
* @param {object|null} viewer - The viewer the layer was drawn on.
*/
destroy(viewer) {
initialized = false;
flightState.initialized = false;
flightState.enabled = false;
flightState.lifetime.abort();
flightState._enrichActive = 0;
+2 -2
View File
@@ -25,7 +25,7 @@ export function createMilitaryFlightLayer({
parts.lifecycle = createLifecycle(context);
parts.testing = createTesting(context);
parts.queries = createQueries(context);
const applySnapshot = createMilitarySnapshotRenderer({
parts.snapshots = createMilitarySnapshotRenderer({
flightState,
records: flightState.records,
groundFloor: services.groundFloor,
@@ -38,7 +38,7 @@ export function createMilitaryFlightLayer({
});
parts.ingestion = createIngestion({
feed: flightState.feed,
applySnapshot,
applySnapshot: parts.snapshots.applySnapshot,
setSourceLabel: (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';
export function createLifecycle({
@@ -10,13 +11,20 @@ export function createLifecycle({
const { clearFocusTarget } = services.focus;
const { holdContinuousRender, releaseContinuousRender } = services.render;
const { ensureGeoidReady } = services.geoid;
const { registerPickOwner, unregisterPickOwner } = services.picking;
const { registerPickOwner } = services.picking;
const { setMilitaryLayerActive } = services.militaryRegistry;
const binding = createAircraftSceneBinding({
id: 'military', flightState, services, parts,
resolveModel: () => resolveAsset(JET_MODEL_URL),
enable: () => methods.enable(),
});
const methods = {
attachRenderer: binding.attachRenderer,
detachRenderer: binding.detachRenderer,
/** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) {
if (flightState._renderer)
if (flightState.initialized)
throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
@@ -35,35 +43,13 @@ export function createLifecycle({
* @param {{ engine?: object }} [context] - LayerLifecycle's context.
*/
init(viewer, { engine = null } = {}) {
if (flightState._renderer)
if (flightState.initialized)
throw new Error('Flight layer is already initialized');
if (typeof flightState.feed._source?.getSnapshot !== 'function')
throw new TypeError('A snapshot source is required');
if (flightState.lifetime.signal.aborted)
flightState.lifetime = new AbortController();
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._cullPositions.clear();
flightState._detectionObjects = new Map();
@@ -82,9 +68,9 @@ export function createLifecycle({
flightState._trackedEntity = null;
flightState._cockpitSubjectId = null;
flightState._cockpitContactMode =
document.body.classList.contains('cockpit-mode');
globalThis.document?.body.classList.contains('cockpit-mode') === true;
flightState._cockpitNearContacts = new Set();
if (!flightState._cockpitModeListener) {
if (globalThis.window && !flightState._cockpitModeListener) {
flightState._cockpitModeListener = (event) =>
parts.tracking._applyCockpitState(event?.detail);
window.addEventListener(
@@ -93,11 +79,13 @@ export function createLifecycle({
);
}
parts.tracking._installClickHandler();
renderer.fleet.restoreOrder();
console.log('[Data:Military] Initialized with billboard icons');
flightState.initialized = true;
try {
if (viewer || engine) binding.attachRenderer(viewer, { engine });
} catch (error) {
methods.destroy(viewer);
throw error;
}
},
/**
@@ -105,11 +93,12 @@ export function createLifecycle({
*/
enable() {
const renderer = flightState._renderer;
flightState.enabled = 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);
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
// 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 */
});
}
setMilitaryLayerActive(true);
if (!renderer) return;
parts.tracking._installClickHandler();
registerPickOwner('military', (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
flightState._lastCamPoseSig = '';
if (!flightState._preRenderRemove && renderer) {
@@ -163,6 +152,7 @@ export function createLifecycle({
* dispatcher so clicks do not get intercepted while the layer is off.
*/
disable() {
flightState.enabled = false;
parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore();
flightState._renderer?.fleet.setVisible(false);
@@ -171,24 +161,8 @@ export function createLifecycle({
flightState._renderer?.models.setVisible(false);
parts.tracking._clearTracking();
parts.tracking._destroyTrail();
// Leave the selection dispatcher (clicks, Escape) while disabled to
// avoid intercepting input when the layer is off
parts.tracking._removeClickHandler();
unregisterPickOwner('military');
// Flights layer takes over rendering known-military aircraft (amber)
binding.removeSceneListeners();
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.
*/
destroy(viewer) {
flightState.initialized = false;
flightState.enabled = false;
setMilitaryLayerActive(false);
flightState.lifetime.abort();
parts.controller._abortActiveUpdates();
releaseContinuousRender('military'); // direct-destroy path (perf wave 2 fix)
@@ -211,38 +188,13 @@ export function createLifecycle({
);
flightState._cockpitModeListener = null;
}
unregisterPickOwner('military');
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();
binding.detachRenderer(viewer);
flightState.records.data.clear();
flightState._positionHistory.clear();
flightState._displayCourse.clear();
flightState._groundSnap.clear();
flightState.records.missingPolls.clear();
flightState.records.geoidNCache.clear();
flightState.feed._count = 0;
flightState.feed._lastUpdate = null;
flightState._cockpitContactMode = false;
@@ -256,8 +208,6 @@ export function createLifecycle({
source: flightState.feed._lastSource,
};
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
*/
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
if (!center || !flightState._renderer?.fleet.visible()) return [];
if (!center || !flightState.enabled) return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
@@ -363,11 +363,14 @@ export function createQueries({
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;
// 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.
if (
bb &&
!aircraftIncludedInNearby({
isTracked,
billboardShown: bb.show,
@@ -380,13 +383,12 @@ export function createQueries({
const trackedPos = isTracked
? parts.motion._trackedDisplayCached()
: null; // cached, no recompute (anti-jitter)
const pos = trackedPos || bb.position;
const pos = trackedPos || bb?.position || parts.motion._deadReckon(icao24);
if (!pos) continue;
const distance = geo.cartesian3.distance(center, pos);
if (distance > maxRange) continue;
const info = flightState.records.data.get(icao24);
nearby.push({
id: info?.callsign?.trim() || info?.registration?.trim() || icao24,
icao24,
@@ -581,16 +583,13 @@ export function createQueries({
* holds no data and therefore cannot answer.
*/
hasContact(icao24) {
if (
!flightState._renderer?.fleet.visible() ||
flightState._billboards.size === 0
)
if (!flightState.enabled || flightState.records.data.size === 0)
return null;
if (!icao24) return false;
const id = String(icao24).trim();
return (
flightState._billboards.has(id) ||
flightState._billboards.has(id.toLowerCase())
flightState.records.data.has(id) ||
flightState.records.data.has(id.toLowerCase())
);
},
@@ -642,10 +641,7 @@ export function createQueries({
* @returns {Array<Object>} See mapAnalystRecord for the record shape.
*/
getAnalystRecords(maxCount = 2000) {
if (
!flightState._renderer?.fleet.visible() ||
flightState.records.data.size === 0
)
if (!flightState.enabled || flightState.records.data.size === 0)
return [];
const limit = Number.isFinite(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');
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;
const { sampleMeshFloorCells } = meshFloor;
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 receiptNowMs = snapshot.observedAtMs;
// 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)) {
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);
}
if (flightState._renderer) presentContact(icao24, meta, position, prevMeta, groundFlipped);
// Takeoff while TRACKED: ground traffic drew no trail, so start one
// from the fresh airborne history (touchdown needs no action — the
// 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,
// budget-capped, viewer-proximate, google-3d regime only), excluding
// this layer's own billboards/models from the probe.
{
if (flightState._renderer) {
const viewerCarto =
flightState._renderer?.view.camera?.positionCartographic || null;
sampleMeshFloorCells(
@@ -269,7 +280,7 @@ export function createMilitarySnapshotRenderer({
// EXCEPTION: likely-landed planes (last fix low + slow) get only
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// 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;
const absence = records.absence(icao24, {
complete: snapshot.complete,
@@ -297,7 +308,8 @@ export function createMilitarySnapshotRenderer({
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);
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
records.forget(icao24);
@@ -313,6 +325,7 @@ export function createMilitarySnapshotRenderer({
// Feed the shared registry so the flights layer can classify/suppress
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 }) {
const { createGroundSnap } = services.groundSnap;
const flightState = { lifetime: new AbortController() };
const flightState = { lifetime: new AbortController(), enabled: false };
flightState.feed = createMilitaryFeed(source);
flightState.records = new MilitaryFlightRecords({
...services.geoid,
+1
View File
@@ -36,6 +36,7 @@ export function createTesting({
models = [],
modelCollection = null,
}) {
flightState.enabled = billboardCollection?.show !== false;
// The production renderer over the test's viewer and (fake) collections.
flightState._renderer = services.renderer.create({
viewer,
+25 -7
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) {
flightState._trail = flightState._renderer.trails.create({
color: TRAIL_COLOR,
@@ -568,11 +576,6 @@ export function createTracking({
});
}
_refreshTrailDisplay();
const oldestFixEpochSec = history.length
? history[0].timeMs / 1000
: Infinity;
_backfillTrail(icao24, flightState._trailBackfillToken, oldestFixEpochSec);
}
/**
@@ -703,6 +706,13 @@ export function createTracking({
function _destroyTrail() {
_clearTrail();
_detachTrail();
}
function _detachTrail() {
if (flightState._trailHeadEntity)
flightState._renderer?.trails.removeHead(flightState._trailHeadEntity);
flightState._trailHeadEntity = null;
if (flightState._trail) {
flightState._trail.destroy();
flightState._trail = null;
@@ -830,8 +840,8 @@ export function createTracking({
* @param {string} icao24 - ICAO hex identifier of the aircraft to track
*/
function _trackFlight(icao24, { origin = 'programmatic' } = {}) {
_clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
function _trackFlight(icao24, { origin = 'programmatic', resume = false, follow = true } = {}) {
if (!resume) _clearTracking(false, { origin }); // switching planes — the new follow-camera takes over
const bb = flightState._billboards.get(icao24);
const info = flightState.records.data.get(icao24);
@@ -958,9 +968,15 @@ export function createTracking({
refreshTrackedReadout(flightState._trackedEntity);
// Camera follows the tracked entity (the renderer cancels any flight
// first and applies the one exact tracked frame).
if (follow) {
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
flightState._trackedEntity,
);
}
if (resume) {
_attachTrail();
return;
}
// Track-history trail (PRD F2): seed from local history + async backfill.
// Ground traffic draws NO trail — if the plane takes off while tracked, the
@@ -1108,6 +1124,8 @@ export function createTracking({
_appendTrailFix,
_refreshTrailDisplay,
_startTrail,
_attachTrail,
_detachTrail,
_backfillTrail,
_clearTrail,
_destroyTrail,