From e1d3a588fc81921d808ad5b8c00037a6936195bf Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 5 Nov 2026 01:11:39 +0400 Subject: [PATCH] Drop the late feature changes of deleted Navara vector layers, which kept the view from going idle --- scripts/package-boundaries.json | 1 + src/globe/navara/createNavaraEngine.js | 4 +++- src/globe/navara/featureQueue.js | 27 ++++++++++++++++++++++++ src/globe/navara/internals.js | 25 ++++++++++++++++++++++ src/globe/navara/internals.test.mjs | 29 ++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/globe/navara/featureQueue.js diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index d2a71274d..cdf804f23 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -3700,6 +3700,7 @@ "src/globe/navara/index.js", "src/globe/navara/input.js", "src/globe/navara/internals.js", + "src/globe/navara/featureQueue.js", "src/globe/navara/look.js", "src/globe/navara/maps.js", "src/globe/navara/sharpen.js", diff --git a/src/globe/navara/createNavaraEngine.js b/src/globe/navara/createNavaraEngine.js index 4c33ef059..2908e55ef 100644 --- a/src/globe/navara/createNavaraEngine.js +++ b/src/globe/navara/createNavaraEngine.js @@ -90,7 +90,7 @@ function rayRaisedEllipsoid({ origin, direction }, height) { /** * @param {object} view An initialised Navara ThreeView. * @param {{ - * hooks: { tileRoots: Function, isIdle: Function, retainTileGeometry: Function }, + * hooks: { tileRoots: Function, isIdle: Function, retainTileGeometry: Function, dropOrphanFeatureChanges?: Function }, * container?: HTMLElement, * fetch?: typeof fetch, * clock?: object, @@ -196,6 +196,8 @@ export function createNavaraEngine( let drawing = false; const handlers = { preUpdate: (time) => { + // Before the view decides whether it is idle (internals.js). + hooks.dropOrphanFeatureChanges?.(); engineClock.beginFrame(); frames.preUpdate.raiseEvent(time); }, diff --git a/src/globe/navara/featureQueue.js b/src/globe/navara/featureQueue.js new file mode 100644 index 000000000..9eac12a70 --- /dev/null +++ b/src/globe/navara/featureQueue.js @@ -0,0 +1,27 @@ +// Logic over a Navara view internal that internals.js reads (the queued +// feature changes), kept apart so Node tests can load it without the SDK. + +/** + * Drop the queued feature changes of vector layers that no longer exist, and + * return how many went. Navara processes a change only for a feature it + * knows, and keeps any other in its queue for good: a change that arrives + * after its layer was deleted (a polygon display replacing its layers while + * their tiles are still being cut) leaves the queue non-empty, so the view + * never goes idle and tilesLoaded() stays false. + * @param {{ events: object[], remove: (index: number) => void } | null} queue + * The queued changes (internals.js featureChangeQueue). + * @param {(layerId: string) => boolean} layerExists + */ +export function dropOrphanFeatureChanges(queue, layerExists) { + if (!queue?.events.length) return 0; + let dropped = 0; + for (let index = queue.events.length - 1; index >= 0; index--) { + const event = queue.events[index]; + const layerId = event?.layer_id; + if (layerId == null || layerExists(String(layerId))) continue; + queue.remove(index); + event.free?.(); + dropped++; + } + return dropped; +} diff --git a/src/globe/navara/internals.js b/src/globe/navara/internals.js index c43878a06..e81db706c 100644 --- a/src/globe/navara/internals.js +++ b/src/globe/navara/internals.js @@ -6,6 +6,7 @@ import { ModelMesh } from '@navaramap/three'; import { patchCascadedLightCopy } from './cascadedLightCopy.js'; +import { dropOrphanFeatureChanges } from './featureQueue.js'; export const NAVARA_INTERNALS_VERSION = '0.2.1'; @@ -156,11 +157,35 @@ export function keepCascadedLightIds(view) { : null; } +/** + * The view's queued feature changes, for featureQueue.js, or null. Navara + * keeps a change for a feature it does not know in that queue for good, so + * a deleted layer's late changes keep the view from going idle. Private: + * view._eventManager (stacks, removeAt). Upstream ask: drop a deleted + * layer's pending feature events, or skip them in needsUpdate. + */ +export function featureChangeQueue(view) { + const manager = view._eventManager; + const events = manager?.stacks?.renderable_feature_changed; + return events + ? { + events, + remove: (index) => + manager.removeAt('renderable_feature_changed', index), + } + : null; +} + /** The hooks createNavaraEngine takes. */ export const navaraHooks = (view) => ({ retainTileGeometry, tileRoots: () => tileRoots(view), isIdle: () => isIdle(view), + dropOrphanFeatureChanges: () => + dropOrphanFeatureChanges( + featureChangeQueue(view), + (id) => view.findLayerById(id) != null, + ), }); /** diff --git a/src/globe/navara/internals.test.mjs b/src/globe/navara/internals.test.mjs index 75777b2b1..7803e0c17 100644 --- a/src/globe/navara/internals.test.mjs +++ b/src/globe/navara/internals.test.mjs @@ -44,3 +44,32 @@ test('only internals.js reaches private Navara members', () => { ); assert.deepEqual(offenders, []); }); + +test('feature changes of a deleted vector layer leave the queue; live ones stay', async () => { + const { dropOrphanFeatureChanges } = await import('./featureQueue.js'); + const freed = []; + const change = (layerId) => ({ + layer_id: layerId, + free: () => freed.push(layerId), + }); + const events = [change('gone'), change('live'), change('gone'), change(null)]; + const queue = { events, remove: (index) => events.splice(index, 1) }; + assert.equal( + dropOrphanFeatureChanges(queue, (id) => id === 'live'), + 2, + ); + assert.deepEqual( + events.map((event) => event.layer_id), + ['live', null], + ); + assert.deepEqual(freed, ['gone', 'gone']); + // No queue, or an empty one, costs nothing. + assert.equal( + dropOrphanFeatureChanges(null, () => false), + 0, + ); + assert.equal( + dropOrphanFeatureChanges({ events: [], remove() {} }, () => false), + 0, + ); +});