Read flight and military billboard state from Core's mirror

Flights and military now add their billboards through addBillboard and
write show, position, scale, rotation, size and distance scaling through
setBillboard, which keeps Core's copy in src/data/billboardMirror.js.
Their reads go to billboardState instead of the Cesium billboard, so the
layers no longer read back what they pushed to the renderer.

A mirrored billboard needs an explicit width and height, because unset
Cesium reports the loaded image's size. Colour is not mirrored yet: the
cyber-sonar theme writes it too and tells the writers apart by the live
value, so it moves with the effects port.
This commit is contained in:
Sameh Khamis committed 2026-11-03 10:56:08 -08:00
1 parent a40e5c334c
commit 1cfff5803a
23 files changed
+424 -172

No files matched your search

+4
View File
@@ -1315,6 +1315,7 @@
"src/layers/flights/policy.js", "src/layers/flights/policy.js",
"src/layers/flights/queries.js", "src/layers/flights/queries.js",
"src/layers/flights/rendering.js", "src/layers/flights/rendering.js",
"src/data/billboardMirror.js",
"src/layers/aircraft/visualState.js", "src/layers/aircraft/visualState.js",
"src/layers/flights/state.js", "src/layers/flights/state.js",
"src/layers/flights/testing.js", "src/layers/flights/testing.js",
@@ -1375,6 +1376,7 @@
"src/layers/military/policy.js", "src/layers/military/policy.js",
"src/layers/military/queries.js", "src/layers/military/queries.js",
"src/layers/military/rendering.js", "src/layers/military/rendering.js",
"src/data/billboardMirror.js",
"src/layers/aircraft/visualState.js", "src/layers/aircraft/visualState.js",
"src/layers/military/state.js", "src/layers/military/state.js",
"src/layers/military/testing.js", "src/layers/military/testing.js",
@@ -2320,6 +2322,7 @@
"src/data/aircraftMeta.js", "src/data/aircraftMeta.js",
"src/data/aircraftNearbyPolicy.js", "src/data/aircraftNearbyPolicy.js",
"src/data/aircraftRecession.js", "src/data/aircraftRecession.js",
"src/data/billboardMirror.js",
"src/data/aisLiveVessels.js", "src/data/aisLiveVessels.js",
"src/data/alprCameras.js", "src/data/alprCameras.js",
"src/data/analystEngine.js", "src/data/analystEngine.js",
@@ -3284,6 +3287,7 @@
"src/data/aircraftMeta.js", "src/data/aircraftMeta.js",
"src/data/aircraftNearbyPolicy.js", "src/data/aircraftNearbyPolicy.js",
"src/data/aircraftRecession.js", "src/data/aircraftRecession.js",
"src/data/billboardMirror.js",
"src/data/cctvCards.js", "src/data/cctvCards.js",
"src/data/cctvFootprint.js", "src/data/cctvFootprint.js",
"src/data/cctvGizmo.js", "src/data/cctvGizmo.js",
+5 -3
View File
@@ -5,6 +5,7 @@
* with the locked NearFarScalar. It never edits that scalar, culls a contact, * with the locked NearFarScalar. It never edits that scalar, culls a contact,
* or allows alpha to reach zero. * or allows alpha to reach zero.
*/ */
import { billboardState, setBillboard } from './billboardMirror.js';
export const DEFAULT_AIRCRAFT_RECESSION_PARAMS = Object.freeze({ export const DEFAULT_AIRCRAFT_RECESSION_PARAMS = Object.freeze({
startLimbRatio: 0.5, startLimbRatio: 0.5,
@@ -220,11 +221,12 @@ export function applyAircraftBillboardTreatment({
const alpha = baseAlpha * composedTreatment; const alpha = baseAlpha * composedTreatment;
let scaleWrites = 0; let scaleWrites = 0;
let alphaWrites = 0; let alphaWrites = 0;
const pushedScale = billboardState(billboard)?.scale;
if ( if (
!Number.isFinite(billboard.scale) || !Number.isFinite(pushedScale) ||
Math.abs(billboard.scale - scale) > tuning.writeEpsilon Math.abs(pushedScale - scale) > tuning.writeEpsilon
) { ) {
billboard.scale = scale; setBillboard(billboard, 'scale', scale);
scaleWrites = 1; scaleWrites = 1;
} }
if ( if (
+5 -4
View File
@@ -7,6 +7,7 @@ import {
applyAircraftModelTreatment, applyAircraftModelTreatment,
cameraLimbDistanceM, cameraLimbDistanceM,
} from './aircraftRecession.js'; } from './aircraftRecession.js';
import { mirrorBillboard } from './billboardMirror.js';
const params = { ...DEFAULT_AIRCRAFT_RECESSION_PARAMS }; const params = { ...DEFAULT_AIRCRAFT_RECESSION_PARAMS };
const cameraHeightM = 1_000_000; const cameraHeightM = 1_000_000;
@@ -68,7 +69,7 @@ test('globe-view transition eases to identity without a threshold pop', () => {
test('aircraft billboard wire writes scale and alpha only for the far treatment', () => { test('aircraft billboard wire writes scale and alpha only for the far treatment', () => {
const color = { withAlpha: (alpha) => ({ alpha }) }; const color = { withAlpha: (alpha) => ({ alpha }) };
const near = { scale: 1.2, color: { alpha: 0.8 } }; const near = mirrorBillboard({ scale: 1.2, color: { alpha: 0.8 } });
const nearResult = applyAircraftBillboardTreatment({ const nearResult = applyAircraftBillboardTreatment({
billboard: near, billboard: near,
baseScale: 1.2, baseScale: 1.2,
@@ -82,7 +83,7 @@ test('aircraft billboard wire writes scale and alpha only for the far treatment'
assert.equal(nearResult.scaleWrites, 0); assert.equal(nearResult.scaleWrites, 0);
assert.equal(nearResult.alphaWrites, 0); assert.equal(nearResult.alphaWrites, 0);
const far = { scale: 1.2, color: { alpha: 0.8 } }; const far = mirrorBillboard({ scale: 1.2, color: { alpha: 0.8 } });
const farResult = applyAircraftBillboardTreatment({ const farResult = applyAircraftBillboardTreatment({
billboard: far, billboard: far,
baseScale: 1.2, baseScale: 1.2,
@@ -101,7 +102,7 @@ test('aircraft billboard wire writes scale and alpha only for the far treatment'
test('combined focus and haze product is clamped at the composed alpha floor', () => { test('combined focus and haze product is clamped at the composed alpha floor', () => {
const color = { withAlpha: (alpha) => ({ alpha }) }; const color = { withAlpha: (alpha) => ({ alpha }) };
const billboard = { scale: 0.45, color: { alpha: 1 } }; const billboard = mirrorBillboard({ scale: 0.45, color: { alpha: 1 } });
const result = applyAircraftBillboardTreatment({ const result = applyAircraftBillboardTreatment({
billboard, billboard,
baseScale: 1, baseScale: 1,
@@ -118,7 +119,7 @@ test('combined focus and haze product is clamped at the composed alpha floor', (
test('applyAircraftBillboardTreatment returns its documented module singleton', () => { test('applyAircraftBillboardTreatment returns its documented module singleton', () => {
const color = { withAlpha: (alpha) => ({ alpha }) }; const color = { withAlpha: (alpha) => ({ alpha }) };
const billboard = { scale: 1, color: { alpha: 1 } }; const billboard = mirrorBillboard({ scale: 1, color: { alpha: 1 } });
const input = { const input = {
billboard, billboard,
baseAlpha: 1, baseAlpha: 1,
+106
View File
@@ -0,0 +1,106 @@
/**
* @module billboardMirror
* @description Core's copy of what a layer pushed to its billboards, so the layer
* reads its own state back from Core instead of from the renderer's objects.
*
* A layer adds billboards with {@link addBillboard}, writes the mirrored fields
* with {@link setBillboard}, and reads them with {@link billboardState}. Fields
* outside the mirror (image, colour, eye offset, …) are written to the billboard
* directly. Colour stays out because the cyber-sonar theme writes it too and
* tells the two writers apart by reading the live value.
*/
import * as Cesium from 'cesium';
import { createEntityStore } from './entityRecords.js';
const mirrors = createEntityStore();
/**
* Mirrored fields with Cesium's defaults for a billboard added without them.
* Width and height have none: unset, Cesium reports the loaded image's size,
* which Core cannot know, so {@link addBillboard} requires them.
*/
const DEFAULTS = Object.freeze({
show: true,
scale: 1,
rotation: 0,
width: undefined,
height: undefined,
scaleByDistance: undefined,
});
/** Cesium copies positions and scalers on write; the mirror keeps its own copy too. */
function copyInto(record, field, value) {
if (field === 'position') {
// Cesium keeps its old position when handed none; so does the mirror.
if (!value && record.position) return;
record.position = Cesium.Cartesian3.clone(
value ?? Cesium.Cartesian3.ZERO,
record.position,
);
} else if (field === 'scaleByDistance') {
record.scaleByDistance = value
? Cesium.NearFarScalar.clone(value, record.scaleByDistance)
: undefined;
} else {
record[field] = value;
}
}
function seed(billboard, source) {
const record = mirrors.write(billboard);
copyInto(record, 'position', source.position);
for (const [field, fallback] of Object.entries(DEFAULTS))
copyInto(record, field, source[field] ?? fallback);
return record;
}
/**
* Add a billboard and remember what was pushed.
* @param {Cesium.BillboardCollection} collection
* @param {Object} options Billboard options, as for `collection.add`, with an
* explicit `width` and `height`.
* @returns {Cesium.Billboard}
*/
export function addBillboard(collection, options) {
if (!Number.isFinite(options?.width) || !Number.isFinite(options?.height))
throw new TypeError(
'A mirrored billboard needs an explicit width and height',
);
const billboard = collection.add(options);
seed(billboard, options);
return billboard;
}
/**
* Start mirroring a billboard Core did not add itself (test seams), from its
* current fields.
* @param {Object} billboard
* @returns {Object} The billboard.
*/
export function mirrorBillboard(billboard) {
if (billboard && typeof billboard === 'object') seed(billboard, billboard);
return billboard;
}
/**
* Write one mirrored field to the billboard and to Core's copy.
* @param {Object} billboard
* @param {'show'|'position'|'scale'|'rotation'|'width'|'height'|'scaleByDistance'} field
* @param {*} value
*/
export function setBillboard(billboard, field, value) {
copyInto(mirrors.write(billboard), field, value);
billboard[field] = value;
}
/**
* Core's copy of a billboard's mirrored fields, or null when it has none (or
* there is no billboard).
* @param {Object|null|undefined} billboard
* @returns {{ show: boolean, position: Cesium.Cartesian3, scale: number,
* rotation: number, width?: number, height?: number,
* scaleByDistance?: Cesium.NearFarScalar }|null}
*/
export function billboardState(billboard) {
return mirrors.read(billboard);
}
+74
View File
@@ -0,0 +1,74 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import {
addBillboard,
billboardState,
mirrorBillboard,
setBillboard,
} from './billboardMirror.js';
const SIZE = Object.freeze({ width: 20, height: 20 });
function fakeCollection() {
return { add: (options) => ({ ...options }) };
}
test('an added billboard is mirrored with Cesium defaults for unset fields', () => {
const position = Cesium.Cartesian3.fromDegrees(10, 20, 30);
const bb = addBillboard(fakeCollection(), { position, width: 20, height: 24 });
const state = billboardState(bb);
assert.equal(state.show, true);
assert.equal(state.scale, 1);
assert.equal(state.rotation, 0);
assert.equal(state.width, 20);
assert.equal(state.height, 24);
assert.ok(Cesium.Cartesian3.equals(state.position, position));
assert.notEqual(state.position, position, 'the mirror keeps its own copy');
});
test('writes reach both the billboard and the mirror', () => {
const bb = addBillboard(fakeCollection(), SIZE);
setBillboard(bb, 'show', false);
setBillboard(bb, 'scale', 0.5);
assert.equal(bb.show, false);
assert.equal(billboardState(bb).show, false);
assert.equal(billboardState(bb).scale, 0.5);
});
test('a reused scratch position does not move the mirror', () => {
const bb = addBillboard(fakeCollection(), SIZE);
const scratch = Cesium.Cartesian3.fromDegrees(1, 2, 3);
setBillboard(bb, 'position', scratch);
const written = Cesium.Cartesian3.clone(scratch);
Cesium.Cartesian3.fromDegrees(50, 50, 50, undefined, scratch);
assert.ok(Cesium.Cartesian3.equals(billboardState(bb).position, written));
});
test('writing no position keeps the last one, as Cesium does', () => {
const position = Cesium.Cartesian3.fromDegrees(1, 2, 3);
const bb = addBillboard(fakeCollection(), { ...SIZE, position });
setBillboard(bb, 'position', undefined);
assert.ok(Cesium.Cartesian3.equals(billboardState(bb).position, position));
});
test('the mirror answers from Core, not from the billboard', () => {
const bb = addBillboard(fakeCollection(), { ...SIZE, show: true });
bb.show = false; // a write that bypassed the mirror
assert.equal(billboardState(bb).show, true);
});
test('a billboard Core did not add can be mirrored from its fields', () => {
const bb = mirrorBillboard({ show: false, scale: 2 });
assert.equal(billboardState(bb).show, false);
assert.equal(billboardState(bb).scale, 2);
assert.ok(Cesium.Cartesian3.equals(billboardState(bb).position, Cesium.Cartesian3.ZERO));
assert.equal(billboardState(null), null);
assert.equal(mirrorBillboard(null), null);
});
test('a billboard without explicit dimensions is refused', () => {
// Unset, Cesium reports the image's size, which the mirror cannot know.
assert.throws(() => addBillboard(fakeCollection(), {}), TypeError);
assert.throws(() => addBillboard(fakeCollection(), { width: 20 }), TypeError);
});
+2 -2
View File
@@ -90,11 +90,11 @@ for (const layer of LAYERS) {
// operator explicitly converted into a TR-3B. // operator explicitly converted into a TR-3B.
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*\)/, 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'); 'near contacts and model fallbacks retain the class-derived aircraft silhouette');
assert.match(source, /bb\.rotation\s*=\s*0;/, assert.match(source, /setBillboard\(\s*bb,\s*'rotation',\s*0\s*\);/,
'far dots are reset to a rotation-free presentation'); 'far dots are reset to a rotation-free presentation');
assert.match(source, /\(\s*!(?:flightState\.)?_cockpitContactMode\s*\|\|\s*isCockpitNear,?\s*\)\s*&&\s*\(\s*doRotations\s*\|\|\s*revealed,?\s*\)/, assert.match(source, /\(\s*!(?:flightState\.)?_cockpitContactMode\s*\|\|\s*isCockpitNear,?\s*\)\s*&&\s*\(\s*doRotations\s*\|\|\s*revealed,?\s*\)/,
'near 2D silhouettes continue to receive projected course'); 'near 2D silhouettes continue to receive projected course');
assert.match(source, /if\s*\(\s*bb\.show,?\s*\)\s*bb\.show\s*=\s*false;\s*\/\/\s*hand\s*off\s*ONLY\s*once\s*the\s*model\s*renders/, assert.match(source, /if\s*\(\s*billboardState\(bb\)\.show,?\s*\)\s*setBillboard\(\s*bb,\s*'show',\s*false\s*\);\s*\/\/\s*hand\s*off\s*ONLY\s*once\s*the\s*model\s*renders/,
'the gap-proof billboard-to-model handoff remains intact'); 'the gap-proof billboard-to-model handoff remains intact');
}); });
+4 -1
View File
@@ -3,6 +3,7 @@ import test from 'node:test';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { routePlausible } from './routePlausible.js'; import { routePlausible } from './routePlausible.js';
import { createTracking } from '../layers/flights/tracking.js'; import { createTracking } from '../layers/flights/tracking.js';
import { addBillboard } from './billboardMirror.js';
const SFO = { lat: 37.6188, lon: -122.3754, code: 'SFO' }; const SFO = { lat: 37.6188, lon: -122.3754, code: 'SFO' };
const LAX = { lat: 33.9416, lon: -118.4085, code: 'LAX' }; const LAX = { lat: 33.9416, lon: -118.4085, code: 'LAX' };
@@ -156,8 +157,10 @@ test('the actual native tracked label hides the wrong leg and keeps a healthy ro
}); });
flightState._billboards.set( flightState._billboards.set(
id, id,
collection.add({ addBillboard(collection, {
id, id,
width: 20,
height: 20,
position: Cesium.Cartesian3.fromDegrees( position: Cesium.Cartesian3.fromDegrees(
position.lon, position.lon,
position.lat, position.lat,
+1 -1
View File
@@ -245,7 +245,7 @@ test('both flight layers keep a converted contact 2D and visible (render invaria
// 2. The billboard is never hidden by that suppression — it must keep // 2. The billboard is never hidden by that suppression — it must keep
// satisfying the getNearby/getDetectableObjects visibility guards, so a // satisfying the getNearby/getDetectableObjects visibility guards, so a
// converted contact still works in Contacts and Cockpit. // converted contact still works in Contacts and Cockpit.
assert.match(source, /if\s*\(\s*bb\s*&&\s*id\s*!==\s*(?:flightState\.)?_trackedIcao,?\s*\)\s*bb\.show\s*=\s*true;|if\s*\(\s*modelled\s*&&\s*id\s*!==\s*(?:flightState\.)?_trackedIcao,?\s*\)\s*modelled\.show\s*=\s*true;/, assert.match(source, /if\s*\(\s*(bb|modelled)\s*&&\s*id\s*!==\s*(?:flightState\.)?_trackedIcao,?\s*\)\s*setBillboard\(\s*\1,\s*'show',\s*true,?\s*\);/,
`${name}: converting restores the billboard the model handoff had hidden`); `${name}: converting restores the billboard the model handoff had hidden`);
// 3. Every aircraftIcon() CALL SITE routes through the kind resolver, so no // 3. Every aircraftIcon() CALL SITE routes through the kind resolver, so no
+7 -6
View File
@@ -45,6 +45,7 @@ import militaryFlightsLayer, {
} from './militaryFlights.js'; } from './militaryFlights.js';
import { clearTr3bRegistry, setTr3b } from './tr3bRegistry.js'; import { clearTr3bRegistry, setTr3b } from './tr3bRegistry.js';
import { reportMeshFloorCell, _clearMeshFloorCellsForTest } from './groundFloor.js'; import { reportMeshFloorCell, _clearMeshFloorCellsForTest } from './groundFloor.js';
import { setBillboard } from './billboardMirror.js';
// Sample altitudes are expressed RELATIVE to the band rather than as absolute // Sample altitudes are expressed RELATIVE to the band rather than as absolute
// offsets. The thresholds have already been retuned once (1_000_000 → 150_000 // offsets. The thresholds have already been retuned once (1_000_000 → 150_000
@@ -351,7 +352,7 @@ for (const fixture of LAYERS) {
onGround: true, onGround: true,
models: [[icao24, model]], models: [[icao24, model]],
}); });
billboard.show = false; setBillboard(billboard, 'show', false);
let ownsVisual = fixture.driveFleetModel({ icao24, position, course: 92 }); let ownsVisual = fixture.driveFleetModel({ icao24, position, course: 92 });
assert.equal(ownsVisual, false, 'cold terrain cannot transfer ownership to 3D'); assert.equal(ownsVisual, false, 'cold terrain cannot transfer ownership to 3D');
@@ -487,7 +488,7 @@ for (const fixture of LAYERS) {
onGround: true, onGround: true,
models: [[icao24, model]], models: [[icao24, model]],
}); });
billboard.show = false; setBillboard(billboard, 'show', false);
const atA = Cesium.Cartesian3.fromDegrees(A_LON, LAT, 0); const atA = Cesium.Cartesian3.fromDegrees(A_LON, LAT, 0);
const atB = Cesium.Cartesian3.fromDegrees(B_LON, LAT, 0); const atB = Cesium.Cartesian3.fromDegrees(B_LON, LAT, 0);
@@ -622,7 +623,7 @@ for (const fixture of LAYERS) {
0, 0,
1e-6, 1e-6,
), 'a hidden tracked model cannot steal the detection anchor'); ), 'a hidden tracked model cannot steal the detection anchor');
billboard.show = false; // tracking hides the fleet sprite; the tracked entity remains the 2D owner setBillboard(billboard, 'show', false); // tracking hides the fleet sprite; the tracked entity remains the 2D owner
const loadingNearby = fixture.layer.getNearby(billboard.position, 1_000, 50) const loadingNearby = fixture.layer.getNearby(billboard.position, 1_000, 50)
.find((object) => object.icao24 === (fixture.name === 'flights' ? 'a0f001' : 'b0f001')); .find((object) => object.icao24 === (fixture.name === 'flights' ? 'a0f001' : 'b0f001'));
assert.ok(loadingNearby, 'the tracked contact remains in proximity while its model loads'); assert.ok(loadingNearby, 'the tracked contact remains in proximity while its model loads');
@@ -661,7 +662,7 @@ for (const fixture of LAYERS) {
viewer.tileset.tilesLoaded = false; viewer.tileset.tilesLoaded = false;
sampledHeight = undefined; sampledHeight = undefined;
billboard.position = Cesium.Cartesian3.fromDegrees(-97.71, 30.21, -500); setBillboard(billboard, 'position', Cesium.Cartesian3.fromDegrees(-97.71, 30.21, -500));
fixture.driveTrackedModel(); fixture.driveTrackedModel();
assert.equal(model.show, true, assert.equal(model.show, true,
'a successful cached snap survives a poll-time vertical datum change'); 'a successful cached snap survives a poll-time vertical datum change');
@@ -671,7 +672,7 @@ for (const fixture of LAYERS) {
// Taxi past the resample threshold with the tiles gone: the tracked model // Taxi past the resample threshold with the tiles gone: the tracked model
// holds its last measured floor rather than handing the operator's own // holds its last measured floor rather than handing the operator's own
// selection back to a 2D glyph for a retry backoff. // selection back to a 2D glyph for a retry backoff.
billboard.position = Cesium.Cartesian3.fromDegrees(-97.709, 30.21, -500); setBillboard(billboard, 'position', Cesium.Cartesian3.fromDegrees(-97.709, 30.21, -500));
fixture.driveTrackedModel(); fixture.driveTrackedModel();
assert.equal(model.show, true, assert.equal(model.show, true,
'a taxi-invalidated tracked snap is HELD through the resample outage'); 'a taxi-invalidated tracked snap is HELD through the resample outage');
@@ -680,7 +681,7 @@ for (const fixture of LAYERS) {
assert.equal(sampleCalls, 1, 'the hold costs no extra synchronous sample'); assert.equal(sampleCalls, 1, 'the hold costs no extra synchronous sample');
// Past the drift bound the memory is released and the gate takes over. // Past the drift bound the memory is released and the gate takes over.
billboard.position = Cesium.Cartesian3.fromDegrees(-97.707, 30.21, -500); setBillboard(billboard, 'position', Cesium.Cartesian3.fromDegrees(-97.707, 30.21, -500));
fixture.driveTrackedModel(); fixture.driveTrackedModel();
assert.equal(model.show, false, assert.equal(model.show, false,
'past HELD_SNAP_MAX_DRIFT_M the hold is released and the model is withheld'); 'past HELD_SNAP_MAX_DRIFT_M the hold is released and the model is withheld');
+5 -6
View File
@@ -9,6 +9,7 @@ import {
ENRICH_AMBIENT_REFILL_WINDOW_MS, ENRICH_AMBIENT_REFILL_WINDOW_MS,
ENRICH_AMBIENT_PER_SWEEP, ENRICH_AMBIENT_PER_SWEEP,
} from './policy.js'; } from './policy.js';
import { billboardState } from '../../data/billboardMirror.js';
export function createEnrichment({ export function createEnrichment({
flightState, flightState,
@@ -199,11 +200,9 @@ export function createEnrichment({
if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session if (flightState._enrichSeen.has(`t:${icao24}`)) continue; // answered / queued / negative this session
if (!/^[0-9a-f]{6}$/i.test(icao24)) continue; // adsbdb keys are 6-char hex only if (!/^[0-9a-f]{6}$/i.test(icao24)) continue; // adsbdb keys are 6-char hex only
if (flightState.records.data.get(icao24)?.onGround) continue; // ground traffic never spends ambient budget (click-to-enrich still works) if (flightState.records.data.get(icao24)?.onGround) continue; // ground traffic never spends ambient budget (click-to-enrich still works)
if (!bb.position || !occluder.isPointVisible(bb.position)) continue; // beyond the limb const { position } = billboardState(bb);
Cesium.Cartesian3.clone( if (!position || !occluder.isPointVisible(position)) continue; // beyond the limb
bb.position, Cesium.Cartesian3.clone(position, flightState._scratchModelBS.center);
flightState._scratchModelBS.center,
);
if ( if (
cull.computeVisibility(flightState._scratchModelBS) === cull.computeVisibility(flightState._scratchModelBS) ===
Cesium.Intersect.OUTSIDE Cesium.Intersect.OUTSIDE
@@ -211,7 +210,7 @@ export function createEnrichment({
continue; // off-screen continue; // off-screen
cand.push([ cand.push([
icao24, icao24,
Cesium.Cartesian3.distanceSquared(camPos, bb.position), Cesium.Cartesian3.distanceSquared(camPos, position),
]); ]);
} }
cand.sort((a, b) => a[1] - b[1]); // nearest first — what the user is looking at resolves first cand.sort((a, b) => a[1] - b[1]); // nearest first — what the user is looking at resolves first
+13 -8
View File
@@ -1,6 +1,11 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { aircraftIcon } from '../../data/aircraftIcons.js'; import { aircraftIcon } from '../../data/aircraftIcons.js';
import { FOCUS_EVIDENCE_DEV } from './policy.js'; import { FOCUS_EVIDENCE_DEV } from './policy.js';
import {
addBillboard,
billboardState,
setBillboard,
} from '../../data/billboardMirror.js';
export function createEvidence({ export function createEvidence({
flightState, flightState,
@@ -83,7 +88,7 @@ export function createEvidence({
flightState.records.data.set(id, meta); flightState.records.data.set(id, meta);
flightState._cullPositions.delete(id); flightState._cullPositions.delete(id);
flightState._focusEvidenceIds.add(id); flightState._focusEvidenceIds.add(id);
const bb = flightState._billboardCollection.add({ const bb = addBillboard(flightState._billboardCollection, {
position, position,
image: aircraftIcon(parts.rendering._iconKind(id, klass)), image: aircraftIcon(parts.rendering._iconKind(id, klass)),
width: 20, width: 20,
@@ -119,7 +124,7 @@ export function createEvidence({
const position = _focusEvidencePosition(record); const position = _focusEvidencePosition(record);
const bb = flightState._billboards.get(id); const bb = flightState._billboards.get(id);
if (!position || !bb) continue; if (!position || !bb) continue;
bb.position = position; setBillboard(bb, 'position', position);
const meta = flightState.records.data.get(id); const meta = flightState.records.data.get(id);
if (meta) { if (meta) {
if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg; if (Number.isFinite(record.trackDeg)) meta.true_track = record.trackDeg;
@@ -139,23 +144,23 @@ export function createEvidence({
if (!FOCUS_EVIDENCE_DEV || !flightState._viewer) return []; if (!FOCUS_EVIDENCE_DEV || !flightState._viewer) return [];
return [...flightState._focusEvidenceIds].map((id) => { return [...flightState._focusEvidenceIds].map((id) => {
const bb = flightState._billboards.get(id); const bb = flightState._billboards.get(id);
const screen = bb?.position const screen = billboardState(bb)?.position
? Cesium.SceneTransforms.worldToWindowCoordinates( ? Cesium.SceneTransforms.worldToWindowCoordinates(
flightState._viewer.scene, flightState._viewer.scene,
bb.position, billboardState(bb).position,
) )
: null; : null;
return { return {
id, id,
show: bb?.show === true, show: billboardState(bb)?.show === true,
scale: bb?.scale ?? null, scale: billboardState(bb)?.scale ?? null,
alpha: bb?.color?.alpha ?? null, alpha: bb?.color?.alpha ?? null,
x: screen?.x ?? null, x: screen?.x ?? null,
y: screen?.y ?? null, y: screen?.y ?? null,
cameraDistanceM: bb?.position cameraDistanceM: billboardState(bb)?.position
? Cesium.Cartesian3.distance( ? Cesium.Cartesian3.distance(
flightState._viewer.camera.positionWC, flightState._viewer.camera.positionWC,
bb.position, billboardState(bb).position,
) )
: null, : null,
}; };
+5 -2
View File
@@ -35,6 +35,7 @@ import {
DISPLAY_CORRIDOR_CELL_BUDGET, DISPLAY_CORRIDOR_CELL_BUDGET,
DISPLAY_CORRIDOR_FAIR_SHARE, DISPLAY_CORRIDOR_FAIR_SHARE,
} from './policy.js'; } from './policy.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createMotion({ export function createMotion({
flightState, flightState,
@@ -109,7 +110,7 @@ export function createMotion({
if (newest) if (newest)
newest.position = Cesium.Cartesian3.clone(position, newest.position); newest.position = Cesium.Cartesian3.clone(position, newest.position);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
if (bb) bb.position = position; if (bb) setBillboard(bb, 'position', position);
} }
} }
@@ -131,7 +132,9 @@ export function createMotion({
function _deadReckon(icao24, result, sampledRenderTime) { function _deadReckon(icao24, result, sampledRenderTime) {
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) { if (FOCUS_EVIDENCE_DEV && flightState._focusEvidenceIds.has(icao24)) {
const position = flightState._billboards.get(icao24)?.position; const position = billboardState(
flightState._billboards.get(icao24),
)?.position;
flightState._drCourseDeg = info?.true_track || 0; flightState._drCourseDeg = info?.true_track || 0;
flightState._drSpeedMps = info?.velocity || 0; flightState._drSpeedMps = info?.velocity || 0;
flightState._drCourseHold = flightState._drCourseHold =
+12 -8
View File
@@ -16,6 +16,7 @@ import {
FLEET_DR_INTERVAL_MS, FLEET_DR_INTERVAL_MS,
} from './policy.js'; } from './policy.js';
import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js'; import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createQueries({ export function createQueries({
flightState, flightState,
@@ -134,7 +135,8 @@ export function createQueries({
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const basePos = const basePos =
parts.motion._deadReckon(icao24) || (bb ? bb.position : null); parts.motion._deadReckon(icao24) ||
(bb ? billboardState(bb).position : null);
if (!basePos) return null; if (!basePos) return null;
const displayed = displayedKinematics({ const displayed = displayedKinematics({
derivedSpeedMps: flightState._drSpeedMps, derivedSpeedMps: flightState._drSpeedMps,
@@ -336,7 +338,8 @@ export function createQueries({
// here would double-image the tracked plane when 3D is turned off mid-track. // here would double-image the tracked plane when 3D is turned off mid-track.
if (flightState._billboardCollection) if (flightState._billboardCollection)
for (const [icao, bb] of flightState._billboards) { for (const [icao, bb] of flightState._billboards) {
if (icao !== flightState._trackedIcao) bb.show = true; if (icao !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
} }
} }
} }
@@ -454,7 +457,8 @@ export function createQueries({
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden. // doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
const model = flightState._models.get(icao24) ?? null; const model = flightState._models.get(icao24) ?? null;
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24, model); const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24, model);
if (!isTracked && !bb.show && !modelOwnsVisual) continue; if (!isTracked && !billboardState(bb).show && !modelOwnsVisual)
continue;
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
let object = flightState._detectionObjects.get(icao24); let object = flightState._detectionObjects.get(icao24);
if (!object) { if (!object) {
@@ -476,7 +480,7 @@ export function createQueries({
? parts.rendering._modelSpec(info?.klass) ? parts.rendering._modelSpec(info?.klass)
: null; : null;
const pos = isTracked const pos = isTracked
? parts.motion._trackedVisualCached() || bb.position ? parts.motion._trackedVisualCached() || billboardState(bb).position
: modelOwnsVisual : modelOwnsVisual
? modelVisualAnchor( ? modelVisualAnchor(
model.modelMatrix, model.modelMatrix,
@@ -486,7 +490,7 @@ export function createQueries({
: spec.scale, : spec.scale,
object._weldPos || (object._weldPos = new Cesium.Cartesian3()), object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
) )
: bb.position; : billboardState(bb).position;
if (!pos) continue; if (!pos) continue;
object.position = pos; object.position = pos;
object.skipLabel = isTracked; object.skipLabel = isTracked;
@@ -600,7 +604,7 @@ export function createQueries({
if ( if (
!aircraftIncludedInNearby({ !aircraftIncludedInNearby({
isTracked, isTracked,
billboardShown: bb.show, billboardShown: billboardState(bb).show,
modelRendering: parts.rendering._modelOwnsVisual(icao24), modelRendering: parts.rendering._modelOwnsVisual(icao24),
includeHidden, includeHidden,
}) })
@@ -610,7 +614,7 @@ 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 || billboardState(bb).position;
if (!pos) continue; if (!pos) continue;
const distance = Cesium.Cartesian3.distance(center, pos); const distance = Cesium.Cartesian3.distance(center, pos);
@@ -692,7 +696,7 @@ export function createQueries({
const result = []; const result = [];
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
const pos = bb.position; const pos = billboardState(bb).position;
if (!pos) continue; if (!pos) continue;
const carto = Cesium.Cartographic.fromCartesian( const carto = Cesium.Cartographic.fromCartesian(
pos, pos,
+18 -6
View File
@@ -3,6 +3,11 @@ import assert from 'node:assert/strict';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { createQueries } from './queries.js'; import { createQueries } from './queries.js';
import { createRendering } from './rendering.js'; import { createRendering } from './rendering.js';
import {
billboardState,
mirrorBillboard,
setBillboard,
} from '../../data/billboardMirror.js';
function fixture() { function fixture() {
const info = { const info = {
@@ -10,7 +15,10 @@ function fixture() {
registration: ' N123 ', registration: ' N123 ',
altitude: 1000, altitude: 1000,
}; };
const bb = { show: true, position: new Cesium.Cartesian3(1, 2, 3) }; const bb = mirrorBillboard({
show: true,
position: new Cesium.Cartesian3(1, 2, 3),
});
const flightState = { const flightState = {
feed: {}, feed: {},
_billboardCollection: { show: true }, _billboardCollection: { show: true },
@@ -48,9 +56,10 @@ test('candidate text follows in-place enrichment without freezing identity or po
const [first] = f.queries.getDetectableObjects(); const [first] = f.queries.getDetectableObjects();
assert.equal(first.id, 'TEST123'); assert.equal(first.id, 'TEST123');
assert.equal(first.sourceId, 'abc123'); assert.equal(first.sourceId, 'abc123');
f.bb.position = new Cesium.Cartesian3(4, 5, 6); setBillboard(f.bb, 'position', new Cesium.Cartesian3(4, 5, 6));
assert.equal(f.queries.getDetectableObjects()[0], first); assert.equal(f.queries.getDetectableObjects()[0], first);
assert.equal(first.position, f.bb.position); assert.equal(first.position, billboardState(f.bb).position);
assert.deepEqual(first.position, new Cesium.Cartesian3(4, 5, 6));
assert.equal(f.formats(), 1); assert.equal(f.formats(), 1);
f.info.callsign = ' NEW456 '; f.info.callsign = ' NEW456 ';
assert.equal(f.queries.getDetectableObjects()[0].id, 'NEW456'); assert.equal(f.queries.getDetectableObjects()[0].id, 'NEW456');
@@ -97,7 +106,7 @@ test('candidate collection resolves models once and keeps handoffs live', (t) =>
scale: 1, scale: 1,
}); });
f.flightState._models.set('abc123', model); f.flightState._models.set('abc123', model);
f.bb.show = false; setBillboard(f.bb, 'show', false);
assert.deepEqual(f.queries.getDetectableObjects(), []); assert.deepEqual(f.queries.getDetectableObjects(), []);
model.ready = true; model.ready = true;
assert.equal(f.queries.getDetectableObjects()[0], candidate); assert.equal(f.queries.getDetectableObjects()[0], candidate);
@@ -111,8 +120,11 @@ test('candidate collection resolves models once and keeps handoffs live', (t) =>
assert.deepEqual(candidate.position, new Cesium.Cartesian3(40, 50, 60)); assert.deepEqual(candidate.position, new Cesium.Cartesian3(40, 50, 60));
model.show = false; model.show = false;
assert.deepEqual(f.queries.getDetectableObjects(), []); assert.deepEqual(f.queries.getDetectableObjects(), []);
f.bb.show = true; setBillboard(f.bb, 'show', true);
assert.equal(f.queries.getDetectableObjects()[0].position, f.bb.position); assert.equal(
f.queries.getDetectableObjects()[0].position,
billboardState(f.bb).position,
);
assert.equal(get.mock.callCount(), 6); assert.equal(get.mock.callCount(), 6);
model.show = true; model.show = true;
assert.equal(f.parts.rendering._modelOwnsVisual('abc123'), true); assert.equal(f.parts.rendering._modelOwnsVisual('abc123'), true);
+47 -38
View File
@@ -39,6 +39,7 @@ import {
} from './policy.js'; } from './policy.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js'; import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
import { aircraftVisuals } from '../aircraft/visualState.js'; import { aircraftVisuals } from '../aircraft/visualState.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createRendering({ export function createRendering({
flightState, flightState,
@@ -151,12 +152,12 @@ export function createRendering({
if (isCockpitContact && !isCockpitNear) { if (isCockpitContact && !isCockpitNear) {
const freshnessAlpha = cyberSonarBaseAlpha(bb); const freshnessAlpha = cyberSonarBaseAlpha(bb);
bb.image = cockpitContactDotImage(); bb.image = cockpitContactDotImage();
bb.width = COCKPIT_CONTACT_SIZE_PX; setBillboard(bb, 'width', COCKPIT_CONTACT_SIZE_PX);
bb.height = COCKPIT_CONTACT_SIZE_PX; setBillboard(bb, 'height', COCKPIT_CONTACT_SIZE_PX);
bb.scale = limbScale; setBillboard(bb, 'scale', limbScale);
bb.scaleByDistance = _cockpitBillboardScaleByDistance(); setBillboard(bb, 'scaleByDistance', _cockpitBillboardScaleByDistance());
bb.color = _cockpitContactColor(icao24).withAlpha(freshnessAlpha); bb.color = _cockpitContactColor(icao24).withAlpha(freshnessAlpha);
bb.rotation = 0; setBillboard(bb, 'rotation', 0);
return; return;
} }
@@ -165,10 +166,14 @@ export function createRendering({
_iconKind(icao24, meta?.klass), _iconKind(icao24, meta?.klass),
aircraftVisuals.read(bb)?.iconLarge ? TRACKED_ICON_PX : undefined, aircraftVisuals.read(bb)?.iconLarge ? TRACKED_ICON_PX : undefined,
); );
bb.width = icao24 === flightState._trackedIcao ? 24 : 20; setBillboard(bb, 'width', icao24 === flightState._trackedIcao ? 24 : 20);
bb.height = icao24 === flightState._trackedIcao ? 24 : 20; setBillboard(bb, 'height', icao24 === flightState._trackedIcao ? 24 : 20);
bb.scale = _fleetBillboardScale(icao24, meta?.klass) * limbScale; setBillboard(
bb.scaleByDistance = _normalBillboardScaleByDistance(); bb,
'scale',
_fleetBillboardScale(icao24, meta?.klass) * limbScale,
);
setBillboard(bb, 'scaleByDistance', _normalBillboardScaleByDistance());
bb.color = _fleetBillboardColor(icao24).withAlpha(cyberSonarBaseAlpha(bb)); bb.color = _fleetBillboardColor(icao24).withAlpha(cyberSonarBaseAlpha(bb));
} }
@@ -324,7 +329,7 @@ export function createRendering({
function _driveFleetModelHandoff(icao24, model, bb, pos, course, beforeShow) { function _driveFleetModelHandoff(icao24, model, bb, pos, course, beforeShow) {
if (!model) { if (!model) {
bb.show = true; setBillboard(bb, 'show', true);
return false; return false;
} }
const displayPos = _modelDisplayPosition( const displayPos = _modelDisplayPosition(
@@ -334,7 +339,7 @@ export function createRendering({
); );
if (!displayPos) { if (!displayPos) {
model.show = false; // no ground evidence → nothing safe to place a depth-tested model at model.show = false; // no ground evidence → nothing safe to place a depth-tested model at
bb.show = true; setBillboard(bb, 'show', true);
return false; return false;
} }
// The matrix is written BEFORE the readiness test on purpose: a model can flip // The matrix is written BEFORE the readiness test on purpose: a model can flip
@@ -343,12 +348,12 @@ export function createRendering({
_modelMatrix(displayPos, course, model.modelMatrix); _modelMatrix(displayPos, course, model.modelMatrix);
if (!model.ready) { if (!model.ready) {
model.show = false; // not loaded yet → keep the 2D icon, no half-model flash model.show = false; // not loaded yet → keep the 2D icon, no half-model flash
bb.show = true; setBillboard(bb, 'show', true);
return false; return false;
} }
beforeShow?.(); beforeShow?.();
if (!model.show) model.show = true; if (!model.show) model.show = true;
if (bb.show) bb.show = false; // hand off ONLY once the model renders if (billboardState(bb).show) setBillboard(bb, 'show', false); // hand off ONLY once the model renders
return true; return true;
} }
@@ -437,7 +442,8 @@ export function createRendering({
(!current && flightState._modelPending.has(icao24)) (!current && flightState._modelPending.has(icao24))
) { ) {
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
if (bb && icao24 !== flightState._trackedIcao) bb.show = true; if (bb && icao24 !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
_releaseModel(icao24); _releaseModel(icao24);
} }
if ( if (
@@ -472,7 +478,8 @@ export function createRendering({
) )
continue; // already right state continue; // already right state
const bb = flightState._billboards.get(icao); const bb = flightState._billboards.get(icao);
if (bb && icao !== flightState._trackedIcao) bb.show = true; if (bb && icao !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
_releaseModel(icao); _releaseModel(icao);
} }
if (flightState._irReloadQueue.length === 0) if (flightState._irReloadQueue.length === 0)
@@ -699,7 +706,8 @@ export function createRendering({
// readout consumers then reuse that exact cached position. // readout consumers then reuse that exact cached position.
const pos = const pos =
parts.motion._trackedDisplayPosition(flightState._trackedIcao) || parts.motion._trackedDisplayPosition(flightState._trackedIcao) ||
flightState._billboards.get(flightState._trackedIcao)?.position; billboardState(flightState._billboards.get(flightState._trackedIcao))
?.position;
if (!pos) { if (!pos) {
if (flightState._trackedModel) flightState._trackedModel.show = false; if (flightState._trackedModel) flightState._trackedModel.show = false;
return; return;
@@ -777,7 +785,9 @@ export function createRendering({
// default here produces a one-frame jump to the Earth's center. // default here produces a one-frame jump to the Earth's center.
const currentPos = const currentPos =
parts.motion._trackedDisplayCached() || parts.motion._trackedDisplayCached() ||
flightState._billboards.get(flightState._trackedIcao)?.position; billboardState(
flightState._billboards.get(flightState._trackedIcao),
)?.position;
if (currentPos) { if (currentPos) {
const displayPos = _modelDisplayPosition( const displayPos = _modelDisplayPosition(
flightState._trackedIcao, flightState._trackedIcao,
@@ -938,12 +948,10 @@ export function createRendering({
// ground or in the air — no distinction"). The cap + nearest-first ordering // ground or in the air — no distinction"). The cap + nearest-first ordering
// below already bound airport clusters; grounded placement is handled by the // below already bound airport clusters; grounded placement is handled by the
// one-shot ground snap in _modelDisplayPosition. // one-shot ground snap in _modelDisplayPosition.
const d2 = Cesium.Cartesian3.distanceSquared(camPos, bb.position); const { position } = billboardState(bb);
const d2 = Cesium.Cartesian3.distanceSquared(camPos, position);
if (d2 > keepDistSq) continue; // beyond the keep radius → never eligible if (d2 > keepDistSq) continue; // beyond the keep radius → never eligible
Cesium.Cartesian3.clone( Cesium.Cartesian3.clone(position, flightState._scratchModelBS.center);
bb.position,
flightState._scratchModelBS.center,
);
cand.push([ cand.push([
icao, icao,
d2, d2,
@@ -973,6 +981,7 @@ export function createRendering({
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion
const icon = billboardState(bb);
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
@@ -996,9 +1005,9 @@ export function createRendering({
// collection's vertex buffer, so skip sub-meter moves. // collection's vertex buffer, so skip sub-meter moves.
if ( if (
display && display &&
Cesium.Cartesian3.distanceSquared(display, bb.position) > 1.0 Cesium.Cartesian3.distanceSquared(display, icon.position) > 1.0
) { ) {
bb.position = display; setBillboard(bb, 'position', display);
} }
// Round 6: occlusion-test a LIFTED point for contacts rendering below // Round 6: occlusion-test a LIFTED point for contacts rendering below
@@ -1007,15 +1016,15 @@ export function createRendering({
// pass would hide a plane that is really just low over high-N terrain // pass would hide a plane that is really just low over high-N terrain
// waiting for its floor to warm (ATL grounded contacts at geoid −31 m). // waiting for its floor to warm (ATL grounded contacts at geoid −31 m).
const beyondHorizon = !occluder.isPointVisible( const beyondHorizon = !occluder.isPointVisible(
flightState._cullPositions.get(icao24) || bb.position, flightState._cullPositions.get(icao24) || icon.position,
); );
// A billboard flipping INTO view (horizon reveal while the camera idles) // A billboard flipping INTO view (horizon reveal while the camera idles)
// gets its rotation refreshed THIS tick even without a pose change — // gets its rotation refreshed THIS tick even without a pose change —
// otherwise it reappears wearing its stale (often creation-north) nose for // otherwise it reappears wearing its stale (often creation-north) nose for
// up to ROTATION_REFRESH_MS. (Model-handed-off planes also read show=false // up to ROTATION_REFRESH_MS. (Model-handed-off planes also read show=false
// here; harmless — the model branch below `continue`s past the rotation.) // here; harmless — the model branch below `continue`s past the rotation.)
const revealed = !beyondHorizon && !bb.show; const revealed = !beyondHorizon && !icon.show;
if (bb.show === beyondHorizon) bb.show = !beyondHorizon; if (icon.show === beyondHorizon) setBillboard(bb, 'show', !beyondHorizon);
if (beyondHorizon) { if (beyondHorizon) {
// Also hide any 3D model — otherwise a model that crossed the limb would keep // Also hide any 3D model — otherwise a model that crossed the limb would keep
// rendering through the hidden globe at its last matrix. // rendering through the hidden globe at its last matrix.
@@ -1031,22 +1040,22 @@ export function createRendering({
// alpha; nearer focus behavior remains tunable rather than universal. // alpha; nearer focus behavior remains tunable rather than universal.
const cameraDistanceM = Cesium.Cartesian3.distance( const cameraDistanceM = Cesium.Cartesian3.distance(
camera.positionWC, camera.positionWC,
bb.position, icon.position,
); );
const distanceScale = nearFarScalarValueAtDistance( const distanceScale = nearFarScalarValueAtDistance(
bb.scaleByDistance, icon.scaleByDistance,
cameraDistanceM, cameraDistanceM,
); );
const focus = advanceProjectedSpriteFocus( const focus = advanceProjectedSpriteFocus(
bb, bb,
bb.position, icon.position,
scene, scene,
camera, camera,
nowMs, nowMs,
focusTarget, focusTarget,
undefined, undefined,
(bb.width || 20) * (bb.scale || 1) * distanceScale * 0.5, (icon.width || 20) * (icon.scale || 1) * distanceScale * 0.5,
(bb.height || 20) * (bb.scale || 1) * distanceScale * 0.5, (icon.height || 20) * (icon.scale || 1) * distanceScale * 0.5,
); );
const isCockpitNear = const isCockpitNear =
flightState._cockpitContactMode && flightState._cockpitContactMode &&
@@ -1077,8 +1086,8 @@ export function createRendering({
// hysteresis so zoom oscillation never thrashes the atlas. // hysteresis so zoom oscillation never thrashes the atlas.
if (!flightState._cockpitContactMode || isCockpitNear) { if (!flightState._cockpitContactMode || isCockpitNear) {
const glyphDevPx = const glyphDevPx =
(bb.width || 20) * (icon.width || 20) *
(bb.scale || 1) * (icon.scale || 1) *
distanceScale * distanceScale *
(globalThis.devicePixelRatio || 1); (globalThis.devicePixelRatio || 1);
const visual = aircraftVisuals.write(bb); const visual = aircraftVisuals.write(bb);
@@ -1160,12 +1169,12 @@ export function createRendering({
) { ) {
const rot = screenProjectedRotation( const rot = screenProjectedRotation(
scene, scene,
bb.position, icon.position,
course, course,
bb.rotation, icon.rotation,
); );
if (rot !== null && Math.abs(rot - bb.rotation) > 0.002) { if (rot !== null && Math.abs(rot - icon.rotation) > 0.002) {
bb.rotation = rot; setBillboard(bb, 'rotation', rot);
} }
} }
} }
+2 -1
View File
@@ -7,6 +7,7 @@ import {
} from '../../data/motionModel.js'; } from '../../data/motionModel.js';
import { aircraftIcon } from '../../data/aircraftIcons.js'; import { aircraftIcon } from '../../data/aircraftIcons.js';
import { POSITION_HISTORY_LIMIT } from './recordPolicy.js'; import { POSITION_HISTORY_LIMIT } from './recordPolicy.js';
import { addBillboard } from '../../data/billboardMirror.js';
/** Apply reconciled aircraft records to Cesium resources and follow state. */ /** Apply reconciled aircraft records to Cesium resources and follow state. */
export function createFlightSnapshotRenderer({ export function createFlightSnapshotRenderer({
@@ -195,7 +196,7 @@ export function createFlightSnapshotRenderer({
rendering._modelOwnsVisual(icao24), rendering._modelOwnsVisual(icao24),
)) || )) ||
position; position;
const bb = flightState._billboardCollection.add({ const bb = addBillboard(flightState._billboardCollection, {
position: displayed, position: displayed,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)), image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20, width: isTracked ? 24 : 20,
+3 -2
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { CYAN_TRANSPARENT } from './policy.js'; import { CYAN_TRANSPARENT } from './policy.js';
import { mirrorBillboard } from '../../data/billboardMirror.js';
export function createTesting({ export function createTesting({
flightState, flightState,
@@ -68,7 +69,7 @@ export function createTesting({
flightState._viewer = viewer; flightState._viewer = viewer;
flightState._modelCollection = modelCollection; flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection; flightState._billboardCollection = billboardCollection;
flightState._billboards = new Map([[icao24, billboard]]); flightState._billboards = new Map([[icao24, mirrorBillboard(billboard)]]);
flightState._cullPositions.clear(); flightState._cullPositions.clear();
flightState._models.clear(); flightState._models.clear();
for (const [key, model] of models) flightState._models.set(key, model); for (const [key, model] of models) flightState._models.set(key, model);
@@ -117,7 +118,7 @@ export function createTesting({
billboard, billboard,
history = [], history = [],
}) { }) {
flightState._billboards.set(icao24, billboard); flightState._billboards.set(icao24, mirrorBillboard(billboard));
flightState.records.data.set(icao24, meta); flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history); flightState._positionHistory.set(icao24, history);
} }
+26 -18
View File
@@ -26,6 +26,7 @@ import {
} from './policy.js'; } from './policy.js';
import { roundHeadingDegrees } from '../../data/headingDegrees.js'; import { roundHeadingDegrees } from '../../data/headingDegrees.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js'; import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createTracking({ export function createTracking({
flightState, flightState,
@@ -65,7 +66,7 @@ export function createTracking({
function _publishTrackedSelection(icao24, origin = 'programmatic') { function _publishTrackedSelection(icao24, origin = 'programmatic') {
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);
if (!bb?.position || !info) return false; if (!billboardState(bb)?.position || !info) return false;
if (flightState._trackedEntity) if (flightState._trackedEntity)
entityRecord(flightState._trackedEntity).selectionOrigin = origin; entityRecord(flightState._trackedEntity).selectionOrigin = origin;
_emitAwarenessEvent('gev:awareness-subject-selected', { _emitAwarenessEvent('gev:awareness-subject-selected', {
@@ -76,7 +77,7 @@ export function createTracking({
// a callsign-less contact reached Context as its raw hex even once adsbdb // a callsign-less contact reached Context as its raw hex even once adsbdb
// had supplied a registration. Identity below stays `icao24`. // had supplied a registration. Identity below stays `icao24`.
label: parts.queries._contactLabel(icao24, info), label: parts.queries._contactLabel(icao24, info),
position: Cesium.Cartesian3.clone(bb.position), position: Cesium.Cartesian3.clone(billboardState(bb).position),
origin, origin,
}); });
selectTrackedSubjectContext(_contextSubjectMetadata(icao24)); selectTrackedSubjectContext(_contextSubjectMetadata(icao24));
@@ -162,12 +163,13 @@ export function createTracking({
const previous = flightState._cockpitNearContacts; const previous = flightState._cockpitNearContacts;
const distancesSquared = []; const distancesSquared = [];
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao || !bb?.position) continue; const position = billboardState(bb)?.position;
if (icao24 === flightState._trackedIcao || !position) continue;
distancesSquared.push([ distancesSquared.push([
icao24, icao24,
Cesium.Cartesian3.distanceSquared( Cesium.Cartesian3.distanceSquared(
flightState._viewer.camera.positionWC, flightState._viewer.camera.positionWC,
bb.position, position,
), ),
]); ]);
} }
@@ -316,7 +318,8 @@ export function createTracking({
if (!flightState._trackedIcao || !flightState._viewer) return; if (!flightState._trackedIcao || !flightState._viewer) return;
const pos = const pos =
parts.motion._trackedDisplayCached() || parts.motion._trackedDisplayCached() ||
flightState._billboards.get(flightState._trackedIcao)?.position; billboardState(flightState._billboards.get(flightState._trackedIcao))
?.position;
if (!pos) return; if (!pos) return;
const projected = screenProjectedRotation( const projected = screenProjectedRotation(
flightState._viewer.scene, flightState._viewer.scene,
@@ -642,17 +645,21 @@ export function createTracking({
// ROTATION_REFRESH_MS of a wrong (possibly reversed) nose on release. // ROTATION_REFRESH_MS of a wrong (possibly reversed) nose on release.
if (flightState._billboards.has(flightState._trackedIcao)) { if (flightState._billboards.has(flightState._trackedIcao)) {
const bb = flightState._billboards.get(flightState._trackedIcao); const bb = flightState._billboards.get(flightState._trackedIcao);
bb.show = true; setBillboard(bb, 'show', true);
bb.width = 20; setBillboard(bb, 'width', 20);
bb.height = 20; setBillboard(bb, 'height', 20);
// Ground/military-aware restore (a plane untracked while taxiing must come // Ground/military-aware restore (a plane untracked while taxiing must come
// back muted-gray at ground scale, not white at full scale). // back muted-gray at ground scale, not white at full scale).
bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao); bb.color = parts.rendering._fleetBillboardColor(flightState._trackedIcao);
bb.scale = parts.rendering._fleetBillboardScale( setBillboard(
flightState._trackedIcao, bb,
flightState.records.data.get(flightState._trackedIcao)?.klass, 'scale',
parts.rendering._fleetBillboardScale(
flightState._trackedIcao,
flightState.records.data.get(flightState._trackedIcao)?.klass,
),
); );
bb.rotation = flightState._lastTrackedRotation; setBillboard(bb, 'rotation', flightState._lastTrackedRotation);
} }
flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick
@@ -830,7 +837,7 @@ export function createTracking({
if (flightState._models.has(id) || flightState._modelPending.has(id)) if (flightState._models.has(id) || flightState._modelPending.has(id))
parts.rendering._releaseModel(id); parts.rendering._releaseModel(id);
const bb = flightState._billboards.get(id); const bb = flightState._billboards.get(id);
if (bb && id !== flightState._trackedIcao) bb.show = true; // horizon cull re-asserts next tick if (bb && id !== flightState._trackedIcao) setBillboard(bb, 'show', true); // horizon cull re-asserts next tick
if (id === flightState._trackedIcao) { if (id === flightState._trackedIcao) {
parts.rendering._releaseTrackedModel(); parts.rendering._releaseTrackedModel();
_syncTracked2dRotation(); _syncTracked2dRotation();
@@ -862,9 +869,9 @@ export function createTracking({
function _routeIsPlausible(icao24, route) { function _routeIsPlausible(icao24, route) {
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
if (!info || !bb || !bb.position) return true; if (!info || !bb || !billboardState(bb).position) return true;
const carto = Cesium.Cartographic.fromCartesian( const carto = Cesium.Cartographic.fromCartesian(
bb.position, billboardState(bb).position,
Cesium.Ellipsoid.WGS84, Cesium.Ellipsoid.WGS84,
flightState._scratchCarto, flightState._scratchCarto,
); );
@@ -906,21 +913,22 @@ export function createTracking({
flightState._trackedIcao = icao24; flightState._trackedIcao = icao24;
_resetTrackedSelectionState(); // fresh selection: enter at the ENTER ceiling, full load-retry budget _resetTrackedSelectionState(); // fresh selection: enter at the ENTER ceiling, full load-retry budget
flightState._cachedDRFrame = -1; flightState._cachedDRFrame = -1;
flightState._lastTrackedRotation = bb.rotation || 0; flightState._lastTrackedRotation = billboardState(bb).rotation || 0;
// Drop any fleet 3D model for this aircraft — the tracked entity now owns its visual (its // Drop any fleet 3D model for this aircraft — the tracked entity now owns its visual (its
// own billboard + model graphic), and the fleet tick skips the tracked icao, so a leftover // own billboard + model graphic), and the fleet tick skips the tracked icao, so a leftover
// fleet model would be orphaned + double-rendered. // fleet model would be orphaned + double-rendered.
parts.rendering._releaseModel(icao24); parts.rendering._releaseModel(icao24);
// Hide the billboard — the tracked entity replaces it visually // Hide the billboard — the tracked entity replaces it visually
bb.show = false; setBillboard(bb, 'show', false);
// Helper: smoothed, per-frame-cached tracked position (see _trackedDisplayPosition — // Helper: smoothed, per-frame-cached tracked position (see _trackedDisplayPosition —
// one computation shared by the position/alignedAxis/rotation/trail-head callbacks, // one computation shared by the position/alignedAxis/rotation/trail-head callbacks,
// with discontinuity reconciliation). Falls back to the last billboard position when // with discontinuity reconciliation). Falls back to the last billboard position when
// the aircraft has no fix. // the aircraft has no fix.
const getTrackedPosition = () => const getTrackedPosition = () =>
parts.motion._trackedDisplayPosition(icao24) || bb.position; parts.motion._trackedDisplayPosition(icao24) ||
billboardState(bb).position;
// Dead-reckoning position property — smooth continuous motion between API updates. // Dead-reckoning position property — smooth continuous motion between API updates.
const positionProperty = new Cesium.CallbackProperty(() => { const positionProperty = new Cesium.CallbackProperty(() => {
+12 -8
View File
@@ -11,6 +11,7 @@ import {
} from '../../data/contactMatch.js'; } from '../../data/contactMatch.js';
import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js'; import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js';
import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js'; import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createQueries({ export function createQueries({
flightState, flightState,
@@ -96,7 +97,8 @@ export function createQueries({
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
const basePos = const basePos =
parts.motion._deadReckon(icao24) || (bb ? bb.position : null); parts.motion._deadReckon(icao24) ||
(bb ? billboardState(bb).position : null);
if (!basePos) return null; if (!basePos) return null;
const displayed = displayedKinematics({ const displayed = displayedKinematics({
derivedSpeedMps: flightState._drSpeedMps, derivedSpeedMps: flightState._drSpeedMps,
@@ -272,7 +274,8 @@ export function createQueries({
// plane's own fleet billboard — its tracked entity is the visual (avoids a double-image). // plane's own fleet billboard — its tracked entity is the visual (avoids a double-image).
if (flightState._billboardCollection) if (flightState._billboardCollection)
for (const [icao, bb] of flightState._billboards) { for (const [icao, bb] of flightState._billboards) {
if (icao !== flightState._trackedIcao) bb.show = true; if (icao !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
} }
} }
} }
@@ -375,7 +378,7 @@ export function createQueries({
if ( if (
!aircraftIncludedInNearby({ !aircraftIncludedInNearby({
isTracked, isTracked,
billboardShown: bb.show, billboardShown: billboardState(bb).show,
modelRendering: parts.rendering._modelOwnsVisual(icao24), modelRendering: parts.rendering._modelOwnsVisual(icao24),
includeHidden, includeHidden,
}) })
@@ -385,7 +388,7 @@ 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 || billboardState(bb).position;
if (!pos) continue; if (!pos) continue;
const distance = Cesium.Cartesian3.distance(center, pos); const distance = Cesium.Cartesian3.distance(center, pos);
@@ -462,7 +465,8 @@ export function createQueries({
// doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden. // doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden.
const model = flightState._models.get(icao24); const model = flightState._models.get(icao24);
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24); const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
if (!isTracked && !bb.show && !modelOwnsVisual) continue; if (!isTracked && !billboardState(bb).show && !modelOwnsVisual)
continue;
const info = flightState.records.data.get(icao24); const info = flightState.records.data.get(icao24);
const callsign = info?.callsign?.trim(); const callsign = info?.callsign?.trim();
const registration = info?.registration?.trim(); const registration = info?.registration?.trim();
@@ -484,7 +488,7 @@ export function createQueries({
? parts.rendering._modelSpec(info?.klass) ? parts.rendering._modelSpec(info?.klass)
: null; : null;
const pos = isTracked const pos = isTracked
? parts.motion._trackedVisualCached() || bb.position ? parts.motion._trackedVisualCached() || billboardState(bb).position
: modelOwnsVisual : modelOwnsVisual
? modelVisualAnchor( ? modelVisualAnchor(
model.modelMatrix, model.modelMatrix,
@@ -494,7 +498,7 @@ export function createQueries({
: spec.scale, : spec.scale,
object._weldPos || (object._weldPos = new Cesium.Cartesian3()), object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
) )
: bb.position; : billboardState(bb).position;
if (!pos) continue; if (!pos) continue;
object.position = pos; object.position = pos;
object.skipLabel = isTracked; object.skipLabel = isTracked;
@@ -616,7 +620,7 @@ export function createQueries({
const result = []; const result = [];
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
const pos = bb.position; const pos = billboardState(bb).position;
if (!pos) continue; if (!pos) continue;
const carto = Cesium.Cartographic.fromCartesian( const carto = Cesium.Cartographic.fromCartesian(
pos, pos,
+43 -38
View File
@@ -55,6 +55,7 @@ import {
} from './policy.js'; } from './policy.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js'; import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
import { aircraftVisuals } from '../aircraft/visualState.js'; import { aircraftVisuals } from '../aircraft/visualState.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createRendering({ export function createRendering({
flightState, flightState,
@@ -159,12 +160,12 @@ export function createRendering({
if (isCockpitContact && !isCockpitNear) { if (isCockpitContact && !isCockpitNear) {
const freshnessAlpha = cyberSonarBaseAlpha(bb); const freshnessAlpha = cyberSonarBaseAlpha(bb);
bb.image = cockpitContactDotImage(); bb.image = cockpitContactDotImage();
bb.width = COCKPIT_CONTACT_SIZE_PX; setBillboard(bb, 'width', COCKPIT_CONTACT_SIZE_PX);
bb.height = COCKPIT_CONTACT_SIZE_PX; setBillboard(bb, 'height', COCKPIT_CONTACT_SIZE_PX);
bb.scale = limbScale; setBillboard(bb, 'scale', limbScale);
bb.scaleByDistance = _cockpitBillboardScaleByDistance(); setBillboard(bb, 'scaleByDistance', _cockpitBillboardScaleByDistance());
bb.color = _fleetBillboardColor(icao24).withAlpha(freshnessAlpha); bb.color = _fleetBillboardColor(icao24).withAlpha(freshnessAlpha);
bb.rotation = 0; setBillboard(bb, 'rotation', 0);
return; return;
} }
@@ -173,10 +174,10 @@ export function createRendering({
_iconKind(icao24, meta?.klass), _iconKind(icao24, meta?.klass),
aircraftVisuals.read(bb)?.iconLarge ? TRACKED_ICON_PX : undefined, aircraftVisuals.read(bb)?.iconLarge ? TRACKED_ICON_PX : undefined,
); );
bb.width = icao24 === flightState._trackedIcao ? 24 : 20; setBillboard(bb, 'width', icao24 === flightState._trackedIcao ? 24 : 20);
bb.height = icao24 === flightState._trackedIcao ? 24 : 20; setBillboard(bb, 'height', icao24 === flightState._trackedIcao ? 24 : 20);
bb.scale = _militaryBillboardScale(icao24) * limbScale; setBillboard(bb, 'scale', _militaryBillboardScale(icao24) * limbScale);
bb.scaleByDistance = _normalBillboardScaleByDistance(); setBillboard(bb, 'scaleByDistance', _normalBillboardScaleByDistance());
bb.color = _fleetBillboardColor(icao24, meta).withAlpha( bb.color = _fleetBillboardColor(icao24, meta).withAlpha(
cyberSonarBaseAlpha(bb), cyberSonarBaseAlpha(bb),
); );
@@ -330,7 +331,7 @@ export function createRendering({
function _driveFleetModelHandoff(icao24, model, bb, pos, course, beforeShow) { function _driveFleetModelHandoff(icao24, model, bb, pos, course, beforeShow) {
if (!model) { if (!model) {
bb.show = true; setBillboard(bb, 'show', true);
return false; return false;
} }
const displayPos = _modelDisplayPosition( const displayPos = _modelDisplayPosition(
@@ -340,7 +341,7 @@ export function createRendering({
); );
if (!displayPos) { if (!displayPos) {
model.show = false; // no ground evidence → nothing safe to place a depth-tested model at model.show = false; // no ground evidence → nothing safe to place a depth-tested model at
bb.show = true; setBillboard(bb, 'show', true);
return false; return false;
} }
// The matrix is written BEFORE the readiness test on purpose: a model can flip // The matrix is written BEFORE the readiness test on purpose: a model can flip
@@ -354,12 +355,12 @@ export function createRendering({
); );
if (!model.ready) { if (!model.ready) {
model.show = false; // not loaded yet → keep the 2D icon, no half-model flash model.show = false; // not loaded yet → keep the 2D icon, no half-model flash
bb.show = true; setBillboard(bb, 'show', true);
return false; return false;
} }
beforeShow?.(); beforeShow?.();
if (!model.show) model.show = true; if (!model.show) model.show = true;
if (bb.show) bb.show = false; // hand off ONLY once the model renders if (billboardState(bb).show) setBillboard(bb, 'show', false); // hand off ONLY once the model renders
return true; return true;
} }
@@ -405,7 +406,8 @@ export function createRendering({
(!current && flightState._modelPending.has(icao24)) (!current && flightState._modelPending.has(icao24))
) { ) {
const bb = flightState._billboards.get(icao24); const bb = flightState._billboards.get(icao24);
if (bb && icao24 !== flightState._trackedIcao) bb.show = true; if (bb && icao24 !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
_releaseModel(icao24); _releaseModel(icao24);
} }
if ( if (
@@ -440,7 +442,8 @@ export function createRendering({
) )
continue; // already right state continue; // already right state
const bb = flightState._billboards.get(icao); const bb = flightState._billboards.get(icao);
if (bb && icao !== flightState._trackedIcao) bb.show = true; if (bb && icao !== flightState._trackedIcao)
setBillboard(bb, 'show', true);
_releaseModel(icao); _releaseModel(icao);
} }
if (flightState._irReloadQueue.length === 0) if (flightState._irReloadQueue.length === 0)
@@ -662,7 +665,8 @@ export function createRendering({
// readout consumers then reuse that exact cached position. // readout consumers then reuse that exact cached position.
const pos = const pos =
parts.motion._trackedDisplayPosition(flightState._trackedIcao) || parts.motion._trackedDisplayPosition(flightState._trackedIcao) ||
flightState._billboards.get(flightState._trackedIcao)?.position; billboardState(flightState._billboards.get(flightState._trackedIcao))
?.position;
if (!pos) { if (!pos) {
if (flightState._trackedModel) flightState._trackedModel.show = false; if (flightState._trackedModel) flightState._trackedModel.show = false;
return; return;
@@ -738,7 +742,9 @@ export function createRendering({
// default here produces a one-frame jump to the Earth's center. // default here produces a one-frame jump to the Earth's center.
const currentPos = const currentPos =
parts.motion._trackedDisplayCached() || parts.motion._trackedDisplayCached() ||
flightState._billboards.get(flightState._trackedIcao)?.position; billboardState(
flightState._billboards.get(flightState._trackedIcao),
)?.position;
if (currentPos) { if (currentPos) {
const displayPos = _modelDisplayPosition( const displayPos = _modelDisplayPosition(
flightState._trackedIcao, flightState._trackedIcao,
@@ -897,12 +903,10 @@ export function createRendering({
// Ground planes compete for model slots like everyone else (owner decision // Ground planes compete for model slots like everyone else (owner decision
// 2026-07-03, mirror of flights.js — no air/ground distinction; grounded // 2026-07-03, mirror of flights.js — no air/ground distinction; grounded
// placement is handled by the one-shot ground snap in _modelDisplayPosition). // placement is handled by the one-shot ground snap in _modelDisplayPosition).
const d2 = Cesium.Cartesian3.distanceSquared(camPos, bb.position); const { position } = billboardState(bb);
const d2 = Cesium.Cartesian3.distanceSquared(camPos, position);
if (d2 > keepDistSq) continue; // beyond keep radius → never eligible if (d2 > keepDistSq) continue; // beyond keep radius → never eligible
Cesium.Cartesian3.clone( Cesium.Cartesian3.clone(position, flightState._scratchModelBS.center);
bb.position,
flightState._scratchModelBS.center,
);
cand.push([ cand.push([
icao, icao,
d2, d2,
@@ -932,27 +936,28 @@ export function createRendering({
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion if (icao24 === flightState._trackedIcao) continue; // tracked entity owns its own motion
const icon = billboardState(bb);
const dr = parts.motion._deadReckon(icao24, flightState._scratchFleetPos); const dr = parts.motion._deadReckon(icao24, flightState._scratchFleetPos);
// Gate the write — assigning Billboard.position dirties the whole // Gate the write — assigning Billboard.position dirties the whole
// collection's vertex buffer, so skip sub-meter moves. // collection's vertex buffer, so skip sub-meter moves.
if (dr && Cesium.Cartesian3.distanceSquared(dr, bb.position) > 1.0) { if (dr && Cesium.Cartesian3.distanceSquared(dr, icon.position) > 1.0) {
bb.position = dr; setBillboard(bb, 'position', dr);
} }
// Round 6: occlusion-test a LIFTED point for contacts at/below the // Round 6: occlusion-test a LIFTED point for contacts at/below the
// ellipsoid (mirror of flights.js — sub-ellipsoid points near the limb // ellipsoid (mirror of flights.js — sub-ellipsoid points near the limb
// read "beyond the horizon" and would hide low contacts awaiting floors). // read "beyond the horizon" and would hide low contacts awaiting floors).
const beyondHorizon = !occluder.isPointVisible( const beyondHorizon = !occluder.isPointVisible(
flightState.records.data.get(icao24)?.cullPosition || bb.position, flightState.records.data.get(icao24)?.cullPosition || icon.position,
); );
// A billboard flipping INTO view (horizon reveal while the camera idles) // A billboard flipping INTO view (horizon reveal while the camera idles)
// gets its rotation refreshed THIS tick even without a pose change — // gets its rotation refreshed THIS tick even without a pose change —
// otherwise it reappears wearing its stale (often creation-north) nose for // otherwise it reappears wearing its stale (often creation-north) nose for
// up to ROTATION_REFRESH_MS. (Model-handed-off planes also read show=false // up to ROTATION_REFRESH_MS. (Model-handed-off planes also read show=false
// here; harmless — the model branch below `continue`s past the rotation.) // here; harmless — the model branch below `continue`s past the rotation.)
const revealed = !beyondHorizon && !bb.show; const revealed = !beyondHorizon && !icon.show;
if (bb.show === beyondHorizon) bb.show = !beyondHorizon; if (icon.show === beyondHorizon) setBillboard(bb, 'show', !beyondHorizon);
if (beyondHorizon) { if (beyondHorizon) {
// Also hide any 3D model — otherwise a model that crossed the limb would keep // Also hide any 3D model — otherwise a model that crossed the limb would keep
// rendering through the hidden globe at its last matrix. // rendering through the hidden globe at its last matrix.
@@ -968,22 +973,22 @@ export function createRendering({
// untouched and multiplicative; there is no cull or zero-alpha path. // untouched and multiplicative; there is no cull or zero-alpha path.
const cameraDistanceM = Cesium.Cartesian3.distance( const cameraDistanceM = Cesium.Cartesian3.distance(
camera.positionWC, camera.positionWC,
bb.position, icon.position,
); );
const distanceScale = nearFarScalarValueAtDistance( const distanceScale = nearFarScalarValueAtDistance(
bb.scaleByDistance, icon.scaleByDistance,
cameraDistanceM, cameraDistanceM,
); );
const focus = advanceProjectedSpriteFocus( const focus = advanceProjectedSpriteFocus(
bb, bb,
bb.position, icon.position,
scene, scene,
camera, camera,
nowMs, nowMs,
focusTarget, focusTarget,
undefined, undefined,
(bb.width || 20) * (bb.scale || 1) * distanceScale * 0.5, (icon.width || 20) * (icon.scale || 1) * distanceScale * 0.5,
(bb.height || 20) * (bb.scale || 1) * distanceScale * 0.5, (icon.height || 20) * (icon.scale || 1) * distanceScale * 0.5,
); );
const isCockpitNear = const isCockpitNear =
flightState._cockpitContactMode && flightState._cockpitContactMode &&
@@ -1007,8 +1012,8 @@ export function createRendering({
// focus/limb recession counts) with hysteresis (atlas has no mips). // focus/limb recession counts) with hysteresis (atlas has no mips).
if (!flightState._cockpitContactMode || isCockpitNear) { if (!flightState._cockpitContactMode || isCockpitNear) {
const glyphDevPx = const glyphDevPx =
(bb.width || 20) * (icon.width || 20) *
(bb.scale || 1) * (icon.scale || 1) *
distanceScale * distanceScale *
(globalThis.devicePixelRatio || 1); (globalThis.devicePixelRatio || 1);
const visual = aircraftVisuals.write(bb); const visual = aircraftVisuals.write(bb);
@@ -1087,12 +1092,12 @@ export function createRendering({
) { ) {
const rot = screenProjectedRotation( const rot = screenProjectedRotation(
scene, scene,
bb.position, icon.position,
course, course,
bb.rotation, icon.rotation,
); );
if (rot !== null && Math.abs(rot - bb.rotation) > 0.002) { if (rot !== null && Math.abs(rot - icon.rotation) > 0.002) {
bb.rotation = rot; setBillboard(bb, 'rotation', rot);
} }
} }
} }
+3 -2
View File
@@ -9,6 +9,7 @@ import {
import { aircraftIcon } from '../../data/aircraftIcons.js'; import { aircraftIcon } from '../../data/aircraftIcons.js';
import { POSITION_HISTORY_LIMIT } from './recordPolicy.js'; import { POSITION_HISTORY_LIMIT } from './recordPolicy.js';
import { BILLBOARD_SCALE, GROUND_SCALE, TRACKED_ICON_COLOR } from './policy.js'; import { BILLBOARD_SCALE, GROUND_SCALE, TRACKED_ICON_COLOR } from './policy.js';
import { addBillboard, setBillboard } from '../../data/billboardMirror.js';
/** Apply military record changes to Cesium resources and existing follow operations. */ /** Apply military record changes to Cesium resources and existing follow operations. */
export function createMilitarySnapshotRenderer({ export function createMilitarySnapshotRenderer({
@@ -150,7 +151,7 @@ export function createMilitarySnapshotRenderer({
// it ahead until the next fleet tick snapped it back. // it ahead until the next fleet tick snapped it back.
const displayed = const displayed =
motion._deadReckon(icao24, new Cesium.Cartesian3()) || position; motion._deadReckon(icao24, new Cesium.Cartesian3()) || position;
const bb = flightState._billboardCollection.add({ const bb = addBillboard(flightState._billboardCollection, {
position: displayed, position: displayed,
image: aircraftIcon(rendering._iconKind(icao24, meta.klass)), image: aircraftIcon(rendering._iconKind(icao24, meta.klass)),
width: isTracked ? 24 : 20, width: isTracked ? 24 : 20,
@@ -226,7 +227,7 @@ export function createMilitarySnapshotRenderer({
if (newest) if (newest)
newest.position = Cesium.Cartesian3.clone(liftedPos, newest.position); newest.position = Cesium.Cartesian3.clone(liftedPos, newest.position);
const bbStale = flightState._billboards.get(icao24); const bbStale = flightState._billboards.get(icao24);
if (bbStale) bbStale.position = liftedPos; if (bbStale) setBillboard(bbStale, 'position', liftedPos);
} }
// 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
+3 -2
View File
@@ -1,4 +1,5 @@
import { AMBER_TRANSPARENT, TRACKED_ICON_COLOR } from './policy.js'; import { AMBER_TRANSPARENT, TRACKED_ICON_COLOR } from './policy.js';
import { mirrorBillboard } from '../../data/billboardMirror.js';
export function createTesting({ export function createTesting({
flightState, flightState,
@@ -39,7 +40,7 @@ export function createTesting({
flightState._viewer = viewer; flightState._viewer = viewer;
flightState._modelCollection = modelCollection; flightState._modelCollection = modelCollection;
flightState._billboardCollection = billboardCollection; flightState._billboardCollection = billboardCollection;
flightState._billboards = new Map([[icao24, billboard]]); flightState._billboards = new Map([[icao24, mirrorBillboard(billboard)]]);
flightState._cullPositions.clear(); flightState._cullPositions.clear();
flightState._models.clear(); flightState._models.clear();
for (const [key, model] of models) flightState._models.set(key, model); for (const [key, model] of models) flightState._models.set(key, model);
@@ -86,7 +87,7 @@ export function createTesting({
billboard, billboard,
history = [], history = [],
}) { }) {
flightState._billboards.set(icao24, billboard); flightState._billboards.set(icao24, mirrorBillboard(billboard));
flightState.records.data.set(icao24, meta); flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history); flightState._positionHistory.set(icao24, history);
} }
+24 -16
View File
@@ -27,6 +27,7 @@ import {
} from './policy.js'; } from './policy.js';
import { roundHeadingDegrees } from '../../data/headingDegrees.js'; import { roundHeadingDegrees } from '../../data/headingDegrees.js';
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js'; import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
export function createTracking({ export function createTracking({
flightState, flightState,
@@ -64,7 +65,7 @@ export function createTracking({
function _publishTrackedSelection(icao24, origin = 'programmatic') { function _publishTrackedSelection(icao24, origin = 'programmatic') {
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);
if (!bb?.position || !info) return false; if (!billboardState(bb)?.position || !info) return false;
if (flightState._trackedEntity) if (flightState._trackedEntity)
entityRecord(flightState._trackedEntity).selectionOrigin = origin; entityRecord(flightState._trackedEntity).selectionOrigin = origin;
_emitAwarenessEvent('gev:awareness-subject-selected', { _emitAwarenessEvent('gev:awareness-subject-selected', {
@@ -74,7 +75,7 @@ export function createTracking({
parts.queries._toCleanText(info.callsign) || parts.queries._toCleanText(info.callsign) ||
parts.queries._toCleanText(info.registration) || parts.queries._toCleanText(info.registration) ||
icao24, icao24,
position: Cesium.Cartesian3.clone(bb.position), position: Cesium.Cartesian3.clone(billboardState(bb).position),
origin, origin,
}); });
selectTrackedSubjectContext(_contextSubjectMetadata(icao24)); selectTrackedSubjectContext(_contextSubjectMetadata(icao24));
@@ -151,12 +152,13 @@ export function createTracking({
const previous = flightState._cockpitNearContacts; const previous = flightState._cockpitNearContacts;
const distancesSquared = []; const distancesSquared = [];
for (const [icao24, bb] of flightState._billboards) { for (const [icao24, bb] of flightState._billboards) {
if (icao24 === flightState._trackedIcao || !bb?.position) continue; const position = billboardState(bb)?.position;
if (icao24 === flightState._trackedIcao || !position) continue;
distancesSquared.push([ distancesSquared.push([
icao24, icao24,
Cesium.Cartesian3.distanceSquared( Cesium.Cartesian3.distanceSquared(
flightState._viewer.camera.positionWC, flightState._viewer.camera.positionWC,
bb.position, position,
), ),
]); ]);
} }
@@ -295,7 +297,8 @@ export function createTracking({
if (flightState._models.has(id) || flightState._modelPending.has(id)) if (flightState._models.has(id) || flightState._modelPending.has(id))
parts.rendering._releaseModel(id); parts.rendering._releaseModel(id);
const modelled = flightState._billboards.get(id); const modelled = flightState._billboards.get(id);
if (modelled && id !== flightState._trackedIcao) modelled.show = true; // horizon cull re-asserts next tick if (modelled && id !== flightState._trackedIcao)
setBillboard(modelled, 'show', true); // horizon cull re-asserts next tick
if (id === flightState._trackedIcao) { if (id === flightState._trackedIcao) {
parts.rendering._releaseTrackedModel(); parts.rendering._releaseTrackedModel();
_syncTracked2dRotation(); _syncTracked2dRotation();
@@ -411,7 +414,8 @@ export function createTracking({
if (!flightState._trackedIcao || !flightState._viewer) return; if (!flightState._trackedIcao || !flightState._viewer) return;
const pos = const pos =
parts.motion._trackedDisplayCached() || parts.motion._trackedDisplayCached() ||
flightState._billboards.get(flightState._trackedIcao)?.position; billboardState(flightState._billboards.get(flightState._trackedIcao))
?.position;
if (!pos) return; if (!pos) return;
const projected = screenProjectedRotation( const projected = screenProjectedRotation(
flightState._viewer.scene, flightState._viewer.scene,
@@ -780,21 +784,24 @@ export function createTracking({
if (flightState._billboards.has(flightState._trackedIcao)) { if (flightState._billboards.has(flightState._trackedIcao)) {
const bb = flightState._billboards.get(flightState._trackedIcao); const bb = flightState._billboards.get(flightState._trackedIcao);
const meta = flightState.records.data.get(flightState._trackedIcao); const meta = flightState.records.data.get(flightState._trackedIcao);
bb.show = true; setBillboard(bb, 'show', true);
bb.width = 20; setBillboard(bb, 'width', 20);
bb.height = 20; setBillboard(bb, 'height', 20);
// Ground-aware restore (a plane untracked while taxiing comes back at // Ground-aware restore (a plane untracked while taxiing comes back at
// ground scale; the fleet tint is amber, or red while broadcasting an // ground scale; the fleet tint is amber, or red while broadcasting an
// emergency, on the ground and in the air). // emergency, on the ground and in the air).
bb.scale = setBillboard(
bb,
'scale',
BILLBOARD_SCALE * BILLBOARD_SCALE *
(CLASS_SCALE_2D[meta?.klass] || 1) * (CLASS_SCALE_2D[meta?.klass] || 1) *
(meta?.onGround ? GROUND_SCALE : 1); (meta?.onGround ? GROUND_SCALE : 1),
);
bb.color = parts.rendering._fleetBillboardColor( bb.color = parts.rendering._fleetBillboardColor(
flightState._trackedIcao, flightState._trackedIcao,
meta, meta,
); );
bb.rotation = flightState._lastTrackedRotation; setBillboard(bb, 'rotation', flightState._lastTrackedRotation);
} }
flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick flightState._lastCamPoseSig = ''; // force a fleet rotation pass on the next tick
@@ -869,21 +876,22 @@ export function createTracking({
flightState._trackedIcao = icao24; flightState._trackedIcao = icao24;
_resetTrackedSelectionState(); // fresh selection: enter at the ENTER ceiling, full load-retry budget _resetTrackedSelectionState(); // fresh selection: enter at the ENTER ceiling, full load-retry budget
flightState._cachedDRFrame = -1; flightState._cachedDRFrame = -1;
flightState._lastTrackedRotation = bb.rotation || 0; flightState._lastTrackedRotation = billboardState(bb).rotation || 0;
// Drop any fleet 3D model for this aircraft — the tracked entity now owns its visual (its // Drop any fleet 3D model for this aircraft — the tracked entity now owns its visual (its
// own billboard + model graphic), and the fleet tick skips the tracked icao, so a leftover // own billboard + model graphic), and the fleet tick skips the tracked icao, so a leftover
// fleet model would be orphaned + double-rendered. // fleet model would be orphaned + double-rendered.
parts.rendering._releaseModel(icao24); parts.rendering._releaseModel(icao24);
// Hide the billboard -- the tracked entity replaces it visually // Hide the billboard -- the tracked entity replaces it visually
bb.show = false; setBillboard(bb, 'show', false);
// Helper: smoothed, per-frame-cached tracked position (see _trackedDisplayPosition — // Helper: smoothed, per-frame-cached tracked position (see _trackedDisplayPosition —
// one computation shared by the position/rotation/trail-head callbacks, with // one computation shared by the position/rotation/trail-head callbacks, with
// discontinuity reconciliation). Falls back to the last billboard position when the // discontinuity reconciliation). Falls back to the last billboard position when the
// aircraft has no fix. // aircraft has no fix.
const getTrackedPosition = () => const getTrackedPosition = () =>
parts.motion._trackedDisplayPosition(icao24) || bb.position; parts.motion._trackedDisplayPosition(icao24) ||
billboardState(bb).position;
// Dead-reckoning position property evaluated every render frame. // Dead-reckoning position property evaluated every render frame.
// isConstant = false so CesiumJS re-evaluates each frame. // isConstant = false so CesiumJS re-evaluates each frame.