mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 04:14:02 +00:00
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:
1 parent
f008120b4c
commit
6035de88e2
35 files changed
+391
-788
No files matched your search
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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`. */
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user