Keep aircraft, installation and viewshed state off Cesium objects

The aircraft layers' model spec key, IR boost and belly offset and the
billboards' large-icon flag move into aircraftVisuals, one store shared by
flights, military and local ADS-B. Installation markers' last drawn height
and selection move into installationMarkers. The CCTV viewshed QA tag is now
the viewshedCameraId record field, which qa-cctv-v2 reads through
entityRecords. Test fixtures drop a placement flag nothing reads.
This commit is contained in:
Sameh Khamis committed 2026-11-03 09:54:41 -08:00
1 parent 42b17ffab6
commit f86dcc9179
11 files changed
+84 -40

No files matched your search

+8
View File
@@ -1315,6 +1315,7 @@
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/layers/aircraft/visualState.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
@@ -1374,6 +1375,7 @@
"src/layers/military/policy.js",
"src/layers/military/queries.js",
"src/layers/military/rendering.js",
"src/layers/aircraft/visualState.js",
"src/layers/military/state.js",
"src/layers/military/testing.js",
"src/layers/military/tracking.js",
@@ -1639,6 +1641,7 @@
"src/layers/installations/model.js",
"src/layers/installations/policy.js",
"src/layers/installations/rendering.js",
"src/layers/installations/markerState.js",
"src/layers/installations/selection.js",
"src/layers/installations/source.js",
"src/layers/installations/state.js",
@@ -1834,6 +1837,7 @@
"src/layers/cctv/controls.js",
"src/layers/cctv/frames.js",
"src/layers/cctv/geometry.js",
"src/data/entityRecords.js",
"src/layers/cctv/geometryQueue.js",
"src/layers/cctv/ground.js",
"src/layers/cctv/headingConfidence.js",
@@ -2529,6 +2533,7 @@
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/layers/aircraft/visualState.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
@@ -2539,6 +2544,7 @@
"src/layers/installations/model.js",
"src/layers/installations/policy.js",
"src/layers/installations/rendering.js",
"src/layers/installations/markerState.js",
"src/layers/installations/selection.js",
"src/layers/installations/source.js",
"src/layers/installations/state.js",
@@ -3435,6 +3441,7 @@
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/layers/aircraft/visualState.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
@@ -3445,6 +3452,7 @@
"src/layers/installations/model.js",
"src/layers/installations/policy.js",
"src/layers/installations/rendering.js",
"src/layers/installations/markerState.js",
"src/layers/installations/selection.js",
"src/layers/installations/source.js",
"src/layers/installations/state.js",
+3 -3
View File
@@ -1669,7 +1669,7 @@ async function main() {
const prims = window.__godsEyeView.viewer.scene.primitives;
let n = 0;
for (let i = 0; i < prims.length; i++) {
if (prims.get(i) && prims.get(i)._gevViewshed) n += 1;
if (window.__godsEyeView.entityRecords.read(prims.get(i))?.viewshedCameraId) n += 1;
}
return n;
});
@@ -1729,7 +1729,7 @@ async function main() {
const prims = window.__godsEyeView.viewer.scene.primitives;
window.__qaViewshedRefs = [];
for (let i = 0; i < prims.length; i++) {
if (prims.get(i) && prims.get(i)._gevViewshed)
if (window.__godsEyeView.entityRecords.read(prims.get(i))?.viewshedCameraId)
window.__qaViewshedRefs.push(prims.get(i));
}
});
@@ -1738,7 +1738,7 @@ async function main() {
const prims = window.__godsEyeView.viewer.scene.primitives;
const now = [];
for (let i = 0; i < prims.length; i++) {
if (prims.get(i) && prims.get(i)._gevViewshed) now.push(prims.get(i));
if (window.__godsEyeView.entityRecords.read(prims.get(i))?.viewshedCameraId) now.push(prims.get(i));
}
const before = window.__qaViewshedRefs || [];
const sameCount = now.length === before.length;
+1 -1
View File
@@ -88,7 +88,7 @@ for (const layer of LAYERS) {
// `_iconKind` is identity for every unconverted contact (see
// tr3bRegistry.test.mjs) — it only swaps the glyph for a contact the
// operator explicitly converted into a TR-3B.
assert.match(source, /bb\.image\s*=\s*aircraftIcon\(\s*_iconKind\(\s*icao24,\s*meta\?\.klass,?\s*\)(,\s*bb\._gevIconLarge\s*\?\s*TRACKED_ICON_PX\s*:\s*undefined)?,?\s*\)/,
assert.match(source, /bb\.image\s*=\s*aircraftIcon\(\s*_iconKind\(\s*icao24,\s*meta\?\.klass,?\s*\)(,\s*aircraftVisuals\.read\(bb\)\?\.iconLarge\s*\?\s*TRACKED_ICON_PX\s*:\s*undefined)?,?\s*\)/,
'near contacts and model fallbacks retain the class-derived aircraft silhouette');
assert.match(source, /bb\.rotation\s*=\s*0;/,
'far dots are reset to a rotation-free presentation');
+1 -4
View File
@@ -97,7 +97,7 @@ for (const fixture of LAYERS) {
// The handoff state: the fleet tick hid the sprite and showed the model.
fixture.seed({
billboardShow: false,
models: [[ICAO, { ready: true, show: true, _gevPlacementReady: true }]],
models: [[ICAO, { ready: true, show: true }]],
});
assert.ok(
nearbyIcaos(fixture.layer).includes(ICAO),
@@ -136,7 +136,6 @@ for (const fixture of LAYERS) {
ready: false,
show: true,
scale: 0,
_gevPlacementReady: false,
}]],
});
assert.ok(
@@ -160,7 +159,6 @@ for (const fixture of LAYERS) {
models: [[ICAO, {
ready: true,
show: true,
_gevPlacementReady: true,
modelMatrix,
computedScale: COMPUTED_SCALE,
}]],
@@ -191,7 +189,6 @@ for (const fixture of LAYERS) {
ready: false,
show: true,
scale: 0,
_gevPlacementReady: false,
modelMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
}]],
});
+11
View File
@@ -0,0 +1,11 @@
import { createEntityStore } from '../../data/entityRecords.js';
/**
* What the aircraft layers (flights, military, local ADS-B) remember about the
* models and billboards they draw, kept beside them rather than on them.
* - `specKey`: the model spec a model was loaded for; a class change reloads.
* - `irBoost`: the IR boost a model was loaded with; a flip reloads.
* - `bellyM`: metres from the model origin to its belly, to sit it on the ground.
* - `iconLarge`: whether a billboard shows the large icon raster.
*/
export const aircraftVisuals = createEntityStore();
+3 -2
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { createFrustumVolumePrimitive } from '../../data/cctvViewshed.js';
import { entityRecord } from '../../data/entityRecords.js';
import {
FRUSTUM_GROUND_CLEARANCE_M,
PROBE_MIN_RANGE_M,
@@ -564,8 +565,8 @@ export function createGeometry({ state: layerState, services, parts, source }) {
record.frustumPositions,
color,
);
// QA tag: the harness counts viewshed volumes by this marker.
primitive._gevViewshed = record.camera.id;
// QA tag: the harness counts viewshed volumes by this record field.
entityRecord(primitive).viewshedCameraId = record.camera.id;
record.viewshedPrimitive =
layerState._viewer.scene.primitives.add(primitive);
record.viewshedActiveTint = !!isActive;
+17 -10
View File
@@ -38,6 +38,7 @@ import {
COURSE_MAX_DPS,
} from './policy.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
import { aircraftVisuals } from '../aircraft/visualState.js';
export function createRendering({
flightState,
@@ -162,7 +163,7 @@ export function createRendering({
const meta = flightState.records.data.get(icao24);
bb.image = aircraftIcon(
_iconKind(icao24, meta?.klass),
bb._gevIconLarge ? TRACKED_ICON_PX : undefined,
aircraftVisuals.read(bb)?.iconLarge ? TRACKED_ICON_PX : undefined,
);
bb.width = icao24 === flightState._trackedIcao ? 24 : 20;
bb.height = icao24 === flightState._trackedIcao ? 24 : 20;
@@ -432,7 +433,7 @@ export function createRendering({
const key = _specKeyFor(flightState.records.data.get(icao24)?.klass);
const current = flightState._models.get(icao24);
if (
(current && current._gevSpecKey !== key) ||
(current && aircraftVisuals.read(current)?.specKey !== key) ||
(!current && flightState._modelPending.has(icao24))
) {
const bb = flightState._billboards.get(icao24);
@@ -442,7 +443,7 @@ export function createRendering({
if (
icao24 === flightState._trackedIcao &&
flightState._trackedModel &&
flightState._trackedModel._gevSpecKey !== key
aircraftVisuals.read(flightState._trackedModel)?.specKey !== key
) {
_releaseTrackedModel();
}
@@ -465,7 +466,11 @@ export function createRendering({
const batch = flightState._irReloadQueue.splice(0, IR_RELOAD_BATCH);
for (const icao of batch) {
const model = flightState._models.get(icao);
if (!model || model._gevIrBoost === flightState._irBoost) continue; // already right state
if (
!model ||
aircraftVisuals.read(model)?.irBoost === flightState._irBoost
)
continue; // already right state
const bb = flightState._billboards.get(icao);
if (bb && icao !== flightState._trackedIcao) bb.show = true;
_releaseModel(icao);
@@ -571,8 +576,9 @@ export function createRendering({
// Keep the pick identity explicit on the resolved primitive. This also
// protects injected/custom loaders that do not copy the creation option.
model.id = icao24;
model._gevSpecKey = specKey; // class-change sync compares against this
model._gevIrBoost = loadIrBoost; // boost-flip reload queue compares against this
const visual = aircraftVisuals.write(model);
visual.specKey = specKey; // class-change sync compares against this
visual.irBoost = loadIrBoost; // boost-flip reload queue compares against this
// Admitted, not yet the visual. Cesium's default is show=true, which would let
// an unplaced primitive claim ownership from the billboard for the frames
// between admission and the next fleet tick (and draw at the identity matrix,
@@ -764,7 +770,7 @@ export function createRendering({
// Assign after resolution as well as in the creation options so the
// standalone primitive always exposes the tracked aircraft pick id.
m.id = flightState._trackedIcao;
m._gevSpecKey = trackedKey; // class-change sync compares against this
aircraftVisuals.write(m).specKey = trackedKey; // class-change sync compares against this
m.show = false; // admitted, not yet the visual — the driver shows it once placed
// Seed the world transform before the primitive enters the scene. A model
// can become ready+shown between render phases; leaving Cesium's identity
@@ -1075,9 +1081,10 @@ export function createRendering({
(bb.scale || 1) *
distanceScale *
(globalThis.devicePixelRatio || 1);
const wantLarge = bb._gevIconLarge ? glyphDevPx > 56 : glyphDevPx > 76;
if (wantLarge !== !!bb._gevIconLarge) {
bb._gevIconLarge = wantLarge;
const visual = aircraftVisuals.write(bb);
const wantLarge = visual.iconLarge ? glyphDevPx > 56 : glyphDevPx > 76;
if (wantLarge !== !!visual.iconLarge) {
visual.iconLarge = wantLarge;
bb.image = aircraftIcon(
_iconKind(icao24, info?.klass),
wantLarge ? TRACKED_ICON_PX : undefined,
+7
View File
@@ -0,0 +1,7 @@
import { createEntityStore } from '../../data/entityRecords.js';
/**
* What the installations layer last drew for a retained marker entity, so an
* unchanged record leaves it alone: `heightM` (surface height) and `selected`.
*/
export const installationMarkers = createEntityStore();
+10 -6
View File
@@ -3,6 +3,7 @@ import * as Cesium from 'cesium';
import { t } from '../../i18n/index.js';
import { LAYER_ID, MAX_RENDERED, COLOR_BY_CLASS } from './policy.js';
import { entityRecord } from '../../data/entityRecords.js';
import { installationMarkers } from './markerState.js';
export function createRendering({
state: layerState,
@@ -219,13 +220,14 @@ export function createRendering({
: null;
if (retained) {
// Same geometry: update the marker in place (selection, floor height).
if (retained.gevInstallationHeightM !== surfaceHeightM) {
retained.gevInstallationHeightM = surfaceHeightM;
const marker = installationMarkers.write(retained);
if (marker.heightM !== surfaceHeightM) {
marker.heightM = surfaceHeightM;
retained.position = displayPosition;
entityRecord(retained).displayPosition = () => displayPosition;
}
if (retained.gevInstallationSelected !== selectedRecord) {
retained.gevInstallationSelected = selectedRecord;
if (marker.selected !== selectedRecord) {
marker.selected = selectedRecord;
retained.point.pixelSize = selectedRecord ? 13 : 9;
retained.point.color = selectedRecord ? Cesium.Color.WHITE : color;
}
@@ -268,8 +270,10 @@ export function createRendering({
outline.installationId = record.id;
entityIds.push(outline.id);
}
entity.gevInstallationHeightM = surfaceHeightM;
entity.gevInstallationSelected = selectedRecord;
Object.assign(installationMarkers.write(entity), {
heightM: surfaceHeightM,
selected: selectedRecord,
});
entityRecord(entity).trackedId = `installations:${record.id}`;
entityRecord(entity).displayPosition = () => displayPosition;
entityRecord(entity).labelModel = {
+6 -4
View File
@@ -15,6 +15,7 @@ import {
MODEL_PROX_KEEP_M,
} from '../flights/policy.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
import { aircraftVisuals } from '../aircraft/visualState.js';
const specKey = (spec) => `${spec.url}@${spec.scale}`;
@@ -78,7 +79,7 @@ export function createLocalAdsbModels({
const spec = civilAircraftModelSpec(marker.klass);
const key = specKey(spec);
const current = models.get(id);
if (current && current._gevSpecKey === key) return;
if (current && aircraftVisuals.read(current)?.specKey === key) return;
if (current) release(id);
if (pending.has(id) || models.size + pending.size >= cap) return;
const loadGeneration = generation;
@@ -113,8 +114,9 @@ export function createLocalAdsbModels({
return;
}
model.id = id;
model._gevSpecKey = key;
model._gevBellyM = spec.bellyM;
const visual = aircraftVisuals.write(model);
visual.specKey = key;
visual.bellyM = spec.bellyM;
// Admitted, not yet the visual: placement turns it on.
model.show = false;
collection.add(model);
@@ -182,7 +184,7 @@ export function createLocalAdsbModels({
return Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
height + (model?._gevBellyM || 0),
height + (aircraftVisuals.read(model)?.bellyM || 0),
Cesium.Ellipsoid.WGS84,
scratchGround,
);
+17 -10
View File
@@ -54,6 +54,7 @@ import {
COURSE_MAX_DPS,
} from './policy.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
import { aircraftVisuals } from '../aircraft/visualState.js';
export function createRendering({
flightState,
@@ -170,7 +171,7 @@ export function createRendering({
const meta = flightState.records.data.get(icao24);
bb.image = aircraftIcon(
_iconKind(icao24, meta?.klass),
bb._gevIconLarge ? TRACKED_ICON_PX : undefined,
aircraftVisuals.read(bb)?.iconLarge ? TRACKED_ICON_PX : undefined,
);
bb.width = icao24 === flightState._trackedIcao ? 24 : 20;
bb.height = icao24 === flightState._trackedIcao ? 24 : 20;
@@ -400,7 +401,7 @@ export function createRendering({
const key = _specKeyFor(flightState.records.data.get(icao24)?.klass);
const current = flightState._models.get(icao24);
if (
(current && current._gevSpecKey !== key) ||
(current && aircraftVisuals.read(current)?.specKey !== key) ||
(!current && flightState._modelPending.has(icao24))
) {
const bb = flightState._billboards.get(icao24);
@@ -410,7 +411,7 @@ export function createRendering({
if (
icao24 === flightState._trackedIcao &&
flightState._trackedModel &&
flightState._trackedModel._gevSpecKey !== key
aircraftVisuals.read(flightState._trackedModel)?.specKey !== key
) {
_releaseTrackedModel();
}
@@ -433,7 +434,11 @@ export function createRendering({
const batch = flightState._irReloadQueue.splice(0, IR_RELOAD_BATCH);
for (const icao of batch) {
const model = flightState._models.get(icao);
if (!model || model._gevIrBoost === flightState._irBoost) continue; // already right state
if (
!model ||
aircraftVisuals.read(model)?.irBoost === flightState._irBoost
)
continue; // already right state
const bb = flightState._billboards.get(icao);
if (bb && icao !== flightState._trackedIcao) bb.show = true;
_releaseModel(icao);
@@ -536,8 +541,9 @@ export function createRendering({
// Keep the pick identity explicit on the resolved primitive. This also
// protects injected/custom loaders that do not copy the creation option.
model.id = icao24;
model._gevSpecKey = specKey; // class-change sync compares against this
model._gevIrBoost = loadIrBoost; // boost-flip reload queue compares against this
const visual = aircraftVisuals.write(model);
visual.specKey = specKey; // class-change sync compares against this
visual.irBoost = loadIrBoost; // boost-flip reload queue compares against this
// Admitted, not yet the visual. Cesium's default is show=true, which would let
// an unplaced primitive claim ownership from the billboard for the frames
// between admission and the next fleet tick (and draw at the identity matrix,
@@ -725,7 +731,7 @@ export function createRendering({
// Assign after resolution as well as in the creation options so the
// standalone primitive always exposes the tracked aircraft pick id.
m.id = flightState._trackedIcao;
m._gevSpecKey = trackedKey; // class-change sync compares against this
aircraftVisuals.write(m).specKey = trackedKey; // class-change sync compares against this
m.show = false; // admitted, not yet the visual — the driver shows it once placed
// Seed the world transform before the primitive enters the scene. A model
// can become ready+shown between render phases; leaving Cesium's identity
@@ -1005,9 +1011,10 @@ export function createRendering({
(bb.scale || 1) *
distanceScale *
(globalThis.devicePixelRatio || 1);
const wantLarge = bb._gevIconLarge ? glyphDevPx > 56 : glyphDevPx > 76;
if (wantLarge !== !!bb._gevIconLarge) {
bb._gevIconLarge = wantLarge;
const visual = aircraftVisuals.write(bb);
const wantLarge = visual.iconLarge ? glyphDevPx > 56 : glyphDevPx > 76;
if (wantLarge !== !!visual.iconLarge) {
visual.iconLarge = wantLarge;
bb.image = aircraftIcon(
_iconKind(icao24, flightState.records.data.get(icao24)?.klass),
wantLarge ? TRACKED_ICON_PX : undefined,