diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6f0f0bc85..3f94a456a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,10 @@ # God's Eye View Current State +Aircraft detection candidates reuse normalized display text until callsign or +registration changes, including in-place enrichment. Each candidate resolves its +fleet model once per collection. Positions, model ownership and tracked-aircraft +anchors remain live on every collection; contact admission is unchanged. + Civil aircraft share one delayed render timestamp per fleet update. Extrapolated fixes reuse their local coordinate frame until their coordinates change, including in-place ground-height corrections. Update cadence, contact counts, interpolation, diff --git a/src/layers/flights/queries.js b/src/layers/flights/queries.js index 5e72e50b4..89b91f125 100644 --- a/src/layers/flights/queries.js +++ b/src/layers/flights/queries.js @@ -452,8 +452,8 @@ export function createQueries({ const isTracked = icao24 === flightState._trackedIcao; // Keep planes rendered as a 3D model (billboard hidden) so the detection box // doesn't vanish on the 2D→3D handoff; bb.position stays current while hidden. - const model = flightState._models.get(icao24); - const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24); + const model = flightState._models.get(icao24) ?? null; + const modelOwnsVisual = parts.rendering._modelOwnsVisual(icao24, model); if (!isTracked && !bb.show && !modelOwnsVisual) continue; const info = flightState.records.data.get(icao24); let object = flightState._detectionObjects.get(icao24); diff --git a/src/layers/flights/queries.test.mjs b/src/layers/flights/queries.test.mjs index a5f25c829..13957d6cc 100644 --- a/src/layers/flights/queries.test.mjs +++ b/src/layers/flights/queries.test.mjs @@ -80,3 +80,70 @@ test('unchanged candidate text is normalized once across paints', (t) => { assert.equal(calls, 1); }); +test('candidate collection resolves models once and keeps handoffs live', (t) => { + const f = fixture(); + const get = t.mock.method(f.flightState._models, 'get'); + const [candidate] = f.queries.getDetectableObjects(); + assert.equal(get.mock.callCount(), 1); + const model = { + ready: false, + show: true, + modelMatrix: Cesium.Matrix4.fromTranslation( + new Cesium.Cartesian3(10, 20, 30), + ), + }; + f.parts.rendering._modelSpec = () => ({ + visualCenterNative: Cesium.Cartesian3.ZERO, + scale: 1, + }); + f.flightState._models.set('abc123', model); + f.bb.show = false; + assert.deepEqual(f.queries.getDetectableObjects(), []); + model.ready = true; + assert.equal(f.queries.getDetectableObjects()[0], candidate); + assert.deepEqual(candidate.position, new Cesium.Cartesian3(10, 20, 30)); + Cesium.Matrix4.setTranslation( + model.modelMatrix, + new Cesium.Cartesian3(40, 50, 60), + model.modelMatrix, + ); + f.queries.getDetectableObjects(); + assert.deepEqual(candidate.position, new Cesium.Cartesian3(40, 50, 60)); + model.show = false; + assert.deepEqual(f.queries.getDetectableObjects(), []); + f.bb.show = true; + assert.equal(f.queries.getDetectableObjects()[0].position, f.bb.position); + assert.equal(get.mock.callCount(), 6); + model.show = true; + assert.equal(f.parts.rendering._modelOwnsVisual('abc123'), true); + assert.equal(get.mock.callCount(), 7); +}); + +test('tracked ownership and live anchors remain independent of fleet models', () => { + const f = fixture(); + f.flightState._trackedIcao = 'abc123'; + f.flightState._trackedModel = { ready: true, show: true }; + f.parts.rendering._modelSpec = () => ({}); + let anchor = new Cesium.Cartesian3(10, 20, 30); + f.parts.motion._trackedVisualCached = () => anchor; + const [candidate] = f.queries.getDetectableObjects(); + assert.equal(candidate.position, anchor); + assert.equal(candidate.skipLabel, true); + assert.equal(f.parts.rendering._modelOwnsVisual('abc123', null), true); + anchor = new Cesium.Cartesian3(40, 50, 60); + assert.equal(f.queries.getDetectableObjects()[0].position, anchor); + for (const tracked of [ + null, + { ready: false, show: true }, + { ready: true, show: false }, + ]) { + f.flightState._trackedModel = tracked; + assert.equal( + f.parts.rendering._modelOwnsVisual('abc123', { ready: true, show: true }), + false, + ); + } + f.flightState._trackedModel = { ready: true, show: true }; + f.parts.tracking._trackedModelRegimeActive = () => false; + assert.equal(f.parts.rendering._modelOwnsVisual('abc123', null), false); +}); diff --git a/src/layers/flights/rendering.js b/src/layers/flights/rendering.js index d0074c9c9..8aff72e38 100644 --- a/src/layers/flights/rendering.js +++ b/src/layers/flights/rendering.js @@ -383,17 +383,20 @@ export function createRendering({ * null or still-loading tracked model from claiming a visual it is not * drawing yet. * @param {string} icao24 + * @param {Cesium.Model|null} [model] Resolved fleet model; null means absent. * @returns {boolean} */ - function _modelOwnsVisual(icao24) { + function _modelOwnsVisual(icao24, model) { if (icao24 === flightState._trackedIcao) { return ( parts.tracking._trackedModelRegimeActive() && _modelIsRendering(flightState._trackedModel) ); } - return _modelIsRendering(flightState._models.get(icao24)); + return _modelIsRendering( + model === undefined ? flightState._models.get(icao24) : model, + ); } /** A model is the visual only once it actually draws — loaded AND shown.