fix(street-level): KartaView review P2s; budget the gate's pick search

- Viewer: resizes while a sharper photo loads start no second load (the
  last size is looked at once it lands); decoded bitmaps nobody will draw
  (an overtaken preview or upgrade) are closed.
- Cones: a photo opened while the drive's first page loads is the one
  they centre on.
- A sequence page that lands while a lost WebGL context recovers keeps
  its cones: no pose is emitted without a renderer, and a viewer error
  never rejects page data.
- Source: an answer turned away unread (HTTP error, 429, length past the
  cap) aborts its request and cancels its body, freeing the connection.
- Gate: findPickPoint no longer picks each centre five times at radius 0,
  aims step 13's cone pick at the drawn billboards, and caps each
  evaluation at 4 s, resuming from a cursor on the next poll, so a slow
  machine cannot spend a whole wait in one evaluation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-14 13:09:40 +01:00
1 parent e9a4dbb062
commit 04b3231534
7 files changed
+350 -30

No files matched your search

@@ -73,6 +73,11 @@ async function loadBitmap(url, timeoutMs) {
}
}
/** Free a decoded bitmap nobody will draw (an <img> has nothing to free). */
function release(image) {
image?.close?.();
}
/** The panorama size a canvas this wide can show sharply at this field of view. */
export function panoSizeFor(canvasPixels, fovDeg) {
const needed = (Math.max(1, canvasPixels) * 360) / Math.max(1, fovDeg);
@@ -130,6 +135,10 @@ export function createKartaViewViewer({
let panoWidth = 0;
/** The flat photo on screen is the 2,560 px version. */
let flatSharp = false;
/** The sharper version loading ({seq, kind}): a resize starts no second one. */
let upgrading = null;
/** A resize came while it loaded: look again once it lands. */
let upgradeAgain = false;
/** The photo an open is fetching: ‹ › step on from it, not the old one. */
let openingId = null;
/** ‹ › chose this photo: keep the panorama's look direction for it. */
@@ -143,6 +152,9 @@ export function createKartaViewViewer({
function emit() {
if (!photo || !showing) return;
// A lost GPU context, still being recovered: the open that redraws the
// panorama poses once it is on screen.
if (showing === 'pano' && !renderer) return;
const view = showing === 'pano' ? renderer.getView() : null;
const heading = cameraBearing(photo);
const pose = {
@@ -485,20 +497,40 @@ export function createKartaViewViewer({
? resizedImageUrl(target.urls.pano, size)
: resizedImageUrl(target.urls.full, size);
if (!url) return;
// Resizes come in bursts while the panel animates: one load at a time
// for this photo, and one more look once it lands.
if (upgrading?.seq === seq && upgrading.kind === kind) {
upgradeAgain = true;
return;
}
const pending = (upgrading = { seq, kind });
upgradeAgain = false;
let image = null;
try {
const image = await load(url, { crossOrigin: kind === 'pano' });
image = await load(url, { crossOrigin: kind === 'pano' });
if (seq !== openSeq || photo !== target || showing !== kind) return;
if (kind === 'pano') {
if (!renderer || size[0] <= panoWidth) return;
renderer.setImage(image);
image = null;
panoWidth = size[0];
} else {
picture.src = image.src;
image = null;
flatSharp = true;
}
requestRender();
} catch {
/* the first version stays */
} finally {
release(image);
if (upgrading === pending) {
upgrading = null;
if (upgradeAgain && seq === openSeq && photo === target) {
upgradeAgain = false;
upgrade(target, seq, showing);
}
}
}
}
@@ -524,10 +556,16 @@ export function createKartaViewViewer({
resizedImageUrl(target.urls.pano, PANO_PREVIEW_SIZE),
{ crossOrigin: true },
);
if (seq !== openSeq) return false;
if (seq !== openSeq) {
release(image);
return false;
}
// The context may have been lost while this loaded: build on the new
// canvas, or let the caller show the front view.
if (!ensureRenderer()) throw new Error(LOAD_FAILED);
if (!ensureRenderer()) {
release(image);
throw new Error(LOAD_FAILED);
}
const restore = restoreView?.id === target.id ? restoreView.view : null;
restoreView = null;
if (restore) renderer.setView(restore);