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