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