mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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:
1 parent
da0d370fa3
commit
ce3f66a837
2 files changed
+131
-16
No files matched your search
@@ -12,6 +12,8 @@ import { densifyLine, MESH_DENSIFY_DEG } from './groundCast.js';
|
|||||||
import { MESH_SAMPLE_RADIUS_M } from './meshSampler.js';
|
import { MESH_SAMPLE_RADIUS_M } from './meshSampler.js';
|
||||||
import { tileBounds, tilesForBbox } from './tileMath.js';
|
import { tileBounds, tilesForBbox } from './tileMath.js';
|
||||||
|
|
||||||
|
/** A tile's shared point cap must move by this much to redraw it. */
|
||||||
|
const SHARE_HYSTERESIS = 0.1;
|
||||||
/** Draped lines stay at most this long while a tile's cast lines build. */
|
/** Draped lines stay at most this long while a tile's cast lines build. */
|
||||||
export const SWAP_MAX_WAIT_MS = 4000;
|
export const SWAP_MAX_WAIT_MS = 4000;
|
||||||
/** Tiles touched by new mesh samples are redrawn at most this often. */
|
/** Tiles touched by new mesh samples are redrawn at most this often. */
|
||||||
@@ -164,9 +166,9 @@ export function createLineCoverage({
|
|||||||
const size = pointsOf(sequence);
|
const size = pointsOf(sequence);
|
||||||
demand += size;
|
demand += size;
|
||||||
// The selected sequence is always drawn: its highlight is the answer
|
// The selected sequence is always drawn: its highlight is the answer
|
||||||
// to a click, whatever the caps leave out.
|
// to a click, whatever the caps leave out. It is outside the cap, so
|
||||||
|
// it is not counted against it.
|
||||||
if (sequence.id === selectedId) {
|
if (sequence.id === selectedId) {
|
||||||
points += size;
|
|
||||||
drawn.push(sequence);
|
drawn.push(sequence);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -204,32 +206,45 @@ export function createLineCoverage({
|
|||||||
left -= cap;
|
left -= cap;
|
||||||
const before = entry.pointCap ?? perTilePointCap;
|
const before = entry.pointCap ?? perTilePointCap;
|
||||||
entry.pointCap = cap;
|
entry.pointCap = cap;
|
||||||
// Only a change that alters what is drawn: more room for a tile that
|
// Only a change that alters what is drawn, by enough to be worth a
|
||||||
// had lines left out, or less than it draws now.
|
// rebuild: more room for a tile that had lines left out, or well
|
||||||
if (
|
// under what it draws now (a small overshoot stays until the next
|
||||||
(cap > before && entry.pointsDrawn < entry.pointDemand) ||
|
// natural redraw).
|
||||||
cap < entry.pointsDrawn
|
const grows =
|
||||||
)
|
cap > before * (1 + SHARE_HYSTERESIS) &&
|
||||||
changed.push(entry);
|
entry.pointsDrawn < entry.pointDemand;
|
||||||
|
const shrinks = cap < entry.pointsDrawn * (1 - SHARE_HYSTERESIS);
|
||||||
|
if (grows || shrinks) changed.push(entry);
|
||||||
});
|
});
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Once loading settles: redraw the tiles the shared budget changes. */
|
/**
|
||||||
|
* Once loading settles: redraw the tiles the shared budget changes,
|
||||||
|
* keeping their old lines until the new ones are ready (no blink).
|
||||||
|
*/
|
||||||
function shareBudget() {
|
function shareBudget() {
|
||||||
const changed = shareCaps();
|
const changed = shareCaps();
|
||||||
for (const entry of changed) redrawEntry(entry);
|
for (const entry of changed) redrawEntry(entry, { swap: true });
|
||||||
if (changed.length) {
|
if (changed.length) {
|
||||||
requestRender();
|
requestRender();
|
||||||
notify();
|
notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Draw one tile again from its decoded lines (caps or selection changed). */
|
/**
|
||||||
function redrawEntry(entry) {
|
* Draw one tile again from its decoded lines (filter, caps or selection
|
||||||
|
* changed). With `swap`, its old lines stay until the new ones are ready.
|
||||||
|
*/
|
||||||
|
function redrawEntry(entry, { swap = false } = {}) {
|
||||||
cancelCast(entry);
|
cancelCast(entry);
|
||||||
entry.castRequested = false;
|
entry.castRequested = false;
|
||||||
entry.meshParts = null;
|
entry.meshParts = null;
|
||||||
|
if (swap && entry.kind === 'sequence' && entry.primitives?.length) {
|
||||||
|
finishSwap(entry);
|
||||||
|
redrawTile(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
detachPrimitive(entry);
|
detachPrimitive(entry);
|
||||||
attachPrimitive(entry);
|
attachPrimitive(entry);
|
||||||
}
|
}
|
||||||
@@ -825,7 +840,10 @@ export function createLineCoverage({
|
|||||||
state.coverage.pending.delete(key);
|
state.coverage.pending.delete(key);
|
||||||
}
|
}
|
||||||
for (const tile of tiles) loadTile(tile, kind);
|
for (const tile of tiles) loadTile(tile, kind);
|
||||||
if (!state.coverage.pending.size) purgeStale();
|
if (!state.coverage.pending.size) {
|
||||||
|
purgeStale();
|
||||||
|
shareBudget();
|
||||||
|
}
|
||||||
// The camera moved: cells that were out of the sampler's range may not be,
|
// The camera moved: cells that were out of the sampler's range may not be,
|
||||||
// and lines left draped by a failed, partial or dropped cast get another try.
|
// and lines left draped by a failed, partial or dropped cast get another try.
|
||||||
if (terrainMode())
|
if (terrainMode())
|
||||||
@@ -943,9 +961,11 @@ export function createLineCoverage({
|
|||||||
if (
|
if (
|
||||||
entry.kind === 'sequence' &&
|
entry.kind === 'sequence' &&
|
||||||
!entry.sequences.has(id) &&
|
!entry.sequences.has(id) &&
|
||||||
entry.sequenceList.some((sequence) => sequence.id === id)
|
entry.sequenceList.some(
|
||||||
|
(sequence) => sequence.id === id && passes(sequence, filter()),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
redrawEntry(entry);
|
redrawEntry(entry, { swap: true });
|
||||||
for (const entry of drawnEntries()) {
|
for (const entry of drawnEntries()) {
|
||||||
const sequence = entry.sequences.get(id);
|
const sequence = entry.sequences.get(id);
|
||||||
if (!sequence) continue;
|
if (!sequence) continue;
|
||||||
|
|||||||
@@ -338,3 +338,98 @@ test('a selected sequence the budget left out is drawn, highlighted', async (t)
|
|||||||
coverage.recolorSequence('short', true);
|
coverage.recolorSequence('short', true);
|
||||||
assert.deepEqual([...tile.sequences.keys()].sort(), ['long', 'short']);
|
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);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user