fix(street-level): sequence loads, playback and nearest-photo edge cases

From gekh's review of #768, each bug with a test that fails before:

- P2: a failed sequence load clears the previous sequence's cones, resets
  the selection and can be retried; a second click while loading starts no
  second lookup. sequences.js had no tests: sequences.test.mjs now covers
  failure and retry, a superseded late answer, the cache and clearSelection.
- P2: closing a photo stops MapillaryJS sequence playback, which kept the
  hidden viewer stepping and downloading images.
- openNearest without the panel reports it instead of loading forever, and
  stops if the layer (or its provider) went off during the lookup; the
  mesh sampler's misses are bounded and forgotten with its samples; the
  sequence count is the union of ids across tiles; a provider withdraws
  only its own error, so a sequence error cannot mask KEY REJECTED; hover
  stays on (throttled) under an orbiting camera and re-picks when it
  stops; "since N days" follows the clock in a long-open tab.
- A prewarm waits for one in flight and then builds if it gave up.
- New coverage for the layer core (credits, KEY REJECTED vs REQUIRED,
  switching a provider off), the refresh wiring of the tilted-view fix and
  the draped-to-cast swap completion.
- Cleanup: loading is derived from pending; freshStreet(); tile ranking
  computes distances once and shares one path for the date line; shared
  metresBetween / whenIdle / redrawTile; one NEAREST_RADIUS_M and one
  source method list; unused contract fields, exports, getImage and the
  decoder's image branch removed; PROVIDER_COLORS lists only Mapillary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-05 18:35:44 +01:00
1 parent 48276450ec
commit 5f98b871e8
31 files changed
+1129 -470

No files matched your search

+43 -13
View File
@@ -18,6 +18,8 @@ function harness() {
globalThis.requestAnimationFrame = (task) => frames.push(task);
globalThis.document = new EventTarget();
const picks = [];
/** What the pointer is over: a line this layer owns, or nothing. */
const scene = { under: null };
const canvas = Object.assign(new EventTarget(), {
style: {},
// Keep Cesium's handler on the canvas; there is no real document here.
@@ -30,7 +32,7 @@ function harness() {
canvas,
pick(position) {
picks.push(position);
return null;
return scene.under && { id: scene.under };
},
},
camera,
@@ -45,7 +47,7 @@ function harness() {
const selection = createSelection({
state,
parts: {
router: { ownsPick: () => false, resolve: () => null },
router: { ownsPick: (id) => id === 'mly:seq:1', resolve: () => null },
hasSelectedSequence: () => false,
clearSequences() {},
},
@@ -56,6 +58,7 @@ function harness() {
);
return {
state,
scene,
canvas,
camera,
picks,
@@ -102,26 +105,53 @@ test('hover picks at most every 120 ms through a burst of moves, ending on the l
}
});
test('hover does not pick while the camera moves or a mouse button is held', () => {
test('hover does not pick while a mouse button is held', () => {
const h = harness();
try {
h.camera.moveStart.raiseEvent();
h.buttons('pointerdown', 1);
h.move(1);
h.wait(200);
assert.equal(h.picks.length, 0, 'camera in motion');
h.camera.moveEnd.raiseEvent();
assert.equal(h.picks.length, 0, 'a drag in progress');
h.buttons('pointerup', 0);
h.move(2);
h.wait(200);
assert.equal(h.picks.length, 1, 'picks again once the camera rests');
assert.equal(h.picks.length, 1);
} finally {
h.restore();
}
});
h.buttons('pointerdown', 1);
h.move(3);
test('a camera that never stops (orbit, tracking) keeps hover throttled, not off (review P3)', () => {
const h = harness();
try {
// An orbiting or tracking camera raises moveStart and never moveEnd.
h.camera.moveStart.raiseEvent();
for (let i = 0; i < 60; i++) {
h.move(i);
h.wait(16);
}
assert.ok(h.picks.length >= 7, `${h.picks.length} picks in ~1 s`);
assert.ok(h.picks.length <= 9, `${h.picks.length} picks in ~1 s`);
} finally {
h.restore();
}
});
test('when the camera stops, the pointer is picked again so the cursor cannot stick (review P3)', () => {
const h = harness();
try {
h.scene.under = 'mly:seq:1';
h.move(5);
h.wait(200);
assert.equal(h.picks.length, 1, 'a drag in progress');
h.buttons('pointerup', 0);
h.move(4);
assert.equal(h.canvas.style.cursor, 'pointer', 'over a line');
// The camera moves the line out from under a still pointer.
h.camera.moveStart.raiseEvent();
h.scene.under = null;
h.camera.moveEnd.raiseEvent();
h.wait(200);
assert.equal(h.picks.length, 2);
assert.equal(h.picks.length, 2, 'one pick when the camera rests');
assert.deepEqual(h.picks.at(-1), { x: 5, y: 10 });
assert.equal(h.canvas.style.cursor, '', 'the pointer cursor is gone');
} finally {
h.restore();
}