Street Level: polar look-at, a bounded terrain wait, takeovers during the flight

- The look-at longitude wraps fully, so a tilted camera at a pole gives a
  point on the globe. The strict off-globe check on street_level is undone:
  points are clamped again, as before.
- A posted view that fails is answered, and the views behind it still apply.
- A posted view waits at most 2 s for the terrain under its camera, then
  keeps its own estimate.
- A camera taken during that wait stays where it is: the rest of the view
  applies, and only its photos stand down.
- show_street_level keeps a search or a press on the globe made during its
  own flight: it adopts only its own navigation stamps on landing, and a
  gesture is never forgiven.
This commit is contained in:
German Khokhlov committed 2026-11-11 18:02:15 +04:00
1 parent ae66d07575
commit 0ebc09079d
8 files changed
+193 -79

No files matched your search

+64 -42
View File
@@ -39,6 +39,8 @@ const FOLLOW_LAYERS = {
const FOLLOW_ATTEMPTS = 20;
const FOLLOW_RETRY_MS = 1000;
const FLIGHT_SECONDS = 2;
/** How long a posted view waits for the terrain under its camera. */
const TERRAIN_WAIT_MS = 2000;
/**
* Whether this page shows the app embedded: `?embed=1` when another page
@@ -495,51 +497,71 @@ export function installViews({
typeof asked === 'object' &&
asked.lat == null &&
asked.lon == null;
queue = queue.then(async () => {
if (signal?.aborted) return;
if (unplaced && view.street_level) {
// Whoever takes the camera while the terrain answers (a drag, a
// search, the assistant) keeps it: the view stands down.
const owner = watchCameraOwner(viewer, shell);
const point = await groundedLookAt(view.camera, terrain, signal)
.catch(() => null)
.finally(() => owner.stop());
queue = queue
.then(async () => {
if (signal?.aborted) return;
if (owner.taken()) {
post(
{
type: EMBED_APPLIED_MESSAGE,
id,
ok: false,
steps: [{ step: 'street_level', ok: false, superseded: true }],
},
replyOrigin,
);
return;
let apply = run;
if (unplaced && view.street_level) {
// A terrain service that does not answer soon leaves the view's
// own estimate.
const wait = AbortSignal.timeout(TERRAIN_WAIT_MS);
const owner = watchCameraOwner(viewer, shell);
const point = await groundedLookAt(
view.camera,
terrain,
signal ? AbortSignal.any([signal, wait]) : wait,
)
.catch(() => null)
.finally(() => owner.stop());
if (signal?.aborted) return;
// Whoever takes the camera meanwhile (a drag, a search, the
// assistant) keeps it: the photos stand down, as when the camera
// is taken during the action, and the rest of the view applies.
if (owner.taken())
apply = (name, ...rest) =>
name === 'show_street_level'
? {
ok: false,
cancelled: true,
superseded: true,
error: 'The camera moved on before the photo opened',
}
: run(name, ...rest);
else if (point)
view = createView({
...event.data.view,
street_level: { ...asked, ...point },
});
}
if (point)
view = createView({
...event.data.view,
street_level: { ...asked, ...point },
});
}
const steps = await applyView(view, {
viewer,
dataManager,
run,
signal,
autoOpen,
});
post(
{
type: EMBED_APPLIED_MESSAGE,
id,
ok: steps.every((step) => step.ok),
steps,
},
replyOrigin,
const steps = await applyView(view, {
viewer,
dataManager,
run: apply,
signal,
autoOpen,
});
post(
{
type: EMBED_APPLIED_MESSAGE,
id,
ok: steps.every((step) => step.ok),
steps,
},
replyOrigin,
);
})
// A view that fails is answered, and the views behind it still apply.
.catch((error) =>
post(
{
type: EMBED_APPLIED_MESSAGE,
id,
ok: false,
error: error?.message || String(error),
},
replyOrigin,
),
);
});
};
windowRef.addEventListener('message', onMessage);
void ready.then(() => {