From d77dccb350c728dfadcbafc99588740ceeb6902d Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Mon, 28 Sep 2026 21:16:54 +0100 Subject: [PATCH] fix(street-level): ignore implausible ground samples when framing a photo Opening a photo frames it from 140 m behind using scene.sampleHeight for the ground. Before the Google 3D tiles under the photo have loaded, that sample can be kilometres wrong: an Austin 360 photo sampled -14,886 m and the camera flew to -14,811 m. A sample now counts only when it is a plausible surface height and, once the bare-earth height is known, lies within -15/+80 m of it; otherwise the bare-earth height is used. FOLLOW's eye height goes through the same check. Co-Authored-By: Claude Opus 5.5 --- src/layers/streetLevel/cameraFollow.js | 32 ++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/layers/streetLevel/cameraFollow.js b/src/layers/streetLevel/cameraFollow.js index e858bea58..dcb4ee7dd 100644 --- a/src/layers/streetLevel/cameraFollow.js +++ b/src/layers/streetLevel/cameraFollow.js @@ -1,29 +1,53 @@ import * as Cesium from 'cesium'; import { FOLLOW_EYE_HEIGHT_M } from './policy.js'; +/** Ellipsoidal heights a land surface can have (Dead Sea shore to Everest, with geoid slack). */ +const SURFACE_MIN_M = -500; +const SURFACE_MAX_M = 9000; +/** Rendered mesh window around the bare earth: roofs and bridges, not whole towers. */ +const MESH_BELOW_DEM_M = 15; +const MESH_ABOVE_DEM_M = 80; + /** * Drive the globe camera from the street-level pose: either continuously * ("camera follows view") or once, framing the image from behind. */ -export function createCameraFollow({ state }) { +export function createCameraFollow({ state, parts }) { const { render } = state.services; function requestRender() { render?.governorRequestRender?.('street-level-follow'); } + /** + * Ground under a photo for framing and following. The rendered surface is + * sampled first, but `sampleHeight` can return nonsense before the tiles + * under the point have loaded (kilometres below the ellipsoid), so a sample + * counts only when it is a plausible surface height and, once the bare-earth + * height is known, lies within the mesh window around it. + */ function groundHeightAt(lon, lat, fallback) { const scene = state.viewer?.scene; const carto = Cesium.Cartographic.fromDegrees(lon, lat); + const caster = parts?.groundCaster; + const dem = caster?.groundAt(lon, lat) ?? null; + if (dem === null) caster?.prepare([[lon, lat]]); + const plausible = (height) => + Number.isFinite(height) && + height >= SURFACE_MIN_M && + height <= SURFACE_MAX_M && + (dem === null || + (height >= dem - MESH_BELOW_DEM_M && height <= dem + MESH_ABOVE_DEM_M)); let height = null; try { if (scene?.sampleHeightSupported) height = scene.sampleHeight(carto); } catch { /* not sampleable yet */ } - if (!Number.isFinite(height)) height = scene?.globe?.getHeight?.(carto); - if (!Number.isFinite(height)) height = fallback; - return Number.isFinite(height) ? height : 0; + if (!plausible(height)) height = dem; + if (!plausible(height)) height = scene?.globe?.getHeight?.(carto); + if (!plausible(height)) height = fallback; + return plausible(height) ? height : 0; } /** Put the Cesium camera where the street-level camera is. */