fix(street-level): IC8 round 3 — no blink when the point budget is re-shared

The round-3 verification found the shared point budget rebuilding a dense
tile by removing its lines before the new ones were built, so central
Paris vanished for 1–2 s after every pan:
- Tiles redrawn for a new share, or for a selected line the cap had left
  out, go through the swap path: the old lines stay until the new ones
  are ready.
- A share must move by 10% to redraw a tile (a 1.6% change rebuilt it).
- The selected line, admitted past the cap, is not counted against it,
  so its tile no longer redraws on every settle.
- A selection the imagery filter hides is not redrawn for.
- Shares are recomputed when a refresh ends loading without a tile
  finishing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-14 15:54:21 +01:00
1 parent da0d370fa3
commit ce3f66a837
2 files changed
+131 -16

No files matched your search

@@ -338,3 +338,98 @@ test('a selected sequence the budget left out is drawn, highlighted', async (t)
coverage.recolorSequence('short', true);
assert.deepEqual([...tile.sequences.keys()].sort(), ['long', 'short']);
});
/** Count the line primitives a coverage adds to the scene. */
function countAdds(viewer) {
const counts = { added: 0 };
for (const collection of [
viewer.scene.groundPrimitives,
viewer.scene.primitives,
]) {
const add = collection.add.bind(collection);
collection.add = (primitive) => {
counts.added++;
return add(primitive);
};
}
return counts;
}
test('a tile redrawn for a new share keeps its old lines until the new ones are ready', async (t) => {
const tuning = {
...TUNING,
maxTiles: 9,
maxSequences: 900,
maxPoints: 90,
keepOrder: true,
};
const { state, coverage, viewer } = setup(t, {
zoomFor: () => ({ zoom: 18, kind: 'sequence' }),
tuning,
decoded: (centre, tile) => {
const dense = tile.x % 2 === 0 && tile.y % 2 === 0;
return {
sequences: dense
? Array.from({ length: 30 }, (_, i) =>
pointsLine(`d${tile.x}/${tile.y}/${i}`, 5, tile, centre),
)
: [pointsLine(`s${tile.x}/${tile.y}`, 2, tile, centre)],
overview: [],
};
},
});
coverage.refresh();
assert.ok(
await until(
() => state.coverage.tiles.size > 1 && !state.coverage.pending.size,
),
);
const redrawn = [...state.coverage.tiles.values()].filter(
(entry) => entry.swap,
);
assert.ok(redrawn.length > 0, 'a dense tile took a bigger share');
for (const entry of redrawn)
for (const { primitive } of entry.swap.old)
assert.ok(
viewer.scene.groundPrimitives.items.has(primitive) ||
viewer.scene.primitives.items.has(primitive),
'the old lines are still on the globe while the new ones build',
);
});
test('a selected line outside the cap does not make its tile redraw on every settle, nor does one the filter hides', async (t) => {
let pano = 'all';
const { state, coverage, viewer } = setup(t, {
tuning: {
...TUNING,
maxTiles: 1,
maxSequences: 10,
maxPoints: 5,
keepOrder: true,
},
decoded: (centre, tile) => ({
sequences: [
pointsLine('long', 5, tile, centre),
{ ...pointsLine('short', 3, tile, centre), isPano: true },
],
overview: [],
}),
});
state.context.getFilter = () => ({ pano, sinceMs: null });
coverage.refresh();
assert.ok(await until(() => state.coverage.tiles.size === 1));
state.sequence.selectedId = 'short';
coverage.recolorSequence('short', true);
const counts = countAdds(viewer);
for (let i = 0; i < 3; i++) {
coverage.refresh();
await new Promise((resolve) => setTimeout(resolve, 20));
}
assert.equal(counts.added, 0, 'settles redraw nothing');
// A selection the filter hides is not drawn, so it is not redrawn for.
pano = 'flat';
coverage.rebuild();
const after = countAdds(viewer);
for (let i = 0; i < 3; i++) coverage.recolorSequence('short', true);
assert.equal(after.added, 0);
});