From 312ed416e9de7a989896dfe79b73eaf9133d0921 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 15 Oct 2026 22:34:25 +0400 Subject: [PATCH] test(globe): pick round-trip and setTerrain checks in the contract suite Two engine-neutral checks: pickPosition then worldToWindow lands within 1.5 px of the screen point (over New York, straight down and tilted; points with no surface are skipped, at least one must hit), and maps.setTerrain resolves for keyless terrain and rejects an unknown kind. The fake engine now rejects unknown terrain kinds, as both engines do. The Navara contract page shows OSM on keyless terrain before the suite, since depth picks need a drawn surface. --- src/globe/testing/contractSuite.mjs | 72 +++++++++++++++++++++++++++++ src/globe/testing/fakeEngine.js | 9 ++++ 2 files changed, 81 insertions(+) diff --git a/src/globe/testing/contractSuite.mjs b/src/globe/testing/contractSuite.mjs index f0e0488b8..2a2f705fc 100644 --- a/src/globe/testing/contractSuite.mjs +++ b/src/globe/testing/contractSuite.mjs @@ -1417,6 +1417,78 @@ export function createContractSuite( ); }, ], + [ + 'picker.pickPosition and projector.worldToWindow round-trip within 1.5 px', + async () => { + if (!engine.picker.pickPositionSupported) return; + // Straight down and tilted; screen points with no surface are skipped. + // Over New York, low enough for map detail. + const views = [ + { height: 20000, heading: 0, pitch: -math.PI_OVER_TWO }, + { height: 5000, heading: 0.7, pitch: -1.0 }, + ]; + let hits = 0; + const misses = []; + for (const { height: viewHeight, heading, pitch } of views) { + camera.setView({ + destination: cartesian3.fromDegrees(-73.78, 40.64, viewHeight), + orientation: { heading, pitch, roll: 0 }, + }); + // Picks read the depth of the last drawn frame: draw this view. + await settle(3); + const { width, height } = engine.host.size(); + const screens = [ + [0.5, 0.5], + [0.3, 0.4], + [0.7, 0.6], + [0.5, 0.75], + ].map(([x, y]) => + cartesian2.create(Math.round(width * x), Math.round(height * y)), + ); + // The depth a pick reads needs drawn frames. + await until( + () => engine.picker.pickPosition(screens[0]) !== undefined, + 'a surface under the centre for pickPosition', + ); + for (const screen of screens) { + const position = engine.picker.pickPosition(screen); + if (position === undefined) continue; + const back = engine.projector.worldToWindow(position); + const error = back + ? Math.hypot(back.x - screen.x, back.y - screen.y) + : Infinity; + hits += 1; + if (!(error <= 1.5)) + misses.push( + `(${screen.x}, ${screen.y}) → ${back ? `(${back.x.toFixed(2)}, ${back.y.toFixed(2)})` : 'undefined'}`, + ); + } + } + ensure(hits > 0, 'pickPosition hit no surface'); + ensure( + misses.length === 0, + `round-trip off by more than 1.5 px: ${misses.join('; ')}`, + ); + }, + ], + [ + 'maps.setTerrain: keyless terrain resolves; an unknown kind rejects', + async () => { + await engine.maps.setTerrain({ id: 'keyless', kind: 'keyless' }); + let rejected = null; + try { + await engine.maps.setTerrain({ + id: 'no-such-terrain', + kind: 'no-such-terrain', + }); + } catch (error) { + rejected = error; + } + ensure(rejected !== null, 'an unknown terrain kind resolved'); + // The keyless terrain still applies after the rejection. + await engine.maps.setTerrain({ id: 'keyless', kind: 'keyless' }); + }, + ], [ 'camera.commanded: fires for each camera command, never for reads', async () => { diff --git a/src/globe/testing/fakeEngine.js b/src/globe/testing/fakeEngine.js index a93d827e2..3ede75e44 100644 --- a/src/globe/testing/fakeEngine.js +++ b/src/globe/testing/fakeEngine.js @@ -24,6 +24,8 @@ const { cartesian2, cartesian3, cartographic, ellipsoid, math, matrix4 } = geo; const WGS84 = ellipsoid.WGS84; const RADII = WGS84.radii; const MAXIMUM_RADIUS = Math.max(RADII.x, RADII.y, RADII.z); +// The terrain kinds the engines build themselves (src/maps/registry.js). +const TERRAIN_KINDS = Object.freeze(['ellipsoid', 'keyless', 'world']); const DEFAULT_FLIGHT_SECONDS = 3; const DEFAULT_ZOOM_AMOUNT = 100000; // Cesium's camera.defaultZoomAmount const ZERO_RADIUS_SPHERE_RANGE = 100; // Cesium's MINIMUM_ZOOM @@ -845,6 +847,13 @@ export function createFakeEngine(options = {}) { }, async setTerrain(source) { record('maps.setTerrain', [source]); + // As the real engines: a prebuilt source or a known kind. + const kind = source?.kind ?? source?.id; + if ( + typeof source?.create !== 'function' && + !TERRAIN_KINDS.includes(kind) + ) + throw new TypeError(`Unknown terrain source: ${kind}`); fake.maps.terrain = source; }, async setPhotorealTiles(source) {