Delete the billboard and model mirrors; models become layer-held records

Aircraft draw from GPU icon sets, so the billboard mirror, the billboard
treatment and the aircraft visual-state module had no callers left. 3D models
are now records the layer holds under the contact's id (modelRecord.js):
show and placement are written through showModel/placeModel and read back
from the record; renderer-measured values stay on record.model.

The tracked-model click check only matches when a tracked model exists, so
an id-only pick is never mistaken for a click on the tracked plane.
This commit is contained in:
Sameh Khamis committed 2026-11-03 22:36:27 -08:00
1 parent f008120b4c
commit 6035de88e2
35 files changed
+391 -788

No files matched your search

+26 -38
View File
@@ -1,11 +1,8 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import {
advanceSpriteFocus,
} from '../src/data/focusDeemphasis.js';
import {
applyAircraftBillboardTreatment,
} from '../src/data/aircraftRecession.js';
import { advanceSpriteFocus } from '../src/data/focusDeemphasis.js';
import { aircraftTreatmentAlpha } from '../src/data/aircraftRecession.js';
import { createIconSet } from '../src/data/iconSet.js';
if (typeof global.gc !== 'function') {
throw new Error('focus-allocation-check.mjs requires node --expose-gc');
@@ -66,17 +63,14 @@ const focusInput = {
target,
};
const baseColor = { withAlpha: (alpha) => ({ alpha }) };
const billboard = { scale: 1, color: { alpha: 1 } };
const aircraftInput = {
billboard,
baseScale: 1,
baseAlpha: 1,
baseColor,
focusFactor: 1,
cameraDistanceM: 100,
cameraHeightM: 1000,
};
const icons = createIconSet({ capacity: 4 });
const row = icons.add('sprite', {});
const baseline = bytesPerCall(() => focusInput.nowMs);
const focus = netOfLoopBaseline(
@@ -84,30 +78,22 @@ const focus = netOfLoopBaseline(
baseline,
);
const aircraft = netOfLoopBaseline(
bytesPerCall(() => applyAircraftBillboardTreatment(aircraftInput).alpha),
bytesPerCall(() => aircraftTreatmentAlpha(aircraftInput)),
baseline,
);
const productionSpriteTick = netOfLoopBaseline(
bytesPerCall(() => {
// Match the hot civilian/military sprite path: the caller constructs fresh
// option literals, consumes the shared focus result immediately, and then
// constructs the composed aircraft-treatment options for the same sprite.
// Match the hot icon-set focus pass: the caller constructs a fresh option
// literal, consumes the shared focus result immediately and writes it to
// the row's focus column (the shader composes it with limb recession).
const focusResult = advanceSpriteFocus(sprite, {
screenPosition: focusInput.screenPosition,
cameraDistance: focusInput.cameraDistance,
nowMs: focusInput.nowMs,
target,
});
const treatment = applyAircraftBillboardTreatment({
billboard,
baseScale: aircraftInput.baseScale,
baseAlpha: aircraftInput.baseAlpha,
baseColor,
focusFactor: focusResult.factor,
cameraDistanceM: aircraftInput.cameraDistanceM,
cameraHeightM: aircraftInput.cameraHeightM,
});
return focusResult.factor + treatment.alpha;
icons.setFocus(row, focusResult.factor);
return focusResult.factor;
}),
baseline,
);
@@ -118,22 +104,24 @@ assert.ok(
);
assert.ok(
aircraft.roundedMedian <= PURE_CALL_BUDGET,
`applyAircraftBillboardTreatment allocated ${aircraft.rawMedian.toFixed(3)} B/call median`,
`aircraftTreatmentAlpha allocated ${aircraft.rawMedian.toFixed(3)} B/call median`,
);
assert.ok(
productionSpriteTick.roundedMedian <= PRODUCTION_SPRITE_TICK_BUDGET,
`production sprite tick allocated ${productionSpriteTick.rawMedian.toFixed(3)} B/call median`,
);
console.log(JSON.stringify({
callsPerTrial: CALLS,
trials: TRIALS,
budgets: {
pureCall: PURE_CALL_BUDGET,
productionSpriteTick: PRODUCTION_SPRITE_TICK_BUDGET,
},
loopBaseline: baseline,
advanceSpriteFocus: focus,
applyAircraftBillboardTreatment: aircraft,
productionSpriteTick,
}));
console.log(
JSON.stringify({
callsPerTrial: CALLS,
trials: TRIALS,
budgets: {
pureCall: PURE_CALL_BUDGET,
productionSpriteTick: PRODUCTION_SPRITE_TICK_BUDGET,
},
loopBaseline: baseline,
advanceSpriteFocus: focus,
aircraftTreatmentAlpha: aircraft,
productionSpriteTick,
}),
);
+5 -16
View File
@@ -1324,9 +1324,7 @@
"src/data/iconSegment.js",
"src/data/iconAtlas.js",
"src/data/aircraftRecession.js",
"src/data/modelMirror.js",
"src/data/billboardMirror.js",
"src/layers/aircraft/visualState.js",
"src/data/modelRecord.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
@@ -1395,9 +1393,7 @@
"src/data/iconSegment.js",
"src/data/iconAtlas.js",
"src/data/aircraftRecession.js",
"src/data/modelMirror.js",
"src/data/billboardMirror.js",
"src/layers/aircraft/visualState.js",
"src/data/modelRecord.js",
"src/layers/military/state.js",
"src/layers/military/testing.js",
"src/layers/military/tracking.js",
@@ -1440,7 +1436,7 @@
"src/data/layerStateCoordinator.js",
"src/data/layerStateTokenReservations.json",
"src/data/modelVisualAnchor.js",
"src/data/modelMirror.js",
"src/data/modelRecord.js",
"src/data/entityRecords.js",
"src/data/aircraftModelLighting.js",
"src/data/motionModel.js",
@@ -1470,7 +1466,6 @@
"src/layers/vessels/queries.js",
"src/layers/vessels/rendering.js",
"src/data/entityRecords.js",
"src/data/billboardMirror.js",
"src/data/iconSetPrimitive.js",
"src/data/iconSet.js",
"src/data/iconSegment.js",
@@ -1564,7 +1559,6 @@
"src/data/iconSegment.js",
"src/data/iconAtlas.js",
"src/data/aircraftRecession.js",
"src/data/billboardMirror.js",
"src/data/motionModel.js",
"src/layers/firms/selection.js",
"src/layers/firms/source.js",
@@ -1895,7 +1889,6 @@
"src/layers/cctv/projection.js",
"src/layers/cctv/videoPlayback.js",
"src/layers/cctv/rendering.js",
"src/data/billboardMirror.js",
"src/data/iconSetPrimitive.js",
"src/data/iconSet.js",
"src/data/iconSegment.js",
@@ -2376,7 +2369,6 @@
"src/data/aircraftMeta.js",
"src/data/aircraftNearbyPolicy.js",
"src/data/aircraftRecession.js",
"src/data/billboardMirror.js",
"src/data/aisLiveVessels.js",
"src/data/alprCameras.js",
"src/data/analystEngine.js",
@@ -2593,8 +2585,7 @@
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/data/modelMirror.js",
"src/layers/aircraft/visualState.js",
"src/data/modelRecord.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
@@ -3352,7 +3343,6 @@
"src/data/aircraftMeta.js",
"src/data/aircraftNearbyPolicy.js",
"src/data/aircraftRecession.js",
"src/data/billboardMirror.js",
"src/data/cctvCards.js",
"src/data/cctvFootprint.js",
"src/data/cctvGizmo.js",
@@ -3514,8 +3504,7 @@
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/data/modelMirror.js",
"src/layers/aircraft/visualState.js",
"src/data/modelRecord.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
+5 -6
View File
@@ -412,15 +412,14 @@ export function createAircraftIcons({
* @param {string} layerId
*/
export function aircraftIconTestSeams(state, parts, layerId) {
/** Give a test contact an icon row held at `billboard.position`, shown as
* `billboard.show` says (hidden by default while tracked). */
function _seedIconForTest(icao24, billboard, tracked) {
/** Give a test contact an icon row held at `icon.position`, shown as
* `icon.show` says (hidden by default while tracked). */
function _seedIconForTest(icao24, icon, tracked) {
const set = state._icons;
const row = set.add(icao24, { layer: layerId, id: icao24 });
if (billboard?.position)
writeHoldSegment(set.segments, row, billboard.position, 0);
if (icon?.position) writeHoldSegment(set.segments, row, icon.position, 0);
set.segmentWritten(row);
set.setShown(row, billboard?.show ?? !tracked);
set.setShown(row, icon?.show ?? !tracked);
return row;
}
+66 -66
View File
@@ -3,8 +3,12 @@ import { selectModelEligible } from './modelEligibility.js';
import { horizonOccluder } from './iconOrientation.js';
import { aircraftTreatmentAlpha } from './aircraftRecession.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from './aircraftModelLighting.js';
import { aircraftVisuals } from '../layers/aircraft/visualState.js';
import { modelState, setModel } from './modelMirror.js';
import {
createModelRecord,
modelIsRendering,
placeModel,
showModel,
} from './modelRecord.js';
/**
* An aircraft layer's 3D models: the fleet regime and its gap-proof handoff
@@ -98,8 +102,8 @@ export function createAircraftModels({
}
/** World model matrix from a position + course heading (pitch/roll 0; ENU frame). Writes into
* `result` (a shared scratch by default) and returns it. Push it with `setModel`, which copies
* it into Core's mirror and the model's own matrix, so models never share one mutable matrix
* `result` (a shared scratch by default) and returns it. Push it with `placeModel`, which copies
* it into the model's record and its own matrix, so models never share one mutable matrix
* object. `Model.modelMatrix` is a plain field (not a cloning setter); Cesium
* clones it per frame in updateModelMatrix(). Sharing a single scratch made every model render at
* the LAST-written transform — all stacked on one plane — and, once the tracked model wrote the
@@ -187,14 +191,14 @@ export function createAircraftModels({
function _driveFleetModelHandoff(
icao24,
model,
record,
row,
pos,
course,
beforeShow,
) {
const icons = state._icons;
if (!model) {
if (!record) {
icons.setShown(row, true);
return false;
}
@@ -204,16 +208,15 @@ export function createAircraftModels({
state._scratchGroundPos,
);
if (!displayPos) {
setModel(model, 'show', false); // no ground evidence → nothing safe to place a depth-tested model at
showModel(record, false); // no ground evidence → nothing safe to place a depth-tested model at
icons.setShown(row, true);
return false;
}
// The matrix is written BEFORE the readiness test on purpose: a model can flip
// `ready` during scene update after this tick, and a first rendered frame on a
// stale load-start matrix is the one-frame jump this ordering prevents.
setModel(
model,
'modelMatrix',
placeModel(
record,
_modelMatrix(
displayPos,
course,
@@ -221,13 +224,13 @@ export function createAircraftModels({
_headingOffsetDeg(state.records.data.get(icao24)?.klass),
),
);
if (!model.ready) {
setModel(model, 'show', false); // not loaded yet → keep the 2D icon, no half-model flash
if (!record.model.ready) {
showModel(record, false); // not loaded yet → keep the 2D icon, no half-model flash
icons.setShown(row, true);
return false;
}
beforeShow?.();
if (!modelState(model).show) setModel(model, 'show', true);
if (!record.show) showModel(record, true);
icons.setShown(row, false); // hand off ONLY once the model renders
return true;
}
@@ -265,33 +268,30 @@ export function createAircraftModels({
* null or still-loading tracked model from claiming a visual it is not
* drawing yet.
* @param {string} icao24
* @param {Cesium.Model|null} [model] Resolved fleet model; null means absent.
* @param {import('./modelRecord.js').ModelRecord|null} [record] Resolved
* fleet model record; null means absent.
* @returns {boolean}
*/
function _modelOwnsVisual(icao24, model) {
function _modelOwnsVisual(icao24, record) {
if (icao24 === state._trackedIcao) {
return (
parts.tracking._trackedModelRegimeActive() &&
_modelIsRendering(state._trackedModel)
modelIsRendering(state._trackedModel)
);
}
return _modelIsRendering(
model === undefined ? state._models.get(icao24) : model,
return modelIsRendering(
record === undefined ? state._models.get(icao24) : record,
);
}
/** A model is the visual only once it actually draws — loaded AND shown.
*
* `show` is sufficient evidence of a safe placement because only ONE site ever
* sets it true for a fleet model (`_driveFleetModelHandoff`, after the matrix is
* committed) and one for the tracked model (`_updateTrackedModel`, likewise);
* everything else — admission, an unresolved ground, a not-yet-ready glTF, the
* limb cull, a regime exit — only ever clears it. */
function _modelIsRendering(model) {
return !!model && model.ready === true && modelState(model)?.show === true;
}
// A model is the visual only once it actually draws, loaded AND shown
// (`modelIsRendering`). `show` is sufficient evidence of a safe placement
// because only ONE site ever sets it true for a fleet model
// (`_driveFleetModelHandoff`, after the matrix is committed) and one for the
// tracked model (`_updateTrackedModel`, likewise); everything else —
// admission, an unresolved ground, a not-yet-ready glTF, the limb cull, a
// regime exit — only ever clears it.
/** Spec identity for a LOADED model: URL and scale together (same-URL classes
* differ by scale — airliner vs quadjet both ship airplane.glb). */
@@ -313,7 +313,7 @@ export function createAircraftModels({
const key = _specKeyFor(state.records.data.get(icao24)?.klass);
const current = state._models.get(icao24);
if (
(current && aircraftVisuals.read(current)?.specKey !== key) ||
(current && current.specKey !== key) ||
(!current && state._modelPending.has(icao24))
) {
_releaseModel(icao24);
@@ -321,7 +321,7 @@ export function createAircraftModels({
if (
icao24 === state._trackedIcao &&
state._trackedModel &&
aircraftVisuals.read(state._trackedModel)?.specKey !== key
state._trackedModel.specKey !== key
) {
_releaseTrackedModel();
}
@@ -340,9 +340,8 @@ export function createAircraftModels({
if (!state._irReloadQueue) return;
const batch = state._irReloadQueue.splice(0, policy.IR_RELOAD_BATCH);
for (const icao of batch) {
const model = state._models.get(icao);
if (!model || aircraftVisuals.read(model)?.irBoost === state._irBoost)
continue; // already right state
const record = state._models.get(icao);
if (!record || record.irBoost === state._irBoost) continue; // already right state
_releaseModel(icao);
}
if (state._irReloadQueue.length === 0) state._irReloadQueue = null;
@@ -435,16 +434,17 @@ export function createAircraftModels({
// 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;
const visual = aircraftVisuals.write(model);
visual.specKey = specKey; // class-change sync compares against this
visual.irBoost = loadIrBoost; // boost-flip reload queue compares against this
const record = createModelRecord(model, {
specKey, // class-change sync compares against this
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 icon for the frames
// between admission and the next fleet tick (and draw at the identity matrix,
// i.e. the Earth's centre). The handoff turns it on once it has a matrix.
setModel(model, 'show', false);
showModel(record, false);
state._modelCollection.add(model);
state._models.set(icao24, model);
state._models.set(icao24, record);
state._planeModelLoaded = true; // GLB is cached now — the tracked entity's model can fade in its billboard
}
@@ -452,17 +452,17 @@ export function createAircraftModels({
* Bumps the load generation so any in-flight load for this icao is rejected on completion. */
function _releaseModel(icao24) {
const m = state._models.get(icao24);
const record = state._models.get(icao24);
const pending = state._modelPending.has(icao24);
// Only bump the generation when there's something to invalidate (an in-flight load or a
// live model) — so removing never-modeled aircraft doesn't grow _modelGen.
if (m || pending) {
if (record || pending) {
state._modelGen.set(icao24, (state._modelGen.get(icao24) || 0) + 1);
}
if (m) {
if (record) {
if (state._modelCollection && !state._modelCollection.isDestroyed()) {
try {
state._modelCollection.remove(m);
state._modelCollection.remove(record.model);
} catch {
/* gone */
}
@@ -492,9 +492,9 @@ export function createAircraftModels({
for (const icao of state._modelPending)
state._modelGen.set(icao, (state._modelGen.get(icao) || 0) + 1);
if (state._modelCollection && !state._modelCollection.isDestroyed()) {
for (const m of state._models.values()) {
for (const record of state._models.values()) {
try {
state._modelCollection.remove(m);
state._modelCollection.remove(record.model);
} catch {
/* gone */
}
@@ -521,7 +521,7 @@ export function createAircraftModels({
if (state._trackedModel) {
if (state._modelCollection && !state._modelCollection.isDestroyed()) {
try {
state._modelCollection.remove(state._trackedModel);
state._modelCollection.remove(state._trackedModel.model);
} catch {
/* gone */
}
@@ -542,7 +542,7 @@ export function createAircraftModels({
state._modelCollection &&
!state._modelCollection.isDestroyed();
if (!active) {
if (state._trackedModel) setModel(state._trackedModel, 'show', false);
if (state._trackedModel) showModel(state._trackedModel, false);
return;
}
// Ask the frame-cached source directly. If the entity callback already ran,
@@ -553,7 +553,7 @@ export function createAircraftModels({
parts.motion._trackedDisplayPosition(state._trackedIcao) ||
parts.icons._iconPosition(state._trackedIcao);
if (!pos) {
if (state._trackedModel) setModel(state._trackedModel, 'show', false);
if (state._trackedModel) showModel(state._trackedModel, false);
return;
}
if (
@@ -621,8 +621,11 @@ export function createAircraftModels({
// Assign after resolution as well as in the creation options so the
// standalone primitive always exposes the tracked aircraft pick id.
m.id = state._trackedIcao;
aircraftVisuals.write(m).specKey = trackedKey; // class-change sync compares against this
setModel(m, 'show', false); // admitted, not yet the visual — the driver shows it once placed
const record = createModelRecord(m, {
specKey: trackedKey, // class-change sync compares against this
irBoost: trackedIrBoost,
});
showModel(record, 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
// default here produces a one-frame jump to the Earth's center.
@@ -636,9 +639,8 @@ export function createAircraftModels({
state._scratchGroundPos,
);
if (displayPos)
setModel(
m,
'modelMatrix',
placeModel(
record,
_modelMatrix(
displayPos,
parts.motion._trackedDisplayCourse(),
@@ -648,7 +650,7 @@ export function createAircraftModels({
),
);
}
state._trackedModel = m;
state._trackedModel = record;
state._trackedModelLoading = false;
// A good load retires this selection's failure budget — a contact that
// recovers after a transient blip is not one attempt from giving up.
@@ -675,15 +677,14 @@ export function createAircraftModels({
state._scratchGroundPos,
);
if (!displayPos) {
setModel(state._trackedModel, 'show', false); // no ground evidence → the billboard carries it
showModel(state._trackedModel, false); // no ground evidence → the icon carries it
return;
}
const spec = _modelSpec(
state.records.data.get(state._trackedIcao)?.klass,
);
setModel(
placeModel(
state._trackedModel,
'modelMatrix',
_modelMatrix(
displayPos,
parts.motion._trackedDisplayCourse(),
@@ -691,8 +692,8 @@ export function createAircraftModels({
spec.headingOffsetDeg ?? policy.MODEL_HEADING_OFFSET_DEG,
),
);
if (!state._trackedModel.ready) return;
state._trackedModel.scale = trackedModelScaleForPixelCap({
if (!state._trackedModel.model.ready) return;
state._trackedModel.model.scale = trackedModelScaleForPixelCap({
baseScale: spec.scale,
nativeRadiusM: spec.nativeRadiusM,
rangeM: Cesium.Cartesian3.distance(
@@ -703,7 +704,7 @@ export function createAircraftModels({
fovyRad: state._viewer.camera.frustum.fovy,
maximumPixelSize: policy.TRACKED_MODEL_MAX_PX,
});
setModel(state._trackedModel, 'show', true);
showModel(state._trackedModel, true);
}
}
@@ -824,22 +825,22 @@ export function createAircraftModels({
if (!parts.icons._aboveHorizon(row, position, occluder)) {
// A model that crossed the limb would keep rendering through the
// hidden globe at its last matrix; the icon's own cull is on the GPU.
const m = state._models.get(icao24);
if (m && modelState(m)?.show) setModel(m, 'show', false);
const hidden = state._models.get(icao24);
if (hidden?.show) showModel(hidden, false);
continue;
}
_ensureModel(icao24);
const model = state._models.get(icao24);
const record = state._models.get(icao24);
const cameraDistanceM = Cesium.Cartesian3.distance(camPos, position);
_driveFleetModelHandoff(
icao24,
model,
record,
row,
position,
icons.course(row, tau),
() => {
applyAircraftModelTreatment({
model,
model: record.model,
// IR boost must survive the per-tick treatment write — otherwise
// any alpha change would repaint the ordinary tint over the hot
// white. Boosted models also skip the recession fade: hot targets
@@ -871,7 +872,6 @@ export function createAircraftModels({
_modelDisplayPosition,
_driveFleetModelHandoff,
_modelOwnsVisual,
_modelIsRendering,
_specKeyFor,
_syncModelToClass,
_reloadModelsForIrBoost,
+5 -77
View File
@@ -1,11 +1,11 @@
/**
* Limb-relative recession for distant aircraft billboards.
* Limb-relative recession for distant aircraft: the factors icon sets apply in
* their shader (ICON_RECEDES) and models apply through their colour alpha.
*
* This multiplies the billboard's ordinary scale, which Cesium then composes
* with the locked NearFarScalar. It never edits that scalar, culls a contact,
* or allows alpha to reach zero.
* It multiplies an icon's ordinary scale, which composes with its locked
* NearFarScalar. It never edits that scalar, culls a contact, or allows alpha
* to reach zero.
*/
import { billboardState, setBillboard } from './billboardMirror.js';
export const DEFAULT_AIRCRAFT_RECESSION_PARAMS = Object.freeze({
startLimbRatio: 0.5,
@@ -20,13 +20,6 @@ export const DEFAULT_AIRCRAFT_RECESSION_PARAMS = Object.freeze({
let _params = { ...DEFAULT_AIRCRAFT_RECESSION_PARAMS };
const _scratchFactors = { scale: 1, alpha: 1, limbRatio: null };
const _treatmentResult = {
scale: 1,
alpha: 1,
scaleWrites: 0,
alphaWrites: 0,
factors: _scratchFactors,
};
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
@@ -178,71 +171,6 @@ export function aircraftRecessionFactors(
);
}
/**
* Production wire helper: compose base scale/alpha, focus emphasis, and limb
* recession into one deadband-gated billboard write site.
* @param {object} input
* @param {object} input.billboard
* @param {number} input.baseScale
* @param {number} input.baseAlpha
* @param {{withAlpha:(alpha:number)=>object}} input.baseColor
* @param {number} input.focusFactor
* @param {number} input.cameraDistanceM
* @param {number} input.cameraHeightM
* @param {Partial<typeof DEFAULT_AIRCRAFT_RECESSION_PARAMS>} [input.params]
*
* IMPORTANT: the returned object (including `factors`) is a module-owned
* mutable singleton. Consume every needed property before the next call;
* never retain it as a snapshot.
* @returns {{scale:number,alpha:number,scaleWrites:number,alphaWrites:number,factors:object}}
*/
export function applyAircraftBillboardTreatment({
billboard,
baseScale,
baseAlpha,
baseColor,
focusFactor,
cameraDistanceM,
cameraHeightM,
params,
}) {
const tuning = resolvedParams(params);
const factors = aircraftRecessionFactorsResolved(
cameraDistanceM,
cameraHeightM,
tuning,
_scratchFactors,
);
const scale = baseScale * factors.scale;
const composedTreatment = Math.max(
tuning.combinedAlphaFloor,
clamp(focusFactor, 0, 1) * factors.alpha,
);
const alpha = baseAlpha * composedTreatment;
let scaleWrites = 0;
let alphaWrites = 0;
const pushedScale = billboardState(billboard)?.scale;
if (
!Number.isFinite(pushedScale) ||
Math.abs(pushedScale - scale) > tuning.writeEpsilon
) {
setBillboard(billboard, 'scale', scale);
scaleWrites = 1;
}
if (
!Number.isFinite(billboard.color?.alpha) ||
Math.abs(billboard.color.alpha - alpha) > tuning.writeEpsilon
) {
billboard.color = baseColor.withAlpha(alpha);
alphaWrites = 1;
}
_treatmentResult.scale = scale;
_treatmentResult.alpha = alpha;
_treatmentResult.scaleWrites = scaleWrites;
_treatmentResult.alphaWrites = alphaWrites;
return _treatmentResult;
}
/**
* The alpha an aircraft draws with: base × max(combined floor, focus × limb
* haze). Icon sets compose the same thing in their shader; models need it here.
+3 -61
View File
@@ -3,11 +3,10 @@ import assert from 'node:assert/strict';
import {
DEFAULT_AIRCRAFT_RECESSION_PARAMS,
aircraftRecessionFactors,
applyAircraftBillboardTreatment,
applyAircraftModelTreatment,
cameraLimbDistanceM,
aircraftTreatmentAlpha,
} from './aircraftRecession.js';
import { mirrorBillboard } from './billboardMirror.js';
const params = { ...DEFAULT_AIRCRAFT_RECESSION_PARAMS };
const cameraHeightM = 1_000_000;
@@ -67,72 +66,15 @@ test('globe-view transition eases to identity without a threshold pop', () => {
assert.deepEqual(end, { scale: 1, alpha: 1, limbRatio: null });
});
test('aircraft billboard wire writes scale and alpha only for the far treatment', () => {
const color = { withAlpha: (alpha) => ({ alpha }) };
const near = mirrorBillboard({ scale: 1.2, color: { alpha: 0.8 } });
const nearResult = applyAircraftBillboardTreatment({
billboard: near,
baseScale: 1.2,
baseAlpha: 0.8,
baseColor: color,
focusFactor: 1,
cameraDistanceM: limb * 0.4,
cameraHeightM,
params,
});
assert.equal(nearResult.scaleWrites, 0);
assert.equal(nearResult.alphaWrites, 0);
const far = mirrorBillboard({ scale: 1.2, color: { alpha: 0.8 } });
const farResult = applyAircraftBillboardTreatment({
billboard: far,
baseScale: 1.2,
baseAlpha: 0.8,
baseColor: color,
focusFactor: 0.5,
cameraDistanceM: limb,
cameraHeightM,
params,
});
assert.equal(farResult.scaleWrites, 1);
assert.equal(farResult.alphaWrites, 1);
assert.ok(Math.abs(far.scale - (1.2 * 0.45)) < 1e-12);
assert.ok(Math.abs(far.color.alpha - (0.8 * 0.20)) < 1e-12);
});
test('combined focus and haze product is clamped at the composed alpha floor', () => {
const color = { withAlpha: (alpha) => ({ alpha }) };
const billboard = mirrorBillboard({ scale: 0.45, color: { alpha: 1 } });
const result = applyAircraftBillboardTreatment({
billboard,
baseScale: 1,
const alpha = aircraftTreatmentAlpha({
baseAlpha: 1,
baseColor: color,
focusFactor: 0.25,
cameraDistanceM: limb,
cameraHeightM,
params,
});
assert.equal(result.alpha, 0.20);
assert.equal(billboard.color.alpha, 0.20);
});
test('applyAircraftBillboardTreatment returns its documented module singleton', () => {
const color = { withAlpha: (alpha) => ({ alpha }) };
const billboard = mirrorBillboard({ scale: 1, color: { alpha: 1 } });
const input = {
billboard,
baseAlpha: 1,
baseColor: color,
focusFactor: 1,
cameraDistanceM: 0,
cameraHeightM,
params,
};
const first = applyAircraftBillboardTreatment({ ...input, baseScale: 1 });
const second = applyAircraftBillboardTreatment({ ...input, baseScale: 0.5 });
assert.strictEqual(first, second);
assert.equal(first.scale, 0.5, 'the prior reference reflects the next call');
assert.equal(alpha, 0.2);
});
test('ambient model presentation receives composed alpha without changing blend semantics', () => {
-104
View File
@@ -1,104 +0,0 @@
/**
* @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. A
// billboard never given one has none here (Cesium would report the origin).
if (!value) return;
record.position = Cesium.Cartesian3.clone(value, 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
@@ -1,74 +0,0 @@
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.equal(billboardState(bb).position, undefined, 'none was pushed');
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);
});
+4 -4
View File
@@ -27,8 +27,8 @@ const LAYERS = [
_setTrackedFlightRefreshStateForTest({
icao24,
entity: null,
billboard: candidateBillboard(icao24),
billboardCollection: { show: true, remove() {} },
icon: candidateBillboard(icao24),
iconLayer: { show: true, remove() {} },
viewer: candidateViewer(),
tracked: false,
meta: {
@@ -48,8 +48,8 @@ const LAYERS = [
_setTrackedMilitaryRefreshStateForTest({
icao24,
entity: null,
billboard: candidateBillboard(icao24),
billboardCollection: { show: true, remove() {} },
icon: candidateBillboard(icao24),
iconLayer: { show: true, remove() {} },
viewer: candidateViewer(),
tracked: false,
meta: {
+24 -24
View File
@@ -188,8 +188,8 @@ test('nonempty OpenSky payload with zero usable rows cannot prove share target a
_setTrackedFlightRefreshStateForTest({
icao24: 'abc123',
entity: withEntityRecord({}, { labelModel: { title: 'WARM', details: [] } }),
billboard: { show: false },
billboardCollection: { show: true, remove() {} },
icon: { show: false },
iconLayer: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
meta: { rawLat: 30.2, rawLon: -97.7, onGround: false },
});
@@ -229,16 +229,16 @@ test('a newly seen aircraft appears at its delayed position, not ahead of it', a
// first displayed position is its fix projected back to that delayed time.
// Creating the icon at the raw fix drew it ahead until the next fleet
// tick, which then jumped it back about one delay of travel.
const billboardCollection = { show: false };
const iconLayer = { show: false };
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedFlightRefreshStateForTest({
icao24: 'seed00',
entity: null,
billboard: {
icon: {
show: false,
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
},
billboardCollection,
iconLayer,
viewer,
meta: { rawLat: 0, rawLon: 0, onGround: false },
tracked: false,
@@ -317,13 +317,13 @@ test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STA
color: Cesium.Color.WHITE,
show: false,
};
const billboardCollection = { show: false, remove() {} };
const iconLayer = { show: false, remove() {} };
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedFlightRefreshStateForTest({
icao24,
entity,
billboard,
billboardCollection,
icon: billboard,
iconLayer,
viewer,
meta: {
callsign: 'OLD1',
@@ -473,8 +473,8 @@ test('real civil track path creates no native label and publishes every cached h
_setTrackedFlightRefreshStateForTest({
icao24,
entity: null,
billboard,
billboardCollection: { show: true, remove() {} },
icon: billboard,
iconLayer: { show: true, remove() {} },
viewer,
tracked: false,
history: [
@@ -597,7 +597,7 @@ test('real civil track path creates no native label and publishes every cached h
);
_addFlightTrackingCandidateForTest({
icao24: 'late001',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.5, 30.4, 9_000),
},
@@ -621,7 +621,7 @@ test('real civil track path creates no native label and publishes every cached h
);
_addFlightTrackingCandidateForTest({
icao24: 'late002',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.4, 30.5, 8_000),
},
@@ -644,7 +644,7 @@ test('real civil track path creates no native label and publishes every cached h
);
_addFlightTrackingCandidateForTest({
icao24: 'late003',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000),
},
@@ -661,7 +661,7 @@ test('real civil track path creates no native label and publishes every cached h
assert.equal(_pendingFlightTrackingRestoreForTest(), 'late004');
_addFlightTrackingCandidateForTest({
icao24: 'late004',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000),
},
@@ -686,7 +686,7 @@ test('real civil track path creates no native label and publishes every cached h
);
_addFlightTrackingCandidateForTest({
icao24: 'late005',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.1, 30.8, 5_000),
},
@@ -732,12 +732,12 @@ function seedLabelContact({
_setTrackedFlightRefreshStateForTest({
icao24: LABEL_ICAO,
entity: null,
billboard: {
icon: {
position: LABEL_POSITION,
color: Cesium.Color.WHITE,
show: true,
},
billboardCollection: { show: true, remove() {} },
iconLayer: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
tracked,
meta: {
@@ -940,12 +940,12 @@ test('civil tracked readout: a callsign-less enriched contact reads as its regis
_setTrackedFlightRefreshStateForTest({
icao24: LABEL_ICAO,
entity,
billboard: {
icon: {
position: LABEL_POSITION,
color: Cesium.Color.WHITE,
show: false,
},
billboardCollection: { show: false, remove() {} },
iconLayer: { show: false, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
meta: {
callsign: ' ',
@@ -1064,16 +1064,16 @@ test('a newly seen taxiing aircraft starts on the floor under its delayed positi
_clearDisplayFloorStateForTest();
_clearMeshFloorCellsForTest();
setMeshFloorPreferred(true);
const billboardCollection = { show: false };
const iconLayer = { show: false };
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedFlightRefreshStateForTest({
icao24: 'seed00',
entity: null,
billboard: {
icon: {
show: false,
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
},
billboardCollection,
iconLayer,
viewer,
meta: { rawLat: 0, rawLon: 0, onGround: false },
tracked: false,
@@ -1154,11 +1154,11 @@ test('far grounded contacts take turns on the floor; near ones refloor every tic
_setTrackedFlightRefreshStateForTest({
icao24: 'seed00',
entity: null,
billboard: {
icon: {
show: false,
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
},
billboardCollection: { show: false },
iconLayer: { show: false },
viewer,
meta: { rawLat: 0, rawLon: 0, onGround: false },
tracked: false,
+1 -1
View File
@@ -23,6 +23,6 @@ test('converged focus treatment stays within the GC-bracketed allocation budget'
assert.equal(result.status, 0, result.stderr || result.stdout);
const report = JSON.parse(result.stdout.trim());
assert.ok(report.advanceSpriteFocus.roundedMedian <= 16, result.stdout);
assert.ok(report.applyAircraftBillboardTreatment.roundedMedian <= 16, result.stdout);
assert.ok(report.aircraftTreatmentAlpha.roundedMedian <= 16, result.stdout);
assert.ok(report.productionSpriteTick.roundedMedian <= 212, result.stdout);
});
+16 -16
View File
@@ -889,8 +889,8 @@ test('Contacts activation adopts the production military tracked-subject descrip
_setTrackedMilitaryRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, show: true },
billboardCollection: { show: true, remove() {} },
icon: { position, show: true },
iconLayer: { show: true, remove() {} },
viewer: sourceViewer,
meta: {
callsign: 'RCH451 ',
@@ -971,8 +971,8 @@ test('Contacts activation adopts the production military tracked-subject descrip
_setTrackedMilitaryRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, show: true },
billboardCollection: { show: true, remove() {} },
icon: { position, show: true },
iconLayer: { show: true, remove() {} },
viewer: sourceViewer,
meta: {},
tracked: false,
@@ -998,8 +998,8 @@ test('production military tracked-subject label falls back to registration befor
_setTrackedMilitaryRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, show: true },
billboardCollection: { show: true, remove() {} },
icon: { position, show: true },
iconLayer: { show: true, remove() {} },
viewer: sourceViewer,
meta: {
callsign: ' ',
@@ -1051,8 +1051,8 @@ test('production military tracked-subject label falls back to registration befor
_setTrackedMilitaryRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, show: true },
billboardCollection: { show: true, remove() {} },
icon: { position, show: true },
iconLayer: { show: true, remove() {} },
viewer: sourceViewer,
meta: {},
tracked: false,
@@ -1097,8 +1097,8 @@ test('a cached Context subject re-reads its label when enrichment lands after se
_setTrackedFlightRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, color: Cesium.Color.WHITE, show: true },
billboardCollection: { show: true, remove() {} },
icon: { position, color: Cesium.Color.WHITE, show: true },
iconLayer: { show: true, remove() {} },
viewer: sourceViewer,
meta,
});
@@ -1185,8 +1185,8 @@ test('a cached Context subject re-reads its label when enrichment lands after se
_setTrackedFlightRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, show: true },
billboardCollection: { show: false, remove() {} },
icon: { position, show: true },
iconLayer: { show: false, remove() {} },
viewer: sourceViewer,
meta: {},
tracked: false,
@@ -1207,8 +1207,8 @@ test('Contacts activation adopts the production civilian tracked-subject descrip
_setTrackedFlightRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, show: true },
billboardCollection: { show: true, remove() {} },
icon: { position, show: true },
iconLayer: { show: true, remove() {} },
viewer: sourceViewer,
meta: {
callsign: 'DAL123 ',
@@ -1296,8 +1296,8 @@ test('Contacts activation adopts the production civilian tracked-subject descrip
_setTrackedFlightRefreshStateForTest({
icao24: id,
entity: null,
billboard: { position, show: true },
billboardCollection: { show: true, remove() {} },
icon: { position, show: true },
iconLayer: { show: true, remove() {} },
viewer: sourceViewer,
meta: {},
tracked: false,
+21 -21
View File
@@ -149,8 +149,8 @@ test('nonempty adsb.lol payload with zero usable rows cannot prove share target
_setTrackedMilitaryRefreshStateForTest({
icao24: 'ae1234',
entity: withEntityRecord({}, { labelModel: { title: 'WARM', details: [] } }),
billboard: { show: false },
billboardCollection: { show: true, remove() {} },
icon: { show: false },
iconLayer: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
meta: { rawLat: 31, rawLon: -97, onGround: false },
});
@@ -190,13 +190,13 @@ test('military poll refreshes tracked callsign/altitude/kts and marks a missed p
color: Cesium.Color.WHITE,
show: false,
};
const billboardCollection = { show: false, remove() {} };
const iconLayer = { show: false, remove() {} };
const viewer = { camera: { positionCartographic: null }, scene: {} };
_setTrackedMilitaryRefreshStateForTest({
icao24,
entity,
billboard,
billboardCollection,
icon: billboard,
iconLayer,
viewer,
meta: {
callsign: 'OLD2',
@@ -266,11 +266,11 @@ test('cached military snapshots retain their observation time and expose stale f
_setTrackedMilitaryRefreshStateForTest({
icao24: 'ae01ce',
entity: withEntityRecord({}, { labelModel: { title: 'RCH451', details: [] } }),
billboard: {
icon: {
position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000),
show: false,
},
billboardCollection: { show: false, remove() {} },
iconLayer: { show: false, remove() {} },
viewer,
history,
meta: { rawLat: 31, rawLon: -97, onGround: false },
@@ -408,8 +408,8 @@ test('real military track path creates no native label and publishes every cache
_setTrackedMilitaryRefreshStateForTest({
icao24,
entity: null,
billboard,
billboardCollection: { show: true, remove() {} },
icon: billboard,
iconLayer: { show: true, remove() {} },
viewer,
tracked: false,
history: [
@@ -528,7 +528,7 @@ test('real military track path creates no native label and publishes every cache
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late001',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000),
},
@@ -555,7 +555,7 @@ test('real military track path creates no native label and publishes every cache
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late002',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000),
},
@@ -581,7 +581,7 @@ test('real military track path creates no native label and publishes every cache
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late003',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000),
},
@@ -601,7 +601,7 @@ test('real military track path creates no native label and publishes every cache
assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late004');
_addMilitaryTrackingCandidateForTest({
icao24: 'late004',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000),
},
@@ -629,7 +629,7 @@ test('real military track path creates no native label and publishes every cache
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late005',
billboard: {
icon: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000),
},
@@ -676,7 +676,7 @@ test('a callsign-less contact is findable by the tail number the app displays',
rawLat: 40.71,
rawLon: -74.01,
},
billboard: {
icon: {
position: Cesium.Cartesian3.fromDegrees(-74.01, 40.71, 167),
color: Cesium.Color.WHITE,
show: true,
@@ -756,7 +756,7 @@ test('a registration never outranks another contact’s exact callsign', () => {
rawLat: 31.0,
rawLon: -97.0,
},
billboard: billboardAt(-97.0, 31.0),
icon: billboardAt(-97.0, 31.0),
});
_addMilitaryTrackingCandidateForTest({
icao24: 'ae0bb2',
@@ -767,7 +767,7 @@ test('a registration never outranks another contact’s exact callsign', () => {
rawLat: 31.2,
rawLon: -97.2,
},
billboard: billboardAt(-97.2, 31.2),
icon: billboardAt(-97.2, 31.2),
});
assert.equal(
@@ -808,7 +808,7 @@ test('two contacts matching at the same strength resolve deterministically', ()
rawLat: 32.0,
rawLon: -98.0,
},
billboard: billboardAt(-98.0, 32.0),
icon: billboardAt(-98.0, 32.0),
});
_addMilitaryTrackingCandidateForTest({
icao24: 'aef001',
@@ -819,7 +819,7 @@ test('two contacts matching at the same strength resolve deterministically', ()
rawLat: 32.1,
rawLon: -98.1,
},
billboard: billboardAt(-98.1, 32.1),
icon: billboardAt(-98.1, 32.1),
});
const first = militaryFlightsLayer.findByQuery('QQ7')?.icao24;
@@ -847,12 +847,12 @@ test('the military tracked descriptor publishes the reported vertical rate', ()
_setTrackedMilitaryRefreshStateForTest({
icao24: 'ae7f01',
entity: withEntityRecord({}, { labelModel: { title: 'TEST', details: [] } }),
billboard: {
icon: {
position: Cesium.Cartesian3.fromDegrees(-97.0, 31.0, 8_000),
color: Cesium.Color.WHITE,
show: true,
},
billboardCollection: { show: true, remove() {} },
iconLayer: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
meta: {
callsign: 'RCH512',
-74
View File
@@ -1,74 +0,0 @@
/**
* @module modelMirror
* @description Core's copy of what a layer pushed to its 3D models, so the layer
* reads its own placement back from Core instead of from the renderer's objects.
*
* A layer writes the mirrored fields with {@link setModel} and reads them with
* {@link modelState}. What the renderer measures (`ready`, `computedScale`) is
* still read from the model. Colour stays out for the same reason as on
* billboards: the cyber-sonar theme writes it too and reads the live value.
*/
import * as Cesium from 'cesium';
import { createEntityStore } from './entityRecords.js';
const mirrors = createEntityStore();
/**
* Core's record for `model`, made on first write with Cesium's defaults for a
* model loaded without `show` or `modelMatrix` options (shown, at the identity).
*/
function recordFor(model) {
const record = mirrors.write(model);
if (!record.modelMatrix) {
record.show = true;
record.modelMatrix = Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY);
}
return record;
}
/**
* Start mirroring a model Core did not write itself (test seams), from its
* current fields.
* @param {Object} model
* @returns {Object} The model.
*/
export function mirrorModel(model) {
if (!model || typeof model !== 'object') return model;
const record = recordFor(model);
record.show = model.show ?? true;
if (model.modelMatrix)
Cesium.Matrix4.clone(model.modelMatrix, record.modelMatrix);
return model;
}
/**
* Write one mirrored field to the model and to Core's copy. A matrix is copied
* into both, so a shared scratch can be passed and reused straight after.
* @param {Object} model
* @param {'show'|'modelMatrix'} field
* @param {*} value
*/
export function setModel(model, field, value) {
if (field !== 'show' && field !== 'modelMatrix')
throw new TypeError(`The model mirror does not hold ${field}`);
const record = recordFor(model);
if (field === 'modelMatrix') {
Cesium.Matrix4.clone(value, record.modelMatrix);
// Cesium clones the model's own matrix each frame, so write into it in place.
if (model.modelMatrix) Cesium.Matrix4.clone(value, model.modelMatrix);
else model.modelMatrix = Cesium.Matrix4.clone(value);
} else {
record[field] = value;
model[field] = value;
}
}
/**
* Core's copy of a model's mirrored fields, or null when nothing was written to
* it yet (or there is no model).
* @param {Object|null|undefined} model
* @returns {{ show: boolean, modelMatrix: Cesium.Matrix4 }|null}
*/
export function modelState(model) {
return mirrors.read(model);
}
-67
View File
@@ -1,67 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { mirrorModel, modelState, setModel } from './modelMirror.js';
function fakeModel() {
return { show: true, modelMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY) };
}
test('a model has no mirror until Core writes to it', () => {
assert.equal(modelState(fakeModel()), null);
assert.equal(modelState(null), null);
});
test('the first write starts from Cesium defaults for the other fields', () => {
const model = fakeModel();
setModel(model, 'show', false);
assert.equal(model.show, false);
assert.equal(modelState(model).show, false);
assert.ok(Cesium.Matrix4.equals(modelState(model).modelMatrix, Cesium.Matrix4.IDENTITY));
});
test('a matrix is copied into both the model and the mirror, never shared', () => {
const model = fakeModel();
const own = model.modelMatrix;
const scratch = Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(1, 2, 3));
setModel(model, 'modelMatrix', scratch);
assert.equal(model.modelMatrix, own, "written into the model's own matrix");
assert.ok(Cesium.Matrix4.equals(model.modelMatrix, scratch));
assert.notEqual(modelState(model).modelMatrix, scratch);
Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(9, 9, 9), scratch);
assert.deepEqual(
Cesium.Matrix4.getTranslation(modelState(model).modelMatrix, new Cesium.Cartesian3()),
new Cesium.Cartesian3(1, 2, 3),
'reusing the scratch moves neither copy',
);
assert.ok(!Cesium.Matrix4.equals(model.modelMatrix, scratch));
});
test('the mirror answers from Core, not from the model', () => {
const model = fakeModel();
setModel(model, 'show', true);
model.show = false; // a write that bypassed the mirror
assert.equal(modelState(model).show, true);
});
test("a model without its own matrix yet gets a copy, not the caller's", () => {
const model = { show: true };
const matrix = Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(1, 2, 3));
setModel(model, 'modelMatrix', matrix);
assert.notEqual(model.modelMatrix, matrix);
assert.ok(Cesium.Matrix4.equals(model.modelMatrix, matrix));
});
test('a model Core did not write can be mirrored from its fields', () => {
const matrix = Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(4, 5, 6));
const model = mirrorModel({ show: false, modelMatrix: matrix });
assert.equal(modelState(model).show, false);
assert.ok(Cesium.Matrix4.equals(modelState(model).modelMatrix, matrix));
assert.notEqual(modelState(model).modelMatrix, matrix);
assert.equal(modelState(mirrorModel({})).show, true, 'Cesium shows by default');
assert.equal(mirrorModel(null), null);
});
test('fields outside the mirror are refused', () => {
assert.throws(() => setModel({}, 'scale', 2), TypeError);
});
+79
View File
@@ -0,0 +1,79 @@
/**
* @module modelRecord
* @description A layer's own record of one 3D model it draws, held in the
* layer's maps under the contact's id, so the layer reads what it pushed back
* from itself instead of from the renderer's object.
*
* Write `show` and the placement with {@link showModel} and {@link placeModel};
* read them from the record. What the renderer measures (`ready`,
* `computedScale`) is still read from `record.model`. Colour stays out: the
* cyber-sonar theme writes it too and reads the live value.
*/
import * as Cesium from 'cesium';
/**
* @typedef {object} ModelRecord
* @property {Cesium.Model} model
* @property {boolean} show Whether the layer last showed the model.
* @property {Cesium.Matrix4} matrix Where the layer last placed it.
* @property {string|null} specKey The model spec it was loaded for; a class
* change reloads.
* @property {boolean|null} irBoost The IR boost it was loaded with; a flip
* reloads.
* @property {number} bellyM Metres from the model origin to its belly, to sit
* it on the ground.
*/
/**
* A record for a model the layer just made, from the options it made it with
* (Cesium's defaults when none: shown, at the identity).
* @param {Cesium.Model} model
* @param {{specKey?: string, irBoost?: boolean, bellyM?: number}} [fields]
* @returns {ModelRecord}
*/
export function createModelRecord(
model,
{ specKey = null, irBoost = null, bellyM = 0 } = {},
) {
return {
model,
show: model.show ?? true,
matrix: Cesium.Matrix4.clone(model.modelMatrix ?? Cesium.Matrix4.IDENTITY),
specKey,
irBoost,
bellyM,
};
}
/**
* Show or hide a model and record it.
* @param {ModelRecord} record
* @param {boolean} show
*/
export function showModel(record, show) {
record.show = show;
record.model.show = show;
}
/**
* Place a model and record where. The matrix is copied into both, so a shared
* scratch can be passed and reused straight after.
* @param {ModelRecord} record
* @param {Cesium.Matrix4} matrix
*/
export function placeModel(record, matrix) {
Cesium.Matrix4.clone(matrix, record.matrix);
const model = record.model;
// Cesium clones the model's own matrix each frame, so write into it in place.
if (model.modelMatrix) Cesium.Matrix4.clone(matrix, model.modelMatrix);
else model.modelMatrix = Cesium.Matrix4.clone(matrix);
}
/**
* Whether a record's model is what the user sees: loaded and shown.
* @param {ModelRecord|null|undefined} record
* @returns {boolean}
*/
export function modelIsRendering(record) {
return !!record && record.model.ready === true && record.show === true;
}
+6 -6
View File
@@ -32,7 +32,7 @@ import {
trailHeadStart,
modelAnchorWorld,
} from './modelVisualAnchor.js';
import { mirrorModel } from './modelMirror.js';
import { createModelRecord } from './modelRecord.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
@@ -798,13 +798,13 @@ test('the trail anchor rides the rendered longitudinal axis at every heading', (
position, hpr, Cesium.Ellipsoid.WGS84, undefined, new Cesium.Matrix4(),
);
for (const [url, anchor] of Object.entries(MODEL_TRAIL_ANCHOR_NATIVE)) {
const model = mirrorModel({
const record = createModelRecord({
modelMatrix,
computedScale: 1,
sceneGraph: { components: { transform: rootTransform } },
});
const origin = Cesium.Matrix4.getTranslation(modelMatrix, new Cesium.Cartesian3());
const world = modelAnchorWorld(model, anchor, new Cesium.Cartesian3());
const world = modelAnchorWorld(record, anchor, new Cesium.Cartesian3());
// Reference axes derived INDEPENDENTLY, from the ENU frame and the same
// heading the matrix was built with — deliberately NOT through
// modelAnchorWorld. Taking them through the function under test makes the
@@ -861,13 +861,13 @@ test('a hovering rotorcraft anchors aft of its own hull, whatever its heading',
const anchor = MODEL_TRAIL_ANCHOR_NATIVE['/models/bell206.glb'];
for (const headingDeg of [0, 137.5, 271.9]) {
const hpr = new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(headingDeg), 0, 0);
const model = mirrorModel({
const record = createModelRecord({
modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame(
position, hpr, Cesium.Ellipsoid.WGS84, undefined, new Cesium.Matrix4()),
computedScale: 1,
});
const origin = Cesium.Matrix4.getTranslation(model.modelMatrix, new Cesium.Cartesian3());
const world = modelAnchorWorld(model, anchor, new Cesium.Cartesian3());
const origin = Cesium.Matrix4.getTranslation(record.matrix, new Cesium.Cartesian3());
const world = modelAnchorWorld(record, anchor, new Cesium.Cartesian3());
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(origin, Cesium.Ellipsoid.WGS84, new Cesium.Matrix4());
const ax = (i) => {
const c = Cesium.Matrix4.getColumn(enu, i, new Cesium.Cartesian4());
+6 -4
View File
@@ -1,5 +1,4 @@
import * as Cesium from 'cesium';
import { modelState } from './modelMirror.js';
/**
* Model-space visual centres measured from the shipped GLBs' scene-space AABBs.
*
@@ -107,13 +106,16 @@ const _chainScratch = new Cesium.Matrix4();
* here, so exactly one transform — Cesium's own — stands between the table and
* the world, and there is no second convention to drift.
*
* @param {object} model Cesium Model (needs modelMatrix, computedScale, sceneGraph).
* @param {import('./modelRecord.js').ModelRecord} record The layer's record of
* the model: where it placed it, and the Cesium Model (computedScale,
* sceneGraph).
* @param {readonly number[]} nativeAnchor Raw glTF `[x, y, z]` native-unit offset.
* @param {{x:number,y:number,z:number}} result Mutable Cartesian-like result.
* @returns {{x:number,y:number,z:number}|null} World position of the anchor.
*/
export function modelAnchorWorld(model, nativeAnchor, result) {
const modelMatrix = modelState(model)?.modelMatrix;
export function modelAnchorWorld(record, nativeAnchor, result) {
const modelMatrix = record?.matrix;
const model = record?.model;
if (!modelMatrix || !nativeAnchor || !result) return null;
const scale = Number.isFinite(model.computedScale) ? model.computedScale : 1;
_anchorScratch.x = (nativeAnchor[0] || 0) * scale;
+4 -4
View File
@@ -51,8 +51,8 @@ const LAYERS = [
_setTrackedFlightRefreshStateForTest({
icao24: ICAO,
entity: null,
billboard: contactBillboard(billboardShow),
billboardCollection: { show: true, remove() {} },
icon: contactBillboard(billboardShow),
iconLayer: { show: true, remove() {} },
viewer: contactViewer(),
tracked: false,
models,
@@ -74,8 +74,8 @@ const LAYERS = [
_setTrackedMilitaryRefreshStateForTest({
icao24: ICAO,
entity: null,
billboard: contactBillboard(billboardShow),
billboardCollection: { show: true, remove() {} },
icon: contactBillboard(billboardShow),
iconLayer: { show: true, remove() {} },
viewer: contactViewer(),
tracked: false,
models,
+8 -8
View File
@@ -164,8 +164,8 @@ test('a conversion survives a poll refresh, in both the billboard and the tracke
_setTrackedFlightRefreshStateForTest({
icao24,
entity,
billboard,
billboardCollection: { show: false, remove() {} },
icon: billboard,
iconLayer: { show: false, remove() {} },
viewer,
meta: {
callsign: 'OLD1',
@@ -298,13 +298,13 @@ test('conversions are session-scoped and no lifecycle path clears them', async (
_setTrackedFlightRefreshStateForTest({
icao24,
entity: null,
billboard: {
icon: {
position: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 9_000),
color: Cesium.Color.WHITE,
image: aircraftIcon('tr3b'),
show: true,
},
billboardCollection: { show: false, remove() {} },
iconLayer: { show: false, remove() {} },
viewer,
tracked: false,
meta: { callsign: 'OLD1', altitude: 9_000, klass: 'airliner', rawLat: 30.2, rawLon: -97.7 },
@@ -462,12 +462,12 @@ test('cockpit class filter matches a converted contact end to end', async () =>
const seed = () => _setTrackedFlightRefreshStateForTest({
icao24,
entity: null,
billboard: {
icon: {
position: Cesium.Cartesian3.fromDegrees(-97.71, 30.21, 10_668),
color: Cesium.Color.WHITE,
show: true,
},
billboardCollection: { show: true, remove() {} },
iconLayer: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
tracked: false,
meta: { callsign: 'SWA696 ', altitude: 10_668, klass: 'airliner', onGround: false },
@@ -508,12 +508,12 @@ test('military records and detection cards agree with the conversion', () => {
const seed = () => _setTrackedMilitaryRefreshStateForTest({
icao24,
entity: null,
billboard: {
icon: {
position: Cesium.Cartesian3.fromDegrees(-97.71, 30.21, 10_668),
color: Cesium.Color.WHITE,
show: true,
},
billboardCollection: { show: true, remove() {} },
iconLayer: { show: true, remove() {} },
viewer: { camera: { positionCartographic: null }, scene: {} },
tracked: false,
meta: { callsign: 'RCH451', altitudeFt: 35_000, klass: 'quadjet', type: 'C-17A', onGround: false },
+10 -11
View File
@@ -49,7 +49,6 @@ import militaryFlightsLayer, {
} from './militaryFlights.js';
import { clearTr3bRegistry, setTr3b } from './tr3bRegistry.js';
import { reportMeshFloorCell, _clearMeshFloorCellsForTest } from './groundFloor.js';
import { setBillboard } from './billboardMirror.js';
// 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
@@ -231,8 +230,8 @@ const LAYERS = [
_setTrackedFlightRefreshStateForTest({
icao24,
entity: null,
billboard,
billboardCollection: { show: true, remove() {} },
icon: billboard,
iconLayer: { show: true, remove() {} },
viewer,
tracked,
models,
@@ -256,8 +255,8 @@ const LAYERS = [
_setTrackedMilitaryRefreshStateForTest({
icao24,
entity: null,
billboard,
billboardCollection: { show: true, remove() {} },
icon: billboard,
iconLayer: { show: true, remove() {} },
viewer,
tracked,
models,
@@ -375,7 +374,7 @@ for (const fixture of LAYERS) {
onGround: true,
models: [[icao24, model]],
});
setBillboard(billboard, 'show', false);
billboard.show = false;
let ownsVisual = fixture.driveFleetModel({ icao24, position, course: 92 });
assert.equal(ownsVisual, false, 'cold terrain cannot transfer ownership to 3D');
@@ -511,7 +510,7 @@ for (const fixture of LAYERS) {
onGround: true,
models: [[icao24, model]],
});
setBillboard(billboard, 'show', false);
billboard.show = false;
const atA = Cesium.Cartesian3.fromDegrees(A_LON, LAT, 0);
const atB = Cesium.Cartesian3.fromDegrees(B_LON, LAT, 0);
@@ -646,7 +645,7 @@ for (const fixture of LAYERS) {
0,
1e-6,
), 'a hidden tracked model cannot steal the detection anchor');
setBillboard(billboard, 'show', false); // tracking hides the fleet sprite; the tracked entity remains the 2D owner
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)
.find((object) => object.icao24 === (fixture.name === 'flights' ? 'a0f001' : 'b0f001'));
assert.ok(loadingNearby, 'the tracked contact remains in proximity while its model loads');
@@ -685,7 +684,7 @@ for (const fixture of LAYERS) {
viewer.tileset.tilesLoaded = false;
sampledHeight = undefined;
setBillboard(billboard, 'position', Cesium.Cartesian3.fromDegrees(-97.71, 30.21, -500));
billboard.position = Cesium.Cartesian3.fromDegrees(-97.71, 30.21, -500);
fixture.driveTrackedModel();
assert.equal(model.show, true,
'a successful cached snap survives a poll-time vertical datum change');
@@ -695,7 +694,7 @@ for (const fixture of LAYERS) {
// Taxi past the resample threshold with the tiles gone: the tracked model
// holds its last measured floor rather than handing the operator's own
// selection back to a 2D glyph for a retry backoff.
setBillboard(billboard, 'position', Cesium.Cartesian3.fromDegrees(-97.709, 30.21, -500));
billboard.position = Cesium.Cartesian3.fromDegrees(-97.709, 30.21, -500);
fixture.driveTrackedModel();
assert.equal(model.show, true,
'a taxi-invalidated tracked snap is HELD through the resample outage');
@@ -704,7 +703,7 @@ for (const fixture of LAYERS) {
assert.equal(sampleCalls, 1, 'the hold costs no extra synchronous sample');
// Past the drift bound the memory is released and the gate takes over.
setBillboard(billboard, 'position', Cesium.Cartesian3.fromDegrees(-97.707, 30.21, -500));
billboard.position = Cesium.Cartesian3.fromDegrees(-97.707, 30.21, -500);
fixture.driveTrackedModel();
assert.equal(model.show, false,
'past HELD_SNAP_MAX_DRIFT_M the hold is released and the model is withheld');
-11
View File
@@ -1,11 +0,0 @@
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();
+6 -7
View File
@@ -32,7 +32,6 @@ import {
DISPLAY_CORRIDOR_CELL_BUDGET,
DISPLAY_CORRIDOR_FAIR_SHARE,
} from './policy.js';
import { modelState } from '../../data/modelMirror.js';
import { readEntityRecord } from '../../data/entityRecords.js';
import {
aircraftMotionRegime,
@@ -476,10 +475,10 @@ export function createMotion({
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
return modelVisualAnchor(
modelState(flightState._trackedModel).modelMatrix,
flightState._trackedModel.matrix,
spec.visualCenterNative,
Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale
Number.isFinite(flightState._trackedModel.model.computedScale)
? flightState._trackedModel.model.computedScale
: spec.scale,
flightState._trackedVisualPos,
);
@@ -505,7 +504,7 @@ export function createMotion({
)
return null;
return Cesium.Matrix4.getTranslation(
modelState(flightState._trackedModel).modelMatrix,
flightState._trackedModel.matrix,
flightState._scratchTrailClip,
);
}
@@ -520,8 +519,8 @@ export function createMotion({
const spec = parts.rendering._modelSpec(
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
const scale = Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale
const scale = Number.isFinite(flightState._trackedModel.model.computedScale)
? flightState._trackedModel.model.computedScale
: spec.scale;
return spec.nativeRadiusM * scale;
}
+8 -6
View File
@@ -16,7 +16,6 @@ import {
FLEET_DR_INTERVAL_MS,
} from './policy.js';
import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js';
import { modelState } from '../../data/modelMirror.js';
export function createQueries({
flightState,
@@ -440,8 +439,11 @@ export function createQueries({
const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (icon hidden) so the detection box
// doesn't vanish on the 2D→3D handoff.
const model = flightState._models.get(icao24) ?? null;
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24, model);
const record = flightState._models.get(icao24) ?? null;
const modelOwnsVisual = parts.rendering._modelOwnsVisual(
icao24,
record,
);
if (!isTracked && !icons.shown(row) && !modelOwnsVisual) continue;
const info = flightState.records.data.get(icao24);
let object = flightState._detectionObjects.get(icao24);
@@ -470,10 +472,10 @@ export function createQueries({
icons.position(row, tau, object._iconPos)
: modelOwnsVisual
? modelVisualAnchor(
modelState(model).modelMatrix,
record.matrix,
spec.visualCenterNative,
Number.isFinite(model.computedScale)
? model.computedScale
Number.isFinite(record.model.computedScale)
? record.model.computedScale
: spec.scale,
object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
)
+17 -14
View File
@@ -6,7 +6,11 @@ import { createRendering } from './rendering.js';
import { createIcons } from './icons.js';
import { createIconSet } from '../../data/iconSet.js';
import { writeHoldSegment } from '../../data/iconSegment.js';
import { mirrorModel, setModel } from '../../data/modelMirror.js';
import {
createModelRecord,
placeModel,
showModel,
} from '../../data/modelRecord.js';
function fixture() {
const info = {
@@ -109,7 +113,7 @@ test('candidate collection resolves models once and keeps handoffs live', (t) =>
const get = t.mock.method(f.flightState._models, 'get');
const [candidate] = f.queries.getDetectableObjects();
assert.equal(get.mock.callCount(), 1);
const model = mirrorModel({
const record = createModelRecord({
ready: false,
show: true,
modelMatrix: Cesium.Matrix4.fromTranslation(
@@ -120,25 +124,24 @@ test('candidate collection resolves models once and keeps handoffs live', (t) =>
visualCenterNative: Cesium.Cartesian3.ZERO,
scale: 1,
});
f.flightState._models.set('abc123', model);
f.flightState._models.set('abc123', record);
f.show(false);
assert.deepEqual(f.queries.getDetectableObjects(), []);
model.ready = true;
record.model.ready = true;
assert.equal(f.queries.getDetectableObjects()[0], candidate);
assert.deepEqual(candidate.position, new Cesium.Cartesian3(10, 20, 30));
setModel(
model,
'modelMatrix',
placeModel(
record,
Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(40, 50, 60)),
);
f.queries.getDetectableObjects();
assert.deepEqual(candidate.position, new Cesium.Cartesian3(40, 50, 60));
setModel(model, 'show', false);
showModel(record, false);
assert.deepEqual(f.queries.getDetectableObjects(), []);
f.show(true);
assert.deepEqual(f.queries.getDetectableObjects()[0].position, f.drawn());
assert.equal(get.mock.callCount(), 6);
setModel(model, 'show', true);
showModel(record, true);
assert.equal(f.parts.rendering._modelOwnsVisual('abc123'), true);
assert.equal(get.mock.callCount(), 7);
});
@@ -146,7 +149,7 @@ test('candidate collection resolves models once and keeps handoffs live', (t) =>
test('tracked ownership and live anchors remain independent of fleet models', () => {
const f = fixture();
f.flightState._trackedIcao = 'abc123';
f.flightState._trackedModel = mirrorModel({ ready: true, show: true });
f.flightState._trackedModel = createModelRecord({ ready: true, show: true });
f.parts.rendering._modelSpec = () => ({});
let anchor = new Cesium.Cartesian3(10, 20, 30);
f.parts.motion._trackedVisualCached = () => anchor;
@@ -158,16 +161,16 @@ test('tracked ownership and live anchors remain independent of fleet models', ()
assert.equal(f.queries.getDetectableObjects()[0].position, anchor);
for (const tracked of [
null,
mirrorModel({ ready: false, show: true }),
mirrorModel({ ready: true, show: false }),
createModelRecord({ ready: false, show: true }),
createModelRecord({ ready: true, show: false }),
]) {
f.flightState._trackedModel = tracked;
assert.equal(
f.parts.rendering._modelOwnsVisual('abc123', mirrorModel({ ready: true, show: true })),
f.parts.rendering._modelOwnsVisual('abc123', createModelRecord({ ready: true, show: true })),
false,
);
}
f.flightState._trackedModel = mirrorModel({ ready: true, show: true });
f.flightState._trackedModel = createModelRecord({ ready: true, show: true });
f.parts.tracking._trackedModelRegimeActive = () => false;
assert.equal(f.parts.rendering._modelOwnsVisual('abc123', null), false);
});
+2 -2
View File
@@ -239,8 +239,8 @@ export function createFlightSnapshotRenderer({
sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, {
excludeObjects: [
...icons._iconIds(),
...flightState._models.values(),
flightState._trackedModel,
...[...flightState._models.values()].map((record) => record.model),
flightState._trackedModel?.model,
].filter(Boolean),
viewerLat: viewerLatDeg,
viewerLon: viewerLonDeg,
+11 -11
View File
@@ -1,6 +1,6 @@
import * as Cesium from 'cesium';
import { CYAN_TRANSPARENT } from './policy.js';
import { mirrorModel } from '../../data/modelMirror.js';
import { createModelRecord } from '../../data/modelRecord.js';
import { createIconSet } from '../../data/iconSet.js';
import { aircraftIconTestSeams } from '../../data/aircraftIconLayer.js';
@@ -49,9 +49,9 @@ export function createTesting({
* @param {string} state.icao24
* @param {object} state.entity
* @param {object} state.meta
* @param {{position?: Cesium.Cartesian3, show?: boolean}} [state.billboard]
* @param {{position?: Cesium.Cartesian3, show?: boolean}} [state.icon]
* Where the contact's icon is held, and whether it is shown.
* @param {{show?: boolean}} [state.billboardCollection] Layer visibility.
* @param {{show?: boolean}} [state.iconLayer] Layer visibility.
* @param {object} state.viewer
* @param {Array<Object>} [state.history=[]]
* @param {boolean} [state.tracked=true]
@@ -63,8 +63,8 @@ export function createTesting({
icao24,
entity,
meta,
billboard,
billboardCollection,
icon,
iconLayer,
viewer,
history = [],
tracked = true,
@@ -76,13 +76,13 @@ export function createTesting({
flightState._icons = createIconSet({
epoch: parts.icons._displayTime(new Cesium.JulianDate()),
});
flightState._icons.show = billboardCollection?.show ?? true;
flightState._icons.show = iconLayer?.show ?? true;
flightState._grounded.clear();
flightState._floorPinned.clear();
_seedIconForTest(icao24, billboard, tracked);
_seedIconForTest(icao24, icon, tracked);
flightState._models.clear();
for (const [key, model] of models)
flightState._models.set(key, mirrorModel(model));
flightState._models.set(key, createModelRecord(model));
flightState._detectionObjects = new Map();
flightState.records.data = new Map([[icao24, meta]]);
flightState._positionHistory = new Map([[icao24, history]]);
@@ -125,10 +125,10 @@ export function createTesting({
function _addFlightTrackingCandidateForTest({
icao24,
meta,
billboard,
icon,
history = [],
}) {
_seedIconForTest(icao24, billboard, false);
_seedIconForTest(icao24, icon, false);
flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history);
}
@@ -205,7 +205,7 @@ export function createTesting({
async function _ensureFleetModelForTest(icao24) {
await parts.rendering._ensureModel(icao24);
return flightState._models.get(icao24) || null;
return flightState._models.get(icao24)?.model ?? null;
}
/** Run the fleet tick's grounded floor pass once, at `renderTime`. */
+2 -1
View File
@@ -1193,7 +1193,8 @@ export function createTracking({
const rawPick =
typeof picked.id === 'string' ? picked.id : picked.primitive?.id;
if (
picked.primitive === flightState._trackedModel ||
(flightState._trackedModel &&
picked.primitive === flightState._trackedModel.model) ||
rawPick === flightState._trackedIcao
)
return;
+27 -24
View File
@@ -15,8 +15,11 @@ import {
MODEL_PROX_KEEP_M,
} from '../flights/policy.js';
import { AIRCRAFT_ENVIRONMENT_MAP_OPTIONS } from '../../data/aircraftModelLighting.js';
import { aircraftVisuals } from '../aircraft/visualState.js';
import { setModel } from '../../data/modelMirror.js';
import {
createModelRecord,
placeModel,
showModel,
} from '../../data/modelRecord.js';
const specKey = (spec) => `${spec.url}@${spec.scale}`;
@@ -48,7 +51,7 @@ export function createLocalAdsbModels({
}) {
const collection = new Cesium.PrimitiveCollection();
viewer.scene.primitives.add(collection);
/** @type {Map<string, object>} id -> admitted model */
/** @type {Map<string, import('../../data/modelRecord.js').ModelRecord>} id -> admitted model */
const models = new Map();
/** @type {Map<string, number>} id -> generation of an in-flight load */
const pending = new Map();
@@ -64,10 +67,10 @@ export function createLocalAdsbModels({
function release(id) {
pending.delete(id);
const model = models.get(id);
if (!model) return;
const record = models.get(id);
if (!record) return;
models.delete(id);
if (!collection.isDestroyed()) collection.remove(model);
if (!collection.isDestroyed()) collection.remove(record.model);
groundSnap?.forget?.(id);
}
@@ -81,7 +84,7 @@ export function createLocalAdsbModels({
const spec = civilAircraftModelSpec(marker.klass);
const key = specKey(spec);
const current = models.get(id);
if (current && aircraftVisuals.read(current)?.specKey === key) return;
if (current?.specKey === key) return;
if (current) release(id);
if (pending.has(id) || models.size + pending.size >= cap) return;
const loadGeneration = generation;
@@ -116,13 +119,14 @@ export function createLocalAdsbModels({
return;
}
model.id = id;
const visual = aircraftVisuals.write(model);
visual.specKey = key;
visual.bellyM = spec.bellyM;
const record = createModelRecord(model, {
specKey: key,
bellyM: spec.bellyM,
});
// Admitted, not yet the visual: placement turns it on.
setModel(model, 'show', false);
showModel(record, false);
collection.add(model);
models.set(id, model);
models.set(id, record);
}
function regime(preferences) {
@@ -171,11 +175,11 @@ export function createLocalAdsbModels({
function placement(id, marker) {
if (!marker.onGround) return marker.position;
const model = models.get(id);
const record = models.get(id);
// The vertical sample must hit the tile skin, not this aircraft's own
// billboard or model.
const height = groundSnap?.heightFor?.(viewer, id, marker.position, () =>
[marker.entity, model].filter(Boolean),
[marker.entity, record?.model].filter(Boolean),
);
if (!Number.isFinite(height)) return null;
const carto = Cesium.Cartographic.fromCartesian(
@@ -186,7 +190,7 @@ export function createLocalAdsbModels({
return Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
height + (aircraftVisuals.read(model)?.bellyM || 0),
height + (record?.bellyM || 0),
Cesium.Ellipsoid.WGS84,
scratchGround,
);
@@ -221,12 +225,12 @@ export function createLocalAdsbModels({
}
const occluder = horizonOccluder(viewer.camera);
for (const [id, marker] of markers) {
const model = models.get(id);
const record = models.get(id);
marker.modelOwnsVisual = false;
if (!model || !marker.position) continue;
if (!record || !marker.position) continue;
const position = placement(id, marker);
if (!position || !occluder.isPointVisible(marker.position)) {
setModel(model, 'show', false);
showModel(record, false);
continue;
}
scratchHpr.heading = Cesium.Math.toRadians(
@@ -234,9 +238,8 @@ export function createLocalAdsbModels({
);
scratchHpr.pitch = 0;
scratchHpr.roll = 0;
setModel(
model,
'modelMatrix',
placeModel(
record,
Cesium.Transforms.headingPitchRollToFixedFrame(
position,
scratchHpr,
@@ -245,11 +248,11 @@ export function createLocalAdsbModels({
scratchMatrix,
),
);
if (!model.ready) {
setModel(model, 'show', false);
if (!record.model.ready) {
showModel(record, false);
continue;
}
setModel(model, 'show', true);
showModel(record, true);
marker.modelOwnsVisual = true;
}
for (const id of [...models.keys()]) if (!markers.has(id)) release(id);
+6 -7
View File
@@ -21,7 +21,6 @@ import {
COURSE_SLEW_DT_MAX_SEC,
COURSE_MAX_DPS,
} from './policy.js';
import { modelState } from '../../data/modelMirror.js';
import { readEntityRecord } from '../../data/entityRecords.js';
import {
aircraftMotionRegime,
@@ -340,10 +339,10 @@ export function createMotion({
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
return modelVisualAnchor(
modelState(flightState._trackedModel).modelMatrix,
flightState._trackedModel.matrix,
spec.visualCenterNative,
Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale
Number.isFinite(flightState._trackedModel.model.computedScale)
? flightState._trackedModel.model.computedScale
: spec.scale,
flightState._trackedVisualPos,
);
@@ -367,7 +366,7 @@ export function createMotion({
)
return null;
return Cesium.Matrix4.getTranslation(
modelState(flightState._trackedModel).modelMatrix,
flightState._trackedModel.matrix,
flightState._scratchTrailClip,
);
}
@@ -382,8 +381,8 @@ export function createMotion({
const spec = parts.rendering._modelSpec(
flightState.records.data.get(flightState._trackedIcao)?.klass,
);
const scale = Number.isFinite(flightState._trackedModel.computedScale)
? flightState._trackedModel.computedScale
const scale = Number.isFinite(flightState._trackedModel.model.computedScale)
? flightState._trackedModel.model.computedScale
: spec.scale;
return spec.nativeRadiusM * scale;
}
+4 -4
View File
@@ -181,11 +181,11 @@ test('a newly seen military contact appears at its delayed position, not ahead o
icao24: 'seed00',
entity: null,
meta: { rawLat: 0, rawLon: 0, onGround: false },
billboard: {
icon: {
show: false,
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
},
billboardCollection: { show: false },
iconLayer: { show: false },
viewer: { camera: { positionCartographic: null }, scene: {} },
tracked: false,
});
@@ -246,8 +246,8 @@ test('a vanished military contact fades while position-less rows keep theirs', a
icao24: 'seed00',
entity: null,
meta: { rawLat: 0, rawLon: 0, onGround: false },
billboard: { show: false, position: Cesium.Cartesian3.fromDegrees(0, 0) },
billboardCollection: { show: true, add: (options) => ({ ...options }) },
icon: { show: false, position: Cesium.Cartesian3.fromDegrees(0, 0) },
iconLayer: { show: true, add: (options) => ({ ...options }) },
viewer,
tracked: false,
});
+4 -5
View File
@@ -11,7 +11,6 @@ import {
} from '../../data/contactMatch.js';
import { LANDED_ALT_MAX_FT, LANDED_SPEED_MAX_MPS } from './policy.js';
import { emergencyBroadcastPhrase } from '../../data/aircraftEmergency.js';
import { modelState } from '../../data/modelMirror.js';
export function createQueries({
flightState,
@@ -450,7 +449,7 @@ export function createQueries({
const isTracked = icao24 === flightState._trackedIcao;
// Keep planes rendered as a 3D model (icon hidden) so the detection box
// doesn't vanish on the 2D→3D handoff.
const model = flightState._models.get(icao24);
const record = flightState._models.get(icao24);
const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24);
if (!isTracked && !icons.shown(row) && !modelOwnsVisual) continue;
const info = flightState.records.data.get(icao24);
@@ -479,10 +478,10 @@ export function createQueries({
icons.position(row, tau, object._iconPos)
: modelOwnsVisual
? modelVisualAnchor(
modelState(model).modelMatrix,
record.matrix,
spec.visualCenterNative,
Number.isFinite(model.computedScale)
? model.computedScale
Number.isFinite(record.model.computedScale)
? record.model.computedScale
: spec.scale,
object._weldPos || (object._weldPos = new Cesium.Cartesian3()),
)
+2 -2
View File
@@ -186,8 +186,8 @@ export function createMilitarySnapshotRenderer({
sampleMeshFloorCells(flightState._viewer?.scene, _floorWarmPoints, {
excludeObjects: [
...icons._iconIds(),
...flightState._models.values(),
flightState._trackedModel,
...[...flightState._models.values()].map((record) => record.model),
flightState._trackedModel?.model,
].filter(Boolean),
viewerLat: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
+11 -11
View File
@@ -1,6 +1,6 @@
import * as Cesium from 'cesium';
import { AMBER_TRANSPARENT, TRACKED_ICON_COLOR } from './policy.js';
import { mirrorModel } from '../../data/modelMirror.js';
import { createModelRecord } from '../../data/modelRecord.js';
import { createIconSet } from '../../data/iconSet.js';
import { aircraftIconTestSeams } from '../../data/aircraftIconLayer.js';
@@ -21,9 +21,9 @@ export function createTesting({
* @param {string} state.icao24
* @param {object} state.entity
* @param {object} state.meta
* @param {{position?: Cesium.Cartesian3, show?: boolean}} [state.billboard]
* @param {{position?: Cesium.Cartesian3, show?: boolean}} [state.icon]
* Where the contact's icon is held, and whether it is shown.
* @param {{show?: boolean}} [state.billboardCollection] Layer visibility.
* @param {{show?: boolean}} [state.iconLayer] Layer visibility.
* @param {object} state.viewer
* @param {Array<Object>} [state.history=[]]
* @param {boolean} [state.tracked=true]
@@ -35,8 +35,8 @@ export function createTesting({
icao24,
entity,
meta,
billboard,
billboardCollection,
icon,
iconLayer,
viewer,
history = [],
tracked = true,
@@ -48,11 +48,11 @@ export function createTesting({
flightState._icons = createIconSet({
epoch: parts.icons._displayTime(new Cesium.JulianDate()),
});
flightState._icons.show = billboardCollection?.show ?? true;
_seedIconForTest(icao24, billboard, tracked);
flightState._icons.show = iconLayer?.show ?? true;
_seedIconForTest(icao24, icon, tracked);
flightState._models.clear();
for (const [key, model] of models)
flightState._models.set(key, mirrorModel(model));
flightState._models.set(key, createModelRecord(model));
flightState._detectionObjects = new Map();
flightState.records.data = new Map([[icao24, meta]]);
flightState._positionHistory = new Map([[icao24, history]]);
@@ -93,10 +93,10 @@ export function createTesting({
function _addMilitaryTrackingCandidateForTest({
icao24,
meta,
billboard,
icon,
history = [],
}) {
_seedIconForTest(icao24, billboard, false);
_seedIconForTest(icao24, icon, false);
flightState.records.data.set(icao24, meta);
flightState._positionHistory.set(icao24, history);
}
@@ -158,7 +158,7 @@ export function createTesting({
async function _ensureFleetModelForTest(icao24) {
await parts.rendering._ensureModel(icao24);
return flightState._models.get(icao24) || null;
return flightState._models.get(icao24)?.model ?? null;
}
return {
+2 -1
View File
@@ -1098,7 +1098,8 @@ export function createTracking({
const rawPick =
typeof picked.id === 'string' ? picked.id : picked.primitive?.id;
if (
picked.primitive === flightState._trackedModel ||
(flightState._trackedModel &&
picked.primitive === flightState._trackedModel.model) ||
rawPick === flightState._trackedIcao
)
return;