mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
refactor(military): Cesium rendering behind a renderer interface
Task 11a. The military flights logic no longer imports Cesium: it draws
through the shared aircraft renderer (src/layers/aircraft/render/cesium.js)
with the military's parameters in src/layers/military/render/cesium.js
(sprite-order slot 'military', jet.glb's 180° nose offset), registered for
'cesium' in the renderer registry and created at init(viewer, {engine}).
The same pure move as flights in 10a:
- Fleet: the BillboardCollection and its sprite-order slot (enable keeps
restoreSpriteOrder, as before); sprites stay Billboard handles.
- Models: fleet/tracked/preload glTF loads (asynchronous:false, MIX tint at
MODEL_COLOR_BLEND_AMOUNT, IR boost as `unlit` → the UNLIT CustomShader),
the model collection, setPose. The per-class specs keep their own nose
offset: setPose takes an optional offsetDeg (default the layer's), on
both aircraft renderers (same arithmetic, test added).
- Tracked: the ENU billboard Entity with its position/colour/rotation
callbacks (#FFD166, transparent amber once the model owns the visual),
viewFrom, followed through viewer.trackedEntity + the one exact tracked
frame; refocusTrackedById through follow.refocus.
- Trail: the entity trail and the floored live head segment.
- View: camera/scene reads, frame number, frustum and horizon tests,
render requests, frame events.
- Clicks and Escape go through the engine's selection dispatcher (the
ScreenSpaceEventHandler and keydown listener are gone); the cross-layer
untrack watches the renderer's follow. Same rules: cockpit clicks are
inert, the tracked visual/model is a no-op, another layer's pick is not
empty space, an unowned pick is.
- Colours are engine-neutral Rgba (MIL_ICON_COLOR #FFB800,
TRACKED_ICON_COLOR #FFD166, AMBER_TRANSPARENT), NearFarScalar →
nearFarScale; Cesium math → src/geo.
- check-import-directions: src/layers/military/ joins the rendered layers
(cesium only in render/cesium*.js).
- The military registry and the flights' duplicate suppression are
untouched.
Source-text tests updated with the moved code, same meaning:
- militaryAwareness.test: military's hasContact guard reads fleet.visible().
- trackingClickGesture.test: the military gesture rules are the
dispatcher's (selection gesture before select, click classifier before
the empty-space clear), military answers the empty click with a
user-origin clear — the same assertion flights already had.
- ownership.test: the services fixture names selection and renderer; the
app-composed services build the layer's Cesium renderer.
- renderers.test: flights and military have extracted Cesium renderers.
New tests: the military logic over a plain-data renderer and a fake engine
(amber fleet, click-select, #FFD166 tracked glyph, no-op on the tracked
visual, empty-click and Escape release, the keyboard left on destroy); the
plain-data renderer moved to src/testSupport for both aircraft layers;
setPose with a per-model offset.
W1 replay vs 7c7422e2 (the 10a reference run): every layer's counts, known
objects, screen points, tracking pose after 2 s, requests and the
city/global/orbit shots (sha256) identical. qa-military-names (it checks
the installations layer): the same verdict as 7c7422e2 (the zoom-markers
assertion fails on both). Main chunk 2,890,763 B (−2.9 KB).
This commit is contained in:
1 parent
e007350645
commit
7c0891deda
26 files changed
+829
-803
No files matched your search
@@ -35,6 +35,7 @@ const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
|
|||||||
// Navara only in render/navara*.js. The list grows with each moved layer.
|
// Navara only in render/navara*.js. The list grows with each moved layer.
|
||||||
const RENDERED_LAYER_DIRS = [
|
const RENDERED_LAYER_DIRS = [
|
||||||
'src/layers/flights/',
|
'src/layers/flights/',
|
||||||
|
'src/layers/military/',
|
||||||
'src/layers/aircraft/',
|
'src/layers/aircraft/',
|
||||||
'src/layers/shared/',
|
'src/layers/shared/',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -326,6 +326,8 @@
|
|||||||
"src/layers/aircraft/render/navara.test.mjs",
|
"src/layers/aircraft/render/navara.test.mjs",
|
||||||
"src/layers/aircraft/render/navara.js",
|
"src/layers/aircraft/render/navara.js",
|
||||||
"src/layers/flights/renderer.test.mjs",
|
"src/layers/flights/renderer.test.mjs",
|
||||||
|
"src/layers/military/renderer.test.mjs",
|
||||||
|
"src/testSupport/aircraftRecordingRenderer.mjs",
|
||||||
"src/layers/aircraft/render/cesium.test.mjs",
|
"src/layers/aircraft/render/cesium.test.mjs",
|
||||||
"src/layers/aircraft/render/cesium.js",
|
"src/layers/aircraft/render/cesium.js",
|
||||||
"src/layers/aircraft/render/contract.js",
|
"src/layers/aircraft/render/contract.js",
|
||||||
@@ -348,6 +350,7 @@
|
|||||||
"src/layers/military/ingestion.js",
|
"src/layers/military/ingestion.js",
|
||||||
"src/layers/military/queries.js",
|
"src/layers/military/queries.js",
|
||||||
"src/layers/military/index.js",
|
"src/layers/military/index.js",
|
||||||
|
"src/layers/military/render/cesium.js",
|
||||||
"src/layers/aircraft/index.js",
|
"src/layers/aircraft/index.js",
|
||||||
"src/layers/aircraft/classification.js",
|
"src/layers/aircraft/classification.js",
|
||||||
"src/layers/military/ownership.test.mjs",
|
"src/layers/military/ownership.test.mjs",
|
||||||
|
|||||||
@@ -935,6 +935,7 @@
|
|||||||
"src/layers/military/records.js",
|
"src/layers/military/records.js",
|
||||||
"src/layers/military/recordPolicy.js",
|
"src/layers/military/recordPolicy.js",
|
||||||
"src/layers/military/snapshotRenderer.js",
|
"src/layers/military/snapshotRenderer.js",
|
||||||
|
"src/layers/shared/color.js",
|
||||||
"src/data/inputOwnership.js",
|
"src/data/inputOwnership.js",
|
||||||
"src/data/modelEligibility.js",
|
"src/data/modelEligibility.js",
|
||||||
"src/geo/index.js",
|
"src/geo/index.js",
|
||||||
@@ -2591,6 +2592,7 @@
|
|||||||
"src/app/layers/renderers.js",
|
"src/app/layers/renderers.js",
|
||||||
"src/layers/aircraft/render/cesium.js",
|
"src/layers/aircraft/render/cesium.js",
|
||||||
"src/layers/flights/render/cesium.js",
|
"src/layers/flights/render/cesium.js",
|
||||||
|
"src/layers/military/render/cesium.js",
|
||||||
"src/data/selectionDispatcher.js",
|
"src/data/selectionDispatcher.js",
|
||||||
"src/layers/shared/color.js",
|
"src/layers/shared/color.js",
|
||||||
"src/layers/aircraft/render/navara.js",
|
"src/layers/aircraft/render/navara.js",
|
||||||
@@ -3169,7 +3171,8 @@
|
|||||||
"src/layers/aircraft/render/cesium.js",
|
"src/layers/aircraft/render/cesium.js",
|
||||||
"src/data/selectionDispatcher.js",
|
"src/data/selectionDispatcher.js",
|
||||||
"src/layers/shared/color.js",
|
"src/layers/shared/color.js",
|
||||||
"src/layers/flights/render/cesium.js"
|
"src/layers/flights/render/cesium.js",
|
||||||
|
"src/layers/military/render/cesium.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createMilitaryFlightLayer } from '../../layers/military/index.js';
|
import { createMilitaryFlightLayer } from '../../layers/military/index.js';
|
||||||
import * as picking from '../../data/pickRegistry.js';
|
import * as picking from '../../data/pickRegistry.js';
|
||||||
import * as sprites from '../../data/spriteOrder.js';
|
import * as selection from '../../data/selectionDispatcher.js';
|
||||||
import * as trails from '../../data/trailRenderer.js';
|
|
||||||
import * as aircraftPresentation from '../../data/tr3bRegistry.js';
|
import * as aircraftPresentation from '../../data/tr3bRegistry.js';
|
||||||
import * as trackedCamera from '../../data/trackedCamera.js';
|
import * as trackedCamera from '../../data/trackedCamera.js';
|
||||||
import { applyTrackedCameraFrame } from '../../globe/cesium/follow.js';
|
import { rendererFor } from './renderers.js';
|
||||||
|
import { createMilitaryCesiumRenderer } from '../../layers/military/render/cesium.js';
|
||||||
import * as labels from '../../data/detectionDraw.js';
|
import * as labels from '../../data/detectionDraw.js';
|
||||||
import * as geoid from '../../data/geoid.js';
|
import * as geoid from '../../data/geoid.js';
|
||||||
import * as focus from '../../data/focusDeemphasis.js';
|
import * as focus from '../../data/focusDeemphasis.js';
|
||||||
@@ -20,6 +20,13 @@ export function createApplicationMilitary({
|
|||||||
militaryRegistry,
|
militaryRegistry,
|
||||||
resolveAsset = (url) =>
|
resolveAsset = (url) =>
|
||||||
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
|
`${import.meta.env?.BASE_URL || '/'}${url.replace(/^\//, '')}`,
|
||||||
|
// Test seam: the renderer factory (default: the engine's, from the registry).
|
||||||
|
renderer = {
|
||||||
|
create: ({ viewer, engine = null, adopt }) =>
|
||||||
|
engine
|
||||||
|
? rendererFor('military', engine, { viewer, adopt })
|
||||||
|
: createMilitaryCesiumRenderer(null, { viewer, adopt }),
|
||||||
|
},
|
||||||
}) {
|
}) {
|
||||||
const { groundFloor, meshFloor, groundSnap } = surface;
|
const { groundFloor, meshFloor, groundSnap } = surface;
|
||||||
return createMilitaryFlightLayer({
|
return createMilitaryFlightLayer({
|
||||||
@@ -27,10 +34,13 @@ export function createApplicationMilitary({
|
|||||||
resolveAsset,
|
resolveAsset,
|
||||||
services: {
|
services: {
|
||||||
picking,
|
picking,
|
||||||
sprites,
|
selection,
|
||||||
trails,
|
// The engine's military renderer from the registry (render/cesium.js
|
||||||
|
// on Cesium); a bare viewer without an engine (unit tests) draws on
|
||||||
|
// Cesium.
|
||||||
|
renderer,
|
||||||
aircraftPresentation,
|
aircraftPresentation,
|
||||||
camera: { ...trackedCamera, applyTrackedCameraFrame },
|
camera: trackedCamera,
|
||||||
militaryRegistry,
|
militaryRegistry,
|
||||||
labels,
|
labels,
|
||||||
groundFloor,
|
groundFloor,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
|
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
|
||||||
import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js';
|
import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js';
|
||||||
|
import { createMilitaryCesiumRenderer } from '../../layers/military/render/cesium.js';
|
||||||
|
|
||||||
const ENGINE_KINDS = new Set(['cesium', 'navara']);
|
const ENGINE_KINDS = new Set(['cesium', 'navara']);
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ export const layerAvailableOn = (layerId, engineKind) =>
|
|||||||
/** Cesium renderers extracted from their layers (render/cesium.js). */
|
/** Cesium renderers extracted from their layers (render/cesium.js). */
|
||||||
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
||||||
flights: createFlightsCesiumRenderer,
|
flights: createFlightsCesiumRenderer,
|
||||||
|
military: createMilitaryCesiumRenderer,
|
||||||
});
|
});
|
||||||
|
|
||||||
// The other application layers still draw themselves on Cesium.
|
// The other application layers still draw themselves on Cesium.
|
||||||
|
|||||||
@@ -56,12 +56,18 @@ test('the default registry: every layer on Cesium, none yet on Navara', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('flights has an extracted Cesium renderer; the others still draw themselves', () => {
|
test('flights and military have extracted Cesium renderers; the others still draw themselves', () => {
|
||||||
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), ['flights']);
|
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), [
|
||||||
|
'flights',
|
||||||
|
'military',
|
||||||
|
]);
|
||||||
const viewer = { scene: {}, camera: {} };
|
const viewer = { scene: {}, camera: {} };
|
||||||
const renderer = rendererFor('flights', { kind: 'cesium' }, { viewer });
|
const renderer = rendererFor('flights', { kind: 'cesium' }, { viewer });
|
||||||
assert.equal(renderer.kind, 'cesium');
|
assert.equal(renderer.kind, 'cesium');
|
||||||
assert.equal(renderer.viewer, viewer);
|
assert.equal(renderer.viewer, viewer);
|
||||||
|
const military = rendererFor('military', { kind: 'cesium' }, { viewer });
|
||||||
|
assert.equal(military.kind, 'cesium');
|
||||||
|
assert.equal(military.viewer, viewer);
|
||||||
assert.equal(rendererFor('satellites', { kind: 'cesium' }), null);
|
assert.equal(rendererFor('satellites', { kind: 'cesium' }), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1369,8 +1369,9 @@ test('hasContact declines while a layer is disabled, whatever its maps still hol
|
|||||||
// Flights asks its renderer whether the fleet is shown (phase 3).
|
// Flights asks its renderer whether the fleet is shown (phase 3).
|
||||||
['flights', readLayerSource(new URL('./flights.js', import.meta.url)),
|
['flights', readLayerSource(new URL('./flights.js', import.meta.url)),
|
||||||
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_renderer\?\.fleet\.visible\(\)\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_renderer\?\.fleet\.visible\(\)\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
||||||
|
// Military asks its renderer too (phase 3).
|
||||||
['militaryFlights', readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
|
['militaryFlights', readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
|
||||||
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_billboardCollection\s*\|\|\s*!(?:flightState\.)?_billboardCollection\.show\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
/hasContact\(\s*icao24,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!(?:flightState\.)?_renderer\?\.fleet\.visible\(\)\s*\|\|\s*(?:flightState\.)?_billboards\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
||||||
['aisLiveVessels', readLayerSource(new URL('./aisLiveVessels.js', import.meta.url)),
|
['aisLiveVessels', readLayerSource(new URL('./aisLiveVessels.js', import.meta.url)),
|
||||||
/hasContact\(\s*mmsi,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!state\.feed\.enabled\s*\|\|\s*!state\.records\.byMmsi\s*\|\|\s*state\.records\.byMmsi\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
/hasContact\(\s*mmsi,?\s*\)\s*\{\s*\n\s*if\s*\(\s*!state\.feed\.enabled\s*\|\|\s*!state\.records\.byMmsi\s*\|\|\s*state\.records\.byMmsi\.size\s*===\s*0,?\s*\)\s*return\s*null;/],
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -114,18 +114,17 @@ test('civilian and military click handlers apply duration only at the deselect b
|
|||||||
readLayerSource(new URL('./flights.js', import.meta.url)),
|
readLayerSource(new URL('./flights.js', import.meta.url)),
|
||||||
readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
|
readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
|
||||||
];
|
];
|
||||||
// Military still owns its handler; flights moved to the selection
|
// Flights and military moved to the selection dispatcher (phase 3), whose
|
||||||
// dispatcher (phase 3), whose source applies the same two rules: the
|
// source applies the same two rules: the selection gesture before a
|
||||||
// selection gesture before a select, the full click classifier before the
|
// select, the full click classifier before the empty-space deselect, which
|
||||||
// empty-space deselect, which flights answers with a user-origin clear.
|
// each layer answers with a user-origin clear.
|
||||||
const military = sources[1];
|
|
||||||
assert.match(military, /isTrackingSelectionGesture\(gesture\)[\s\S]+scene\.pick/);
|
|
||||||
assert.match(military, /isTrackingClickGesture\(gesture\)[\s\S]+(?:parts\.\w+\.)?_clearTracking\([^)]*\{ origin: 'user' \}\)/);
|
|
||||||
const dispatcher = readFileSync(new URL('./selectionDispatcher.js', import.meta.url), 'utf8');
|
const dispatcher = readFileSync(new URL('./selectionDispatcher.js', import.meta.url), 'utf8');
|
||||||
assert.match(dispatcher, /if \(!isTrackingSelectionGesture\(gesture\)\) return;[\s\S]+onSelect/);
|
assert.match(dispatcher, /if \(!isTrackingSelectionGesture\(gesture\)\) return;[\s\S]+onSelect/);
|
||||||
assert.match(dispatcher, /if \(!isTrackingClickGesture\(gesture\)\) return;\s*for \(const owner of \[\.\.\.owners\.values\(\)\]\) owner\.onEmptyClick/);
|
assert.match(dispatcher, /if \(!isTrackingClickGesture\(gesture\)\) return;\s*for \(const owner of \[\.\.\.owners\.values\(\)\]\) owner\.onEmptyClick/);
|
||||||
assert.match(sources[0], /dispatcher\.register\('flights', \{[\s\S]+onEmptyClick: _onEmptyClick/);
|
for (const [layer, source] of [['flights', sources[0]], ['military', sources[1]]]) {
|
||||||
assert.match(sources[0], /function _onEmptyClick\(\) \{[\s\S]+?_clearTracking\(false, \{ origin: 'user' \}\)/);
|
assert.match(source, new RegExp(`dispatcher\\.register\\('${layer}', \\{[\\s\\S]+onEmptyClick: _onEmptyClick`));
|
||||||
|
assert.match(source, /function _onEmptyClick\(\) \{[\s\S]+?_clearTracking\(false, \{ origin: 'user' \}\)/);
|
||||||
|
}
|
||||||
assert.doesNotMatch(
|
assert.doesNotMatch(
|
||||||
sources[0],
|
sources[0],
|
||||||
/(?:flightState\.)?_trackedEntity = (?:flightState\.)?_viewer\.entities\.add\(\{\s*id:/,
|
/(?:flightState\.)?_trackedEntity = (?:flightState\.)?_viewer\.entities\.add\(\{\s*id:/,
|
||||||
|
|||||||
@@ -263,12 +263,11 @@ export function createAircraftCesiumRenderer({
|
|||||||
* written into the model's OWN modelMatrix: Model.modelMatrix is a plain
|
* written into the model's OWN modelMatrix: Model.modelMatrix is a plain
|
||||||
* field Cesium clones per frame, so models never share one mutable
|
* field Cesium clones per frame, so models never share one mutable
|
||||||
* matrix (a shared scratch stacked every model on the last-written
|
* matrix (a shared scratch stacked every model on the last-written
|
||||||
* plane: the "flickering like mad" bug).
|
* plane: the "flickering like mad" bug). `offsetDeg`: this model's nose
|
||||||
|
* offset when the layer's models differ (default: the layer's).
|
||||||
*/
|
*/
|
||||||
setPose(model, position, headingDeg) {
|
setPose(model, position, headingDeg, offsetDeg = headingOffsetDeg) {
|
||||||
scratchHpr.heading = Cesium.Math.toRadians(
|
scratchHpr.heading = Cesium.Math.toRadians((headingDeg || 0) + offsetDeg);
|
||||||
(headingDeg || 0) + headingOffsetDeg,
|
|
||||||
);
|
|
||||||
scratchHpr.pitch = 0;
|
scratchHpr.pitch = 0;
|
||||||
scratchHpr.roll = 0;
|
scratchHpr.roll = 0;
|
||||||
return Cesium.Transforms.headingPitchRollToFixedFrame(
|
return Cesium.Transforms.headingPitchRollToFixedFrame(
|
||||||
|
|||||||
@@ -110,6 +110,23 @@ test('a model pose is the old heading-pitch-roll frame with the nose offset', ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a model pose takes a per-model nose offset (military per-class specs)', () => {
|
||||||
|
const { viewer } = stubViewer();
|
||||||
|
const r = renderer(viewer);
|
||||||
|
const model = { modelMatrix: new Cesium.Matrix4() };
|
||||||
|
const position = geo.cartesian3.fromDegrees(-97.7, 30.2, 3000);
|
||||||
|
r.models.setPose(model, position, 75, 90);
|
||||||
|
const expected = Cesium.Transforms.headingPitchRollToFixedFrame(
|
||||||
|
position,
|
||||||
|
new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(75 + 90), 0, 0),
|
||||||
|
Cesium.Ellipsoid.WGS84,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
[...Cesium.Matrix4.toArray(model.modelMatrix)],
|
||||||
|
[...Cesium.Matrix4.toArray(expected)],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('model loads carry the layer spec; the IR boost loads an UNLIT model', async () => {
|
test('model loads carry the layer spec; the IR boost loads an UNLIT model', async () => {
|
||||||
const { viewer } = stubViewer();
|
const { viewer } = stubViewer();
|
||||||
const r = renderer(viewer);
|
const r = renderer(viewer);
|
||||||
|
|||||||
@@ -71,7 +71,8 @@
|
|||||||
* restoreOrder(), restoreOrderOnEnable().
|
* restoreOrder(), restoreOrderOnEnable().
|
||||||
* @property {object} models live(), setVisible(bool), loadFleet(spec),
|
* @property {object} models live(), setVisible(bool), loadFleet(spec),
|
||||||
* loadTracked(spec), preload(url) → Promise<ModelHandle>, add(model),
|
* loadTracked(spec), preload(url) → Promise<ModelHandle>, add(model),
|
||||||
* remove(model), destroy(model), setPose(model, position, headingDeg).
|
* remove(model), destroy(model), setPose(model, position, headingDeg,
|
||||||
|
* offsetDeg?) (offsetDeg: the model's nose offset, default the layer's).
|
||||||
* spec: { url, minimumPixelSize, scale, color, blendAmount, unlit, id }.
|
* spec: { url, minimumPixelSize, scale, color, blendAmount, unlit, id }.
|
||||||
* @property {object} tracked create(spec) → TrackedHandle, remove(handle),
|
* @property {object} tracked create(spec) → TrackedHandle, remove(handle),
|
||||||
* isLive(handle), setImage(handle, image), setViewFrom(handle, enu),
|
* isLive(handle), setImage(handle, image), setViewFrom(handle, enu),
|
||||||
|
|||||||
@@ -719,11 +719,10 @@ export function createAircraftNavaraRenderer({
|
|||||||
model?.gevBank?.remove(model);
|
model?.gevBank?.remove(model);
|
||||||
},
|
},
|
||||||
/** The same HPR frame as Cesium (modelMatrix); the instance turned by
|
/** The same HPR frame as Cesium (modelMatrix); the instance turned by
|
||||||
* Navara's glTF axes (NAVARA_GLTF_AXIS). */
|
* Navara's glTF axes (NAVARA_GLTF_AXIS). `offsetDeg`: this model's
|
||||||
setPose(model, position, headingDeg) {
|
* nose offset when the layer's models differ (default: the layer's). */
|
||||||
scratchHpr.heading = geo.math.toRadians(
|
setPose(model, position, headingDeg, offsetDeg = headingOffsetDeg) {
|
||||||
(headingDeg || 0) + headingOffsetDeg,
|
scratchHpr.heading = geo.math.toRadians((headingDeg || 0) + offsetDeg);
|
||||||
);
|
|
||||||
geo.transforms.headingPitchRollToFixedFrame(
|
geo.transforms.headingPitchRollToFixedFrame(
|
||||||
position,
|
position,
|
||||||
scratchHpr,
|
scratchHpr,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import * as militaryRegistry from '../../data/militaryRegistry.js';
|
|||||||
import { openSkySnapshot } from '../../sources/live/aircraft.js';
|
import { openSkySnapshot } from '../../sources/live/aircraft.js';
|
||||||
import { missingAircraftRendererMembers } from '../aircraft/render/contract.js';
|
import { missingAircraftRendererMembers } from '../aircraft/render/contract.js';
|
||||||
import { Rgba } from '../shared/color.js';
|
import { Rgba } from '../shared/color.js';
|
||||||
|
import { recordingAircraftRenderer as recordingRenderer } from '../../testSupport/aircraftRecordingRenderer.mjs';
|
||||||
|
|
||||||
globalThis.window ||= {};
|
globalThis.window ||= {};
|
||||||
globalThis.window.dispatchEvent ||= () => true;
|
globalThis.window.dispatchEvent ||= () => true;
|
||||||
@@ -31,123 +32,6 @@ globalThis.document ||= {
|
|||||||
removeEventListener() {},
|
removeEventListener() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A renderer of plain records, for the logic alone. */
|
|
||||||
function recordingRenderer(engine) {
|
|
||||||
const calls = [];
|
|
||||||
const sprites = new Set();
|
|
||||||
const camera = {
|
|
||||||
positionWC: geo.cartesian3.fromDegrees(-97.7, 30.2, 400_000),
|
|
||||||
positionCartographic: geo.cartographic.fromDegrees(-97.7, 30.2, 400_000),
|
|
||||||
rightWC: geo.cartesian3.create(1, 0, 0),
|
|
||||||
upWC: geo.cartesian3.create(0, 0, 1),
|
|
||||||
directionWC: geo.cartesian3.create(0, -1, 0),
|
|
||||||
heading: 0,
|
|
||||||
pitch: -Math.PI / 2,
|
|
||||||
roll: 0,
|
|
||||||
frustum: { fovy: 1 },
|
|
||||||
};
|
|
||||||
const scene = {
|
|
||||||
camera,
|
|
||||||
canvas: { clientHeight: 768 },
|
|
||||||
frameState: { frameNumber: 1 },
|
|
||||||
};
|
|
||||||
let trackedSeq = 0;
|
|
||||||
let following;
|
|
||||||
const renderer = {
|
|
||||||
kind: 'record',
|
|
||||||
engine,
|
|
||||||
calls,
|
|
||||||
sprites,
|
|
||||||
view: {
|
|
||||||
camera,
|
|
||||||
scene,
|
|
||||||
surfaceOwner: engine,
|
|
||||||
surfaceScene: engine,
|
|
||||||
frameNumber: () => 1,
|
|
||||||
viewportHeightPx: () => 768,
|
|
||||||
frustumTester: () => () => true,
|
|
||||||
horizon: () => ({ isPointVisible: () => true }),
|
|
||||||
worldToWindow: () => ({ x: 0, y: 0 }),
|
|
||||||
requestRender: () => calls.push(['requestRender']),
|
|
||||||
on: () => () => {},
|
|
||||||
holdFrames() {},
|
|
||||||
closeFrameLatch() {},
|
|
||||||
releaseFrames() {},
|
|
||||||
},
|
|
||||||
fleet: {
|
|
||||||
shown: false,
|
|
||||||
attach: () => calls.push(['attach']),
|
|
||||||
detach: () => calls.push(['detach']),
|
|
||||||
attached: () => true,
|
|
||||||
visible: () => renderer.fleet.shown,
|
|
||||||
setVisible(visible) {
|
|
||||||
renderer.fleet.shown = visible;
|
|
||||||
},
|
|
||||||
add(spec) {
|
|
||||||
const sprite = { ...spec };
|
|
||||||
sprites.add(sprite);
|
|
||||||
return sprite;
|
|
||||||
},
|
|
||||||
remove: (sprite) => sprites.delete(sprite),
|
|
||||||
restoreOrder() {},
|
|
||||||
restoreOrderOnEnable() {},
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
live: () => true,
|
|
||||||
setVisible() {},
|
|
||||||
loadFleet: () => Promise.reject(new Error('no models here')),
|
|
||||||
loadTracked: () => Promise.reject(new Error('no models here')),
|
|
||||||
preload: () => Promise.reject(new Error('no models here')),
|
|
||||||
add() {},
|
|
||||||
remove() {},
|
|
||||||
destroy() {},
|
|
||||||
setPose() {},
|
|
||||||
},
|
|
||||||
tracked: {
|
|
||||||
create(spec) {
|
|
||||||
const handle = { id: `tracked-${++trackedSeq}`, spec };
|
|
||||||
calls.push(['tracked.create', handle.id]);
|
|
||||||
return handle;
|
|
||||||
},
|
|
||||||
remove: (handle) => calls.push(['tracked.remove', handle.id]),
|
|
||||||
isLive: () => true,
|
|
||||||
setImage() {},
|
|
||||||
setViewFrom(handle, offset) {
|
|
||||||
handle.viewFrom = offset;
|
|
||||||
},
|
|
||||||
billboardSize: () => null,
|
|
||||||
},
|
|
||||||
follow: {
|
|
||||||
start(handle) {
|
|
||||||
following = handle;
|
|
||||||
calls.push(['follow.start', handle.id]);
|
|
||||||
return () => calls.push(['follow.frameStop']);
|
|
||||||
},
|
|
||||||
refocus: (handle) => renderer.follow.start(handle),
|
|
||||||
release() {
|
|
||||||
following = undefined;
|
|
||||||
calls.push(['follow.release']);
|
|
||||||
},
|
|
||||||
isFollowing: (handle) => following === handle,
|
|
||||||
current: () => following,
|
|
||||||
onChanged: () => () => {},
|
|
||||||
},
|
|
||||||
trails: {
|
|
||||||
create: () => ({
|
|
||||||
setPositions() {},
|
|
||||||
setVisible() {},
|
|
||||||
clear() {},
|
|
||||||
destroy() {},
|
|
||||||
}),
|
|
||||||
createHead: ({ id }) => ({ id, show: true }),
|
|
||||||
removeHead() {},
|
|
||||||
},
|
|
||||||
surfaceExclusions: () => [],
|
|
||||||
destroy: () => calls.push(['destroy']),
|
|
||||||
};
|
|
||||||
return renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapshotOf(ids) {
|
function snapshotOf(ids) {
|
||||||
const nowSec = Math.floor(Date.now() / 1000);
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
return openSkySnapshot({
|
return openSkySnapshot({
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Cesium from 'cesium';
|
|
||||||
import { JET_MODEL_URL } from './policy.js';
|
import { JET_MODEL_URL } from './policy.js';
|
||||||
|
|
||||||
export function createLifecycle({
|
export function createLifecycle({
|
||||||
@@ -9,7 +8,6 @@ export function createLifecycle({
|
|||||||
resolveAsset,
|
resolveAsset,
|
||||||
}) {
|
}) {
|
||||||
const { clearFocusTarget } = services.focus;
|
const { clearFocusTarget } = services.focus;
|
||||||
const { registerSpriteCollection, restoreSpriteOrder } = services.sprites;
|
|
||||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||||
const { ensureGeoidReady } = services.geoid;
|
const { ensureGeoidReady } = services.geoid;
|
||||||
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
const { registerPickOwner, unregisterPickOwner } = services.picking;
|
||||||
@@ -18,7 +16,7 @@ export function createLifecycle({
|
|||||||
const methods = {
|
const methods = {
|
||||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
/** Configure the source before initialization; an active layer keeps its owner. */
|
||||||
setSource(source) {
|
setSource(source) {
|
||||||
if (flightState._viewer)
|
if (flightState._renderer)
|
||||||
throw new Error('Configure the source before layer initialization');
|
throw new Error('Configure the source before layer initialization');
|
||||||
if (typeof source?.getSnapshot !== 'function')
|
if (typeof source?.getSnapshot !== 'function')
|
||||||
throw new TypeError('A snapshot source is required');
|
throw new TypeError('A snapshot source is required');
|
||||||
@@ -29,43 +27,37 @@ export function createLifecycle({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the layer: create the billboard collection, reset all state,
|
* Initialize the layer: create the engine's aircraft renderer (fleet
|
||||||
* and install the click-to-track handler.
|
* sprites, models), reset all state, and join the engine's selection
|
||||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
|
* dispatcher.
|
||||||
|
* @param {object|null} viewer - The Cesium viewer (null on an engine
|
||||||
|
* without one).
|
||||||
|
* @param {{ engine?: object }} [context] - LayerLifecycle's context.
|
||||||
*/
|
*/
|
||||||
init(viewer) {
|
init(viewer, { engine = null } = {}) {
|
||||||
if (flightState._viewer)
|
if (flightState._renderer)
|
||||||
throw new Error('Flight layer is already initialized');
|
throw new Error('Flight layer is already initialized');
|
||||||
if (typeof flightState.feed._source?.getSnapshot !== 'function')
|
if (typeof flightState.feed._source?.getSnapshot !== 'function')
|
||||||
throw new TypeError('A snapshot source is required');
|
throw new TypeError('A snapshot source is required');
|
||||||
if (flightState.lifetime.signal.aborted)
|
if (flightState.lifetime.signal.aborted)
|
||||||
flightState.lifetime = new AbortController();
|
flightState.lifetime = new AbortController();
|
||||||
clearFocusTarget('militaryFlights');
|
clearFocusTarget('militaryFlights');
|
||||||
flightState._viewer = viewer;
|
const renderer = services.renderer.create({ viewer, engine });
|
||||||
flightState._billboardCollection = new Cesium.BillboardCollection();
|
flightState._renderer = renderer;
|
||||||
viewer.scene.primitives.add(flightState._billboardCollection);
|
renderer.fleet.attach();
|
||||||
registerSpriteCollection('military', flightState._billboardCollection);
|
|
||||||
flightState._modelCollection = new Cesium.PrimitiveCollection();
|
|
||||||
viewer.scene.primitives.add(flightState._modelCollection);
|
|
||||||
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
// Warm the glTF cache so the tracked plane's model instantiates instantly when first needed
|
||||||
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
// (keeps the retained instance referenced; never rendered). Captured against this epoch so a
|
||||||
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
// destroy/re-init mid-load doesn't flip the flag for a torn-down lifecycle.
|
||||||
if (!flightState._preloadModel) {
|
if (!flightState._preloadModel) {
|
||||||
const epoch = flightState._modelEpoch;
|
const epoch = flightState._modelEpoch;
|
||||||
Cesium.Model.fromGltfAsync({
|
renderer.models
|
||||||
url: resolveAsset(JET_MODEL_URL),
|
.preload(resolveAsset(JET_MODEL_URL))
|
||||||
asynchronous: false,
|
|
||||||
})
|
|
||||||
.then((m) => {
|
.then((m) => {
|
||||||
if (epoch === flightState._modelEpoch) {
|
if (epoch === flightState._modelEpoch) {
|
||||||
flightState._preloadModel = m;
|
flightState._preloadModel = m;
|
||||||
flightState._planeModelLoaded = true;
|
flightState._planeModelLoaded = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
renderer.models.destroy(m);
|
||||||
m.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -101,23 +93,21 @@ export function createLifecycle({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.tracking._installClickHandler(viewer);
|
parts.tracking._installClickHandler();
|
||||||
|
|
||||||
restoreSpriteOrder(viewer);
|
renderer.fleet.restoreOrder();
|
||||||
|
|
||||||
console.log('[Data:Military] Initialized with billboard icons');
|
console.log('[Data:Military] Initialized with billboard icons');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the layer and re-install the click handler.
|
* Show the layer and re-join the selection dispatcher.
|
||||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
|
|
||||||
*/
|
*/
|
||||||
enable(viewer) {
|
enable() {
|
||||||
if (flightState._billboardCollection)
|
const renderer = flightState._renderer;
|
||||||
flightState._billboardCollection.show = true;
|
renderer?.fleet.setVisible(true);
|
||||||
holdContinuousRender('military'); // per-frame animator (perf wave 2)
|
holdContinuousRender('military'); // per-frame animator (perf wave 2)
|
||||||
if (flightState._modelCollection)
|
renderer?.models.setVisible(true);
|
||||||
flightState._modelCollection.show = true;
|
|
||||||
parts.tracking._setCockpitContactMode(
|
parts.tracking._setCockpitContactMode(
|
||||||
document.body.classList.contains('cockpit-mode'),
|
document.body.classList.contains('cockpit-mode'),
|
||||||
);
|
);
|
||||||
@@ -134,7 +124,7 @@ export function createLifecycle({
|
|||||||
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
|
/* geoid grid failed to load — baro path stays un-geoid-corrected until retried */
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
parts.tracking._installClickHandler(viewer);
|
parts.tracking._installClickHandler();
|
||||||
registerPickOwner('military', (pickedId) =>
|
registerPickOwner('military', (pickedId) =>
|
||||||
flightState._billboards.has(pickedId),
|
flightState._billboards.has(pickedId),
|
||||||
);
|
);
|
||||||
@@ -142,60 +132,48 @@ export function createLifecycle({
|
|||||||
setMilitaryLayerActive(true);
|
setMilitaryLayerActive(true);
|
||||||
// Force a fresh rotation pass on the first tick after re-enable
|
// Force a fresh rotation pass on the first tick after re-enable
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._lastCamPoseSig = '';
|
||||||
if (!flightState._preRenderRemove && viewer?.scene) {
|
if (!flightState._preRenderRemove && renderer) {
|
||||||
flightState._preRenderRemove = viewer.scene.preRender.addEventListener(
|
flightState._preRenderRemove = renderer.view.on(
|
||||||
|
'preRender',
|
||||||
parts.rendering._fleetTick,
|
parts.rendering._fleetTick,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!flightState._trackedModelPreUpdateRemove && viewer?.scene) {
|
if (!flightState._trackedModelPreUpdateRemove && renderer) {
|
||||||
flightState._trackedModelPreUpdateRemove =
|
flightState._trackedModelPreUpdateRemove = renderer.view.on(
|
||||||
viewer.scene.preUpdate.addEventListener(
|
'preUpdate',
|
||||||
parts.rendering._updateTrackedModel,
|
parts.rendering._updateTrackedModel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!flightState._moveEndRemove && viewer?.camera) {
|
if (!flightState._moveEndRemove && renderer) {
|
||||||
// Arrival polish (mirror of flights.js — see the comment there): a settled
|
// Arrival polish (mirror of flights.js — see the comment there): a settled
|
||||||
// camera move forces a full rotation pass on the very next frame, because
|
// camera move forces a full rotation pass on the very next frame, because
|
||||||
// the pose-signature gate can eat the settle (final easing frames land
|
// the pose-signature gate can eat the settle (final easing frames land
|
||||||
// inside one quantization bucket) and leave stale noses for up to
|
// inside one quantization bucket) and leave stale noses for up to
|
||||||
// ROTATION_REFRESH_MS. One extra pass per gesture — nothing per-frame.
|
// ROTATION_REFRESH_MS. One extra pass per gesture — nothing per-frame.
|
||||||
flightState._moveEndRemove = viewer.camera.moveEnd.addEventListener(
|
flightState._moveEndRemove = renderer.view.on('moveEnd', () => {
|
||||||
() => {
|
flightState._lastCamPoseSig = '';
|
||||||
flightState._lastCamPoseSig = '';
|
flightState._lastFleetTickMs = 0;
|
||||||
flightState._lastFleetTickMs = 0;
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
restoreSpriteOrder(viewer);
|
renderer?.fleet.restoreOrder();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the layer, clear any active tracking, and remove input handlers
|
* Hide the layer, clear any active tracking, and leave the selection
|
||||||
* so clicks do not get intercepted while the layer is off.
|
* dispatcher so clicks do not get intercepted while the layer is off.
|
||||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
|
|
||||||
*/
|
*/
|
||||||
disable(viewer) {
|
disable() {
|
||||||
parts.controller._abortActiveUpdates();
|
parts.controller._abortActiveUpdates();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
if (flightState._billboardCollection)
|
flightState._renderer?.fleet.setVisible(false);
|
||||||
flightState._billboardCollection.show = false;
|
|
||||||
releaseContinuousRender('military');
|
releaseContinuousRender('military');
|
||||||
parts.rendering._releaseModels();
|
parts.rendering._releaseModels();
|
||||||
if (flightState._modelCollection)
|
flightState._renderer?.models.setVisible(false);
|
||||||
flightState._modelCollection.show = false;
|
|
||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
parts.tracking._destroyTrail();
|
parts.tracking._destroyTrail();
|
||||||
// Remove click handler + keydown listener while disabled to avoid
|
// Leave the selection dispatcher (clicks, Escape) while disabled to
|
||||||
// intercepting input when the layer is off
|
// avoid intercepting input when the layer is off
|
||||||
if (flightState._clickHandler) {
|
parts.tracking._removeClickHandler();
|
||||||
flightState._clickHandler.destroy();
|
|
||||||
flightState._clickHandler = null;
|
|
||||||
}
|
|
||||||
if (flightState._trackedEntityChangedRemove) {
|
|
||||||
flightState._trackedEntityChangedRemove();
|
|
||||||
flightState._trackedEntityChangedRemove = null;
|
|
||||||
}
|
|
||||||
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
|
||||||
unregisterPickOwner('military');
|
unregisterPickOwner('military');
|
||||||
// Flights layer takes over rendering known-military aircraft (amber)
|
// Flights layer takes over rendering known-military aircraft (amber)
|
||||||
setMilitaryLayerActive(false);
|
setMilitaryLayerActive(false);
|
||||||
@@ -214,9 +192,9 @@ export function createLifecycle({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tear down the layer: clear tracking, remove handlers, remove the billboard
|
* Tear down the layer: clear tracking, remove handlers, remove its
|
||||||
* collection from the scene, and release all state.
|
* drawings, and release all state.
|
||||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
|
* @param {object|null} viewer - The viewer the layer was drawn on.
|
||||||
*/
|
*/
|
||||||
destroy(viewer) {
|
destroy(viewer) {
|
||||||
flightState.lifetime.abort();
|
flightState.lifetime.abort();
|
||||||
@@ -225,15 +203,7 @@ export function createLifecycle({
|
|||||||
parts.tracking._clearTracking();
|
parts.tracking._clearTracking();
|
||||||
parts.tracking._destroyTrail();
|
parts.tracking._destroyTrail();
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
if (flightState._clickHandler) {
|
parts.tracking._removeClickHandler();
|
||||||
flightState._clickHandler.destroy();
|
|
||||||
flightState._clickHandler = null;
|
|
||||||
}
|
|
||||||
if (flightState._trackedEntityChangedRemove) {
|
|
||||||
flightState._trackedEntityChangedRemove();
|
|
||||||
flightState._trackedEntityChangedRemove = null;
|
|
||||||
}
|
|
||||||
document.removeEventListener('keydown', parts.tracking._onKeyDown);
|
|
||||||
if (flightState._cockpitModeListener) {
|
if (flightState._cockpitModeListener) {
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
'gev:cockpit-mode-changed',
|
'gev:cockpit-mode-changed',
|
||||||
@@ -255,23 +225,13 @@ export function createLifecycle({
|
|||||||
flightState._moveEndRemove = null;
|
flightState._moveEndRemove = null;
|
||||||
}
|
}
|
||||||
parts.rendering._releaseModels();
|
parts.rendering._releaseModels();
|
||||||
if (flightState._billboardCollection) {
|
// Removing the model collection destroys it and its models.
|
||||||
viewer.scene.primitives.remove(flightState._billboardCollection);
|
flightState._renderer?.fleet.detach(viewer);
|
||||||
flightState._billboardCollection = null;
|
|
||||||
}
|
|
||||||
if (flightState._modelCollection) {
|
|
||||||
viewer.scene.primitives.remove(flightState._modelCollection); // removing destroys it + its models
|
|
||||||
flightState._modelCollection = null;
|
|
||||||
}
|
|
||||||
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
flightState._modelEpoch += 1; // invalidate any in-flight load from this lifecycle (settles post-destroy)
|
||||||
flightState._modelPending.clear();
|
flightState._modelPending.clear();
|
||||||
flightState._modelGen.clear();
|
flightState._modelGen.clear();
|
||||||
if (flightState._preloadModel) {
|
if (flightState._preloadModel) {
|
||||||
try {
|
flightState._renderer?.models.destroy(flightState._preloadModel);
|
||||||
flightState._preloadModel.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
flightState._preloadModel = null;
|
flightState._preloadModel = null;
|
||||||
}
|
}
|
||||||
flightState._planeModelLoaded = false;
|
flightState._planeModelLoaded = false;
|
||||||
@@ -296,7 +256,8 @@ export function createLifecycle({
|
|||||||
source: flightState.feed._lastSource,
|
source: flightState.feed._lastSource,
|
||||||
};
|
};
|
||||||
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
parts.tracking._resetTrackedSelectionState(); // next lifecycle re-evaluates against the ENTER ceiling
|
||||||
flightState._viewer = null;
|
flightState._renderer?.destroy();
|
||||||
|
flightState._renderer = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -203,8 +203,9 @@ export function createMotion({
|
|||||||
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
/** Resolve the selected aircraft's actual rendered square extent this frame. */
|
||||||
|
|
||||||
function _trackedFocusSizePx(icao24, position) {
|
function _trackedFocusSizePx(icao24, position) {
|
||||||
const camera = flightState._viewer?.camera;
|
const view = flightState._renderer?.view;
|
||||||
const scene = flightState._viewer?.scene;
|
const camera = view?.camera;
|
||||||
|
const scene = view?.scene;
|
||||||
if (!camera?.positionWC || !position || !scene) return 28;
|
if (!camera?.positionWC || !position || !scene) return 28;
|
||||||
const rangeM = geo.cartesian3.distance(camera.positionWC, position);
|
const rangeM = geo.cartesian3.distance(camera.positionWC, position);
|
||||||
if (parts.rendering._modelOwnsVisual(icao24)) {
|
if (parts.rendering._modelOwnsVisual(icao24)) {
|
||||||
@@ -215,12 +216,12 @@ export function createMotion({
|
|||||||
baseScale: spec.scale,
|
baseScale: spec.scale,
|
||||||
nativeRadiusM: spec.nativeRadiusM,
|
nativeRadiusM: spec.nativeRadiusM,
|
||||||
rangeM,
|
rangeM,
|
||||||
viewportHeightPx: scene.canvas.clientHeight,
|
viewportHeightPx: view.viewportHeightPx(),
|
||||||
fovyRad: camera.frustum.fovy,
|
fovyRad: camera.frustum.fovy,
|
||||||
maximumPixelSize: TRACKED_MODEL_MAX_PX,
|
maximumPixelSize: TRACKED_MODEL_MAX_PX,
|
||||||
});
|
});
|
||||||
const focalLengthPx =
|
const focalLengthPx =
|
||||||
scene.canvas.clientHeight / (2 * Math.tan(camera.frustum.fovy / 2));
|
view.viewportHeightPx() / (2 * Math.tan(camera.frustum.fovy / 2));
|
||||||
const projectedDiameterPx =
|
const projectedDiameterPx =
|
||||||
(2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM;
|
(2 * spec.nativeRadiusM * scale * focalLengthPx) / rangeM;
|
||||||
return Math.max(
|
return Math.max(
|
||||||
@@ -229,17 +230,17 @@ export function createMotion({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const billboard = flightState._trackedEntity?.billboard;
|
const billboard = flightState._renderer.tracked.billboardSize(
|
||||||
const time = flightState._viewer.clock.currentTime;
|
flightState._trackedEntity,
|
||||||
const width = billboard?.width?.getValue(time) ?? 28;
|
);
|
||||||
const height = billboard?.height?.getValue(time) ?? 28;
|
const width = billboard?.width ?? 28;
|
||||||
|
const height = billboard?.height ?? 28;
|
||||||
const scale =
|
const scale =
|
||||||
billboard?.scale?.getValue(time) ??
|
billboard?.scale ??
|
||||||
BILLBOARD_SCALE *
|
BILLBOARD_SCALE *
|
||||||
(CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1);
|
(CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1);
|
||||||
const scaleByDistance =
|
const scaleByDistance =
|
||||||
billboard?.scaleByDistance?.getValue(time) ??
|
billboard?.scaleByDistance ?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
|
||||||
TRACKED_BILLBOARD_SCALE_BY_DISTANCE;
|
|
||||||
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
|
const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM);
|
||||||
return Math.max(width, height) * scale * distanceScale;
|
return Math.max(width, height) * scale * distanceScale;
|
||||||
}
|
}
|
||||||
@@ -254,7 +255,7 @@ export function createMotion({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _trackedDisplayPosition(icao24) {
|
function _trackedDisplayPosition(icao24) {
|
||||||
const frame = flightState._viewer?.scene?.frameState?.frameNumber ?? -1;
|
const frame = flightState._renderer?.view.frameNumber() ?? -1;
|
||||||
if (
|
if (
|
||||||
frame === flightState._cachedDRFrame &&
|
frame === flightState._cachedDRFrame &&
|
||||||
icao24 === flightState._drReconcileIcao
|
icao24 === flightState._drReconcileIcao
|
||||||
@@ -324,8 +325,8 @@ export function createMotion({
|
|||||||
publishFocusTargetFromCachedPosition({
|
publishFocusTargetFromCachedPosition({
|
||||||
ownerLayer: 'militaryFlights',
|
ownerLayer: 'militaryFlights',
|
||||||
id: icao24,
|
id: icao24,
|
||||||
scene: flightState._viewer?.scene,
|
scene: flightState._renderer?.view.scene,
|
||||||
camera: flightState._viewer?.camera,
|
camera: flightState._renderer?.view.camera,
|
||||||
displayPosition: flightState._cachedDRPosition,
|
displayPosition: flightState._cachedDRPosition,
|
||||||
widthPx: focusSizePx,
|
widthPx: focusSizePx,
|
||||||
heightPx: focusSizePx,
|
heightPx: focusSizePx,
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { createFlightState } from './state.js';
|
|||||||
function services() {
|
function services() {
|
||||||
const names = [
|
const names = [
|
||||||
'picking',
|
'picking',
|
||||||
'sprites',
|
'selection',
|
||||||
'trails',
|
'renderer',
|
||||||
'aircraftPresentation',
|
'aircraftPresentation',
|
||||||
'camera',
|
'camera',
|
||||||
'militaryRegistry',
|
'militaryRegistry',
|
||||||
@@ -119,18 +119,19 @@ test('a normalized source can retain its stale reason without changing standalon
|
|||||||
async function firstPlacement(appClockLagMs) {
|
async function firstPlacement(appClockLagMs) {
|
||||||
const Cesium = await import('cesium');
|
const Cesium = await import('cesium');
|
||||||
const { RENDER_DELAY_SEC } = await import('./policy.js');
|
const { RENDER_DELAY_SEC } = await import('./policy.js');
|
||||||
|
const { createMilitaryCesiumRenderer } = await import('./render/cesium.js');
|
||||||
// The application's own scene services, as src/app/layers/militaryFlights.js
|
// The application's own scene services, as src/app/layers/militaryFlights.js
|
||||||
// composes them; only the surface samplers and registry are stubbed.
|
// composes them; only the surface samplers and registry are stubbed.
|
||||||
const supplied = {
|
const supplied = {
|
||||||
picking: await import('../../data/pickRegistry.js'),
|
picking: await import('../../data/pickRegistry.js'),
|
||||||
sprites: await import('../../data/spriteOrder.js'),
|
selection: await import('../../data/selectionDispatcher.js'),
|
||||||
trails: await import('../../data/trailRenderer.js'),
|
// The layer's Cesium renderer, as the app creates it without an engine.
|
||||||
aircraftPresentation: await import('../../data/tr3bRegistry.js'),
|
renderer: {
|
||||||
camera: {
|
create: ({ viewer, adopt }) =>
|
||||||
...(await import('../../data/trackedCamera.js')),
|
createMilitaryCesiumRenderer(null, { viewer, adopt }),
|
||||||
applyTrackedCameraFrame: (await import('../../globe/cesium/follow.js'))
|
|
||||||
.applyTrackedCameraFrame,
|
|
||||||
},
|
},
|
||||||
|
aircraftPresentation: await import('../../data/tr3bRegistry.js'),
|
||||||
|
camera: await import('../../data/trackedCamera.js'),
|
||||||
labels: await import('../../data/detectionDraw.js'),
|
labels: await import('../../data/detectionDraw.js'),
|
||||||
geoid: await import('../../data/geoid.js'),
|
geoid: await import('../../data/geoid.js'),
|
||||||
focus: await import('../../data/focusDeemphasis.js'),
|
focus: await import('../../data/focusDeemphasis.js'),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export {
|
|||||||
MISSING_POLL_LIMIT,
|
MISSING_POLL_LIMIT,
|
||||||
ERROR_BACKOFF_INTERVAL,
|
ERROR_BACKOFF_INTERVAL,
|
||||||
} from './recordPolicy.js';
|
} from './recordPolicy.js';
|
||||||
import * as Cesium from 'cesium';
|
import { constantRgba, nearFarScale, rgbaFromCss } from '../shared/color.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module militaryFlights
|
* @module militaryFlights
|
||||||
@@ -32,13 +32,13 @@ import * as Cesium from 'cesium';
|
|||||||
|
|
||||||
export const BILLBOARD_SCALE = 0.7;
|
export const BILLBOARD_SCALE = 0.7;
|
||||||
|
|
||||||
/** @constant {Cesium.Color} Default amber tint for untracked military billboards */
|
/** @constant {import('../shared/color.js').Rgba} Default amber tint for untracked military billboards */
|
||||||
|
|
||||||
export const MIL_ICON_COLOR = Cesium.Color.fromCssColorString('#FFB800');
|
export const MIL_ICON_COLOR = Object.freeze(rgbaFromCss('#FFB800'));
|
||||||
|
|
||||||
/** @constant {Cesium.Color} Lighter amber tint applied to the actively tracked aircraft */
|
/** @constant {import('../shared/color.js').Rgba} Lighter amber tint applied to the actively tracked aircraft */
|
||||||
|
|
||||||
export const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166');
|
export const TRACKED_ICON_COLOR = Object.freeze(rgbaFromCss('#FFD166'));
|
||||||
|
|
||||||
// --- Ground traffic (owner reversal 2026-07-03; mirror of flights.js) ---------------
|
// --- Ground traffic (owner reversal 2026-07-03; mirror of flights.js) ---------------
|
||||||
// adsb.lol/readsb flags ground traffic with alt_baro === "ground" (no separate
|
// adsb.lol/readsb flags ground traffic with alt_baro === "ground" (no separate
|
||||||
@@ -149,15 +149,17 @@ export const MODEL_BELLY_OFFSET_NATIVE = 5.631;
|
|||||||
|
|
||||||
/** Amber fade target for the tracked billboard once the model takes over (mirrors flights.js CYAN_TRANSPARENT). */
|
/** Amber fade target for the tracked billboard once the model takes over (mirrors flights.js CYAN_TRANSPARENT). */
|
||||||
|
|
||||||
export const AMBER_TRANSPARENT = MIL_ICON_COLOR.withAlpha(0);
|
export const AMBER_TRANSPARENT = constantRgba(
|
||||||
|
MIL_ICON_COLOR.red,
|
||||||
|
MIL_ICON_COLOR.green,
|
||||||
|
MIL_ICON_COLOR.blue,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
export const COCKPIT_CONTACT_SIZE_PX = 6;
|
export const COCKPIT_CONTACT_SIZE_PX = 6;
|
||||||
|
|
||||||
export const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar(
|
export const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = Object.freeze(
|
||||||
1000,
|
nearFarScale(1000, 3.0, 8000000, 0.5),
|
||||||
3.0,
|
|
||||||
8000000,
|
|
||||||
0.5,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export function createQueries({
|
|||||||
}) {
|
}) {
|
||||||
const { tr3bAircraftClass, tr3bTypeLabel } = services.aircraftPresentation;
|
const { tr3bAircraftClass, tr3bTypeLabel } = services.aircraftPresentation;
|
||||||
const { formatFlightLevel } = services.labels;
|
const { formatFlightLevel } = services.labels;
|
||||||
const { applyTrackedCameraFrame } = services.camera;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coerce a value to a finite number, returning null if not possible.
|
* Coerce a value to a finite number, returning null if not possible.
|
||||||
@@ -241,7 +240,7 @@ export function createQueries({
|
|||||||
parts.tracking._syncTracked2dRotation();
|
parts.tracking._syncTracked2dRotation();
|
||||||
// Restore fleet billboards (horizon-cull re-asserts next tick), but NEVER the tracked
|
// Restore fleet billboards (horizon-cull re-asserts next tick), but NEVER the tracked
|
||||||
// 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._renderer?.fleet.attached())
|
||||||
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) bb.show = true;
|
||||||
}
|
}
|
||||||
@@ -324,12 +323,7 @@ export function createQueries({
|
|||||||
* @returns {Array<Object>} Sorted array of nearby aircraft descriptors
|
* @returns {Array<Object>} Sorted array of nearby aircraft descriptors
|
||||||
*/
|
*/
|
||||||
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
|
getNearby(center, range, maxCount = 50, { includeHidden = false } = {}) {
|
||||||
if (
|
if (!center || !flightState._renderer?.fleet.visible()) return [];
|
||||||
!center ||
|
|
||||||
!flightState._billboardCollection ||
|
|
||||||
!flightState._billboardCollection.show
|
|
||||||
)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
const limit = Number.isFinite(maxCount)
|
const limit = Number.isFinite(maxCount)
|
||||||
? Math.max(1, Math.floor(maxCount))
|
? Math.max(1, Math.floor(maxCount))
|
||||||
@@ -401,11 +395,7 @@ export function createQueries({
|
|||||||
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
* @returns {Array<{position: Cesium.Cartesian3, id: string, type: string, skipLabel: boolean}>}
|
||||||
*/
|
*/
|
||||||
getDetectableObjects(options = {}) {
|
getDetectableObjects(options = {}) {
|
||||||
if (
|
if (!flightState._renderer?.fleet.visible()) return [];
|
||||||
!flightState._billboardCollection ||
|
|
||||||
!flightState._billboardCollection.show
|
|
||||||
)
|
|
||||||
return [];
|
|
||||||
const maxCount = Number.isFinite(options.maxCount)
|
const maxCount = Number.isFinite(options.maxCount)
|
||||||
? Math.max(1, Math.floor(options.maxCount))
|
? Math.max(1, Math.floor(options.maxCount))
|
||||||
: flightState._billboards.size;
|
: flightState._billboards.size;
|
||||||
@@ -562,8 +552,7 @@ export function createQueries({
|
|||||||
*/
|
*/
|
||||||
hasContact(icao24) {
|
hasContact(icao24) {
|
||||||
if (
|
if (
|
||||||
!flightState._billboardCollection ||
|
!flightState._renderer?.fleet.visible() ||
|
||||||
!flightState._billboardCollection.show ||
|
|
||||||
flightState._billboards.size === 0
|
flightState._billboards.size === 0
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
@@ -577,7 +566,7 @@ export function createQueries({
|
|||||||
|
|
||||||
getAllPositions(maxCount = 500) {
|
getAllPositions(maxCount = 500) {
|
||||||
if (
|
if (
|
||||||
!flightState._billboardCollection ||
|
!flightState._renderer?.fleet.attached() ||
|
||||||
flightState._billboards.size === 0
|
flightState._billboards.size === 0
|
||||||
)
|
)
|
||||||
return [];
|
return [];
|
||||||
@@ -624,8 +613,7 @@ export function createQueries({
|
|||||||
*/
|
*/
|
||||||
getAnalystRecords(maxCount = 2000) {
|
getAnalystRecords(maxCount = 2000) {
|
||||||
if (
|
if (
|
||||||
!flightState._billboardCollection ||
|
!flightState._renderer?.fleet.visible() ||
|
||||||
!flightState._billboardCollection.show ||
|
|
||||||
flightState.records.data.size === 0
|
flightState.records.data.size === 0
|
||||||
)
|
)
|
||||||
return [];
|
return [];
|
||||||
@@ -713,7 +701,7 @@ export function createQueries({
|
|||||||
if (
|
if (
|
||||||
!icao24 ||
|
!icao24 ||
|
||||||
flightState._cockpitContactMode ||
|
flightState._cockpitContactMode ||
|
||||||
!flightState._viewer ||
|
!flightState._renderer ||
|
||||||
!flightState._trackedEntity
|
!flightState._trackedEntity
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
@@ -721,19 +709,13 @@ export function createQueries({
|
|||||||
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
if (!flightState._billboards.has(id)) id = id.toLowerCase();
|
||||||
if (
|
if (
|
||||||
id !== flightState._trackedIcao ||
|
id !== flightState._trackedIcao ||
|
||||||
!flightState._viewer.entities?.contains?.(flightState._trackedEntity) ||
|
!flightState._renderer.tracked.isLive(flightState._trackedEntity) ||
|
||||||
flightState._viewer.trackedEntity !== flightState._trackedEntity
|
!flightState._renderer.follow.isFollowing(flightState._trackedEntity)
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
flightState._trackedCameraFrameStop?.();
|
flightState._trackedCameraFrameStop?.();
|
||||||
flightState._viewer.camera.cancelFlight();
|
|
||||||
flightState._viewer.trackedEntity = flightState._trackedEntity;
|
|
||||||
flightState._trackedCameraFrameStop =
|
flightState._trackedCameraFrameStop =
|
||||||
applyTrackedCameraFrame(
|
flightState._renderer.follow.refocus(flightState._trackedEntity);
|
||||||
flightState._viewer,
|
|
||||||
flightState._trackedEntity,
|
|
||||||
flightState._trackedEntity.viewFrom,
|
|
||||||
) || null;
|
|
||||||
parts.tracking._publishTrackedSelection(id, origin);
|
parts.tracking._publishTrackedSelection(id, origin);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// The military flights layer's renderer on Cesium: the shared aircraft
|
||||||
|
// renderer (src/layers/aircraft/render/cesium.js) with the military's
|
||||||
|
// sprite-order slot and jet.glb's nose offset (the per-class specs pass
|
||||||
|
// their own offset to setPose). The military's presentation choices (amber
|
||||||
|
// #FFB800 fleet, #FFD166 tracked, the IR boost as an unlit white model) are
|
||||||
|
// the layer's logic and reach the renderer as plain data.
|
||||||
|
|
||||||
|
import { createAircraftCesiumRenderer } from '../../aircraft/render/cesium.js';
|
||||||
|
import { MODEL_HEADING_OFFSET_DEG } from '../policy.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry factory (src/app/layers/renderers.js): `(engine, options)`.
|
||||||
|
* @param {object|null} engine The Cesium globe engine (null in unit tests).
|
||||||
|
* @param {{ viewer: object, adopt?: object }} options
|
||||||
|
*/
|
||||||
|
export function createMilitaryCesiumRenderer(engine, { viewer, adopt } = {}) {
|
||||||
|
return createAircraftCesiumRenderer({
|
||||||
|
viewer,
|
||||||
|
engine,
|
||||||
|
spriteLayer: 'military',
|
||||||
|
headingOffsetDeg: MODEL_HEADING_OFFSET_DEG,
|
||||||
|
adopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// The military logic draws only through its renderer (phase 3 Task 11a): with
|
||||||
|
// a renderer that is plain data (no Cesium), the layer builds its amber
|
||||||
|
// fleet, selects a contact on a click through the engine's selection
|
||||||
|
// dispatcher, tracks and follows it, and releases it on an empty click or
|
||||||
|
// Escape. This is the seam the Navara renderer (Task 11b) plugs into.
|
||||||
|
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
|
||||||
|
import { createApplicationMilitary } from '../../app/layers/militaryFlights.js';
|
||||||
|
import { defaultSurface } from '../../data/surfaceServices.js';
|
||||||
|
import * as militaryRegistry from '../../data/militaryRegistry.js';
|
||||||
|
import { missingAircraftRendererMembers } from '../aircraft/render/contract.js';
|
||||||
|
import { Rgba } from '../shared/color.js';
|
||||||
|
import { MIL_ICON_COLOR, TRACKED_ICON_COLOR } from './policy.js';
|
||||||
|
import { recordingAircraftRenderer } from '../../testSupport/aircraftRecordingRenderer.mjs';
|
||||||
|
|
||||||
|
const keyListeners = new Set();
|
||||||
|
globalThis.window ||= {};
|
||||||
|
globalThis.window.dispatchEvent ||= () => true;
|
||||||
|
globalThis.window.addEventListener ||= () => {};
|
||||||
|
globalThis.window.removeEventListener ||= () => {};
|
||||||
|
globalThis.CustomEvent ||= class CustomEvent extends Event {
|
||||||
|
constructor(type, init) {
|
||||||
|
super(type);
|
||||||
|
this.detail = init?.detail;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
globalThis.document ||= {
|
||||||
|
body: { classList: { contains: () => false } },
|
||||||
|
addEventListener(type, listener) {
|
||||||
|
if (type === 'keydown') keyListeners.add(listener);
|
||||||
|
},
|
||||||
|
removeEventListener(type, listener) {
|
||||||
|
if (type === 'keydown') keyListeners.delete(listener);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A military snapshot (the source's record shape) of `ids`, now. */
|
||||||
|
function militarySnapshotOf(ids, nowMs = Date.now()) {
|
||||||
|
return {
|
||||||
|
source: 'Fixture military',
|
||||||
|
complete: true,
|
||||||
|
observedAtMs: nowMs,
|
||||||
|
records: ids.map((id, index) => ({
|
||||||
|
id,
|
||||||
|
callsign: `RCH${index}`,
|
||||||
|
latitude: 30.3,
|
||||||
|
longitude: -97.6 + index * 0.1,
|
||||||
|
onGround: false,
|
||||||
|
baroAltitudeM: 9_000,
|
||||||
|
courseDeg: 90,
|
||||||
|
speedMps: 200,
|
||||||
|
positionTimeMs: nowMs - 2_000,
|
||||||
|
contactTimeMs: nowMs - 2_000,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const click = (engine) => {
|
||||||
|
engine.fake.input('down', { position: { x: 10, y: 10 } });
|
||||||
|
engine.fake.input('up', { position: { x: 10, y: 10 } });
|
||||||
|
engine.fake.input('click', { position: { x: 10, y: 10 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
test('the military logic draws, selects and follows only through its renderer', async () => {
|
||||||
|
const engine = createFakeEngine();
|
||||||
|
let renderer = null;
|
||||||
|
const layer = createApplicationMilitary({
|
||||||
|
surface: defaultSurface,
|
||||||
|
source: {
|
||||||
|
label: 'Fixture military',
|
||||||
|
getSnapshot: async () => militarySnapshotOf(['ae0001', 'ae0002']),
|
||||||
|
},
|
||||||
|
militaryRegistry,
|
||||||
|
renderer: {
|
||||||
|
create: ({ viewer, engine: given }) => {
|
||||||
|
assert.equal(viewer, null, 'no viewer on an engine without one');
|
||||||
|
renderer = recordingAircraftRenderer(given);
|
||||||
|
return renderer;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
layer.init(null, { engine });
|
||||||
|
assert.deepEqual(missingAircraftRendererMembers(renderer), []);
|
||||||
|
layer.enable();
|
||||||
|
try {
|
||||||
|
await layer.update(null);
|
||||||
|
assert.equal(renderer.sprites.size, 2, 'one sprite per contact');
|
||||||
|
const sprite = [...renderer.sprites].find(({ id }) => id === 'ae0001');
|
||||||
|
assert.ok(sprite.color instanceof Rgba, 'colours are engine-neutral');
|
||||||
|
assert.ok(sprite.color.equals(MIL_ICON_COLOR), 'the fleet is amber');
|
||||||
|
assert.equal(layer.getAllPositions().length, 2);
|
||||||
|
|
||||||
|
// A click on a contact: the engine's pick, through the dispatcher.
|
||||||
|
engine.fake.script('pick', () => ({ id: 'ae0002' }));
|
||||||
|
engine.fake.script('drillPick', () => [{ id: 'ae0002' }]);
|
||||||
|
click(engine);
|
||||||
|
assert.equal(layer.getTrackedInfo()?.icao24, 'ae0002');
|
||||||
|
const created = renderer.calls.filter(
|
||||||
|
([name]) => name.startsWith('tracked') || name.startsWith('follow'),
|
||||||
|
);
|
||||||
|
assert.deepEqual(created, [
|
||||||
|
['tracked.create', 'tracked-1'],
|
||||||
|
['follow.start', 'tracked-1'],
|
||||||
|
]);
|
||||||
|
// The tracked glyph is the lighter amber.
|
||||||
|
const trackedSpec = renderer.follow.current().spec;
|
||||||
|
assert.ok(trackedSpec.color().equals(TRACKED_ICON_COLOR));
|
||||||
|
|
||||||
|
// Clicking the tracked visual itself is a no-op.
|
||||||
|
engine.fake.script('pick', () => ({ id: 'tracked-1' }));
|
||||||
|
engine.fake.script('drillPick', () => [{ id: 'tracked-1' }]);
|
||||||
|
click(engine);
|
||||||
|
assert.equal(layer.getTrackedInfo()?.icao24, 'ae0002');
|
||||||
|
|
||||||
|
// Empty space releases the follow and removes the tracked visual.
|
||||||
|
engine.fake.script('pick', () => undefined);
|
||||||
|
engine.fake.script('drillPick', () => []);
|
||||||
|
click(engine);
|
||||||
|
assert.equal(layer.getTrackedInfo(), null);
|
||||||
|
assert.ok(
|
||||||
|
renderer.calls.some(([name]) => name === 'follow.release') &&
|
||||||
|
renderer.calls.some(
|
||||||
|
([name, id]) => name === 'tracked.remove' && id === 'tracked-1',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Escape releases too.
|
||||||
|
engine.fake.script('pick', () => ({ id: 'ae0001' }));
|
||||||
|
engine.fake.script('drillPick', () => [{ id: 'ae0001' }]);
|
||||||
|
click(engine);
|
||||||
|
assert.equal(layer.getTrackedInfo()?.icao24, 'ae0001');
|
||||||
|
for (const listener of keyListeners) listener({ key: 'Escape' });
|
||||||
|
assert.equal(layer.getTrackedInfo(), null);
|
||||||
|
} finally {
|
||||||
|
layer.disable();
|
||||||
|
layer.destroy(null);
|
||||||
|
}
|
||||||
|
assert.ok(renderer.calls.some(([name]) => name === 'destroy'));
|
||||||
|
assert.equal(keyListeners.size, 0, 'the dispatcher left the keyboard');
|
||||||
|
});
|
||||||
@@ -7,14 +7,14 @@ import {
|
|||||||
visualCenterForModel,
|
visualCenterForModel,
|
||||||
trailAnchorForModel,
|
trailAnchorForModel,
|
||||||
} from '../../data/modelVisualAnchor.js';
|
} from '../../data/modelVisualAnchor.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as geo from '../../geo/index.js';
|
||||||
|
import { WHITE, nearFarScale } from '../shared/color.js';
|
||||||
import { selectModelEligible } from '../../data/modelEligibility.js';
|
import { selectModelEligible } from '../../data/modelEligibility.js';
|
||||||
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
|
import { cyberSonarBaseAlpha } from '../../cyberSonar.js';
|
||||||
import { cockpitContactDotImage } from '../../data/cockpitContactDot.js';
|
import { cockpitContactDotImage } from '../../data/cockpitContactDot.js';
|
||||||
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
||||||
import {
|
import {
|
||||||
cameraPoseSignature,
|
cameraPoseSignature,
|
||||||
horizonOccluder,
|
|
||||||
screenProjectedRotation,
|
screenProjectedRotation,
|
||||||
} from '../../data/iconOrientation.js';
|
} from '../../data/iconOrientation.js';
|
||||||
import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js';
|
import { limitCourseStep, courseSlewCapDps } from '../../data/motionModel.js';
|
||||||
@@ -127,11 +127,11 @@ export function createRendering({
|
|||||||
function _normalBillboardScaleByDistance() {
|
function _normalBillboardScaleByDistance() {
|
||||||
// Match commercial flights and preserve the established close-range 3×
|
// Match commercial flights and preserve the established close-range 3×
|
||||||
// default. Smaller owner-visible scaling must be proposed separately.
|
// default. Smaller owner-visible scaling must be proposed separately.
|
||||||
return new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5);
|
return nearFarScale(1000, 3.0, 8000000, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _cockpitBillboardScaleByDistance() {
|
function _cockpitBillboardScaleByDistance() {
|
||||||
return new Cesium.NearFarScalar(1000, 1.15, 8000000, 0.65);
|
return nearFarScale(1000, 1.15, 8000000, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _militaryBillboardScale(icao24) {
|
function _militaryBillboardScale(icao24) {
|
||||||
@@ -204,7 +204,8 @@ export function createRendering({
|
|||||||
function _modelRegimeActive() {
|
function _modelRegimeActive() {
|
||||||
if (!flightState._models3dEnabled) return false;
|
if (!flightState._models3dEnabled) return false;
|
||||||
const h =
|
const h =
|
||||||
flightState._viewer?.camera?.positionCartographic?.height ?? Infinity;
|
flightState._renderer?.view.camera?.positionCartographic?.height ??
|
||||||
|
Infinity;
|
||||||
return h < MODEL_ALT_CEIL_M;
|
return h < MODEL_ALT_CEIL_M;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,28 +236,23 @@ export function createRendering({
|
|||||||
: MODEL_PROX_KEEP_M;
|
: MODEL_PROX_KEEP_M;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** World model matrix from a position + course heading (pitch/roll 0; ENU frame). Writes into
|
/** World model transform from a position + course heading (pitch/roll 0; ENU frame), written by
|
||||||
* `result` and returns it — pass each model's OWN `.modelMatrix` so models never share one mutable
|
* the renderer into the model's OWN transform — never a shared scratch (see the renderer's
|
||||||
* matrix object (see flights.js for the full rationale: sharing one scratch stacked every model on
|
* setPose and flights.js for the full rationale: sharing one scratch stacked every model on the
|
||||||
* the last-written transform, and the per-frame tracked write made it flicker). */
|
* last-written transform, and the per-frame tracked write made it flicker). `offsetDeg` is the
|
||||||
|
* model's nose offset (its spec's headingOffsetDeg). */
|
||||||
|
|
||||||
function _modelMatrix(
|
function _modelMatrix(
|
||||||
pos,
|
pos,
|
||||||
headingDeg,
|
headingDeg,
|
||||||
result = flightState._scratchModelMtx,
|
model,
|
||||||
offsetDeg = MODEL_HEADING_OFFSET_DEG,
|
offsetDeg = MODEL_HEADING_OFFSET_DEG,
|
||||||
) {
|
) {
|
||||||
flightState._scratchModelHpr.heading = Cesium.Math.toRadians(
|
return flightState._renderer.models.setPose(
|
||||||
(headingDeg || 0) + offsetDeg,
|
model,
|
||||||
);
|
|
||||||
flightState._scratchModelHpr.pitch = 0;
|
|
||||||
flightState._scratchModelHpr.roll = 0;
|
|
||||||
return Cesium.Transforms.headingPitchRollToFixedFrame(
|
|
||||||
pos,
|
pos,
|
||||||
flightState._scratchModelHpr,
|
headingDeg,
|
||||||
Cesium.Ellipsoid.WGS84,
|
offsetDeg,
|
||||||
undefined,
|
|
||||||
result,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,23 +281,23 @@ export function createRendering({
|
|||||||
const meta = flightState.records.data.get(icao24);
|
const meta = flightState.records.data.get(icao24);
|
||||||
if (!meta || !meta.onGround) return pos;
|
if (!meta || !meta.onGround) return pos;
|
||||||
const h = flightState._groundSnap.heightFor(
|
const h = flightState._groundSnap.heightFor(
|
||||||
flightState._viewer,
|
flightState._renderer?.view.surfaceOwner,
|
||||||
icao24,
|
icao24,
|
||||||
pos,
|
pos,
|
||||||
_groundSampleExclusions,
|
_groundSampleExclusions,
|
||||||
);
|
);
|
||||||
if (h == null) return null;
|
if (h == null) return null;
|
||||||
const carto = Cesium.Cartographic.fromCartesian(
|
const carto = geo.cartographic.fromCartesian(
|
||||||
pos,
|
pos,
|
||||||
Cesium.Ellipsoid.WGS84,
|
geo.ellipsoid.WGS84,
|
||||||
flightState._scratchGroundCarto,
|
flightState._scratchGroundCarto,
|
||||||
);
|
);
|
||||||
carto.height = h + _modelSpec(meta.klass).bellyM;
|
carto.height = h + _modelSpec(meta.klass).bellyM;
|
||||||
return Cesium.Cartesian3.fromRadians(
|
return geo.cartesian3.fromRadians(
|
||||||
carto.longitude,
|
carto.longitude,
|
||||||
carto.latitude,
|
carto.latitude,
|
||||||
carto.height,
|
carto.height,
|
||||||
Cesium.Ellipsoid.WGS84,
|
geo.ellipsoid.WGS84,
|
||||||
result,
|
result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -333,7 +329,7 @@ export function createRendering({
|
|||||||
_modelMatrix(
|
_modelMatrix(
|
||||||
displayPos,
|
displayPos,
|
||||||
course,
|
course,
|
||||||
model.modelMatrix,
|
model,
|
||||||
_modelSpec(flightState.records.data.get(icao24)?.klass).headingOffsetDeg,
|
_modelSpec(flightState.records.data.get(icao24)?.klass).headingOffsetDeg,
|
||||||
);
|
);
|
||||||
if (!model.ready) {
|
if (!model.ready) {
|
||||||
@@ -454,21 +450,21 @@ export function createRendering({
|
|||||||
// Boost state likewise — creation options bake it in.
|
// Boost state likewise — creation options bake it in.
|
||||||
const specKey = _specKeyFor(flightState.records.data.get(icao24)?.klass);
|
const specKey = _specKeyFor(flightState.records.data.get(icao24)?.klass);
|
||||||
const loadIrBoost = flightState._irBoost;
|
const loadIrBoost = flightState._irBoost;
|
||||||
|
const renderer = flightState._renderer;
|
||||||
try {
|
try {
|
||||||
const spec = _modelSpec(flightState.records.data.get(icao24)?.klass);
|
const spec = _modelSpec(flightState.records.data.get(icao24)?.klass);
|
||||||
model = await Cesium.Model.fromGltfAsync({
|
model = await renderer.models.loadFleet({
|
||||||
url: resolveAsset(spec.url),
|
url: resolveAsset(spec.url),
|
||||||
asynchronous: false,
|
|
||||||
minimumPixelSize: MODEL_MIN_PX,
|
minimumPixelSize: MODEL_MIN_PX,
|
||||||
scale: spec.scale,
|
scale: spec.scale,
|
||||||
color: flightState._irBoost ? Cesium.Color.WHITE : _modelColor(icao24),
|
// Bounding radius per scale unit (an engine that keeps
|
||||||
colorBlendMode: Cesium.ColorBlendMode.MIX,
|
// minimumPixelSize on the CPU reports the drawn scale with it).
|
||||||
|
radiusM: spec.nativeRadiusM,
|
||||||
|
color: flightState._irBoost ? WHITE : _modelColor(icao24),
|
||||||
// near self-illuminated tint so planes read uniform near AND far; IR boost → flat UNLIT white (hot)
|
// near self-illuminated tint so planes read uniform near AND far; IR boost → flat UNLIT white (hot)
|
||||||
colorBlendAmount: flightState._irBoost ? 1.0 : MODEL_COLOR_BLEND_AMOUNT,
|
blendAmount: flightState._irBoost ? 1.0 : MODEL_COLOR_BLEND_AMOUNT,
|
||||||
customShader: flightState._irBoost
|
unlit: flightState._irBoost,
|
||||||
? flightState._IR_UNLIT_SHADER
|
id: icao24, // so a pick returns the icao for click-to-track
|
||||||
: undefined,
|
|
||||||
id: icao24, // so scene.pick returns the icao for click-to-track
|
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// asset/decode fail — stay billboard. Only touch this lifecycle's state if still current
|
// asset/decode fail — stay billboard. Only touch this lifecycle's state if still current
|
||||||
@@ -482,11 +478,7 @@ export function createRendering({
|
|||||||
// A load from a PREVIOUS lifecycle (destroy→init happened mid-load) must NOT mutate the new
|
// A load from a PREVIOUS lifecycle (destroy→init happened mid-load) must NOT mutate the new
|
||||||
// epoch's _modelPending/_modelGen or add to the new collection — just drop its model.
|
// epoch's _modelPending/_modelGen or add to the new collection — just drop its model.
|
||||||
if (epoch !== flightState._modelEpoch) {
|
if (epoch !== flightState._modelEpoch) {
|
||||||
try {
|
renderer.models.destroy(model);
|
||||||
model.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flightState._modelPending.delete(icao24);
|
flightState._modelPending.delete(icao24);
|
||||||
@@ -498,8 +490,7 @@ export function createRendering({
|
|||||||
const stale =
|
const stale =
|
||||||
(flightState._modelGen.get(icao24) || 0) !== gen ||
|
(flightState._modelGen.get(icao24) || 0) !== gen ||
|
||||||
!_modelRegimeActive() ||
|
!_modelRegimeActive() ||
|
||||||
!flightState._modelCollection ||
|
!flightState._renderer?.models.live() ||
|
||||||
flightState._modelCollection.isDestroyed() ||
|
|
||||||
!flightState.records.data.has(icao24) ||
|
!flightState.records.data.has(icao24) ||
|
||||||
icao24 === flightState._trackedIcao ||
|
icao24 === flightState._trackedIcao ||
|
||||||
flightState._models.has(icao24) ||
|
flightState._models.has(icao24) ||
|
||||||
@@ -509,11 +500,7 @@ export function createRendering({
|
|||||||
// IR boost flipped mid-load → this model baked the wrong shader/tint.
|
// IR boost flipped mid-load → this model baked the wrong shader/tint.
|
||||||
flightState._irBoost !== loadIrBoost;
|
flightState._irBoost !== loadIrBoost;
|
||||||
if (stale) {
|
if (stale) {
|
||||||
try {
|
renderer.models.destroy(model);
|
||||||
model.destroy();
|
|
||||||
} catch {
|
|
||||||
/* already gone */
|
|
||||||
}
|
|
||||||
_cleanupModelGen(icao24); // bound the map
|
_cleanupModelGen(icao24); // bound the map
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -527,7 +514,7 @@ export function createRendering({
|
|||||||
// between admission and the next fleet tick (and draw at the identity matrix,
|
// between admission and the next fleet tick (and draw at the identity matrix,
|
||||||
// i.e. the Earth's centre). The handoff turns it on once it has a matrix.
|
// i.e. the Earth's centre). The handoff turns it on once it has a matrix.
|
||||||
model.show = false;
|
model.show = false;
|
||||||
flightState._modelCollection.add(model);
|
flightState._renderer.models.add(model);
|
||||||
flightState._models.set(icao24, model);
|
flightState._models.set(icao24, model);
|
||||||
flightState._planeModelLoaded = true; // GLB is cached now — the tracked entity's model can fade in its billboard
|
flightState._planeModelLoaded = true; // GLB is cached now — the tracked entity's model can fade in its billboard
|
||||||
}
|
}
|
||||||
@@ -547,16 +534,7 @@ export function createRendering({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (m) {
|
if (m) {
|
||||||
if (
|
flightState._renderer?.models.remove(m);
|
||||||
flightState._modelCollection &&
|
|
||||||
!flightState._modelCollection.isDestroyed()
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
flightState._modelCollection.remove(m);
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flightState._models.delete(icao24);
|
flightState._models.delete(icao24);
|
||||||
}
|
}
|
||||||
// Do NOT clear _modelPending here — the in-flight load's OWN post-await removes it. Keeping
|
// Do NOT clear _modelPending here — the in-flight load's OWN post-await removes it. Keeping
|
||||||
@@ -586,17 +564,9 @@ export function createRendering({
|
|||||||
icao,
|
icao,
|
||||||
(flightState._modelGen.get(icao) || 0) + 1,
|
(flightState._modelGen.get(icao) || 0) + 1,
|
||||||
);
|
);
|
||||||
if (
|
if (flightState._renderer?.models.live()) {
|
||||||
flightState._modelCollection &&
|
for (const m of flightState._models.values())
|
||||||
!flightState._modelCollection.isDestroyed()
|
flightState._renderer.models.remove(m);
|
||||||
) {
|
|
||||||
for (const m of flightState._models.values()) {
|
|
||||||
try {
|
|
||||||
flightState._modelCollection.remove(m);
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
flightState._models.clear();
|
flightState._models.clear();
|
||||||
}
|
}
|
||||||
@@ -607,16 +577,7 @@ export function createRendering({
|
|||||||
flightState._trackedModelGen++;
|
flightState._trackedModelGen++;
|
||||||
flightState._trackedModelLoading = false;
|
flightState._trackedModelLoading = false;
|
||||||
if (flightState._trackedModel) {
|
if (flightState._trackedModel) {
|
||||||
if (
|
flightState._renderer?.models.remove(flightState._trackedModel);
|
||||||
flightState._modelCollection &&
|
|
||||||
!flightState._modelCollection.isDestroyed()
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
flightState._modelCollection.remove(flightState._trackedModel);
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flightState._trackedModel = null;
|
flightState._trackedModel = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,8 +589,7 @@ export function createRendering({
|
|||||||
const active =
|
const active =
|
||||||
flightState._trackedIcao &&
|
flightState._trackedIcao &&
|
||||||
parts.tracking._trackedModelRegimeActive() &&
|
parts.tracking._trackedModelRegimeActive() &&
|
||||||
flightState._modelCollection &&
|
flightState._renderer?.models.live();
|
||||||
!flightState._modelCollection.isDestroyed();
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
if (flightState._trackedModel) flightState._trackedModel.show = false;
|
if (flightState._trackedModel) flightState._trackedModel.show = false;
|
||||||
return;
|
return;
|
||||||
@@ -659,34 +619,28 @@ export function createRendering({
|
|||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
||||||
);
|
);
|
||||||
const trackedIrBoost = flightState._irBoost;
|
const trackedIrBoost = flightState._irBoost;
|
||||||
Cesium.Model.fromGltfAsync({
|
const renderer = flightState._renderer;
|
||||||
url: resolveAsset(trackedSpec.url),
|
renderer.models
|
||||||
asynchronous: false,
|
.loadTracked({
|
||||||
minimumPixelSize: TRACKED_MODEL_MIN_PX,
|
url: resolveAsset(trackedSpec.url),
|
||||||
scale: trackedSpec.scale,
|
minimumPixelSize: TRACKED_MODEL_MIN_PX,
|
||||||
color: flightState._irBoost ? Cesium.Color.WHITE : TRACKED_ICON_COLOR,
|
scale: trackedSpec.scale,
|
||||||
colorBlendMode: Cesium.ColorBlendMode.MIX,
|
radiusM: trackedSpec.nativeRadiusM,
|
||||||
// near self-illuminated tint so planes read uniform near AND far; IR boost → flat UNLIT white (hot)
|
color: flightState._irBoost ? WHITE : TRACKED_ICON_COLOR,
|
||||||
colorBlendAmount: flightState._irBoost ? 1.0 : MODEL_COLOR_BLEND_AMOUNT,
|
// near self-illuminated tint so planes read uniform near AND far; IR boost → flat UNLIT white (hot)
|
||||||
customShader: flightState._irBoost
|
blendAmount: flightState._irBoost ? 1.0 : MODEL_COLOR_BLEND_AMOUNT,
|
||||||
? flightState._IR_UNLIT_SHADER
|
unlit: flightState._irBoost,
|
||||||
: undefined,
|
// Pick id (H1): without it, clicking the very plane being tracked read as
|
||||||
// Pick id (H1): without it, clicking the very plane being tracked read as
|
// EMPTY SPACE (a pick with no id) → an unintended deselect. With the
|
||||||
// EMPTY SPACE (scene.pick → primitive with no id) → an unintended
|
// icao, the click handler recognizes it as ours.
|
||||||
// deselect. With the icao, the click handler recognizes it as ours.
|
id: flightState._trackedIcao,
|
||||||
id: flightState._trackedIcao,
|
})
|
||||||
})
|
|
||||||
.then((m) => {
|
.then((m) => {
|
||||||
if (
|
if (
|
||||||
gen !== flightState._trackedModelGen ||
|
gen !== flightState._trackedModelGen ||
|
||||||
!flightState._modelCollection ||
|
!flightState._renderer?.models.live()
|
||||||
flightState._modelCollection.isDestroyed()
|
|
||||||
) {
|
) {
|
||||||
try {
|
renderer.models.destroy(m);
|
||||||
m.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Class reclassified OR boost flipped mid-load (no release ran —
|
// Class reclassified OR boost flipped mid-load (no release ran —
|
||||||
@@ -697,11 +651,7 @@ export function createRendering({
|
|||||||
) !== trackedKey ||
|
) !== trackedKey ||
|
||||||
flightState._irBoost !== trackedIrBoost
|
flightState._irBoost !== trackedIrBoost
|
||||||
) {
|
) {
|
||||||
try {
|
renderer.models.destroy(m);
|
||||||
m.destroy();
|
|
||||||
} catch {
|
|
||||||
/* gone */
|
|
||||||
}
|
|
||||||
flightState._trackedModelLoading = false;
|
flightState._trackedModelLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -726,7 +676,7 @@ export function createRendering({
|
|||||||
_modelMatrix(
|
_modelMatrix(
|
||||||
displayPos,
|
displayPos,
|
||||||
parts.motion._trackedDisplayCourse(),
|
parts.motion._trackedDisplayCourse(),
|
||||||
m.modelMatrix,
|
m,
|
||||||
trackedSpec.headingOffsetDeg,
|
trackedSpec.headingOffsetDeg,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -738,7 +688,7 @@ export function createRendering({
|
|||||||
flightState._trackedModelFailIcao = null;
|
flightState._trackedModelFailIcao = null;
|
||||||
flightState._trackedModelFailCount = 0;
|
flightState._trackedModelFailCount = 0;
|
||||||
flightState._trackedModelRetryAtMs = 0;
|
flightState._trackedModelRetryAtMs = 0;
|
||||||
flightState._modelCollection.add(m);
|
flightState._renderer.models.add(m);
|
||||||
flightState._planeModelLoaded = true;
|
flightState._planeModelLoaded = true;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -767,19 +717,17 @@ export function createRendering({
|
|||||||
_modelMatrix(
|
_modelMatrix(
|
||||||
displayPos,
|
displayPos,
|
||||||
parts.motion._trackedDisplayCourse(),
|
parts.motion._trackedDisplayCourse(),
|
||||||
flightState._trackedModel.modelMatrix,
|
flightState._trackedModel,
|
||||||
spec.headingOffsetDeg,
|
spec.headingOffsetDeg,
|
||||||
);
|
);
|
||||||
if (!flightState._trackedModel.ready) return;
|
if (!flightState._trackedModel.ready) return;
|
||||||
|
const view = flightState._renderer.view;
|
||||||
flightState._trackedModel.scale = trackedModelScaleForPixelCap({
|
flightState._trackedModel.scale = trackedModelScaleForPixelCap({
|
||||||
baseScale: spec.scale,
|
baseScale: spec.scale,
|
||||||
nativeRadiusM: spec.nativeRadiusM,
|
nativeRadiusM: spec.nativeRadiusM,
|
||||||
rangeM: Cesium.Cartesian3.distance(
|
rangeM: geo.cartesian3.distance(view.camera.positionWC, displayPos),
|
||||||
flightState._viewer.camera.positionWC,
|
viewportHeightPx: view.viewportHeightPx(),
|
||||||
displayPos,
|
fovyRad: view.camera.frustum.fovy,
|
||||||
),
|
|
||||||
viewportHeightPx: flightState._viewer.scene.canvas.clientHeight,
|
|
||||||
fovyRad: flightState._viewer.camera.frustum.fovy,
|
|
||||||
maximumPixelSize: TRACKED_MODEL_MAX_PX,
|
maximumPixelSize: TRACKED_MODEL_MAX_PX,
|
||||||
});
|
});
|
||||||
flightState._trackedModel.show = true;
|
flightState._trackedModel.show = true;
|
||||||
@@ -797,14 +745,10 @@ export function createRendering({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function _fleetTick() {
|
function _fleetTick() {
|
||||||
if (
|
if (!flightState._renderer?.fleet.visible()) return;
|
||||||
!flightState._viewer ||
|
const view = flightState._renderer.view;
|
||||||
!flightState._billboardCollection ||
|
const scene = view.scene;
|
||||||
!flightState._billboardCollection.show
|
const camera = view.camera;
|
||||||
)
|
|
||||||
return;
|
|
||||||
const scene = flightState._viewer.scene;
|
|
||||||
const camera = flightState._viewer.camera;
|
|
||||||
const nowMs = focusNowMs(Date.now());
|
const nowMs = focusNowMs(Date.now());
|
||||||
|
|
||||||
// (The tracked trail head is now the per-frame _trailHeadEntity segment — no 1 Hz
|
// (The tracked trail head is now the per-frame _trailHeadEntity segment — no 1 Hz
|
||||||
@@ -833,7 +777,7 @@ export function createRendering({
|
|||||||
flightState._lastRotPassMs = nowMs;
|
flightState._lastRotPassMs = nowMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
const occluder = horizonOccluder(camera);
|
const occluder = view.horizon();
|
||||||
const focusTarget = getFocusTarget();
|
const focusTarget = getFocusTarget();
|
||||||
|
|
||||||
// 3D model regime: only when enabled AND the camera is zoomed in past the altitude ceiling.
|
// 3D model regime: only when enabled AND the camera is zoomed in past the altitude ceiling.
|
||||||
@@ -859,11 +803,7 @@ export function createRendering({
|
|||||||
const addDistSq = addM * addM;
|
const addDistSq = addM * addM;
|
||||||
const keepM = _modelKeepDistM();
|
const keepM = _modelKeepDistM();
|
||||||
const keepDistSq = keepM * keepM;
|
const keepDistSq = keepM * keepM;
|
||||||
const cull = camera.frustum.computeCullingVolume(
|
const inFrustum = view.frustumTester();
|
||||||
camPos,
|
|
||||||
camera.directionWC,
|
|
||||||
camera.upWC,
|
|
||||||
);
|
|
||||||
const cand = [];
|
const cand = [];
|
||||||
for (const [icao, bb] of flightState._billboards) {
|
for (const [icao, bb] of flightState._billboards) {
|
||||||
if (icao === flightState._trackedIcao) continue;
|
if (icao === flightState._trackedIcao) continue;
|
||||||
@@ -874,18 +814,9 @@ 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 d2 = geo.cartesian3.distanceSquared(camPos, bb.position);
|
||||||
if (d2 > keepDistSq) continue; // beyond keep radius → never eligible
|
if (d2 > keepDistSq) continue; // beyond keep radius → never eligible
|
||||||
Cesium.Cartesian3.clone(
|
cand.push([icao, d2, inFrustum(bb.position)]);
|
||||||
bb.position,
|
|
||||||
flightState._scratchModelBS.center,
|
|
||||||
);
|
|
||||||
cand.push([
|
|
||||||
icao,
|
|
||||||
d2,
|
|
||||||
cull.computeVisibility(flightState._scratchModelBS) !==
|
|
||||||
Cesium.Intersect.OUTSIDE,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
cand.sort((a, b) => a[1] - b[1]);
|
cand.sort((a, b) => a[1] - b[1]);
|
||||||
if (cand.length > cap && nowMs - flightState._lastModelCapWarnMs > 5000) {
|
if (cand.length > cap && nowMs - flightState._lastModelCapWarnMs > 5000) {
|
||||||
@@ -913,7 +844,7 @@ export function createRendering({
|
|||||||
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 && geo.cartesian3.distanceSquared(dr, bb.position) > 1.0) {
|
||||||
bb.position = dr;
|
bb.position = dr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,7 +874,7 @@ export function createRendering({
|
|||||||
// One sprite-owned write site composes freshness × focus × limb haze and
|
// One sprite-owned write site composes freshness × focus × limb haze and
|
||||||
// base class/ground scale × limb taper. The locked NearFarScalar remains
|
// base class/ground scale × limb taper. The locked NearFarScalar remains
|
||||||
// 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 = geo.cartesian3.distance(
|
||||||
camera.positionWC,
|
camera.positionWC,
|
||||||
bb.position,
|
bb.position,
|
||||||
);
|
);
|
||||||
@@ -1047,9 +978,7 @@ export function createRendering({
|
|||||||
// IR boost must survive the per-tick treatment write; boosted
|
// IR boost must survive the per-tick treatment write; boosted
|
||||||
// models also skip the recession fade (mirror of flights.js —
|
// models also skip the recession fade (mirror of flights.js —
|
||||||
// billboards keep their normal fade, hot MODELS stay full-strength).
|
// billboards keep their normal fade, hot MODELS stay full-strength).
|
||||||
baseColor: flightState._irBoost
|
baseColor: flightState._irBoost ? WHITE : _modelColor(icao24),
|
||||||
? Cesium.Color.WHITE
|
|
||||||
: _modelColor(icao24),
|
|
||||||
alpha: flightState._irBoost ? 1 : treatment.alpha,
|
alpha: flightState._irBoost ? 1 : treatment.alpha,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
MIL_ICON_COLOR,
|
MIL_ICON_COLOR,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
|
|
||||||
/** Apply military record changes to Cesium resources and existing follow operations. */
|
/** Apply military record changes to the renderer's fleet and existing follow operations. */
|
||||||
export function createMilitarySnapshotRenderer({
|
export function createMilitarySnapshotRenderer({
|
||||||
flightState,
|
flightState,
|
||||||
records,
|
records,
|
||||||
@@ -153,7 +153,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, geo.cartesian3.create()) || position;
|
motion._deadReckon(icao24, geo.cartesian3.create()) || position;
|
||||||
const bb = flightState._billboardCollection.add({
|
const bb = flightState._renderer.fleet.add({
|
||||||
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,
|
||||||
@@ -235,20 +235,27 @@ export function createMilitarySnapshotRenderer({
|
|||||||
// this layer's own billboards/models from the probe.
|
// this layer's own billboards/models from the probe.
|
||||||
{
|
{
|
||||||
const viewerCarto =
|
const viewerCarto =
|
||||||
flightState._viewer?.camera?.positionCartographic || null;
|
flightState._renderer?.view.camera?.positionCartographic || null;
|
||||||
sampleMeshFloorCells(flightState._viewer?.scene, _floorWarmPoints, {
|
sampleMeshFloorCells(
|
||||||
excludeObjects: [
|
flightState._renderer?.view.surfaceScene,
|
||||||
...flightState._billboards.values(),
|
_floorWarmPoints,
|
||||||
...flightState._models.values(),
|
{
|
||||||
flightState._trackedModel,
|
excludeObjects:
|
||||||
].filter(Boolean),
|
flightState._renderer?.surfaceExclusions({
|
||||||
viewerLat: viewerCarto
|
sprites: flightState._billboards.values(),
|
||||||
? geo.math.toDegrees(viewerCarto.latitude)
|
models: [
|
||||||
: undefined,
|
...flightState._models.values(),
|
||||||
viewerLon: viewerCarto
|
flightState._trackedModel,
|
||||||
? geo.math.toDegrees(viewerCarto.longitude)
|
],
|
||||||
: undefined,
|
}) ?? [],
|
||||||
});
|
viewerLat: viewerCarto
|
||||||
|
? geo.math.toDegrees(viewerCarto.latitude)
|
||||||
|
: undefined,
|
||||||
|
viewerLon: viewerCarto
|
||||||
|
? geo.math.toDegrees(viewerCarto.longitude)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove aircraft only after MISSING_POLL_LIMIT consecutive absences.
|
// Remove aircraft only after MISSING_POLL_LIMIT consecutive absences.
|
||||||
@@ -285,7 +292,7 @@ export function createMilitarySnapshotRenderer({
|
|||||||
tracking._clearTracking(false, { evicted: true });
|
tracking._clearTracking(false, { evicted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
flightState._billboardCollection.remove(bb);
|
flightState._renderer.fleet.remove(bb);
|
||||||
flightState._billboards.delete(icao24);
|
flightState._billboards.delete(icao24);
|
||||||
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
|
rendering._releaseModel(icao24); // aged-out aircraft: drop its 3D model (no orphan / cap leak)
|
||||||
records.forget(icao24);
|
records.forget(icao24);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { MilitaryFlightRecords } from './records.js';
|
import { MilitaryFlightRecords } from './records.js';
|
||||||
import { createMilitaryFeed } from './ingestion.js';
|
import { createMilitaryFeed } from './ingestion.js';
|
||||||
import * as Cesium from 'cesium';
|
|
||||||
import * as geo from '../../geo/index.js';
|
import * as geo from '../../geo/index.js';
|
||||||
|
|
||||||
export function createFlightState({ source, services }) {
|
export function createFlightState({ source, services }) {
|
||||||
@@ -33,11 +32,14 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._scratchGroundPos = geo.cartesian3.create();
|
flightState._scratchGroundPos = geo.cartesian3.create();
|
||||||
|
|
||||||
/** @type {Cesium.PrimitiveCollection|null} */
|
/** @type {import('../aircraft/render/contract.js').AircraftRenderer|null}
|
||||||
|
* The engine's aircraft renderer for this lifecycle (render/cesium.js on
|
||||||
|
* Cesium): the fleet sprites, the models and their collection, the
|
||||||
|
* tracked visual, the trail, the follow camera and the view reads. */
|
||||||
|
|
||||||
flightState._modelCollection = null;
|
flightState._renderer = null;
|
||||||
|
|
||||||
/** @type {Map<string, Cesium.Model>} icao24 → model */
|
/** @type {Map<string, object>} icao24 → model handle */
|
||||||
|
|
||||||
flightState._models = new Map();
|
flightState._models = new Map();
|
||||||
|
|
||||||
@@ -76,19 +78,10 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._planeModelLoaded = false;
|
flightState._planeModelLoaded = false;
|
||||||
|
|
||||||
/** @type {Cesium.Model|null} retained preload that keeps the glTF cache warm */
|
/** @type {object|null} retained preload that keeps the glTF cache warm */
|
||||||
|
|
||||||
flightState._preloadModel = null;
|
flightState._preloadModel = null;
|
||||||
|
|
||||||
flightState._scratchModelHpr = new Cesium.HeadingPitchRoll(0, 0, 0);
|
|
||||||
|
|
||||||
flightState._scratchModelMtx = geo.matrix4.create();
|
|
||||||
|
|
||||||
flightState._scratchModelBS = new Cesium.BoundingSphere(
|
|
||||||
geo.cartesian3.create(),
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
// frustum-visibility test
|
|
||||||
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
|
/** Last limb taper per billboard, retained across class/ground/cockpit repaints. */
|
||||||
|
|
||||||
flightState._billboardLimbScale = new WeakMap();
|
flightState._billboardLimbScale = new WeakMap();
|
||||||
@@ -97,11 +90,8 @@ export function createFlightState({ source, services }) {
|
|||||||
// Module-level state
|
// Module-level state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/** @type {Cesium.BillboardCollection|null} Single GPU-batched collection for all aircraft */
|
/** @type {Map<string, object>} ICAO hex -> fleet sprite handle (the
|
||||||
|
* renderer's single GPU-batched fleet) */
|
||||||
flightState._billboardCollection = null;
|
|
||||||
|
|
||||||
/** @type {Map<string, Cesium.Billboard>} ICAO hex -> billboard primitive */
|
|
||||||
|
|
||||||
flightState._billboards = new Map();
|
flightState._billboards = new Map();
|
||||||
|
|
||||||
@@ -109,7 +99,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._detectionObjects = new Map();
|
flightState._detectionObjects = new Map();
|
||||||
|
|
||||||
/** @type {Map<string, Array<{timeMs: number, epochMs: number, position: Cesium.Cartesian3}>>} ICAO hex -> recent position samples for dead reckoning (timeMs: whole ms since the epoch, the app clock's unit) */
|
/** @type {Map<string, Array<{timeMs: number, epochMs: number, position: object}>>} ICAO hex -> recent position samples for dead reckoning (timeMs: whole ms since the epoch, the app clock's unit) */
|
||||||
|
|
||||||
flightState._positionHistory = new Map();
|
flightState._positionHistory = new Map();
|
||||||
|
|
||||||
@@ -122,7 +112,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._trackingIntentGeneration = 0;
|
flightState._trackingIntentGeneration = 0;
|
||||||
|
|
||||||
/** @type {Cesium.Entity|null} Entity created for the tracked aircraft (camera follows this) */
|
/** @type {object|null} The renderer's tracked handle (the follow target) */
|
||||||
|
|
||||||
flightState._trackedEntity = null;
|
flightState._trackedEntity = null;
|
||||||
|
|
||||||
@@ -130,7 +120,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._trackedCameraFrameStop = null;
|
flightState._trackedCameraFrameStop = null;
|
||||||
|
|
||||||
/** @type {Cesium.Model|null} Standalone 3D model for the tracked aircraft — NOT a graphic on
|
/** @type {object|null} Standalone 3D model for the tracked aircraft — NOT a graphic on
|
||||||
* _trackedEntity, so viewer.trackedEntity's follow-camera always has a ready bounding sphere
|
* _trackedEntity, so viewer.trackedEntity's follow-camera always has a ready bounding sphere
|
||||||
* (a model graphic reports PENDING until loaded, which freezes the centering on 3D-toggle). */
|
* (a model graphic reports PENDING until loaded, which freezes the centering on 3D-toggle). */
|
||||||
|
|
||||||
@@ -140,13 +130,10 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._trackedModelLoading = false;
|
flightState._trackedModelLoading = false;
|
||||||
|
|
||||||
/** @type {Cesium.ScreenSpaceEventHandler|null} Click handler for selecting aircraft */
|
/** @type {(() => void)|null} Unregisters this layer from the engine's
|
||||||
|
* selection dispatcher (clicks and Escape). */
|
||||||
|
|
||||||
flightState._clickHandler = null;
|
flightState._selectionRemove = null;
|
||||||
|
|
||||||
/** @type {Cesium.Viewer|null} Cached viewer reference */
|
|
||||||
|
|
||||||
flightState._viewer = null;
|
|
||||||
|
|
||||||
/** Cockpit presentation switches ambient AIR contacts between near aircraft and far dots. */
|
/** Cockpit presentation switches ambient AIR contacts between near aircraft and far dots. */
|
||||||
|
|
||||||
@@ -168,7 +155,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._trail = null;
|
flightState._trail = null;
|
||||||
|
|
||||||
/** @type {Cesium.Entity|null} Per-frame head segment: last body point → live icon */
|
/** @type {object|null} Per-frame head segment: last body point → live icon */
|
||||||
|
|
||||||
flightState._trailHeadEntity = null;
|
flightState._trailHeadEntity = null;
|
||||||
|
|
||||||
@@ -176,7 +163,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._trailHeadSeq = 0;
|
flightState._trailHeadSeq = 0;
|
||||||
|
|
||||||
/** @type {Cesium.Cartesian3[]} Chronological tracked-aircraft fixes (oldest first) */
|
/** @type {object[]} Chronological tracked-aircraft fixes (oldest first) */
|
||||||
|
|
||||||
flightState._trailPositions = [];
|
flightState._trailPositions = [];
|
||||||
|
|
||||||
@@ -200,17 +187,17 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._lastTrackedRotation = 0;
|
flightState._lastTrackedRotation = 0;
|
||||||
|
|
||||||
/** @type {Cesium.Event.RemoveCallback|null} preRender listener disposer */
|
/** @type {(() => void)|null} preRender listener disposer */
|
||||||
|
|
||||||
flightState._preRenderRemove = null;
|
flightState._preRenderRemove = null;
|
||||||
|
|
||||||
flightState._trackedModelPreUpdateRemove = null;
|
flightState._trackedModelPreUpdateRemove = null;
|
||||||
|
|
||||||
/** @type {Cesium.Event.RemoveCallback|null} camera.moveEnd listener disposer (arrival rotation pass) */
|
/** @type {(() => void)|null} camera.moveEnd listener disposer (arrival rotation pass) */
|
||||||
|
|
||||||
flightState._moveEndRemove = null;
|
flightState._moveEndRemove = null;
|
||||||
|
|
||||||
/** @type {Cesium.Event.RemoveCallback|null} trackedEntityChanged listener disposer (cross-layer untrack) */
|
/** @type {(() => void)|null} follow-change listener disposer (cross-layer untrack) */
|
||||||
|
|
||||||
flightState._trackedEntityChangedRemove = null;
|
flightState._trackedEntityChangedRemove = null;
|
||||||
|
|
||||||
@@ -233,7 +220,7 @@ export function createFlightState({ source, services }) {
|
|||||||
flightState._trackedPosHolder = geo.cartesian3.create();
|
flightState._trackedPosHolder = geo.cartesian3.create();
|
||||||
|
|
||||||
// -- Per-frame dead-reckoning cache for the tracked entity --
|
// -- Per-frame dead-reckoning cache for the tracked entity --
|
||||||
/** @type {Cesium.Cartesian3|null} Cached DR position for the current render frame */
|
/** @type {object|null} Cached DR position for the current render frame */
|
||||||
|
|
||||||
flightState._cachedDRPosition = null;
|
flightState._cachedDRPosition = null;
|
||||||
|
|
||||||
@@ -298,7 +285,7 @@ export function createFlightState({ source, services }) {
|
|||||||
|
|
||||||
flightState._drReconcileIcao = null;
|
flightState._drReconcileIcao = null;
|
||||||
|
|
||||||
/** @type {Cesium.Cartesian3} Scratch for the tracked model's rendered translation. */
|
/** @type {object} Scratch for the tracked model's rendered translation. */
|
||||||
|
|
||||||
flightState._trackedVisualPos = geo.cartesian3.create();
|
flightState._trackedVisualPos = geo.cartesian3.create();
|
||||||
|
|
||||||
@@ -328,24 +315,18 @@ export function createFlightState({ source, services }) {
|
|||||||
flightState._trackedModelRetryAtMs = 0;
|
flightState._trackedModelRetryAtMs = 0;
|
||||||
|
|
||||||
/** IR hot-target mode — mirror of flights.js: under the luminance-mapped
|
/** IR hot-target mode — mirror of flights.js: under the luminance-mapped
|
||||||
* NVG/FLIR post-styles every model renders flat white (hottest); the dominant
|
* NVG/FLIR post-styles every model renders flat white (hottest) and UNLIT
|
||||||
* amber/tracked tint restores on style exit. */
|
* (`unlit` in the model spec: material-stage tint still sun-shades, so
|
||||||
|
* side-on planes read near-black under a high sun); the dominant
|
||||||
|
* amber/tracked tint restores on style exit. The shader is baked at
|
||||||
|
* creation (assigning it to a ready Cesium model is a silent no-op), so the
|
||||||
|
* boost flips by release-and-reload. */
|
||||||
|
|
||||||
flightState._irBoost = false;
|
flightState._irBoost = false;
|
||||||
|
|
||||||
/** Boosted models render UNLIT — mirror of flights.js (owner cockpit-FLIR
|
|
||||||
* field rounds): material-stage tint still sun-shades, so side-on planes
|
|
||||||
* read near-black under a high sun; UNLIT emits flat white regardless.
|
|
||||||
* customShader on a READY model is a silent no-op (field-verified), so the
|
|
||||||
* boost flips by release-and-reload — creation-time options carry it. */
|
|
||||||
|
|
||||||
flightState._IR_UNLIT_SHADER = new Cesium.CustomShader({
|
|
||||||
lightingModel: Cesium.LightingModel.UNLIT,
|
|
||||||
});
|
|
||||||
|
|
||||||
flightState._irReloadQueue = null;
|
flightState._irReloadQueue = null;
|
||||||
|
|
||||||
/** @type {Cesium.Cartographic} Scratch for _trailFloorPosition (per-frame safe). */
|
/** @type {object} Scratch (Cartographic) for _trailFloorPosition (per-frame safe). */
|
||||||
|
|
||||||
flightState._scratchTrailCarto = geo.cartographic.create();
|
flightState._scratchTrailCarto = geo.cartographic.create();
|
||||||
return flightState;
|
return flightState;
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ export function createTesting({
|
|||||||
models = [],
|
models = [],
|
||||||
modelCollection = null,
|
modelCollection = null,
|
||||||
}) {
|
}) {
|
||||||
flightState._viewer = viewer;
|
// The production renderer over the test's viewer and (fake) collections.
|
||||||
flightState._modelCollection = modelCollection;
|
flightState._renderer = services.renderer.create({
|
||||||
flightState._billboardCollection = billboardCollection;
|
viewer,
|
||||||
|
adopt: { billboardCollection, modelCollection },
|
||||||
|
});
|
||||||
flightState._billboards = new Map([[icao24, billboard]]);
|
flightState._billboards = new Map([[icao24, billboard]]);
|
||||||
flightState._cullPositions.clear();
|
flightState._cullPositions.clear();
|
||||||
flightState._models.clear();
|
flightState._models.clear();
|
||||||
|
|||||||
+231
-284
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as geo from '../../geo/index.js';
|
||||||
|
import { nearFarScale } from '../shared/color.js';
|
||||||
import { appClock } from '../../time/clock.js';
|
import { appClock } from '../../time/clock.js';
|
||||||
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
import { nextCockpitNearContacts } from '../../data/cockpitAirLod.js';
|
||||||
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
import { aircraftIcon, TRACKED_ICON_PX } from '../../data/aircraftIcons.js';
|
||||||
@@ -9,11 +10,6 @@ import {
|
|||||||
} from '../../data/iconOrientation.js';
|
} from '../../data/iconOrientation.js';
|
||||||
import { trailHeadStart } from '../../data/modelVisualAnchor.js';
|
import { trailHeadStart } from '../../data/modelVisualAnchor.js';
|
||||||
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
import { CLASS_SCALE_2D } from '../../data/aircraftClass.js';
|
||||||
import {
|
|
||||||
bindTrackingClickGesture,
|
|
||||||
isTrackingSelectionGesture,
|
|
||||||
isTrackingClickGesture,
|
|
||||||
} from '../../data/trackingClickGesture.js';
|
|
||||||
import {
|
import {
|
||||||
TRACKED_MODEL_MAX_LOAD_FAILS,
|
TRACKED_MODEL_MAX_LOAD_FAILS,
|
||||||
TRACKED_MODEL_RETRY_BACKOFF_MS,
|
TRACKED_MODEL_RETRY_BACKOFF_MS,
|
||||||
@@ -44,11 +40,9 @@ export function createTracking({
|
|||||||
const { trackedLabelModelFromText, refreshTrackedReadout } = services.readout;
|
const { trackedLabelModelFromText, refreshTrackedReadout } = services.readout;
|
||||||
const { floorAltitudeM, cachedGroundFloor, resolveGroundFloorCellsBounded } =
|
const { floorAltitudeM, cachedGroundFloor, resolveGroundFloorCellsBounded } =
|
||||||
services.groundFloor;
|
services.groundFloor;
|
||||||
const { createTrail } = services.trails;
|
|
||||||
const { ensureGeoidReady, geoidHeight } = services.geoid;
|
const { ensureGeoidReady, geoidHeight } = services.geoid;
|
||||||
const { clearFocusTarget } = services.focus;
|
const { clearFocusTarget } = services.focus;
|
||||||
const { applyTrackedCameraFrame } = services.camera;
|
const { selectionDispatcherFor } = services.selection;
|
||||||
const { resolvePickId, isOwnedByOtherLayer } = services.picking;
|
|
||||||
|
|
||||||
function _emitAwarenessEvent(type, detail) {
|
function _emitAwarenessEvent(type, detail) {
|
||||||
if (
|
if (
|
||||||
@@ -73,7 +67,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: geo.cartesian3.clone(bb.position),
|
||||||
origin,
|
origin,
|
||||||
});
|
});
|
||||||
selectTrackedSubjectContext(_contextSubjectMetadata(icao24));
|
selectTrackedSubjectContext(_contextSubjectMetadata(icao24));
|
||||||
@@ -138,10 +132,8 @@ export function createTracking({
|
|||||||
/** Refresh the AIR-only Cockpit near band independently from model admission. */
|
/** Refresh the AIR-only Cockpit near band independently from model admission. */
|
||||||
|
|
||||||
function _refreshCockpitNearContacts() {
|
function _refreshCockpitNearContacts() {
|
||||||
if (
|
const camera = flightState._renderer?.view.camera;
|
||||||
!flightState._cockpitContactMode ||
|
if (!flightState._cockpitContactMode || !camera?.positionWC) {
|
||||||
!flightState._viewer?.camera?.positionWC
|
|
||||||
) {
|
|
||||||
if (flightState._cockpitNearContacts.size)
|
if (flightState._cockpitNearContacts.size)
|
||||||
flightState._cockpitNearContacts = new Set();
|
flightState._cockpitNearContacts = new Set();
|
||||||
return;
|
return;
|
||||||
@@ -152,10 +144,7 @@ export function createTracking({
|
|||||||
if (icao24 === flightState._trackedIcao || !bb?.position) continue;
|
if (icao24 === flightState._trackedIcao || !bb?.position) continue;
|
||||||
distancesSquared.push([
|
distancesSquared.push([
|
||||||
icao24,
|
icao24,
|
||||||
Cesium.Cartesian3.distanceSquared(
|
geo.cartesian3.distanceSquared(camera.positionWC, bb.position),
|
||||||
flightState._viewer.camera.positionWC,
|
|
||||||
bb.position,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
const next = nextCockpitNearContacts(
|
const next = nextCockpitNearContacts(
|
||||||
@@ -185,7 +174,7 @@ export function createTracking({
|
|||||||
// The collection stays visible in cockpit. Near AIR contacts retain their
|
// The collection stays visible in cockpit. Near AIR contacts retain their
|
||||||
// aircraft silhouette until a ready model takes over; far contacts are pips.
|
// aircraft silhouette until a ready model takes over; far contacts are pips.
|
||||||
// Never bulk-destroy here: that stalls the renderer exactly as cockpit begins.
|
// Never bulk-destroy here: that stalls the renderer exactly as cockpit begins.
|
||||||
if (flightState._modelCollection) flightState._modelCollection.show = true;
|
flightState._renderer?.models.setVisible(true);
|
||||||
flightState._trail?.setVisible(!next);
|
flightState._trail?.setVisible(!next);
|
||||||
if (flightState._trailHeadEntity) flightState._trailHeadEntity.show = !next;
|
if (flightState._trailHeadEntity) flightState._trailHeadEntity.show = !next;
|
||||||
for (const [icao24, bb] of flightState._billboards)
|
for (const [icao24, bb] of flightState._billboards)
|
||||||
@@ -264,14 +253,16 @@ export function createTracking({
|
|||||||
/** Re-image the tracked entity's billboard from the current class/conversion. */
|
/** Re-image the tracked entity's billboard from the current class/conversion. */
|
||||||
|
|
||||||
function _syncTrackedBillboardImage() {
|
function _syncTrackedBillboardImage() {
|
||||||
if (!flightState._trackedIcao || !flightState._trackedEntity?.billboard)
|
if (!flightState._trackedIcao || !flightState._trackedEntity) return;
|
||||||
return;
|
flightState._renderer.tracked.setImage(
|
||||||
flightState._trackedEntity.billboard.image = aircraftIcon(
|
flightState._trackedEntity,
|
||||||
parts.rendering._iconKind(
|
aircraftIcon(
|
||||||
flightState._trackedIcao,
|
parts.rendering._iconKind(
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
flightState._trackedIcao,
|
||||||
|
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
||||||
|
),
|
||||||
|
TRACKED_ICON_PX,
|
||||||
),
|
),
|
||||||
TRACKED_ICON_PX,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +295,7 @@ export function createTracking({
|
|||||||
_syncTrackedBillboardImage();
|
_syncTrackedBillboardImage();
|
||||||
_updateTrackedLabelModel(id);
|
_updateTrackedLabelModel(id);
|
||||||
}
|
}
|
||||||
flightState._viewer?.scene?.requestRender?.();
|
flightState._renderer?.view.requestRender();
|
||||||
return flightState._billboards.has(id) || id === flightState._trackedIcao;
|
return flightState._billboards.has(id) || id === flightState._trackedIcao;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +387,7 @@ export function createTracking({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
flightState._trackedZoomLatched = trackedModelZoomActive(
|
flightState._trackedZoomLatched = trackedModelZoomActive(
|
||||||
flightState._viewer?.camera?.positionCartographic?.height,
|
flightState._renderer?.view.camera?.positionCartographic?.height,
|
||||||
flightState._trackedZoomLatched,
|
flightState._trackedZoomLatched,
|
||||||
);
|
);
|
||||||
return flightState._trackedZoomLatched;
|
return flightState._trackedZoomLatched;
|
||||||
@@ -405,13 +396,13 @@ export function createTracking({
|
|||||||
/** Seed the tracked billboard's 2D orientation before a 3D→2D handoff. */
|
/** Seed the tracked billboard's 2D orientation before a 3D→2D handoff. */
|
||||||
|
|
||||||
function _syncTracked2dRotation() {
|
function _syncTracked2dRotation() {
|
||||||
if (!flightState._trackedIcao || !flightState._viewer) return;
|
if (!flightState._trackedIcao || !flightState._renderer) return;
|
||||||
const pos =
|
const pos =
|
||||||
parts.motion._trackedDisplayCached() ||
|
parts.motion._trackedDisplayCached() ||
|
||||||
flightState._billboards.get(flightState._trackedIcao)?.position;
|
flightState._billboards.get(flightState._trackedIcao)?.position;
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
const projected = screenProjectedRotation(
|
const projected = screenProjectedRotation(
|
||||||
flightState._viewer.scene,
|
flightState._renderer.view.scene,
|
||||||
pos,
|
pos,
|
||||||
parts.motion._trackedDisplayCourse(),
|
parts.motion._trackedDisplayCourse(),
|
||||||
flightState._lastTrackedRotation,
|
flightState._lastTrackedRotation,
|
||||||
@@ -432,31 +423,31 @@ export function createTracking({
|
|||||||
* invariant), so every position entering the TRAIL subsystem (seed, append,
|
* invariant), so every position entering the TRAIL subsystem (seed, append,
|
||||||
* per-frame head) goes through this instead — the visible taxi history sits
|
* per-frame head) goes through this instead — the visible taxi history sits
|
||||||
* on the surface without touching the billboard/model machinery.
|
* on the surface without touching the billboard/model machinery.
|
||||||
* @param {Cesium.Cartesian3} position - Owned position (mutated/replaced freely).
|
* @param {object} position - Owned position (Cartesian3; mutated/replaced freely).
|
||||||
* @returns {Cesium.Cartesian3} The same or a lifted position.
|
* @returns {object} The same or a lifted position.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _trailFloorPosition(position) {
|
function _trailFloorPosition(position) {
|
||||||
const carto = Cesium.Cartographic.fromCartesian(
|
const carto = geo.cartographic.fromCartesian(
|
||||||
position,
|
position,
|
||||||
Cesium.Ellipsoid.WGS84,
|
geo.ellipsoid.WGS84,
|
||||||
flightState._scratchTrailCarto,
|
flightState._scratchTrailCarto,
|
||||||
);
|
);
|
||||||
if (!carto) return position;
|
if (!carto) return position;
|
||||||
const latDeg = Cesium.Math.toDegrees(carto.latitude);
|
const latDeg = geo.math.toDegrees(carto.latitude);
|
||||||
const lonDeg = Cesium.Math.toDegrees(carto.longitude);
|
const lonDeg = geo.math.toDegrees(carto.longitude);
|
||||||
const floored = floorAltitudeM(
|
const floored = floorAltitudeM(
|
||||||
carto.height,
|
carto.height,
|
||||||
cachedGroundFloor(latDeg, lonDeg),
|
cachedGroundFloor(latDeg, lonDeg),
|
||||||
);
|
);
|
||||||
if (floored == null || floored === carto.height) return position;
|
if (floored == null || floored === carto.height) return position;
|
||||||
return Cesium.Cartesian3.fromDegrees(lonDeg, latDeg, floored);
|
return geo.cartesian3.fromDegrees(lonDeg, latDeg, floored);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append one fix to the tracked aircraft's trail accumulation and refresh
|
* Append one fix to the tracked aircraft's trail accumulation and refresh
|
||||||
* the rendered trail. Caller passes an owned (cloned) Cartesian3.
|
* the rendered trail. Caller passes an owned (cloned) Cartesian3.
|
||||||
* @param {Cesium.Cartesian3} position - New fix position, appended at the head.
|
* @param {object} position - New fix position (Cartesian3), appended at the head.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _appendTrailFix(position) {
|
function _appendTrailFix(position) {
|
||||||
@@ -505,12 +496,12 @@ export function createTracking({
|
|||||||
// fixes carry the pre-datum default height — the trail copy sits on
|
// fixes carry the pre-datum default height — the trail copy sits on
|
||||||
// the surface instead (pure lift; airborne fixes pass through).
|
// the surface instead (pure lift; airborne fixes pass through).
|
||||||
flightState._trailPositions.push(
|
flightState._trailPositions.push(
|
||||||
_trailFloorPosition(Cesium.Cartesian3.clone(fix.position)),
|
_trailFloorPosition(geo.cartesian3.clone(fix.position)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!flightState._trail && flightState._viewer) {
|
if (!flightState._trail && flightState._renderer) {
|
||||||
flightState._trail = createTrail(flightState._viewer, {
|
flightState._trail = flightState._renderer.trails.create({
|
||||||
color: TRAIL_COLOR,
|
color: TRAIL_COLOR,
|
||||||
width: 2.5,
|
width: 2.5,
|
||||||
});
|
});
|
||||||
@@ -520,64 +511,58 @@ export function createTracking({
|
|||||||
// every frame via a CallbackProperty (Cesium updates entity-polyline positions cheaply,
|
// every frame via a CallbackProperty (Cesium updates entity-polyline positions cheaply,
|
||||||
// unlike the trail primitive which fully rebuilds on setPositions). Keeps the head glued
|
// unlike the trail primitive which fully rebuilds on setPositions). Keeps the head glued
|
||||||
// to the 12 Hz icon instead of lagging ~1 s behind it.
|
// to the 12 Hz icon instead of lagging ~1 s behind it.
|
||||||
if (!flightState._trailHeadEntity && flightState._viewer) {
|
if (!flightState._trailHeadEntity && flightState._renderer) {
|
||||||
flightState._trailHeadEntity = flightState._viewer.entities.add({
|
flightState._trailHeadEntity = flightState._renderer.trails.createHead({
|
||||||
// 'gev-trail' namespace (round 6): claimed by trailRenderer's pick
|
// 'gev-trail' namespace (round 6): claimed by trailRenderer's pick
|
||||||
// owner so a click on the head segment never reads as empty space.
|
// owner so a click on the head segment never reads as empty space.
|
||||||
id: `gev-trail:mil-head-${++flightState._trailHeadSeq}`,
|
id: `gev-trail:mil-head-${++flightState._trailHeadSeq}`,
|
||||||
show: !flightState._cockpitContactMode,
|
show: !flightState._cockpitContactMode,
|
||||||
polyline: {
|
positions: () => {
|
||||||
positions: new Cesium.CallbackProperty(() => {
|
// Need ≥2 accumulated points: the body draws all-but-newest, so the head must
|
||||||
// Need ≥2 accumulated points: the body draws all-but-newest, so the head must
|
// start at the last DISPLAYED body point (index n−2). With a single fix that
|
||||||
// start at the last DISPLAYED body point (index n−2). With a single fix that
|
// point would be the sole raw fix — ~now, AHEAD of the delayed icon — so the
|
||||||
// point would be the sole raw fix — ~now, AHEAD of the delayed icon — so the
|
// segment would draw IN FRONT of the plane. Likewise during warm-up the icon
|
||||||
// segment would draw IN FRONT of the plane. Likewise during warm-up the icon
|
// predates all real history, so there is no valid body point behind it.
|
||||||
// predates all real history, so there is no valid body point behind it.
|
if (
|
||||||
if (
|
!flightState._trackedIcao ||
|
||||||
!flightState._trackedIcao ||
|
flightState._trailPositions.length < 2 ||
|
||||||
flightState._trailPositions.length < 2 ||
|
parts.motion._isTrackWarmingUp()
|
||||||
parts.motion._isTrackWarmingUp()
|
)
|
||||||
)
|
return [];
|
||||||
return [];
|
const head =
|
||||||
const head =
|
parts.motion._trackedTrailCached() ||
|
||||||
parts.motion._trackedTrailCached() ||
|
parts.motion._trackedDisplayPosition(flightState._trackedIcao);
|
||||||
parts.motion._trackedDisplayPosition(flightState._trackedIcao);
|
if (!head) return [];
|
||||||
if (!head) return [];
|
// body[n−2] (last displayed body point) → delayed head: runs FORWARD, never a
|
||||||
// body[n−2] (last displayed body point) → delayed head: runs FORWARD, never a
|
// backward/reversing segment.
|
||||||
// backward/reversing segment.
|
const start =
|
||||||
const start =
|
flightState._trailPositions[flightState._trailPositions.length - 2];
|
||||||
flightState._trailPositions[
|
// Floor the head too (round 2): a grounded tracked contact's DR
|
||||||
flightState._trailPositions.length - 2
|
// display height is the pre-datum default — without this the last
|
||||||
];
|
// segment dives underground while taxiing.
|
||||||
// Floor the head too (round 2): a grounded tracked contact's DR
|
const end = _trailFloorPosition(geo.cartesian3.clone(head));
|
||||||
// display height is the pre-datum default — without this the last
|
// On a contact that has not moved this segment runs from inside the
|
||||||
// segment dives underground while taxiing.
|
// model out to its own anchor — a line through the fuselage. The END
|
||||||
const end = _trailFloorPosition(Cesium.Cartesian3.clone(head));
|
// never gives, so a moving trail still terminates on the tail; the
|
||||||
// On a contact that has not moved this segment runs from inside the
|
// START is what slides, from nothing on a parked contact out to the
|
||||||
// model out to its own anchor — a line through the fuselage. The END
|
// whole segment once it has cleared its own envelope. Measured against
|
||||||
// never gives, so a moving trail still terminates on the tail; the
|
// the FLOORED endpoint, which is the one actually drawn.
|
||||||
// START is what slides, from nothing on a parked contact out to the
|
// See trailHeadStart.
|
||||||
// whole segment once it has cleared its own envelope. Measured against
|
const from = trailHeadStart(
|
||||||
// the FLOORED endpoint, which is the one actually drawn.
|
start,
|
||||||
// See trailHeadStart.
|
end,
|
||||||
const from = trailHeadStart(
|
parts.motion._trackedModelCenterWorld(),
|
||||||
start,
|
parts.motion._trackedModelEnvelopeM(),
|
||||||
end,
|
flightState._scratchTrailHead,
|
||||||
parts.motion._trackedModelCenterWorld(),
|
);
|
||||||
parts.motion._trackedModelEnvelopeM(),
|
if (!from) return [];
|
||||||
flightState._scratchTrailHead,
|
return [from, end];
|
||||||
);
|
|
||||||
if (!from) return [];
|
|
||||||
return [from, end];
|
|
||||||
}, false),
|
|
||||||
width: 2.5,
|
|
||||||
material: Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.9),
|
|
||||||
// Round 4: the head must never vanish into the mesh either (dimmed
|
|
||||||
// when occluded so depth still reads).
|
|
||||||
depthFailMaterial:
|
|
||||||
Cesium.Color.fromCssColorString(TRAIL_COLOR).withAlpha(0.45),
|
|
||||||
arcType: Cesium.ArcType.GEODESIC, // round 8: consistent with the trail body (no chords)
|
|
||||||
},
|
},
|
||||||
|
width: 2.5,
|
||||||
|
// Round 4: the head must never vanish into the mesh either (dimmed
|
||||||
|
// when occluded so depth still reads); round 8: geodesic, consistent
|
||||||
|
// with the trail body (no chords).
|
||||||
|
color: TRAIL_COLOR,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_refreshTrailDisplay();
|
_refreshTrailDisplay();
|
||||||
@@ -673,7 +658,7 @@ export function createTracking({
|
|||||||
// sentinel (an arbitrary placeholder, never a reported altitude).
|
// sentinel (an arbitrary placeholder, never a reported altitude).
|
||||||
if (altM == null) altM = lastAltM != null ? lastAltM : 50;
|
if (altM == null) altM = lastAltM != null ? lastAltM : 50;
|
||||||
lastAltM = altM;
|
lastAltM = altM;
|
||||||
older.push(Cesium.Cartesian3.fromDegrees(lon, lat, altM));
|
older.push(geo.cartesian3.fromDegrees(lon, lat, altM));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stride-thin the backfill so it plus the live fine segment fit the cap.
|
// Stride-thin the backfill so it plus the live fine segment fit the cap.
|
||||||
@@ -705,17 +690,8 @@ export function createTracking({
|
|||||||
flightState._trailBackfillToken += 1;
|
flightState._trailBackfillToken += 1;
|
||||||
flightState._trailPositions = [];
|
flightState._trailPositions = [];
|
||||||
if (flightState._trail) flightState._trail.clear();
|
if (flightState._trail) flightState._trail.clear();
|
||||||
if (
|
if (flightState._trailHeadEntity)
|
||||||
flightState._trailHeadEntity &&
|
flightState._renderer?.trails.removeHead(flightState._trailHeadEntity);
|
||||||
flightState._viewer &&
|
|
||||||
!flightState._viewer.isDestroyed()
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
flightState._viewer.entities.remove(flightState._trailHeadEntity);
|
|
||||||
} catch {
|
|
||||||
/* already gone */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flightState._trailHeadEntity = null;
|
flightState._trailHeadEntity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,11 +767,11 @@ export function createTracking({
|
|||||||
// follow-camera, so tear down our state but DON'T clear viewer.trackedEntity (they own it now).
|
// follow-camera, so tear down our state but DON'T clear viewer.trackedEntity (they own it now).
|
||||||
// Releasing trackedEntity does NOT move the camera: Cesium resets the lookAt transform in
|
// Releasing trackedEntity does NOT move the camera: Cesium resets the lookAt transform in
|
||||||
// place, so the view stays where the follow left it and the user can immediately orbit/zoom.
|
// place, so the view stays where the follow left it and the user can immediately orbit/zoom.
|
||||||
if (flightState._viewer && !skipViewerUntrack) {
|
if (flightState._renderer && !skipViewerUntrack) {
|
||||||
flightState._viewer.trackedEntity = undefined;
|
flightState._renderer.follow.release();
|
||||||
}
|
}
|
||||||
if (flightState._trackedEntity) {
|
if (flightState._trackedEntity) {
|
||||||
flightState._viewer.entities.remove(flightState._trackedEntity);
|
flightState._renderer.tracked.remove(flightState._trackedEntity);
|
||||||
flightState._trackedEntity = null;
|
flightState._trackedEntity = null;
|
||||||
}
|
}
|
||||||
parts.rendering._releaseTrackedModel();
|
parts.rendering._releaseTrackedModel();
|
||||||
@@ -826,7 +802,7 @@ export function createTracking({
|
|||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
if (
|
if (
|
||||||
!flightState._billboardCollection?.show ||
|
!flightState._renderer?.fleet.visible() ||
|
||||||
!flightState._billboards.has(pending.id)
|
!flightState._billboards.has(pending.id)
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
@@ -874,12 +850,6 @@ export function createTracking({
|
|||||||
const getTrackedPosition = () =>
|
const getTrackedPosition = () =>
|
||||||
parts.motion._trackedDisplayPosition(icao24) || bb.position;
|
parts.motion._trackedDisplayPosition(icao24) || bb.position;
|
||||||
|
|
||||||
// Dead-reckoning position property evaluated every render frame.
|
|
||||||
// isConstant = false so CesiumJS re-evaluates each frame.
|
|
||||||
const positionProperty = new Cesium.CallbackProperty(() => {
|
|
||||||
return getTrackedPosition();
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
// World-space orientation for the 3D model: nose along the course heading (ENU, pitch/roll 0).
|
// World-space orientation for the 3D model: nose along the course heading (ENU, pitch/roll 0).
|
||||||
// Create tracked entity: a 2D billboard when zoomed out, a 3D model when zoomed in past the
|
// Create tracked entity: a 2D billboard when zoomed out, a 3D model when zoomed in past the
|
||||||
// TRACKED altitude ceiling. That handoff is DEFAULT behaviour — it does NOT wait on the
|
// TRACKED altitude ceiling. That handoff is DEFAULT behaviour — it does NOT wait on the
|
||||||
@@ -896,65 +866,56 @@ export function createTracking({
|
|||||||
// The tracked entity is a PURE BILLBOARD — no label or model graphic. The tracked plane's 3D
|
// The tracked entity is a PURE BILLBOARD — no label or model graphic. The tracked plane's 3D
|
||||||
// model is the standalone primitive driven by _updateTrackedModel(); keeping it off the entity is
|
// model is the standalone primitive driven by _updateTrackedModel(); keeping it off the entity is
|
||||||
// what makes the follow-camera's bounding sphere always ready (see _trackedModel).
|
// what makes the follow-camera's bounding sphere always ready (see _trackedModel).
|
||||||
flightState._trackedEntity = flightState._viewer.entities.add({
|
// The renderer keeps Cesium's EntityView in the same ENU frame as the
|
||||||
position: positionProperty,
|
// close-range camera guard (AUTO may otherwise alternate with a velocity frame).
|
||||||
// Keep Cesium's EntityView in the same ENU frame as the close-range
|
flightState._trackedEntity = flightState._renderer.tracked.create({
|
||||||
// camera guard; AUTO may otherwise alternate with a velocity frame.
|
// Dead-reckoning position, evaluated every render frame.
|
||||||
trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU,
|
position: getTrackedPosition,
|
||||||
billboard: {
|
image: aircraftIcon(
|
||||||
image: aircraftIcon(
|
parts.rendering._iconKind(
|
||||||
parts.rendering._iconKind(
|
flightState._trackedIcao,
|
||||||
flightState._trackedIcao,
|
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass,
|
|
||||||
),
|
|
||||||
TRACKED_ICON_PX,
|
|
||||||
),
|
),
|
||||||
width: 28,
|
TRACKED_ICON_PX,
|
||||||
height: 28,
|
),
|
||||||
scale:
|
width: 28,
|
||||||
BILLBOARD_SCALE *
|
height: 28,
|
||||||
(CLASS_SCALE_2D[
|
scale:
|
||||||
flightState.records.data.get(flightState._trackedIcao)?.klass
|
BILLBOARD_SCALE *
|
||||||
] || 1),
|
(CLASS_SCALE_2D[
|
||||||
// Solid amber when the billboard is the visual (zoomed out, 3D off, or model still loading);
|
flightState.records.data.get(flightState._trackedIcao)?.klass
|
||||||
// transparent once the STANDALONE tracked model is actually up (ready + shown).
|
] || 1),
|
||||||
color: new Cesium.CallbackProperty(
|
// Solid amber when the billboard is the visual (zoomed out, 3D off, or model still loading);
|
||||||
() =>
|
// transparent once the STANDALONE tracked model is actually up (ready + shown).
|
||||||
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
color: () =>
|
||||||
? AMBER_TRANSPARENT
|
parts.rendering._modelOwnsVisual(flightState._trackedIcao)
|
||||||
: TRACKED_ICON_COLOR,
|
? AMBER_TRANSPARENT
|
||||||
false,
|
: TRACKED_ICON_COLOR,
|
||||||
),
|
scaleByDistance: nearFarScale(1000, 3.0, 8000000, 0.5),
|
||||||
sizeInMeters: false,
|
// The tracked target must never vanish into tile geometry — tracking a
|
||||||
scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5),
|
// taxiing plane at street level would otherwise bury the amber icon inside
|
||||||
alignedAxis: Cesium.Cartesian3.ZERO,
|
// the runway skin exactly like the fleet ground icons (_groundDepthDistance);
|
||||||
// The tracked target must never vanish into tile geometry — tracking a
|
// its shared-host tracked card is top-composited separately.
|
||||||
// taxiing plane at street level would otherwise bury the amber icon inside
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||||
// the runway skin exactly like the fleet ground icons (_groundDepthDistance);
|
// Screen-projected rotation, evaluated per frame: exact in tracked-orbit
|
||||||
// its shared-host tracked card is top-composited separately.
|
// mode where camera.heading lives in the entity's reference frame.
|
||||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
rotation: () => {
|
||||||
// Screen-projected rotation, evaluated per frame: exact in tracked-orbit
|
const tracked = flightState.records.data.get(flightState._trackedIcao);
|
||||||
// mode where camera.heading lives in the entity's reference frame.
|
const pos = getTrackedPosition();
|
||||||
rotation: new Cesium.CallbackProperty(() => {
|
if (!tracked || !pos || !flightState._renderer)
|
||||||
const tracked = flightState.records.data.get(
|
|
||||||
flightState._trackedIcao,
|
|
||||||
);
|
|
||||||
const pos = getTrackedPosition();
|
|
||||||
if (!tracked || !pos || !flightState._viewer)
|
|
||||||
return flightState._lastTrackedRotation;
|
|
||||||
const projected = screenProjectedRotation(
|
|
||||||
flightState._viewer.scene,
|
|
||||||
pos,
|
|
||||||
parts.motion._trackedDisplayCourse(),
|
|
||||||
flightState._lastTrackedRotation,
|
|
||||||
);
|
|
||||||
const rot = stabilizeScreenRotation(
|
|
||||||
flightState._lastTrackedRotation,
|
|
||||||
projected,
|
|
||||||
);
|
|
||||||
if (rot !== null) flightState._lastTrackedRotation = rot;
|
|
||||||
return flightState._lastTrackedRotation;
|
return flightState._lastTrackedRotation;
|
||||||
}, false),
|
const projected = screenProjectedRotation(
|
||||||
|
flightState._renderer.view.scene,
|
||||||
|
pos,
|
||||||
|
parts.motion._trackedDisplayCourse(),
|
||||||
|
flightState._lastTrackedRotation,
|
||||||
|
);
|
||||||
|
const rot = stabilizeScreenRotation(
|
||||||
|
flightState._lastTrackedRotation,
|
||||||
|
projected,
|
||||||
|
);
|
||||||
|
if (rot !== null) flightState._lastTrackedRotation = rot;
|
||||||
|
return flightState._lastTrackedRotation;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
flightState._trackedEntity.gevSelectionOrigin = origin;
|
flightState._trackedEntity.gevSelectionOrigin = origin;
|
||||||
@@ -970,11 +931,11 @@ export function createTracking({
|
|||||||
// initial frame with surrounding context. (ENU: east=+X, north=+Y, up=+Z.)
|
// initial frame with surrounding context. (ENU: east=+X, north=+Y, up=+Z.)
|
||||||
const altM = info?.altitudeFt ? info.altitudeFt * 0.3048 : 1500;
|
const altM = info?.altitudeFt ? info.altitudeFt * 0.3048 : 1500;
|
||||||
const followRange = Math.min(Math.max(altM * 1.1 + 2500, 3000), 30000);
|
const followRange = Math.min(Math.max(altM * 1.1 + 2500, 3000), 30000);
|
||||||
flightState._trackedEntity.viewFrom = new Cesium.Cartesian3(
|
flightState._renderer.tracked.setViewFrom(flightState._trackedEntity, {
|
||||||
0,
|
x: 0,
|
||||||
-followRange * 0.8,
|
y: -followRange * 0.8,
|
||||||
followRange * 0.55,
|
z: followRange * 0.55,
|
||||||
);
|
});
|
||||||
|
|
||||||
// Cancel any in-progress camera flight first — otherwise Cesium won't apply the tracked
|
// Cancel any in-progress camera flight first — otherwise Cesium won't apply the tracked
|
||||||
// entity's viewFrom on the first frame, so voice-initiated tracking (which often fires
|
// entity's viewFrom on the first frame, so voice-initiated tracking (which often fires
|
||||||
@@ -989,15 +950,11 @@ export function createTracking({
|
|||||||
flightState._trackedEntity.gevVisualPosition =
|
flightState._trackedEntity.gevVisualPosition =
|
||||||
parts.motion._trackedVisualCached;
|
parts.motion._trackedVisualCached;
|
||||||
refreshTrackedReadout(flightState._trackedEntity);
|
refreshTrackedReadout(flightState._trackedEntity);
|
||||||
flightState._viewer.camera.cancelFlight();
|
// Camera follows the tracked entity (the renderer cancels any flight
|
||||||
// Camera follows the tracked entity
|
// first and applies the one exact tracked frame).
|
||||||
flightState._viewer.trackedEntity = flightState._trackedEntity;
|
flightState._trackedCameraFrameStop = flightState._renderer.follow.start(
|
||||||
flightState._trackedCameraFrameStop =
|
flightState._trackedEntity,
|
||||||
applyTrackedCameraFrame(
|
);
|
||||||
flightState._viewer,
|
|
||||||
flightState._trackedEntity,
|
|
||||||
flightState._trackedEntity.viewFrom,
|
|
||||||
) || null;
|
|
||||||
|
|
||||||
// Track-history trail (PRD F2): seed from local history + async backfill.
|
// Track-history trail (PRD F2): seed from local history + async backfill.
|
||||||
// Ground traffic draws NO trail — if the plane takes off while tracked, the
|
// Ground traffic draws NO trail — if the plane takes off while tracked, the
|
||||||
@@ -1016,125 +973,112 @@ export function createTracking({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global keydown handler: pressing Escape clears the active aircraft tracking.
|
* Escape (through the selection dispatcher) clears the active aircraft tracking.
|
||||||
* @param {KeyboardEvent} e - The keyboard event
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _onKeyDown(e) {
|
function _onEscape() {
|
||||||
if (e.key === 'Escape' && flightState._trackedIcao) {
|
if (flightState._trackedIcao) {
|
||||||
_cancelPendingTrackingRestore();
|
_cancelPendingTrackingRestore();
|
||||||
_clearTracking(false, { origin: 'user' });
|
_clearTracking(false, { origin: 'user' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a left-click handler on the scene canvas for aircraft selection.
|
* A click the selection dispatcher hands this layer: one of its aircraft
|
||||||
* Clicking a military billboard starts tracking that aircraft; clicking empty
|
* (a fleet sprite or model, or the tracked plane's own visual). The
|
||||||
* space or a non-military object deselects. Also attaches the Escape key
|
* dispatcher has already applied the gesture rules (a drag selects
|
||||||
* listener via {@link _onKeyDown}. Idempotent -- no-ops if already installed.
|
* nothing; isTrackingSelectionGesture) and the pointer claim.
|
||||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
|
* @param {string} id - The picked id this layer owns.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function _installClickHandler(viewer) {
|
function _onSelect(id) {
|
||||||
if (flightState._clickHandler) return; // already installed
|
// Cockpit mode owns the camera and keeps the current aircraft as its
|
||||||
|
// first-person reference: a globe click selects nothing here.
|
||||||
|
if (document.body.classList.contains('cockpit-mode')) return;
|
||||||
|
// Clicking the tracked plane itself — its tracked visual or its
|
||||||
|
// standalone 3D model (pick id = its icao, H1) — is a no-op, not a
|
||||||
|
// deselect and not a re-track.
|
||||||
|
if (
|
||||||
|
id === flightState._trackedEntity?.id ||
|
||||||
|
(flightState._trackedIcao && id === flightState._trackedIcao)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (flightState._billboards.has(id)) {
|
||||||
|
_cancelPendingTrackingRestore();
|
||||||
|
_trackFlight(id, { origin: 'user' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty space (or a pick no layer owns). The dispatcher calls this only
|
||||||
|
* for a clean, short click (isTrackingClickGesture): a slow stationary
|
||||||
|
* press may select, but cannot release existing tracking. A pick another
|
||||||
|
* layer owns (commercial flight, satellite, vessel, station, CCTV
|
||||||
|
* camera…) never reaches here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function _onEmptyClick() {
|
||||||
|
if (document.body.classList.contains('cockpit-mode')) return;
|
||||||
|
if (flightState._trackedIcao) {
|
||||||
|
_cancelPendingTrackingRestore();
|
||||||
|
_clearTracking(false, { origin: 'user' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join the engine's selection dispatcher (clicks with picks, Escape) and
|
||||||
|
* watch the follow camera for another layer taking it.
|
||||||
|
* Idempotent -- no-ops if already joined.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function _installClickHandler() {
|
||||||
|
const renderer = flightState._renderer;
|
||||||
|
if (!renderer) return;
|
||||||
|
|
||||||
// Cross-layer untrack (mirror of flights): if another layer grabs the follow-camera, drop our
|
// Cross-layer untrack (mirror of flights): if another layer grabs the follow-camera, drop our
|
||||||
// tracking without touching viewer.trackedEntity (the new owner controls it).
|
// tracking without touching the follow target (the new owner controls it).
|
||||||
if (!flightState._trackedEntityChangedRemove) {
|
if (!flightState._trackedEntityChangedRemove) {
|
||||||
flightState._trackedEntityChangedRemove =
|
flightState._trackedEntityChangedRemove = renderer.follow.onChanged(
|
||||||
viewer.trackedEntityChanged.addEventListener(() => {
|
() => {
|
||||||
|
const current = flightState._renderer?.follow.current();
|
||||||
if (
|
if (
|
||||||
flightState._trackedIcao &&
|
flightState._trackedIcao &&
|
||||||
flightState._viewer &&
|
current &&
|
||||||
flightState._viewer.trackedEntity &&
|
current !== flightState._trackedEntity
|
||||||
flightState._viewer.trackedEntity !== flightState._trackedEntity
|
|
||||||
) {
|
) {
|
||||||
_clearTracking(true, {
|
_clearTracking(true, {
|
||||||
origin:
|
origin: current?.gevSelectionOrigin || 'programmatic',
|
||||||
flightState._viewer.trackedEntity?.gevSelectionOrigin ||
|
|
||||||
'programmatic',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
flightState._clickHandler = new Cesium.ScreenSpaceEventHandler(
|
if (flightState._selectionRemove) return; // already joined
|
||||||
viewer.scene.canvas,
|
const dispatcher = selectionDispatcherFor(renderer.engine);
|
||||||
);
|
if (!dispatcher) return;
|
||||||
bindTrackingClickGesture(flightState._clickHandler, (click, gesture) => {
|
flightState._selectionRemove = dispatcher.register('military', {
|
||||||
// Camera drags never select or deselect, even if they finish over a plane.
|
// The fleet's icaos, and the tracked visual's own id: clicking it is
|
||||||
// Duration alone is allowed through so a stationary long press can still
|
// this layer's no-op, never empty space.
|
||||||
// select/switch contacts; the destructive empty-space branch below applies
|
owns: (id) =>
|
||||||
// the full travel + duration click classifier.
|
flightState._billboards.has(id) ||
|
||||||
if (!isTrackingSelectionGesture(gesture)) return;
|
(!!flightState._trackedEntity &&
|
||||||
// Cockpit mode owns the camera and keeps the current aircraft as its
|
id === String(flightState._trackedEntity.id)),
|
||||||
// first-person reference. Empty globe clicks are inert until the user
|
onSelect: _onSelect,
|
||||||
// exits with C, Escape, or the dedicated button.
|
onEmptyClick: _onEmptyClick,
|
||||||
if (document.body.classList.contains('cockpit-mode')) return;
|
onEscape: _onEscape,
|
||||||
const picked = viewer.scene.pick(click.position);
|
|
||||||
|
|
||||||
if (picked) {
|
|
||||||
// Clicking the tracked entity itself -- ignore (don't deselect)
|
|
||||||
if (picked.id === flightState._trackedEntity) return;
|
|
||||||
|
|
||||||
// Clicking the plane we're ALREADY tracking (its standalone 3D model or
|
|
||||||
// any pick carrying its icao) — same no-op as the 2D tracked-entity click
|
|
||||||
// above. H1: the model used to have no pick id, so this fell through to
|
|
||||||
// "empty space" and deselected the very plane being tracked.
|
|
||||||
if (flightState._trackedIcao) {
|
|
||||||
const rawPick =
|
|
||||||
typeof picked.id === 'string' ? picked.id : picked.primitive?.id;
|
|
||||||
if (
|
|
||||||
picked.primitive === flightState._trackedModel ||
|
|
||||||
rawPick === flightState._trackedIcao
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// BillboardCollection picks: CesiumJS may expose the billboard as
|
|
||||||
// picked.primitive (with .id = icao24) or directly as picked.id
|
|
||||||
const billboard = picked.primitive;
|
|
||||||
if (
|
|
||||||
billboard &&
|
|
||||||
billboard.id &&
|
|
||||||
flightState._billboards.has(billboard.id)
|
|
||||||
) {
|
|
||||||
_cancelPendingTrackingRestore();
|
|
||||||
_trackFlight(billboard.id, { origin: 'user' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Fallback: some CesiumJS versions surface the id string at picked.id
|
|
||||||
if (
|
|
||||||
picked.id &&
|
|
||||||
typeof picked.id === 'string' &&
|
|
||||||
flightState._billboards.has(picked.id)
|
|
||||||
) {
|
|
||||||
_cancelPendingTrackingRestore();
|
|
||||||
_trackFlight(picked.id, { origin: 'user' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A pick that belongs to a sibling layer (commercial flight, satellite,
|
|
||||||
// vessel, station, CCTV camera…) is not "empty space" — leave tracking
|
|
||||||
// alone and let that layer handle it. resolvePickId String()-coerces the
|
|
||||||
// heterogeneous pick ids (numeric NORAD ids, AIS record objects) so the
|
|
||||||
// registry predicates can recognize them (H2).
|
|
||||||
if (picked) {
|
|
||||||
const pickedId = resolvePickId(picked);
|
|
||||||
if (pickedId && isOwnedByOtherLayer('military', pickedId)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clicked empty space -- deselect only for a clean, short click. A slow
|
|
||||||
// stationary press may select above, but cannot release existing tracking.
|
|
||||||
if (!isTrackingClickGesture(gesture)) return;
|
|
||||||
if (flightState._trackedIcao) {
|
|
||||||
_cancelPendingTrackingRestore();
|
|
||||||
_clearTracking(false, { origin: 'user' });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', _onKeyDown);
|
/** Leave the selection dispatcher and stop watching the follow camera. */
|
||||||
|
|
||||||
|
function _removeClickHandler() {
|
||||||
|
flightState._selectionRemove?.();
|
||||||
|
flightState._selectionRemove = null;
|
||||||
|
if (flightState._trackedEntityChangedRemove) {
|
||||||
|
flightState._trackedEntityChangedRemove();
|
||||||
|
flightState._trackedEntityChangedRemove = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
_emitAwarenessEvent,
|
_emitAwarenessEvent,
|
||||||
@@ -1165,7 +1109,10 @@ export function createTracking({
|
|||||||
_applyPendingTrackingRestore,
|
_applyPendingTrackingRestore,
|
||||||
_cancelPendingTrackingRestore,
|
_cancelPendingTrackingRestore,
|
||||||
_trackFlight,
|
_trackFlight,
|
||||||
_onKeyDown,
|
_onEscape,
|
||||||
|
_onSelect,
|
||||||
|
_onEmptyClick,
|
||||||
_installClickHandler,
|
_installClickHandler,
|
||||||
|
_removeClickHandler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// An aircraft renderer of plain records (src/layers/aircraft/render/
|
||||||
|
// contract.js), for testing an aircraft layer's logic alone: no Cesium, no
|
||||||
|
// Navara. `calls` logs the tracked/follow calls; `sprites` holds the fleet.
|
||||||
|
|
||||||
|
import * as geo from '../geo/index.js';
|
||||||
|
|
||||||
|
/** A renderer of plain records, for the logic alone. */
|
||||||
|
export function recordingAircraftRenderer(engine) {
|
||||||
|
const calls = [];
|
||||||
|
const sprites = new Set();
|
||||||
|
const camera = {
|
||||||
|
positionWC: geo.cartesian3.fromDegrees(-97.7, 30.2, 400_000),
|
||||||
|
positionCartographic: geo.cartographic.fromDegrees(-97.7, 30.2, 400_000),
|
||||||
|
rightWC: geo.cartesian3.create(1, 0, 0),
|
||||||
|
upWC: geo.cartesian3.create(0, 0, 1),
|
||||||
|
directionWC: geo.cartesian3.create(0, -1, 0),
|
||||||
|
heading: 0,
|
||||||
|
pitch: -Math.PI / 2,
|
||||||
|
roll: 0,
|
||||||
|
frustum: { fovy: 1 },
|
||||||
|
};
|
||||||
|
const scene = {
|
||||||
|
camera,
|
||||||
|
canvas: { clientHeight: 768 },
|
||||||
|
frameState: { frameNumber: 1 },
|
||||||
|
};
|
||||||
|
let trackedSeq = 0;
|
||||||
|
let following;
|
||||||
|
const renderer = {
|
||||||
|
kind: 'record',
|
||||||
|
engine,
|
||||||
|
calls,
|
||||||
|
sprites,
|
||||||
|
view: {
|
||||||
|
camera,
|
||||||
|
scene,
|
||||||
|
surfaceOwner: engine,
|
||||||
|
surfaceScene: engine,
|
||||||
|
frameNumber: () => 1,
|
||||||
|
viewportHeightPx: () => 768,
|
||||||
|
frustumTester: () => () => true,
|
||||||
|
horizon: () => ({ isPointVisible: () => true }),
|
||||||
|
worldToWindow: () => ({ x: 0, y: 0 }),
|
||||||
|
requestRender: () => calls.push(['requestRender']),
|
||||||
|
on: () => () => {},
|
||||||
|
holdFrames() {},
|
||||||
|
closeFrameLatch() {},
|
||||||
|
releaseFrames() {},
|
||||||
|
},
|
||||||
|
fleet: {
|
||||||
|
shown: false,
|
||||||
|
attach: () => calls.push(['attach']),
|
||||||
|
detach: () => calls.push(['detach']),
|
||||||
|
attached: () => true,
|
||||||
|
visible: () => renderer.fleet.shown,
|
||||||
|
setVisible(visible) {
|
||||||
|
renderer.fleet.shown = visible;
|
||||||
|
},
|
||||||
|
add(spec) {
|
||||||
|
const sprite = { ...spec };
|
||||||
|
sprites.add(sprite);
|
||||||
|
return sprite;
|
||||||
|
},
|
||||||
|
remove: (sprite) => sprites.delete(sprite),
|
||||||
|
restoreOrder() {},
|
||||||
|
restoreOrderOnEnable() {},
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
live: () => true,
|
||||||
|
setVisible() {},
|
||||||
|
loadFleet: () => Promise.reject(new Error('no models here')),
|
||||||
|
loadTracked: () => Promise.reject(new Error('no models here')),
|
||||||
|
preload: () => Promise.reject(new Error('no models here')),
|
||||||
|
add() {},
|
||||||
|
remove() {},
|
||||||
|
destroy() {},
|
||||||
|
setPose() {},
|
||||||
|
},
|
||||||
|
tracked: {
|
||||||
|
create(spec) {
|
||||||
|
const handle = { id: `tracked-${++trackedSeq}`, spec };
|
||||||
|
calls.push(['tracked.create', handle.id]);
|
||||||
|
return handle;
|
||||||
|
},
|
||||||
|
remove: (handle) => calls.push(['tracked.remove', handle.id]),
|
||||||
|
isLive: () => true,
|
||||||
|
setImage() {},
|
||||||
|
setViewFrom(handle, offset) {
|
||||||
|
handle.viewFrom = offset;
|
||||||
|
},
|
||||||
|
billboardSize: () => null,
|
||||||
|
},
|
||||||
|
follow: {
|
||||||
|
start(handle) {
|
||||||
|
following = handle;
|
||||||
|
calls.push(['follow.start', handle.id]);
|
||||||
|
return () => calls.push(['follow.frameStop']);
|
||||||
|
},
|
||||||
|
refocus: (handle) => renderer.follow.start(handle),
|
||||||
|
release() {
|
||||||
|
following = undefined;
|
||||||
|
calls.push(['follow.release']);
|
||||||
|
},
|
||||||
|
isFollowing: (handle) => following === handle,
|
||||||
|
current: () => following,
|
||||||
|
onChanged: () => () => {},
|
||||||
|
},
|
||||||
|
trails: {
|
||||||
|
create: () => ({
|
||||||
|
setPositions() {},
|
||||||
|
setVisible() {},
|
||||||
|
clear() {},
|
||||||
|
destroy() {},
|
||||||
|
}),
|
||||||
|
createHead: ({ id }) => ({ id, show: true }),
|
||||||
|
removeHead() {},
|
||||||
|
},
|
||||||
|
surfaceExclusions: () => [],
|
||||||
|
destroy: () => calls.push(['destroy']),
|
||||||
|
};
|
||||||
|
return renderer;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user