mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 13:22:18 +00:00
Read vessel, CCTV and FIRMS billboard state from Core's mirror
The three layers now add their billboards through addBillboard and write the mirrored fields through setBillboard, so selection, cards, queries, focus, view thinning and the FIRMS horizon cull read what was pushed from Core instead of from Cesium. Vessels and FIRMS state their sprites' own sizes (32 px ship artwork, the glow canvas at twice the core size), which the mirror needs. A vessel helper, iconPosition, replaces the repeated mirror-or-visual position lookups, and the vessel visibility pass writes show only when it changes, as CCTV and FIRMS already did. A billboard never given a position now has none in the mirror rather than the origin.
This commit is contained in:
1 parent
1cfff5803a
commit
47f7eeb00e
23 files changed
+152
-97
No files matched your search
@@ -1447,6 +1447,8 @@
|
|||||||
"src/layers/vessels/policy.js",
|
"src/layers/vessels/policy.js",
|
||||||
"src/layers/vessels/queries.js",
|
"src/layers/vessels/queries.js",
|
||||||
"src/layers/vessels/rendering.js",
|
"src/layers/vessels/rendering.js",
|
||||||
|
"src/data/entityRecords.js",
|
||||||
|
"src/data/billboardMirror.js",
|
||||||
"src/layers/vessels/selection.js",
|
"src/layers/vessels/selection.js",
|
||||||
"src/layers/vessels/state.js",
|
"src/layers/vessels/state.js",
|
||||||
"src/layers/vessels/testing.js",
|
"src/layers/vessels/testing.js",
|
||||||
@@ -1528,6 +1530,8 @@
|
|||||||
"src/layers/firms/policy.js",
|
"src/layers/firms/policy.js",
|
||||||
"src/layers/firms/queries.js",
|
"src/layers/firms/queries.js",
|
||||||
"src/layers/firms/rendering.js",
|
"src/layers/firms/rendering.js",
|
||||||
|
"src/data/entityRecords.js",
|
||||||
|
"src/data/billboardMirror.js",
|
||||||
"src/layers/firms/selection.js",
|
"src/layers/firms/selection.js",
|
||||||
"src/layers/firms/source.js",
|
"src/layers/firms/source.js",
|
||||||
"src/layers/firms/state.js",
|
"src/layers/firms/state.js",
|
||||||
@@ -1857,6 +1861,7 @@
|
|||||||
"src/layers/cctv/projection.js",
|
"src/layers/cctv/projection.js",
|
||||||
"src/layers/cctv/videoPlayback.js",
|
"src/layers/cctv/videoPlayback.js",
|
||||||
"src/layers/cctv/rendering.js",
|
"src/layers/cctv/rendering.js",
|
||||||
|
"src/data/billboardMirror.js",
|
||||||
"src/layers/cctv/selection.js",
|
"src/layers/cctv/selection.js",
|
||||||
"src/layers/cctv/source.js",
|
"src/layers/cctv/source.js",
|
||||||
"src/layers/cctv/state.js",
|
"src/layers/cctv/state.js",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
presentSelectedVessel,
|
presentSelectedVessel,
|
||||||
subscribeSelectedVessel,
|
subscribeSelectedVessel,
|
||||||
} from '../hudReadouts.js';
|
} from '../hudReadouts.js';
|
||||||
|
import { mirrorBillboard, setBillboard } from './billboardMirror.js';
|
||||||
|
|
||||||
test('open feed with vessels is healthy (null)', () => {
|
test('open feed with vessels is healthy (null)', () => {
|
||||||
assert.equal(deriveAisFeedError({ status: 'open', lastMessageAt: 1, error: null }, 42), null);
|
assert.equal(deriveAisFeedError({ status: 'open', lastMessageAt: 1, error: null }, 42), null);
|
||||||
@@ -659,12 +660,12 @@ test('vessel focus wire animates alpha with deadband and restores after tracking
|
|||||||
let writes = 0;
|
let writes = 0;
|
||||||
const makeColor = (alpha) => ({ alpha, withAlpha: (next) => makeColor(next) });
|
const makeColor = (alpha) => ({ alpha, withAlpha: (next) => makeColor(next) });
|
||||||
let color = makeColor(1);
|
let color = makeColor(1);
|
||||||
const billboard = {
|
const billboard = mirrorBillboard({
|
||||||
position: { x: 1, y: 2, z: 3 },
|
position: { x: 1, y: 2, z: 3 },
|
||||||
show: true,
|
show: true,
|
||||||
get color() { return color; },
|
get color() { return color; },
|
||||||
set color(next) { writes += 1; color = next; },
|
set color(next) { writes += 1; color = next; },
|
||||||
};
|
});
|
||||||
const records = [{ position: billboard.position, billboard }];
|
const records = [{ position: billboard.position, billboard }];
|
||||||
const target = {
|
const target = {
|
||||||
screenRect: { left: 40, top: 40, right: 60, bottom: 60 },
|
screenRect: { left: 40, top: 40, right: 60, bottom: 60 },
|
||||||
@@ -713,12 +714,12 @@ test('vessel focus wire animates alpha with deadband and restores after tracking
|
|||||||
|
|
||||||
test('vessel focus wire performs no alpha writes when nothing has been tracked', () => {
|
test('vessel focus wire performs no alpha writes when nothing has been tracked', () => {
|
||||||
let writes = 0;
|
let writes = 0;
|
||||||
const billboard = {
|
const billboard = mirrorBillboard({
|
||||||
position: { x: 0, y: 0, z: 0 },
|
position: { x: 0, y: 0, z: 0 },
|
||||||
show: true,
|
show: true,
|
||||||
get color() { return { alpha: 1 }; },
|
get color() { return { alpha: 1 }; },
|
||||||
set color(_next) { writes += 1; },
|
set color(_next) { writes += 1; },
|
||||||
};
|
});
|
||||||
const result = applyVesselFocusDeemphasis({
|
const result = applyVesselFocusDeemphasis({
|
||||||
records: [{ position: billboard.position, billboard }],
|
records: [{ position: billboard.position, billboard }],
|
||||||
target: null,
|
target: null,
|
||||||
@@ -737,11 +738,11 @@ test('vessel focus wire performs no alpha writes when nothing has been tracked',
|
|||||||
|
|
||||||
test('vessel focus wire restores a hidden sprite before releasing the active pass', () => {
|
test('vessel focus wire restores a hidden sprite before releasing the active pass', () => {
|
||||||
const makeColor = (alpha) => ({ alpha, withAlpha: (next) => makeColor(next) });
|
const makeColor = (alpha) => ({ alpha, withAlpha: (next) => makeColor(next) });
|
||||||
const billboard = {
|
const billboard = mirrorBillboard({
|
||||||
position: { x: 1, y: 2, z: 3 },
|
position: { x: 1, y: 2, z: 3 },
|
||||||
show: true,
|
show: true,
|
||||||
color: makeColor(1),
|
color: makeColor(1),
|
||||||
};
|
});
|
||||||
const records = [{ billboard }];
|
const records = [{ billboard }];
|
||||||
const target = {
|
const target = {
|
||||||
screenRect: { left: 40, top: 40, right: 60, bottom: 60 },
|
screenRect: { left: 40, top: 40, right: 60, bottom: 60 },
|
||||||
@@ -771,7 +772,7 @@ test('vessel focus wire restores a hidden sprite before releasing the active pas
|
|||||||
assert.equal(dimmed.activeCount, 1);
|
assert.equal(dimmed.activeCount, 1);
|
||||||
assert.equal(billboard.color.alpha, params.dimFloor);
|
assert.equal(billboard.color.alpha, params.dimFloor);
|
||||||
|
|
||||||
billboard.show = false;
|
setBillboard(billboard, 'show', false);
|
||||||
const releaseStart = pass(params.attackMs, null, dimmed.activeCount);
|
const releaseStart = pass(params.attackMs, null, dimmed.activeCount);
|
||||||
const restored = pass(params.attackMs + params.releaseMs, null, releaseStart.activeCount);
|
const restored = pass(params.attackMs + params.releaseMs, null, releaseStart.activeCount);
|
||||||
assert.equal(restored.ran, true);
|
assert.equal(restored.ran, true);
|
||||||
@@ -1350,9 +1351,11 @@ test('buildVesselCard: unnamed vessels title as MMSI; long names truncate', () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('buildVesselCard: anchors to the billboard position when present', () => {
|
test('buildVesselCard: anchors to the billboard position when present', () => {
|
||||||
const rendered = { x: 9, y: 9, z: 9 };
|
const rendered = new Cesium.Cartesian3(9, 9, 9);
|
||||||
const card = buildVesselCard(makeRecord({ billboard: { position: rendered } }));
|
const card = buildVesselCard(
|
||||||
assert.equal(card.position, rendered);
|
makeRecord({ billboard: mirrorBillboard({ position: rendered }) }),
|
||||||
|
);
|
||||||
|
assert.deepEqual(card.position, rendered);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildVesselCard: tanker types carry the amber accent', () => {
|
test('buildVesselCard: tanker types carry the amber accent', () => {
|
||||||
@@ -1742,11 +1745,12 @@ test('vessel selection passes the opaque source reference to optional history',
|
|||||||
|
|
||||||
test('vessel focus wire skips hidden sprites at rest while a target is tracked', () => {
|
test('vessel focus wire skips hidden sprites at rest while a target is tracked', () => {
|
||||||
const makeColor = (alpha) => ({ alpha, withAlpha: (next) => makeColor(next) });
|
const makeColor = (alpha) => ({ alpha, withAlpha: (next) => makeColor(next) });
|
||||||
const sprite = (show) => ({
|
const sprite = (show) =>
|
||||||
position: { x: 1, y: 2, z: 3 },
|
mirrorBillboard({
|
||||||
show,
|
position: { x: 1, y: 2, z: 3 },
|
||||||
color: makeColor(1),
|
show,
|
||||||
});
|
color: makeColor(1),
|
||||||
|
});
|
||||||
const visible = sprite(true);
|
const visible = sprite(true);
|
||||||
const hidden = Array.from({ length: 50 }, () => sprite(false));
|
const hidden = Array.from({ length: 50 }, () => sprite(false));
|
||||||
const records = [visible, ...hidden].map((billboard) => ({
|
const records = [visible, ...hidden].map((billboard) => ({
|
||||||
|
|||||||
@@ -31,12 +31,10 @@ const DEFAULTS = Object.freeze({
|
|||||||
/** Cesium copies positions and scalers on write; the mirror keeps its own copy too. */
|
/** Cesium copies positions and scalers on write; the mirror keeps its own copy too. */
|
||||||
function copyInto(record, field, value) {
|
function copyInto(record, field, value) {
|
||||||
if (field === 'position') {
|
if (field === 'position') {
|
||||||
// Cesium keeps its old position when handed none; so does the mirror.
|
// Cesium keeps its old position when handed none; so does the mirror. A
|
||||||
if (!value && record.position) return;
|
// billboard never given one has none here (Cesium would report the origin).
|
||||||
record.position = Cesium.Cartesian3.clone(
|
if (!value) return;
|
||||||
value ?? Cesium.Cartesian3.ZERO,
|
record.position = Cesium.Cartesian3.clone(value, record.position);
|
||||||
record.position,
|
|
||||||
);
|
|
||||||
} else if (field === 'scaleByDistance') {
|
} else if (field === 'scaleByDistance') {
|
||||||
record.scaleByDistance = value
|
record.scaleByDistance = value
|
||||||
? Cesium.NearFarScalar.clone(value, record.scaleByDistance)
|
? Cesium.NearFarScalar.clone(value, record.scaleByDistance)
|
||||||
@@ -97,7 +95,7 @@ export function setBillboard(billboard, field, value) {
|
|||||||
* Core's copy of a billboard's mirrored fields, or null when it has none (or
|
* Core's copy of a billboard's mirrored fields, or null when it has none (or
|
||||||
* there is no billboard).
|
* there is no billboard).
|
||||||
* @param {Object|null|undefined} billboard
|
* @param {Object|null|undefined} billboard
|
||||||
* @returns {{ show: boolean, position: Cesium.Cartesian3, scale: number,
|
* @returns {{ show: boolean, position?: Cesium.Cartesian3, scale: number,
|
||||||
* rotation: number, width?: number, height?: number,
|
* rotation: number, width?: number, height?: number,
|
||||||
* scaleByDistance?: Cesium.NearFarScalar }|null}
|
* scaleByDistance?: Cesium.NearFarScalar }|null}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ test('a billboard Core did not add can be mirrored from its fields', () => {
|
|||||||
const bb = mirrorBillboard({ show: false, scale: 2 });
|
const bb = mirrorBillboard({ show: false, scale: 2 });
|
||||||
assert.equal(billboardState(bb).show, false);
|
assert.equal(billboardState(bb).show, false);
|
||||||
assert.equal(billboardState(bb).scale, 2);
|
assert.equal(billboardState(bb).scale, 2);
|
||||||
assert.ok(Cesium.Cartesian3.equals(billboardState(bb).position, Cesium.Cartesian3.ZERO));
|
assert.equal(billboardState(bb).position, undefined, 'none was pushed');
|
||||||
assert.equal(billboardState(null), null);
|
assert.equal(billboardState(null), null);
|
||||||
assert.equal(mirrorBillboard(null), null);
|
assert.equal(mirrorBillboard(null), null);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ import {
|
|||||||
IDLE_CAMERA_COLOR,
|
IDLE_CAMERA_COLOR,
|
||||||
LIVE_CAMERA_COLOR,
|
LIVE_CAMERA_COLOR,
|
||||||
} from '../layers/cctv/policy.js';
|
} from '../layers/cctv/policy.js';
|
||||||
|
import { mirrorBillboard } from './billboardMirror.js';
|
||||||
|
|
||||||
const ASPECT = 16 / 9;
|
const ASPECT = 16 / 9;
|
||||||
const toRad = (deg) => (deg * Math.PI) / 180;
|
const toRad = (deg) => (deg * Math.PI) / 180;
|
||||||
@@ -355,7 +356,7 @@ test('default COVERAGE refresh materializes the active and visible camera frustu
|
|||||||
test('camera icons are violet for video feeds, cyan for stills, amber when active', () => {
|
test('camera icons are violet for video feeds, cyan for stills, amber when active', () => {
|
||||||
const record = (id, feedType) => ({
|
const record = (id, feedType) => ({
|
||||||
camera: { ...UNCLAMPED_CAMERA, id, feedType, groundElevationM: 0 },
|
camera: { ...UNCLAMPED_CAMERA, id, feedType, groundElevationM: 0 },
|
||||||
billboard: {},
|
billboard: mirrorBillboard({}),
|
||||||
coverageEntities: [],
|
coverageEntities: [],
|
||||||
projection: null,
|
projection: null,
|
||||||
});
|
});
|
||||||
@@ -682,7 +683,7 @@ test('CCTV disable hide sweep hides record visuals and destroys viewsheds withou
|
|||||||
camera: { id: 'a' },
|
camera: { id: 'a' },
|
||||||
activationDone: true,
|
activationDone: true,
|
||||||
probeClampRangeM: 105,
|
probeClampRangeM: 105,
|
||||||
billboard: { color: 'keep-color', scale: 1.25 },
|
billboard: mirrorBillboard({ color: 'keep-color', scale: 1.25 }),
|
||||||
coverageEntities: [{ show: true }, { show: true }],
|
coverageEntities: [{ show: true }, { show: true }],
|
||||||
projection: { planeEntity: { show: true } },
|
projection: { planeEntity: { show: true } },
|
||||||
viewshedPrimitive: viewshedA,
|
viewshedPrimitive: viewshedA,
|
||||||
@@ -691,7 +692,7 @@ test('CCTV disable hide sweep hides record visuals and destroys viewsheds withou
|
|||||||
camera: { id: 'b' },
|
camera: { id: 'b' },
|
||||||
activationDone: true,
|
activationDone: true,
|
||||||
probeClampRangeM: 146,
|
probeClampRangeM: 146,
|
||||||
billboard: { color: 'keep-color', scale: 1 },
|
billboard: mirrorBillboard({ color: 'keep-color', scale: 1 }),
|
||||||
projection: null,
|
projection: null,
|
||||||
viewshedPrimitive: viewshedB,
|
viewshedPrimitive: viewshedB,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import cctvLayer, {
|
|||||||
_viewRecordIdsForTest,
|
_viewRecordIdsForTest,
|
||||||
refreshHorizonCulling,
|
refreshHorizonCulling,
|
||||||
} from './cctv.js';
|
} from './cctv.js';
|
||||||
|
import { mirrorBillboard } from './billboardMirror.js';
|
||||||
|
|
||||||
const WIDTH = 800;
|
const WIDTH = 800;
|
||||||
const HEIGHT = 600;
|
const HEIGHT = 600;
|
||||||
@@ -40,7 +41,7 @@ function recordsAt(
|
|||||||
mountHeightM: 8,
|
mountHeightM: 8,
|
||||||
},
|
},
|
||||||
position,
|
position,
|
||||||
billboard: { position, show: true },
|
billboard: mirrorBillboard({ position, show: true }),
|
||||||
coverageEntities: [],
|
coverageEntities: [],
|
||||||
projection: null,
|
projection: null,
|
||||||
};
|
};
|
||||||
@@ -59,7 +60,11 @@ function viewerAt(heightM, screenByPosition, shiftX = 0) {
|
|||||||
scene: {
|
scene: {
|
||||||
canvas: { clientWidth: WIDTH, clientHeight: HEIGHT },
|
canvas: { clientWidth: WIDTH, clientHeight: HEIGHT },
|
||||||
cartesianToCanvasCoordinates: (position) => {
|
cartesianToCanvasCoordinates: (position) => {
|
||||||
const screen = screenByPosition.get(position);
|
// The mirror holds its own copy of each position, so match by value.
|
||||||
|
const key = [...screenByPosition.keys()].find((candidate) =>
|
||||||
|
Cesium.Cartesian3.equals(candidate, position),
|
||||||
|
);
|
||||||
|
const screen = screenByPosition.get(key);
|
||||||
return screen && { x: screen.x + shiftX, y: screen.y };
|
return screen && { x: screen.x + shiftX, y: screen.y };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from './groundFloor.js';
|
} from './groundFloor.js';
|
||||||
import { unregisterSpriteCollection } from './spriteOrder.js';
|
import { unregisterSpriteCollection } from './spriteOrder.js';
|
||||||
import { isOverlayPointVisible, normalizeOverlayEntry } from '../overlays/worldOverlay.js';
|
import { isOverlayPointVisible, normalizeOverlayEntry } from '../overlays/worldOverlay.js';
|
||||||
|
import { mirrorBillboard } from './billboardMirror.js';
|
||||||
|
|
||||||
const AUSTIN = { lon: -97.7, lat: 30.2 };
|
const AUSTIN = { lon: -97.7, lat: 30.2 };
|
||||||
/** Antipode of Austin — as far behind the limb as a point on Earth can be. */
|
/** Antipode of Austin — as far behind the limb as a point on Earth can be. */
|
||||||
@@ -34,13 +35,13 @@ const LIMB_DLON = 41.87;
|
|||||||
* the pass does not dirty the collection's vertex buffer needlessly.
|
* the pass does not dirty the collection's vertex buffer needlessly.
|
||||||
*/
|
*/
|
||||||
function billboard(position, show = true) {
|
function billboard(position, show = true) {
|
||||||
return {
|
return mirrorBillboard({
|
||||||
position,
|
position,
|
||||||
_show: show,
|
_show: show,
|
||||||
writes: 0,
|
writes: 0,
|
||||||
get show() { return this._show; },
|
get show() { return this._show; },
|
||||||
set show(value) { this._show = value; this.writes += 1; },
|
set show(value) { this._show = value; this.writes += 1; },
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cesium BillboardCollection-shaped stub. */
|
/** Cesium BillboardCollection-shaped stub. */
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { applyVesselFocusDeemphasis } from './aisLiveVessels.js';
|
|||||||
import { applyCctvFocusDeemphasis } from './cctv.js';
|
import { applyCctvFocusDeemphasis } from './cctv.js';
|
||||||
import { applySatellitePointFocusDeemphasis } from './satellites.js';
|
import { applySatellitePointFocusDeemphasis } from './satellites.js';
|
||||||
import { DEFAULT_FOCUS_DEEMPHASIS_PARAMS } from './focusDeemphasis.js';
|
import { DEFAULT_FOCUS_DEEMPHASIS_PARAMS } from './focusDeemphasis.js';
|
||||||
|
import { mirrorBillboard } from './billboardMirror.js';
|
||||||
|
|
||||||
const params = { ...DEFAULT_FOCUS_DEEMPHASIS_PARAMS, paddingPx: 0 };
|
const params = { ...DEFAULT_FOCUS_DEEMPHASIS_PARAMS, paddingPx: 0 };
|
||||||
const target = {
|
const target = {
|
||||||
@@ -51,14 +52,14 @@ function assertResumeGate(name, makePass) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assertResumeGate('vessel', () => {
|
assertResumeGate('vessel', () => {
|
||||||
const billboard = {
|
const billboard = mirrorBillboard({
|
||||||
position: { x: 1, y: 2, z: 3 },
|
position: { x: 1, y: 2, z: 3 },
|
||||||
show: true,
|
show: true,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
color: color(),
|
color: color(),
|
||||||
};
|
});
|
||||||
return {
|
return {
|
||||||
pass: ({ nowMs, target: focusTarget, previousActiveCount }) => (
|
pass: ({ nowMs, target: focusTarget, previousActiveCount }) => (
|
||||||
applyVesselFocusDeemphasis({
|
applyVesselFocusDeemphasis({
|
||||||
@@ -76,14 +77,14 @@ assertResumeGate('vessel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assertResumeGate('CCTV', () => {
|
assertResumeGate('CCTV', () => {
|
||||||
const billboard = {
|
const billboard = mirrorBillboard({
|
||||||
position: { x: 1, y: 2, z: 3 },
|
position: { x: 1, y: 2, z: 3 },
|
||||||
show: true,
|
show: true,
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
color: color(),
|
color: color(),
|
||||||
};
|
});
|
||||||
const record = { camera: { id: 'cam-1' }, billboard };
|
const record = { camera: { id: 'cam-1' }, billboard };
|
||||||
return {
|
return {
|
||||||
pass: ({ nowMs, target: focusTarget, previousActiveCount }) => (
|
pass: ({ nowMs, target: focusTarget, previousActiveCount }) => (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
poseHash,
|
poseHash,
|
||||||
requiredPlaneLift,
|
requiredPlaneLift,
|
||||||
} from '../../data/cctvFootprint.js';
|
} from '../../data/cctvFootprint.js';
|
||||||
|
import { setBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createGeometry({ state: layerState, services, parts, source }) {
|
export function createGeometry({ state: layerState, services, parts, source }) {
|
||||||
const { warmGroundFloor, cachedGroundFloor } = services.ground;
|
const { warmGroundFloor, cachedGroundFloor } = services.ground;
|
||||||
@@ -347,7 +348,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
|||||||
record.position = positions.mount;
|
record.position = positions.mount;
|
||||||
record.camera.absoluteHeightM = geometry.mount.alt;
|
record.camera.absoluteHeightM = geometry.mount.alt;
|
||||||
if (record.billboard) {
|
if (record.billboard) {
|
||||||
record.billboard.position = positions.mount;
|
setBillboard(record.billboard, 'position', positions.mount);
|
||||||
}
|
}
|
||||||
if (record.coverageEntities?.length >= 5) {
|
if (record.coverageEntities?.length >= 5) {
|
||||||
record.coverageEntities[0].polyline.positions = [
|
record.coverageEntities[0].polyline.positions = [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
planeSupportPoints,
|
planeSupportPoints,
|
||||||
poseHash,
|
poseHash,
|
||||||
} from '../../data/cctvFootprint.js';
|
} from '../../data/cctvFootprint.js';
|
||||||
|
import { setBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
/** Cooldown before a provisional (geoid-fallback / partial) footprint is retried. */
|
/** Cooldown before a provisional (geoid-fallback / partial) footprint is retried. */
|
||||||
const FOOTPRINT_RETRY_MS = 60_000;
|
const FOOTPRINT_RETRY_MS = 60_000;
|
||||||
@@ -238,7 +239,8 @@ export function createGround({ state: layerState, services, parts, source }) {
|
|||||||
undefined,
|
undefined,
|
||||||
record.position,
|
record.position,
|
||||||
);
|
);
|
||||||
if (record.billboard) record.billboard.position = record.position;
|
if (record.billboard)
|
||||||
|
setBillboard(record.billboard, 'position', record.position);
|
||||||
moved += 1;
|
moved += 1;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
VIEW_MOTION_MIN_TURN_DEG,
|
VIEW_MOTION_MIN_TURN_DEG,
|
||||||
VIEW_MOTION_COST_FACTOR,
|
VIEW_MOTION_COST_FACTOR,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { addBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
const MIN_TURN_COS = Math.cos((VIEW_MOTION_MIN_TURN_DEG * Math.PI) / 180);
|
const MIN_TURN_COS = Math.cos((VIEW_MOTION_MIN_TURN_DEG * Math.PI) / 180);
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ export function createLifecycle({
|
|||||||
camera.lat,
|
camera.lat,
|
||||||
camera.absoluteHeightM,
|
camera.absoluteHeightM,
|
||||||
);
|
);
|
||||||
const billboard = layerState._billboards.add({
|
const billboard = addBillboard(layerState._billboards, {
|
||||||
id: camera.id,
|
id: camera.id,
|
||||||
image: CAMERA_ICON,
|
image: CAMERA_ICON,
|
||||||
position,
|
position,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
VIEW_ICON_MARGIN,
|
VIEW_ICON_MARGIN,
|
||||||
VIEW_MIN_CELL_M,
|
VIEW_MIN_CELL_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
/** FNV-1a over a camera id: a stable, well-spread order for crowded cells. */
|
/** FNV-1a over a camera id: a stable, well-spread order for crowded cells. */
|
||||||
function idHash(id) {
|
function idHash(id) {
|
||||||
@@ -110,19 +111,20 @@ export function createRendering({
|
|||||||
for (const record of records || []) {
|
for (const record of records || []) {
|
||||||
const bb = record.billboard;
|
const bb = record.billboard;
|
||||||
if (!bb) continue;
|
if (!bb) continue;
|
||||||
const position = bb.position;
|
const icon = billboardState(bb);
|
||||||
|
const position = icon.position;
|
||||||
// CCTV never publishes a tracked focus target, so every icon is ambient.
|
// CCTV never publishes a tracked focus target, so every icon is ambient.
|
||||||
const focus = advanceSpriteFocus(bb, {
|
const focus = advanceSpriteFocus(bb, {
|
||||||
// Keep hidden icons in the state/release pass so the active count cannot
|
// Keep hidden icons in the state/release pass so the active count cannot
|
||||||
// drop while a stale dim alpha remains waiting to reappear.
|
// drop while a stale dim alpha remains waiting to reappear.
|
||||||
screenPosition:
|
screenPosition:
|
||||||
bb.show === false || !position ? null : screenPositionFor(position),
|
icon.show === false || !position ? null : screenPositionFor(position),
|
||||||
cameraDistance: position ? cameraDistanceFor(position) : Number.NaN,
|
cameraDistance: position ? cameraDistanceFor(position) : Number.NaN,
|
||||||
nowMs,
|
nowMs,
|
||||||
target,
|
target,
|
||||||
params,
|
params,
|
||||||
spriteHalfWidthPx: (bb.width || 24) * (bb.scale || 1) * 0.5,
|
spriteHalfWidthPx: (icon.width || 24) * (icon.scale || 1) * 0.5,
|
||||||
spriteHalfHeightPx: (bb.height || 24) * (bb.scale || 1) * 0.5,
|
spriteHalfHeightPx: (icon.height || 24) * (icon.scale || 1) * 0.5,
|
||||||
});
|
});
|
||||||
transitioning ||= focus.transitioning;
|
transitioning ||= focus.transitioning;
|
||||||
if (focus.active) activeCount += 1;
|
if (focus.active) activeCount += 1;
|
||||||
@@ -200,7 +202,7 @@ export function createRendering({
|
|||||||
const shown = [];
|
const shown = [];
|
||||||
const cells = new Map();
|
const cells = new Map();
|
||||||
for (const record of layerState._records) {
|
for (const record of layerState._records) {
|
||||||
const position = record.billboard?.position;
|
const position = billboardState(record.billboard)?.position;
|
||||||
if (!position) continue;
|
if (!position) continue;
|
||||||
const vx = position.x * inverseRadii.x - cx;
|
const vx = position.x * inverseRadii.x - cx;
|
||||||
const vy = position.y * inverseRadii.y - cy;
|
const vy = position.y * inverseRadii.y - cy;
|
||||||
@@ -245,7 +247,8 @@ export function createRendering({
|
|||||||
const bb = record.billboard;
|
const bb = record.billboard;
|
||||||
if (!bb) continue;
|
if (!bb) continue;
|
||||||
const visible = record.viewPass === pass;
|
const visible = record.viewPass === pass;
|
||||||
if (bb.show !== visible) bb.show = visible;
|
if (billboardState(bb).show !== visible)
|
||||||
|
setBillboard(bb, 'show', visible);
|
||||||
}
|
}
|
||||||
const previous = layerState._viewRecords;
|
const previous = layerState._viewRecords;
|
||||||
const changed =
|
const changed =
|
||||||
@@ -397,7 +400,8 @@ export function createRendering({
|
|||||||
if (!Cesium.Color.equals(record.billboard.color, color))
|
if (!Cesium.Color.equals(record.billboard.color, color))
|
||||||
record.billboard.color = color;
|
record.billboard.color = color;
|
||||||
const scale = isActive ? 1.25 : 1.0;
|
const scale = isActive ? 1.25 : 1.0;
|
||||||
if (record.billboard.scale !== scale) record.billboard.scale = scale;
|
if (billboardState(record.billboard).scale !== scale)
|
||||||
|
setBillboard(record.billboard, 'scale', scale);
|
||||||
// disableDepthTestDistance stays POSITIVE_INFINITY for every billboard
|
// disableDepthTestDistance stays POSITIVE_INFINITY for every billboard
|
||||||
// (set at creation) — see the field-test far-zoom submerge fix there.
|
// (set at creation) — see the field-test far-zoom submerge fix there.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
LIVE_CAMERA_COLOR,
|
LIVE_CAMERA_COLOR,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
import { isVideoFeedType } from '../../sources/cctvTypes.js';
|
import { isVideoFeedType } from '../../sources/cctvTypes.js';
|
||||||
|
import { mirrorBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
const FOCUS_FACTOR = 0.5;
|
const FOCUS_FACTOR = 0.5;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ test('focus de-emphasis fades each icon from its own color', () => {
|
|||||||
camera: { id, feedType },
|
camera: { id, feedType },
|
||||||
// No position, so the pass skips screen projection; the focus stub below
|
// No position, so the pass skips screen projection; the focus stub below
|
||||||
// decides the factor on its own.
|
// decides the factor on its own.
|
||||||
billboard: { show: true },
|
billboard: mirrorBillboard({ show: true }),
|
||||||
});
|
});
|
||||||
const idleVideo = record('idle-hls', 'hls');
|
const idleVideo = record('idle-hls', 'hls');
|
||||||
const idleStill = record('idle-image', 'image');
|
const idleStill = record('idle-image', 'image');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
CULL_LIFT_M,
|
CULL_LIFT_M,
|
||||||
LABEL_MIN_SEP_PX,
|
LABEL_MIN_SEP_PX,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { billboardState, setBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createModel({
|
export function createModel({
|
||||||
layerState,
|
layerState,
|
||||||
@@ -273,9 +274,9 @@ export function createModel({
|
|||||||
* the opposite side of the Earth shine through the planet at tilted mid-altitude
|
* the opposite side of the Earth shine through the planet at tilted mid-altitude
|
||||||
* views. Same pattern as the flights/CCTV layers' `EllipsoidalOccluder` passes.
|
* views. Same pattern as the flights/CCTV layers' `EllipsoidalOccluder` passes.
|
||||||
*
|
*
|
||||||
* Pure and allocation-free: reads `billboard.position`, writes `billboard.show`
|
* Pure and allocation-free: reads the billboard's mirrored position and show,
|
||||||
* only when it actually flips (assigning `show` dirties the collection's vertex
|
* and writes `show` only when it actually flips (assigning `show` dirties the
|
||||||
* buffer). Accepts any `{length, get(i)}` shape so it is unit-testable without
|
* collection's vertex buffer). Accepts any `{length, get(i)}` shape so it is unit-testable without
|
||||||
* a WebGL scene.
|
* a WebGL scene.
|
||||||
*
|
*
|
||||||
* @param {?{length: number, get: function(number): (Object|undefined)}} billboards
|
* @param {?{length: number, get: function(number): (Object|undefined)}} billboards
|
||||||
@@ -295,9 +296,10 @@ export function createModel({
|
|||||||
for (let i = 0; i < total; i += 1) {
|
for (let i = 0; i < total; i += 1) {
|
||||||
const billboard = billboards.get(i);
|
const billboard = billboards.get(i);
|
||||||
if (!billboard) continue;
|
if (!billboard) continue;
|
||||||
const point = (cullPositions && cullPositions[i]) || billboard.position;
|
const pushed = billboardState(billboard);
|
||||||
|
const point = (cullPositions && cullPositions[i]) || pushed.position;
|
||||||
const visible = occluder.isPointVisible(point) === true;
|
const visible = occluder.isPointVisible(point) === true;
|
||||||
if (billboard.show !== visible) billboard.show = visible;
|
if (pushed.show !== visible) setBillboard(billboard, 'show', visible);
|
||||||
if (visible) visibleCount += 1;
|
if (visible) visibleCount += 1;
|
||||||
}
|
}
|
||||||
return visibleCount;
|
return visibleCount;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as Cesium from 'cesium';
|
|||||||
import { FIRMS_OVERLAY_SOURCE_ID } from '../../data/firmsLabels.js';
|
import { FIRMS_OVERLAY_SOURCE_ID } from '../../data/firmsLabels.js';
|
||||||
import { horizonOccluder } from '../../data/iconOrientation.js';
|
import { horizonOccluder } from '../../data/iconOrientation.js';
|
||||||
import { LOD_LEVELS, CONTEXT_TOP_N } from './policy.js';
|
import { LOD_LEVELS, CONTEXT_TOP_N } from './policy.js';
|
||||||
|
import { addBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createRendering({
|
export function createRendering({
|
||||||
layerState,
|
layerState,
|
||||||
@@ -237,13 +238,17 @@ export function createRendering({
|
|||||||
const pickId = `firms-${fire.index}`;
|
const pickId = `firms-${fire.index}`;
|
||||||
layerState._pickIndexById.set(pickId, fire);
|
layerState._pickIndexById.set(pickId, fire);
|
||||||
layerState._cullPositions.push(cullPosition);
|
layerState._cullPositions.push(cullPosition);
|
||||||
layerState._billboards.add({
|
const corePx = components.model.sizeBucket(coreSize);
|
||||||
|
addBillboard(layerState._billboards, {
|
||||||
id: pickId,
|
id: pickId,
|
||||||
position,
|
position,
|
||||||
image: components.model.glowSprite(
|
image: components.model.glowSprite(
|
||||||
components.model.detectionColorStop(fire),
|
components.model.detectionColorStop(fire),
|
||||||
components.model.sizeBucket(coreSize),
|
corePx,
|
||||||
),
|
),
|
||||||
|
// The glow sprite's own size (see glowSprite), stated for Core's mirror.
|
||||||
|
width: corePx * 2,
|
||||||
|
height: corePx * 2,
|
||||||
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
||||||
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ export function createCards({
|
|||||||
return {
|
return {
|
||||||
id: vesselOverlayEntryId(record),
|
id: vesselOverlayEntryId(record),
|
||||||
actionable: Boolean(record?.mmsi),
|
actionable: Boolean(record?.mmsi),
|
||||||
position:
|
position: components.rendering.iconPosition(record),
|
||||||
components.rendering.getVisual(record).billboard?.position ||
|
|
||||||
components.rendering.getVisual(record).position,
|
|
||||||
gapPx: 10,
|
gapPx: 10,
|
||||||
accent: accentForVesselType(record.type),
|
accent: accentForVesselType(record.type),
|
||||||
title: trimHudValue(displayVesselName(record), 26),
|
title: trimHudValue(displayVesselName(record), 26),
|
||||||
@@ -102,9 +100,7 @@ export function createCards({
|
|||||||
return {
|
return {
|
||||||
id: vesselOverlayEntryId(record),
|
id: vesselOverlayEntryId(record),
|
||||||
actionable: Boolean(record?.mmsi),
|
actionable: Boolean(record?.mmsi),
|
||||||
position:
|
position: components.rendering.iconPosition(record),
|
||||||
components.rendering.getVisual(record).billboard?.position ||
|
|
||||||
components.rendering.getVisual(record).position,
|
|
||||||
gapPx: 12,
|
gapPx: 12,
|
||||||
accent: accentForVesselType(record.type),
|
accent: accentForVesselType(record.type),
|
||||||
title: trimHudValue(displayVesselName(record), 32),
|
title: trimHudValue(displayVesselName(record), 32),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { FOCUS_EVIDENCE_DEV } from './policy.js';
|
import { FOCUS_EVIDENCE_DEV } from './policy.js';
|
||||||
|
import { billboardState } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createEvidence({
|
export function createEvidence({
|
||||||
vesselState,
|
vesselState,
|
||||||
@@ -40,15 +41,16 @@ export function createEvidence({
|
|||||||
if (!FOCUS_EVIDENCE_DEV || !state.viewer) return [];
|
if (!FOCUS_EVIDENCE_DEV || !state.viewer) return [];
|
||||||
return state.records.all.map((record) => {
|
return state.records.all.map((record) => {
|
||||||
const bb = components.rendering.getVisual(record).billboard;
|
const bb = components.rendering.getVisual(record).billboard;
|
||||||
const screen = bb?.position
|
const icon = billboardState(bb);
|
||||||
|
const screen = icon?.position
|
||||||
? Cesium.SceneTransforms.worldToWindowCoordinates(
|
? Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
state.viewer.scene,
|
state.viewer.scene,
|
||||||
bb.position,
|
icon.position,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
id: record.mmsi,
|
id: record.mmsi,
|
||||||
show: bb?.show === true,
|
show: icon?.show === true,
|
||||||
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,
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ test('refresh keeps pick identity and renderer geometry separate, and eviction r
|
|||||||
for (const key of ['position', 'surfacePosition', 'normal', 'billboard']) {
|
for (const key of ['position', 'surfacePosition', 'normal', 'billboard']) {
|
||||||
assert.equal(Object.hasOwn(record, key), false);
|
assert.equal(Object.hasOwn(record, key), false);
|
||||||
}
|
}
|
||||||
assert.equal(layer.findByQuery('111').position, primitive.position);
|
assert.deepEqual(layer.findByQuery('111').position, primitive.position);
|
||||||
const oldPosition = primitive.position;
|
const oldPosition = primitive.position;
|
||||||
current = snapshot([{ ...observation('111'), longitude: 4.06 }]);
|
current = snapshot([{ ...observation('111'), longitude: 4.06 }]);
|
||||||
await layer.update();
|
await layer.update();
|
||||||
@@ -300,7 +300,7 @@ test('refresh keeps pick identity and renderer geometry separate, and eviction r
|
|||||||
assert.equal([...primitives][0], primitive);
|
assert.equal([...primitives][0], primitive);
|
||||||
assert.equal(primitive.id, record);
|
assert.equal(primitive.id, record);
|
||||||
assert.notDeepEqual(primitive.position, oldPosition);
|
assert.notDeepEqual(primitive.position, oldPosition);
|
||||||
assert.equal(layer.findByQuery('111').position, primitive.position);
|
assert.deepEqual(layer.findByQuery('111').position, primitive.position);
|
||||||
current = snapshot([observation('222')]);
|
current = snapshot([observation('222')]);
|
||||||
await layer.update();
|
await layer.update();
|
||||||
assert.equal(primitives.has(primitive), false);
|
assert.equal(primitives.has(primitive), false);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
REFRESH_MS,
|
REFRESH_MS,
|
||||||
FOCUS_EVIDENCE_DEV,
|
FOCUS_EVIDENCE_DEV,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { billboardState } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createQueries({
|
export function createQueries({
|
||||||
vesselState,
|
vesselState,
|
||||||
@@ -295,9 +296,7 @@ export function createQueries({
|
|||||||
}
|
}
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
const position =
|
const position = components.rendering.iconPosition(record);
|
||||||
components.rendering.getVisual(record).billboard?.position ||
|
|
||||||
components.rendering.getVisual(record).position;
|
|
||||||
if (!position) return null;
|
if (!position) return null;
|
||||||
return {
|
return {
|
||||||
mmsi: record.mmsi,
|
mmsi: record.mmsi,
|
||||||
@@ -328,10 +327,9 @@ export function createQueries({
|
|||||||
|
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const visual = components.rendering.getVisual(record);
|
|
||||||
if (!Number.isFinite(record.lat) || !Number.isFinite(record.lon))
|
if (!Number.isFinite(record.lat) || !Number.isFinite(record.lon))
|
||||||
continue;
|
continue;
|
||||||
const position = visual.billboard?.position || visual.position;
|
const position = components.rendering.iconPosition(record);
|
||||||
if (!position) continue;
|
if (!position) continue;
|
||||||
const distanceM = Cesium.Cartesian3.distance(centerCartesian, position);
|
const distanceM = Cesium.Cartesian3.distance(centerCartesian, position);
|
||||||
if (!Number.isFinite(distanceM) || distanceM > range) continue;
|
if (!Number.isFinite(distanceM) || distanceM > range) continue;
|
||||||
@@ -381,9 +379,8 @@ export function createQueries({
|
|||||||
Number.isFinite(maxCount) && maxCount > 0 ? Math.floor(maxCount) : 800;
|
Number.isFinite(maxCount) && maxCount > 0 ? Math.floor(maxCount) : 800;
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const visual = components.rendering.getVisual(record);
|
|
||||||
if (result.length >= cap) break;
|
if (result.length >= cap) break;
|
||||||
const position = visual.billboard?.position || visual.position;
|
const position = components.rendering.iconPosition(record);
|
||||||
if (!position) continue;
|
if (!position) continue;
|
||||||
result.push({
|
result.push({
|
||||||
id: record.mmsi,
|
id: record.mmsi,
|
||||||
@@ -495,8 +492,9 @@ export function createQueries({
|
|||||||
if ((idx - start) % stride !== 0) continue;
|
if ((idx - start) % stride !== 0) continue;
|
||||||
const record = records[idx];
|
const record = records[idx];
|
||||||
const visual = components.rendering.getVisual(record);
|
const visual = components.rendering.getVisual(record);
|
||||||
if (visual.billboard && !visual.billboard.show) continue;
|
const icon = billboardState(visual.billboard);
|
||||||
const position = visual.billboard?.position || visual.position;
|
if (icon && !icon.show) continue;
|
||||||
|
const position = icon?.position || visual.position;
|
||||||
if (!position) continue;
|
if (!position) continue;
|
||||||
result.push({
|
result.push({
|
||||||
position,
|
position,
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ import {
|
|||||||
LABEL_GRID_PX,
|
LABEL_GRID_PX,
|
||||||
CARD_MIN_SEP_PX,
|
CARD_MIN_SEP_PX,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import {
|
||||||
|
addBillboard,
|
||||||
|
billboardState,
|
||||||
|
setBillboard,
|
||||||
|
} from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createRendering({
|
export function createRendering({
|
||||||
vesselState,
|
vesselState,
|
||||||
@@ -36,6 +41,12 @@ export function createRendering({
|
|||||||
return visualRecords.get(record) || record;
|
return visualRecords.get(record) || record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Where a record's icon was drawn, from Core's mirror, else its visual position. */
|
||||||
|
function iconPosition(record) {
|
||||||
|
const visual = getVisual(record);
|
||||||
|
return billboardState(visual.billboard)?.position || visual.position;
|
||||||
|
}
|
||||||
|
|
||||||
function prepareRecordVisual(record) {
|
function prepareRecordVisual(record) {
|
||||||
let visual = visualRecords.get(record);
|
let visual = visualRecords.get(record);
|
||||||
if (!visual) {
|
if (!visual) {
|
||||||
@@ -121,10 +132,13 @@ export function createRendering({
|
|||||||
const visible =
|
const visible =
|
||||||
state.feed.enabled &&
|
state.feed.enabled &&
|
||||||
isVisible(getVisual(record).surfacePosition, occluder);
|
isVisible(getVisual(record).surfacePosition, occluder);
|
||||||
getVisual(record).billboard = state.billboardCollection.add({
|
getVisual(record).billboard = addBillboard(state.billboardCollection, {
|
||||||
position: getVisual(record).position,
|
position: getVisual(record).position,
|
||||||
show: visible,
|
show: visible,
|
||||||
image: shipIcon(record, false),
|
image: shipIcon(record, false),
|
||||||
|
// The ship artwork's own size, stated so Core's mirror knows it.
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
scale: shipScale(record),
|
scale: shipScale(record),
|
||||||
// Screen-projected rotation lands on the next visibility/rotation pass.
|
// Screen-projected rotation lands on the next visibility/rotation pass.
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -286,19 +300,18 @@ export function createRendering({
|
|||||||
const visual = getVisual(record);
|
const visual = getVisual(record);
|
||||||
const visible = isVisible(visual.surfacePosition, occluder);
|
const visible = isVisible(visual.surfacePosition, occluder);
|
||||||
if (visual.billboard) {
|
if (visual.billboard) {
|
||||||
visual.billboard.show = visible;
|
if (billboardState(visual.billboard).show !== visible)
|
||||||
|
setBillboard(visual.billboard, 'show', visible);
|
||||||
if (visible && doRotations && scene) {
|
if (visible && doRotations && scene) {
|
||||||
|
const { rotation } = billboardState(visual.billboard);
|
||||||
const rot = screenProjectedRotation(
|
const rot = screenProjectedRotation(
|
||||||
scene,
|
scene,
|
||||||
visual.position,
|
visual.position,
|
||||||
vesselCourseDeg(record),
|
vesselCourseDeg(record),
|
||||||
visual.billboard.rotation,
|
rotation,
|
||||||
);
|
);
|
||||||
if (
|
if (rot !== null && Math.abs(rot - rotation) > 0.002) {
|
||||||
rot !== null &&
|
setBillboard(visual.billboard, 'rotation', rot);
|
||||||
Math.abs(rot - visual.billboard.rotation) > 0.002
|
|
||||||
) {
|
|
||||||
visual.billboard.rotation = rot;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,14 +366,15 @@ export function createRendering({
|
|||||||
for (const record of records || []) {
|
for (const record of records || []) {
|
||||||
const visual = getVisual(record);
|
const visual = getVisual(record);
|
||||||
const bb = visual?.billboard;
|
const bb = visual?.billboard;
|
||||||
const position = bb?.position || visual?.position;
|
const icon = billboardState(bb);
|
||||||
|
const position = icon?.position || visual?.position;
|
||||||
if (!bb || !position) continue;
|
if (!bb || !position) continue;
|
||||||
// Most of a worldwide feed sits behind the horizon. A hidden sprite with
|
// Most of a worldwide feed sits behind the horizon. A hidden sprite with
|
||||||
// no pending focus state and full alpha would advance to exactly what it
|
// no pending focus state and full alpha would advance to exactly what it
|
||||||
// already is, so skip it instead of paying its distance and state work
|
// already is, so skip it instead of paying its distance and state work
|
||||||
// every 80 ms while anything is tracked.
|
// every 80 ms while anything is tracked.
|
||||||
if (
|
if (
|
||||||
bb.show === false &&
|
icon.show === false &&
|
||||||
spriteFocusAtRest(bb) &&
|
spriteFocusAtRest(bb) &&
|
||||||
!focusAlphaNeedsWrite(bb.color?.alpha, 1, params)
|
!focusAlphaNeedsWrite(bb.color?.alpha, 1, params)
|
||||||
)
|
)
|
||||||
@@ -368,15 +382,16 @@ export function createRendering({
|
|||||||
const focus = advanceSpriteFocus(bb, {
|
const focus = advanceSpriteFocus(bb, {
|
||||||
// Hidden/far-side sprites still finish any pending release so the active
|
// Hidden/far-side sprites still finish any pending release so the active
|
||||||
// count remains truthful and they cannot reappear with stale dim alpha.
|
// count remains truthful and they cannot reappear with stale dim alpha.
|
||||||
screenPosition: bb.show === false ? null : screenPositionFor(position),
|
screenPosition:
|
||||||
|
icon.show === false ? null : screenPositionFor(position),
|
||||||
cameraDistance: cameraDistanceFor(position),
|
cameraDistance: cameraDistanceFor(position),
|
||||||
nowMs,
|
nowMs,
|
||||||
target,
|
target,
|
||||||
params,
|
params,
|
||||||
// Vessel artwork is 32 px before billboard scale. Including the
|
// Vessel artwork is 32 px before billboard scale. Including the
|
||||||
// ambient chevron's own rendered extent prevents edge-overlap misses.
|
// ambient chevron's own rendered extent prevents edge-overlap misses.
|
||||||
spriteHalfWidthPx: (bb.width || 32) * (bb.scale || 1) * 0.5,
|
spriteHalfWidthPx: (icon.width || 32) * (icon.scale || 1) * 0.5,
|
||||||
spriteHalfHeightPx: (bb.height || 32) * (bb.scale || 1) * 0.5,
|
spriteHalfHeightPx: (icon.height || 32) * (icon.scale || 1) * 0.5,
|
||||||
});
|
});
|
||||||
transitioning ||= focus.transitioning;
|
transitioning ||= focus.transitioning;
|
||||||
if (focus.active) activeCount += 1;
|
if (focus.active) activeCount += 1;
|
||||||
@@ -461,7 +476,7 @@ export function createRendering({
|
|||||||
if (selected) {
|
if (selected) {
|
||||||
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
|
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||||
scene,
|
scene,
|
||||||
getVisual(selected).billboard?.position || getVisual(selected).position,
|
iconPosition(selected),
|
||||||
);
|
);
|
||||||
if (screen) accepted.push({ x: screen.x, y: screen.y });
|
if (screen) accepted.push({ x: screen.x, y: screen.y });
|
||||||
}
|
}
|
||||||
@@ -561,6 +576,7 @@ export function createRendering({
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
getVisual,
|
getVisual,
|
||||||
|
iconPosition,
|
||||||
prepareRecordVisual,
|
prepareRecordVisual,
|
||||||
resetRecordVisuals,
|
resetRecordVisuals,
|
||||||
renderRowLimit,
|
renderRowLimit,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { isPointerFree } from '../../data/inputOwnership.js';
|
import { isPointerFree } from '../../data/inputOwnership.js';
|
||||||
import { VESSEL_OVERLAY_SOURCE_ID } from '../../data/vesselLabels.js';
|
import { VESSEL_OVERLAY_SOURCE_ID } from '../../data/vesselLabels.js';
|
||||||
|
import { setBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createSelection({
|
export function createSelection({
|
||||||
vesselState,
|
vesselState,
|
||||||
@@ -131,9 +132,7 @@ export function createSelection({
|
|||||||
kind: 'vessel',
|
kind: 'vessel',
|
||||||
id: record.mmsi,
|
id: record.mmsi,
|
||||||
label: record.name || record.mmsi,
|
label: record.name || record.mmsi,
|
||||||
position:
|
position: components.rendering.iconPosition(record),
|
||||||
components.rendering.getVisual(record).billboard?.position ||
|
|
||||||
components.rendering.getVisual(record).position,
|
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -174,8 +173,11 @@ export function createSelection({
|
|||||||
if (components.rendering.getVisual(record).billboard) {
|
if (components.rendering.getVisual(record).billboard) {
|
||||||
components.rendering.getVisual(record).billboard.image =
|
components.rendering.getVisual(record).billboard.image =
|
||||||
components.rendering.shipIcon(record, true);
|
components.rendering.shipIcon(record, true);
|
||||||
components.rendering.getVisual(record).billboard.scale =
|
setBillboard(
|
||||||
components.rendering.shipScale(record) * 1.2;
|
components.rendering.getVisual(record).billboard,
|
||||||
|
'scale',
|
||||||
|
components.rendering.shipScale(record) * 1.2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Rebuild the card set immediately so the full-detail card appears on the
|
// Rebuild the card set immediately so the full-detail card appears on the
|
||||||
// click, not up to VISIBILITY_UPDATE_MS later.
|
// click, not up to VISIBILITY_UPDATE_MS later.
|
||||||
@@ -230,8 +232,11 @@ export function createSelection({
|
|||||||
if (components.rendering.getVisual(record)?.billboard) {
|
if (components.rendering.getVisual(record)?.billboard) {
|
||||||
components.rendering.getVisual(record).billboard.image =
|
components.rendering.getVisual(record).billboard.image =
|
||||||
components.rendering.shipIcon(record, false);
|
components.rendering.shipIcon(record, false);
|
||||||
components.rendering.getVisual(record).billboard.scale =
|
setBillboard(
|
||||||
components.rendering.shipScale(record);
|
components.rendering.getVisual(record).billboard,
|
||||||
|
'scale',
|
||||||
|
components.rendering.shipScale(record),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
state.selectedRecord = null;
|
state.selectedRecord = null;
|
||||||
// Drop the full-detail card right away (no-op when the layer is disabled —
|
// Drop the full-detail card right away (no-op when the layer is disabled —
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/** Connect portable vessel reconciliation to scene and selection operations. */
|
/** Connect portable vessel reconciliation to scene and selection operations. */
|
||||||
|
import { setBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createVesselSnapshotRenderer({
|
export function createVesselSnapshotRenderer({
|
||||||
state,
|
state,
|
||||||
records,
|
records,
|
||||||
@@ -29,9 +31,12 @@ export function createVesselSnapshotRenderer({
|
|||||||
const selected = record === state.selectedRecord;
|
const selected = record === state.selectedRecord;
|
||||||
const visual = rendering.prepareRecordVisual(record);
|
const visual = rendering.prepareRecordVisual(record);
|
||||||
if (visual.billboard) {
|
if (visual.billboard) {
|
||||||
visual.billboard.position = visual.position;
|
setBillboard(visual.billboard, 'position', visual.position);
|
||||||
visual.billboard.scale =
|
setBillboard(
|
||||||
rendering.shipScale(record) * (selected ? 1.2 : 1);
|
visual.billboard,
|
||||||
|
'scale',
|
||||||
|
rendering.shipScale(record) * (selected ? 1.2 : 1),
|
||||||
|
);
|
||||||
const nextIcon = rendering.shipIcon(record, selected);
|
const nextIcon = rendering.shipIcon(record, selected);
|
||||||
if (nextIcon !== prevIcon) visual.billboard.image = nextIcon;
|
if (nextIcon !== prevIcon) visual.billboard.image = nextIcon;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TRAIL_MAX_POINTS,
|
TRAIL_MAX_POINTS,
|
||||||
TRAIL_MIN_MOVE_M,
|
TRAIL_MIN_MOVE_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
import { setBillboard } from '../../data/billboardMirror.js';
|
||||||
|
|
||||||
export function createTracking({
|
export function createTracking({
|
||||||
vesselState,
|
vesselState,
|
||||||
@@ -71,7 +72,8 @@ export function createTracking({
|
|||||||
record.lat,
|
record.lat,
|
||||||
heightM,
|
heightM,
|
||||||
);
|
);
|
||||||
if (visual.billboard) visual.billboard.position = visual.position;
|
if (visual.billboard)
|
||||||
|
setBillboard(visual.billboard, 'position', visual.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user