diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index e759dd493..a76faec8d 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -1867,6 +1867,7 @@ "src/layers/cctv/rendering.js", "src/data/billboardMirror.js", "src/layers/cctv/selection.js", + "src/layers/cctv/pickState.js", "src/layers/cctv/source.js", "src/layers/cctv/state.js", "src/layers/cctv/testing.js", @@ -2518,6 +2519,7 @@ "src/layers/cctv/videoPlayback.js", "src/layers/cctv/rendering.js", "src/layers/cctv/selection.js", + "src/layers/cctv/pickState.js", "src/layers/cctv/source.js", "src/layers/cctv/state.js", "src/layers/cctv/testing.js", @@ -3429,6 +3431,7 @@ "src/layers/cctv/videoPlayback.js", "src/layers/cctv/rendering.js", "src/layers/cctv/selection.js", + "src/layers/cctv/pickState.js", "src/layers/cctv/source.js", "src/layers/cctv/state.js", "src/layers/cctv/testing.js", diff --git a/src/annotations/screenAnnotationRenderer.js b/src/annotations/screenAnnotationRenderer.js index 54ca3205f..27ae2fcb4 100644 --- a/src/annotations/screenAnnotationRenderer.js +++ b/src/annotations/screenAnnotationRenderer.js @@ -424,7 +424,6 @@ export function createScreenAnnotationRenderer( let bottom = painted ? painted.y + painted.h : undefined; const ent = viewer.trackedEntity; - const now = Cesium.JulianDate.now(); const displayPosition = readEntityRecord(ent)?.displayPosition; const world = typeof displayPosition === 'function' ? displayPosition() : null; @@ -436,13 +435,14 @@ export function createScreenAnnotationRenderer( ) : null; - // Billboard box — centered on the anchor, magnified by scaleByDistance. - const bb = ent?.billboard; - if (bb && win && Number.isFinite(win.x) && Number.isFinite(win.y)) { - const baseW = bb.width?.getValue?.(now) ?? 28; - const baseH = bb.height?.getValue?.(now) ?? 28; + // Billboard box — centered on the anchor, magnified by scaleByDistance, from + // the size the tracking layer recorded for its sprite. + const sprite = readEntityRecord(ent)?.sprite; + if (sprite && win && Number.isFinite(win.x) && Number.isFinite(win.y)) { + const baseW = sprite.width ?? 28; + const baseH = sprite.height ?? 28; let scale = 1; - const sbd = bb.scaleByDistance?.getValue?.(now); + const sbd = sprite.scaleByDistance; if (sbd) scale = nearFarValue( sbd, diff --git a/src/data/cctv.test.mjs b/src/data/cctv.test.mjs index d7cc131b5..483dec399 100644 --- a/src/data/cctv.test.mjs +++ b/src/data/cctv.test.mjs @@ -80,6 +80,7 @@ import { LIVE_CAMERA_COLOR, } from '../layers/cctv/policy.js'; import { mirrorBillboard } from './billboardMirror.js'; +import { cctvPicks } from '../layers/cctv/pickState.js'; const ASPECT = 16 / 9; const toRad = (deg) => (deg * Math.PI) / 180; @@ -1130,10 +1131,10 @@ test('CCTV empty-click gate excludes every identified scene object, ADJUST, and test('CCTV camera extraction requires layer ownership, not a colliding sibling ID', () => { const billboard = { id: 'shared-id' }; - const ownedCoverageEntity = { - id: 'cctv-shared-id-cap', - properties: { cctvCameraId: 'shared-id' }, - }; + const ownedCoverageEntity = { id: 'cctv-shared-id-cap' }; + cctvPicks.write(ownedCoverageEntity).cameraId = 'shared-id'; + const strayEntity = { id: 'cctv-shared-id-stray' }; + cctvPicks.write(strayEntity).cameraId = 'shared-id'; const record = { ...makeDeselectRecord('shared-id'), billboard, @@ -1176,6 +1177,11 @@ test('CCTV camera extraction requires layer ownership, not a colliding sibling I null, 'a sibling cannot impersonate CCTV by copying its property shape', ); + assert.equal( + _extractPickedCameraIdForTest({ id: strayEntity }), + null, + 'a camera id alone is not ownership: the record must hold the entity', + ); } finally { _setCctvCoverageStateForTest({ enabled: false }); } diff --git a/src/data/cctvGizmo.js b/src/data/cctvGizmo.js index fa15c5532..8e3efc863 100644 --- a/src/data/cctvGizmo.js +++ b/src/data/cctvGizmo.js @@ -118,8 +118,8 @@ export function signedAngleDelta(fromRad, toRad) { // --------------------------------------------------------------------------- /** Entity id prefix for every gizmo part — the layer's pick-owner regex and - * click-to-select guard key off this. Gizmo entities NEVER carry a - * `cctvCameraId` property, so the layer's selection path ignores them. */ + * click-to-select guard key off this. Gizmo entities NEVER get a camera id in + * the layer's pick store, so the layer's selection path ignores them. */ export const GIZMO_ID_PREFIX = 'cctv-gizmo-'; const RING_SEGMENTS = 96; diff --git a/src/data/entityRecords.js b/src/data/entityRecords.js index d0fddf1dc..4cf601ac7 100644 --- a/src/data/entityRecords.js +++ b/src/data/entityRecords.js @@ -12,6 +12,8 @@ * - `visualPosition()`: where a 3D model is actually drawn, for the tracked card. * - `labelModel`: the tracked card's `{ title, details, accent, … }`. * - `selectionOrigin`: what started the selection (`'user'`, …). + * - `sprite`: a tracked billboard's pushed `{ width, height, scale, scaleByDistance }`, for + * the close-range camera guard and the screen annotations' tracked footprint. * * A layer's private per-entity state goes in its own store ({@link createEntityStore}). */ diff --git a/src/layers/cctv/geometry.js b/src/layers/cctv/geometry.js index 7a816374d..f80650854 100644 --- a/src/layers/cctv/geometry.js +++ b/src/layers/cctv/geometry.js @@ -17,6 +17,7 @@ import { requiredPlaneLift, } from '../../data/cctvFootprint.js'; import { setBillboard } from '../../data/billboardMirror.js'; +import { cctvPicks } from './pickState.js'; export function createGeometry({ state: layerState, services, parts, source }) { const { warmGroundFloor, cachedGroundFloor } = services.ground; @@ -700,16 +701,18 @@ export function createGeometry({ state: layerState, services, parts, source }) { record.position = positions.mount; } - const addPolyline = (role, linePositions) => - layerState._viewer.entities.add({ + const addPolyline = (role, linePositions) => { + const entity = layerState._viewer.entities.add({ id: `cctv-${camera.id}-${role}`, - properties: { cctvCameraId: camera.id }, polyline: { positions: linePositions, width: 1.2, material: IDLE_COVERAGE_COLOR, }, }); + cctvPicks.write(entity).cameraId = camera.id; + return entity; + }; const entities = [ addPolyline('ray-tl', [positions.mount, positions.tl]), diff --git a/src/layers/cctv/pickState.js b/src/layers/cctv/pickState.js new file mode 100644 index 000000000..e24012d97 --- /dev/null +++ b/src/layers/cctv/pickState.js @@ -0,0 +1,7 @@ +import { createEntityStore } from '../../data/entityRecords.js'; + +/** + * The camera a picked coverage ray or projection plane belongs to: `cameraId` + * is the upstream camera id. Gizmo entities never get one. + */ +export const cctvPicks = createEntityStore(); diff --git a/src/layers/cctv/projection.js b/src/layers/cctv/projection.js index e32db4fd4..64b044104 100644 --- a/src/layers/cctv/projection.js +++ b/src/layers/cctv/projection.js @@ -7,6 +7,7 @@ import { PROJECTION_CANVAS_WIDTH, PROJECTION_CANVAS_HEIGHT, } from './policy.js'; +import { cctvPicks } from './pickState.js'; export function createProjection({ state: layerState, @@ -134,7 +135,6 @@ export function createProjection({ }); runtime.planeEntity = layerState._viewer.entities.add({ id: `cctv-${record.camera.id}-plane`, - properties: { cctvCameraId: record.camera.id }, show: false, position: positions.capCenter, orientation: parts.model.planeOrientationFor( @@ -152,6 +152,7 @@ export function createProjection({ outlineColor: PLANE_OUTLINE_COLOR, }, }); + cctvPicks.write(runtime.planeEntity).cameraId = record.camera.id; return runtime.planeEntity; } diff --git a/src/layers/cctv/selection.js b/src/layers/cctv/selection.js index c86425fd0..23473258c 100644 --- a/src/layers/cctv/selection.js +++ b/src/layers/cctv/selection.js @@ -3,6 +3,7 @@ import { isTrackingClickGesture, } from '../../data/trackingClickGesture.js'; import * as Cesium from 'cesium'; +import { cctvPicks } from './pickState.js'; export function createSelection({ state: layerState, @@ -207,7 +208,7 @@ export function createSelection({ /** * Extracts a camera ID from a Cesium pick result by checking billboard IDs, - * primitive IDs, and entity cctvCameraId properties. + * primitive IDs, and the camera ids the layer keeps for its coverage entities. * @param {Object|null} picked - Result from scene.pick(). * @returns {string|null} Camera ID, or null if the pick is not a CCTV entity. */ @@ -215,17 +216,13 @@ export function createSelection({ function extractPickedCameraId(picked) { if (!picked) return null; - const entity = picked.id?.properties + const entity = cctvPicks.read(picked.id) ? picked.id - : picked.primitive?.id?.properties + : cctvPicks.read(picked.primitive?.id) ? picked.primitive.id : null; - const maybeProp = entity?.properties?.cctvCameraId; - if (maybeProp) { - const value = - typeof maybeProp.getValue === 'function' - ? maybeProp.getValue(Cesium.JulianDate.now()) - : maybeProp; + const value = cctvPicks.read(entity)?.cameraId; + if (value !== undefined) { const record = typeof value === 'string' ? layerState._recordById.get(value) : null; const ownsCoverageEntity = Boolean( diff --git a/src/layers/earthquakes/index.js b/src/layers/earthquakes/index.js index 0d522fcb9..63d764a7f 100644 --- a/src/layers/earthquakes/index.js +++ b/src/layers/earthquakes/index.js @@ -23,6 +23,8 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) { let _viewer = null; let _request = null; let _dataSource = null; + // What the discs were drawn from, so analyst reads come from Core, not the entities. + let _events = []; let _count = 0; let _lastUpdate = null; let _lastError = null; @@ -77,6 +79,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) { return false; const nextEntities = []; + const nextEvents = []; let count = 0; const overlayEntries = []; @@ -98,6 +101,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) { const outlineAlpha = isSignificant ? 1.0 : 0.8; const position = Cesium.Cartesian3.fromDegrees(lon, lat); + nextEvents.push({ usgsId, mag, place, time, depthKm, lon, lat }); nextEntities.push( new Cesium.Entity({ id: `earthquake:${stableId}`, @@ -115,14 +119,6 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) { outlineWidth: isSignificant ? 3 : 2, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, }, - properties: { - // Analyst seam (additive): the USGS event id (e.g. "us7000abcd"). - usgsId, - mag, - place, - time, - depth: depthKm, - }, }), ); overlayEntries.push( @@ -137,6 +133,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) { _dataSource.entities.removeAll(); for (const entity of nextEntities) _dataSource.entities.add(entity); + _events = nextEvents; if (_enabled) { overlayHost.setEntries( EARTHQUAKE_OVERLAY_SOURCE_ID, @@ -176,6 +173,7 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) { viewer.dataSources.remove(_dataSource, true); _dataSource = null; } + _events = []; _count = 0; _lastUpdate = null; _lastError = null; @@ -190,39 +188,24 @@ export function createEarthquakesLayer({ source, overlayHost } = {}) { * @returns {Array} See mapAnalystRecord for the record shape. */ getAnalystRecords(maxCount = 2000) { - if (!_dataSource || !_dataSource.show) return []; - const entities = _dataSource.entities.values; - if (!entities.length) return []; + if (!_dataSource || !_enabled || !_events.length) return []; const limit = Number.isFinite(maxCount) ? Math.max(1, Math.floor(maxCount)) : 2000; - const now = Cesium.JulianDate.now(); - const result = []; - for (const entity of entities) { - if (result.length >= limit) break; - const cartesian = entity.position - ? entity.position.getValue(now) - : null; - const carto = cartesian - ? Cesium.Cartographic.fromCartesian(cartesian) - : null; - const p = entity.properties; - result.push( - mapAnalystRecord( - { - id: p?.usgsId?.getValue(now) ?? null, - mag: p?.mag?.getValue(now), - place: p?.place?.getValue(now), - time: p?.time?.getValue(now), - depth: p?.depth?.getValue(now), - lat: carto ? Cesium.Math.toDegrees(carto.latitude) : null, - lon: carto ? Cesium.Math.toDegrees(carto.longitude) : null, - }, - result.length, - ), - ); - } - return result; + return _events.slice(0, limit).map((event, index) => + mapAnalystRecord( + { + id: event.usgsId ?? null, + mag: event.mag, + place: event.place, + time: event.time, + depth: event.depthKm, + lat: event.lat, + lon: event.lon, + }, + index, + ), + ); }, getStats() { diff --git a/src/layers/earthquakes/ownership.test.mjs b/src/layers/earthquakes/ownership.test.mjs index 9951935de..b13cc005e 100644 --- a/src/layers/earthquakes/ownership.test.mjs +++ b/src/layers/earthquakes/ownership.test.mjs @@ -88,3 +88,38 @@ test('USGS body completion honors cancellation even with an uncooperative transp name: 'AbortError', }); }); +test('analyst records come from the events the layer drew, and only while it is on', async () => { + const h = harness({ + getSnapshot: async () => [ + row, + { ...row, stableId: 'event-b', usgsId: 'event-b', mag: 5.5 }, + ], + }); + await h.layer.update(h.viewer); + assert.deepEqual(h.layer.getAnalystRecords(), [ + { + id: 'event-a', + magnitude: 4, + depthKm: 3, + lat: 20, + lon: 30, + timeMs: 1000, + place: 'Fixture', + }, + { + id: 'event-b', + magnitude: 5.5, + depthKm: 3, + lat: 20, + lon: 30, + timeMs: 1000, + place: 'Fixture', + }, + ]); + assert.equal(h.layer.getAnalystRecords(1).length, 1); + const [entity] = h.sources[0].entities.values; + assert.equal(entity.properties, undefined, 'the entity carries drawing only'); + h.layer.disable(h.viewer); + assert.deepEqual(h.layer.getAnalystRecords(), []); + h.layer.destroy(h.viewer); +}); diff --git a/src/layers/flights/motion.js b/src/layers/flights/motion.js index 6fe988f12..309966b9b 100644 --- a/src/layers/flights/motion.js +++ b/src/layers/flights/motion.js @@ -37,6 +37,7 @@ import { } from './policy.js'; import { billboardState, setBillboard } from '../../data/billboardMirror.js'; import { modelState } from '../../data/modelMirror.js'; +import { readEntityRecord } from '../../data/entityRecords.js'; export function createMotion({ flightState, @@ -372,16 +373,14 @@ export function createMotion({ ); } - const billboard = flightState._trackedEntity?.billboard; - const time = flightState._viewer.clock.currentTime; - const width = billboard?.width?.getValue(time) ?? 28; - const height = billboard?.height?.getValue(time) ?? 28; + const sprite = readEntityRecord(flightState._trackedEntity)?.sprite; + const width = sprite?.width ?? 28; + const height = sprite?.height ?? 28; const scale = - billboard?.scale?.getValue(time) ?? + sprite?.scale ?? (CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1); const scaleByDistance = - billboard?.scaleByDistance?.getValue(time) ?? - TRACKED_BILLBOARD_SCALE_BY_DISTANCE; + sprite?.scaleByDistance ?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE; const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM); return Math.max(width, height) * scale * distanceScale; } diff --git a/src/layers/flights/tracking.js b/src/layers/flights/tracking.js index 9969ad88e..c29ff9b20 100644 --- a/src/layers/flights/tracking.js +++ b/src/layers/flights/tracking.js @@ -23,6 +23,7 @@ import { RENDER_DELAY_SEC, TRAIL_COLOR, CYAN_TRANSPARENT, + TRACKED_BILLBOARD_SCALE_BY_DISTANCE, } from './policy.js'; import { roundHeadingDegrees } from '../../data/headingDegrees.js'; import { entityRecord, readEntityRecord } from '../../data/entityRecords.js'; @@ -952,6 +953,17 @@ export function createTracking({ // entity is what makes the follow-camera's bounding sphere always ready (see _trackedModel). // Keep Cesium's generated entity ID: re-init without destroy can temporarily // overlap collections, and an explicit ICAO ID would throw on that duplicate. + // What the tracked billboard is drawn at, kept in its entity record so the + // camera guard and screen annotations size it from Core. + const sprite = { + width: 28, + height: 28, + scale: + CLASS_SCALE_2D[ + flightState.records.data.get(flightState._trackedIcao)?.klass + ] || 1, + scaleByDistance: TRACKED_BILLBOARD_SCALE_BY_DISTANCE, + }; flightState._trackedEntity = flightState._viewer.entities.add({ position: positionProperty, // Force Cesium's built-in EntityView and our close-range camera guard to @@ -967,12 +979,9 @@ export function createTracking({ ), TRACKED_ICON_PX, ), - width: 28, - height: 28, - scale: - CLASS_SCALE_2D[ - flightState.records.data.get(flightState._trackedIcao)?.klass - ] || 1, + width: sprite.width, + height: sprite.height, + scale: sprite.scale, // Solid cyan 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). color: new Cesium.CallbackProperty( @@ -983,7 +992,7 @@ export function createTracking({ false, ), sizeInMeters: false, - scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5), + scaleByDistance: sprite.scaleByDistance, alignedAxis: Cesium.Cartesian3.ZERO, // The tracked target must never vanish into tile geometry — tracking a // taxiing plane at street level would otherwise bury the cyan icon inside @@ -1016,6 +1025,7 @@ export function createTracking({ }); entityRecord(flightState._trackedEntity).selectionOrigin = origin; entityRecord(flightState._trackedEntity).trackedId = `flights:${icao24}`; + entityRecord(flightState._trackedEntity).sprite = sprite; entityRecord(flightState._trackedEntity).labelModel = trackedLabelModelFromText(_trackedLabelText(icao24), '#39d0ff'); diff --git a/src/layers/localAdsb/index.js b/src/layers/localAdsb/index.js index 2eb13b80a..8fde6069d 100644 --- a/src/layers/localAdsb/index.js +++ b/src/layers/localAdsb/index.js @@ -45,6 +45,12 @@ const FEET_TO_METERS = 0.3048; const RENDER_HOLD_ID = 'local-adsb'; /** Picks looked through under a click for a local aircraft. */ const CLICK_DRILL_LIMIT = 8; +/** Marker billboard size and distance scaling, as pushed and as recorded. */ +const MARKER_SPRITE = Object.freeze({ + width: 20, + height: 20, + scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8_000_000, 0.5), +}); /** Trail lines pass through their aircraft; a click never stops on one. */ const TRAIL_PICK_PREFIX = 'gev-trail:'; /** The DISPLAY rail's first-run 3D preference, used when no reader is wired. */ @@ -265,6 +271,7 @@ export function createLocalAdsbLayer({ if (marker.iconScale !== scale) { marker.iconScale = scale; billboard.scale = scale; + entityRecord(marker.entity).sprite.scale = scale; } } @@ -437,12 +444,12 @@ export function createLocalAdsbLayer({ position: new Cesium.CallbackProperty(() => marker.position, false), billboard: { image: aircraftIcon('airliner'), - width: 20, - height: 20, + width: MARKER_SPRITE.width, + height: MARKER_SPRITE.height, scale: 1, color, sizeInMeters: false, - scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8_000_000, 0.5), + scaleByDistance: MARKER_SPRITE.scaleByDistance, alignedAxis: Cesium.Cartesian3.ZERO, rotation: new Cesium.CallbackProperty( () => markerRotation(marker), @@ -460,6 +467,8 @@ export function createLocalAdsbLayer({ }, }); entityRecord(marker.entity).trackedId = id; + // Sized from Core when Cesium tracks the marker (screen annotations). + entityRecord(marker.entity).sprite = { ...MARKER_SPRITE, scale: 1 }; entityRecord(marker.entity).displayPosition = () => marker.position; } refreshClass(marker); diff --git a/src/layers/military/motion.js b/src/layers/military/motion.js index eb7ef13b9..9309d4378 100644 --- a/src/layers/military/motion.js +++ b/src/layers/military/motion.js @@ -25,6 +25,7 @@ import { COURSE_MAX_DPS, } from './policy.js'; import { modelState } from '../../data/modelMirror.js'; +import { readEntityRecord } from '../../data/entityRecords.js'; export function createMotion({ flightState, @@ -250,17 +251,15 @@ export function createMotion({ ); } - const billboard = flightState._trackedEntity?.billboard; - const time = flightState._viewer.clock.currentTime; - const width = billboard?.width?.getValue(time) ?? 28; - const height = billboard?.height?.getValue(time) ?? 28; + const sprite = readEntityRecord(flightState._trackedEntity)?.sprite; + const width = sprite?.width ?? 28; + const height = sprite?.height ?? 28; const scale = - billboard?.scale?.getValue(time) ?? + sprite?.scale ?? BILLBOARD_SCALE * (CLASS_SCALE_2D[flightState.records.data.get(icao24)?.klass] || 1); const scaleByDistance = - billboard?.scaleByDistance?.getValue(time) ?? - TRACKED_BILLBOARD_SCALE_BY_DISTANCE; + sprite?.scaleByDistance ?? TRACKED_BILLBOARD_SCALE_BY_DISTANCE; const distanceScale = nearFarScalarValueAtDistance(scaleByDistance, rangeM); return Math.max(width, height) * scale * distanceScale; } diff --git a/src/layers/military/tracking.js b/src/layers/military/tracking.js index 7000950ad..24db00d62 100644 --- a/src/layers/military/tracking.js +++ b/src/layers/military/tracking.js @@ -24,6 +24,7 @@ import { GROUND_SCALE, AMBER_TRANSPARENT, TRACKED_ICON_COLOR, + TRACKED_BILLBOARD_SCALE_BY_DISTANCE, } from './policy.js'; import { roundHeadingDegrees } from '../../data/headingDegrees.js'; import { entityRecord, readEntityRecord } from '../../data/entityRecords.js'; @@ -915,6 +916,18 @@ export function createTracking({ // 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 // what makes the follow-camera's bounding sphere always ready (see _trackedModel). + // What the tracked billboard is drawn at, kept in its entity record so the + // camera guard and screen annotations size it from Core. + const sprite = { + width: 28, + height: 28, + scale: + BILLBOARD_SCALE * + (CLASS_SCALE_2D[ + flightState.records.data.get(flightState._trackedIcao)?.klass + ] || 1), + scaleByDistance: TRACKED_BILLBOARD_SCALE_BY_DISTANCE, + }; flightState._trackedEntity = flightState._viewer.entities.add({ position: positionProperty, // Keep Cesium's EntityView in the same ENU frame as the close-range @@ -928,13 +941,9 @@ export function createTracking({ ), TRACKED_ICON_PX, ), - width: 28, - height: 28, - scale: - BILLBOARD_SCALE * - (CLASS_SCALE_2D[ - flightState.records.data.get(flightState._trackedIcao)?.klass - ] || 1), + width: sprite.width, + height: sprite.height, + scale: sprite.scale, // 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). color: new Cesium.CallbackProperty( @@ -945,7 +954,7 @@ export function createTracking({ false, ), sizeInMeters: false, - scaleByDistance: new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5), + scaleByDistance: sprite.scaleByDistance, alignedAxis: Cesium.Cartesian3.ZERO, // The tracked target must never vanish into tile geometry — tracking a // taxiing plane at street level would otherwise bury the amber icon inside @@ -978,6 +987,7 @@ export function createTracking({ }); entityRecord(flightState._trackedEntity).selectionOrigin = origin; entityRecord(flightState._trackedEntity).trackedId = `military:${icao24}`; + entityRecord(flightState._trackedEntity).sprite = sprite; entityRecord(flightState._trackedEntity).labelModel = trackedLabelModelFromText(_buildTrackedLabel(info, icao24), '#ffd166');