diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 5fbd46610..1450306f9 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -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", diff --git a/scripts/qa-cctv-v2.mjs b/scripts/qa-cctv-v2.mjs index 88e0a5a2c..d315d3163 100644 --- a/scripts/qa-cctv-v2.mjs +++ b/scripts/qa-cctv-v2.mjs @@ -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; diff --git a/src/data/cockpitModels.test.mjs b/src/data/cockpitModels.test.mjs index 8177b7414..fcadcfa51 100644 --- a/src/data/cockpitModels.test.mjs +++ b/src/data/cockpitModels.test.mjs @@ -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'); diff --git a/src/data/nearbyModelHandoff.test.mjs b/src/data/nearbyModelHandoff.test.mjs index 98b32341f..665ef72b5 100644 --- a/src/data/nearbyModelHandoff.test.mjs +++ b/src/data/nearbyModelHandoff.test.mjs @@ -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), }]], }); diff --git a/src/layers/aircraft/visualState.js b/src/layers/aircraft/visualState.js new file mode 100644 index 000000000..b9b1f191c --- /dev/null +++ b/src/layers/aircraft/visualState.js @@ -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(); diff --git a/src/layers/cctv/geometry.js b/src/layers/cctv/geometry.js index 362ad4789..808ddda25 100644 --- a/src/layers/cctv/geometry.js +++ b/src/layers/cctv/geometry.js @@ -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; diff --git a/src/layers/flights/rendering.js b/src/layers/flights/rendering.js index d4075a565..147d730ad 100644 --- a/src/layers/flights/rendering.js +++ b/src/layers/flights/rendering.js @@ -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, diff --git a/src/layers/installations/markerState.js b/src/layers/installations/markerState.js new file mode 100644 index 000000000..7738011ac --- /dev/null +++ b/src/layers/installations/markerState.js @@ -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(); diff --git a/src/layers/installations/rendering.js b/src/layers/installations/rendering.js index 94c679b48..e1a46f700 100644 --- a/src/layers/installations/rendering.js +++ b/src/layers/installations/rendering.js @@ -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 = { diff --git a/src/layers/localAdsb/models.js b/src/layers/localAdsb/models.js index 55fc2d11a..c9b9a03ae 100644 --- a/src/layers/localAdsb/models.js +++ b/src/layers/localAdsb/models.js @@ -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, ); diff --git a/src/layers/military/rendering.js b/src/layers/military/rendering.js index 504d79c80..f65103887 100644 --- a/src/layers/military/rendering.js +++ b/src/layers/military/rendering.js @@ -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,