Drop the late feature changes of deleted Navara vector layers, which kept the view from going idle

This commit is contained in:
German Khokhlov committed 2026-11-05 01:11:39 +04:00
1 parent 843c4a111d
commit e1d3a588fc
5 files changed
+85 -1

No files matched your search

+1
View File
@@ -3700,6 +3700,7 @@
"src/globe/navara/index.js", "src/globe/navara/index.js",
"src/globe/navara/input.js", "src/globe/navara/input.js",
"src/globe/navara/internals.js", "src/globe/navara/internals.js",
"src/globe/navara/featureQueue.js",
"src/globe/navara/look.js", "src/globe/navara/look.js",
"src/globe/navara/maps.js", "src/globe/navara/maps.js",
"src/globe/navara/sharpen.js", "src/globe/navara/sharpen.js",
+3 -1
View File
@@ -90,7 +90,7 @@ function rayRaisedEllipsoid({ origin, direction }, height) {
/** /**
* @param {object} view An initialised Navara ThreeView. * @param {object} view An initialised Navara ThreeView.
* @param {{ * @param {{
* hooks: { tileRoots: Function, isIdle: Function, retainTileGeometry: Function }, * hooks: { tileRoots: Function, isIdle: Function, retainTileGeometry: Function, dropOrphanFeatureChanges?: Function },
* container?: HTMLElement, * container?: HTMLElement,
* fetch?: typeof fetch, * fetch?: typeof fetch,
* clock?: object, * clock?: object,
@@ -196,6 +196,8 @@ export function createNavaraEngine(
let drawing = false; let drawing = false;
const handlers = { const handlers = {
preUpdate: (time) => { preUpdate: (time) => {
// Before the view decides whether it is idle (internals.js).
hooks.dropOrphanFeatureChanges?.();
engineClock.beginFrame(); engineClock.beginFrame();
frames.preUpdate.raiseEvent(time); frames.preUpdate.raiseEvent(time);
}, },
+27
View File
@@ -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;
}
+25
View File
@@ -6,6 +6,7 @@
import { ModelMesh } from '@navaramap/three'; import { ModelMesh } from '@navaramap/three';
import { patchCascadedLightCopy } from './cascadedLightCopy.js'; import { patchCascadedLightCopy } from './cascadedLightCopy.js';
import { dropOrphanFeatureChanges } from './featureQueue.js';
export const NAVARA_INTERNALS_VERSION = '0.2.1'; export const NAVARA_INTERNALS_VERSION = '0.2.1';
@@ -156,11 +157,35 @@ export function keepCascadedLightIds(view) {
: null; : 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. */ /** The hooks createNavaraEngine takes. */
export const navaraHooks = (view) => ({ export const navaraHooks = (view) => ({
retainTileGeometry, retainTileGeometry,
tileRoots: () => tileRoots(view), tileRoots: () => tileRoots(view),
isIdle: () => isIdle(view), isIdle: () => isIdle(view),
dropOrphanFeatureChanges: () =>
dropOrphanFeatureChanges(
featureChangeQueue(view),
(id) => view.findLayerById(id) != null,
),
}); });
/** /**
+29
View File
@@ -44,3 +44,32 @@ test('only internals.js reaches private Navara members', () => {
); );
assert.deepEqual(offenders, []); 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,
);
});