From be56144d75d1333d1617f37eaf39c652f1aee0e6 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 15 Oct 2026 18:34:39 +0400 Subject: [PATCH] refactor(services): use the globe engine groundSnap.heightFor and meshFloorSampler.sampleMeshFloorCells take the globe engine: engine.surface.sampleHeight for the probe, engine.surface.tilesLoaded (and, for mesh cells, maps.photorealActive) for readiness, camera.height for the mesh sampler's altitude gate. Callers that still hold a viewer or scene (the layers until phase 1c) keep the unchanged Cesium path. --- src/data/meshFloorSampler.test.mjs | 40 ++++++++++++++++++++++ src/services/groundSnap.js | 21 +++++++----- src/services/groundSnap.test.mjs | 53 ++++++++++++++++++++++++++++++ src/services/meshFloorSampler.js | 49 ++++++++++++++++++++------- 4 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 src/services/groundSnap.test.mjs diff --git a/src/data/meshFloorSampler.test.mjs b/src/data/meshFloorSampler.test.mjs index 8c629e50f..4b9c649dd 100644 --- a/src/data/meshFloorSampler.test.mjs +++ b/src/data/meshFloorSampler.test.mjs @@ -128,3 +128,43 @@ test('sampler: outside the google-3d regime nothing is sampled at all', () => { setMeshFloorPreferred(true); assert.equal(cachedMeshFloor(cell.lat, cell.lon), null); }); + +// --- On a globe engine (engine.surface / camera.height / maps.photorealActive) + +async function fakeEngineAt(cell, { heightM = 900, tilesLoaded = true, photoreal = true, sample } = {}) { + const { createFakeEngine } = await import('../globe/testing/fakeEngine.js'); + const engine = createFakeEngine({ + longitude: Cesium.Math.toRadians(cell.lon), + latitude: Cesium.Math.toRadians(cell.lat), + altitude: heightM, + }); + if (photoreal) await engine.maps.setPhotorealTiles({ tileset: {} }); + engine.fake.script('tilesLoaded', tilesLoaded); + engine.fake.script('sampleHeight', sample); + return engine; +} + +test('sampler on a globe engine: samples engine.surface and latches against the prior', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + const engine = await fakeEngineAt(cell, { sample: () => 137.0 }); + sampleMeshFloorCells(engine, [cell], { viewerLat: cell.lat, viewerLon: cell.lon, excludeObjects: ['own'] }); + assert.equal(cachedMeshFloor(cell.lat, cell.lon), 137.0); + const [call] = engine.fake.callsOf('surface.sampleHeight'); + assert.ok(Math.abs(Cesium.Math.toDegrees(call.args[0].latitude) - cell.lat) < 1e-9); + assert.deepEqual(call.args[1], ['own']); +}); + +test('sampler on a globe engine: a high camera, loading tiles or no photoreal tiles sample nothing', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + const opts = { viewerLat: cell.lat, viewerLon: cell.lon }; + for (const options of [{ heightM: 90_000 }, { tilesLoaded: false }, { photoreal: false }]) { + const engine = await fakeEngineAt(cell, { ...options, sample: () => 138.0 }); + sampleMeshFloorCells(engine, [cell], opts); + assert.equal(engine.fake.callsOf('surface.sampleHeight').length, 0, JSON.stringify(options)); + } + assert.equal(cachedMeshFloor(cell.lat, cell.lon), null); +}); diff --git a/src/services/groundSnap.js b/src/services/groundSnap.js index 44964f99a..be3e9c706 100644 --- a/src/services/groundSnap.js +++ b/src/services/groundSnap.js @@ -47,6 +47,7 @@ */ import * as geo from '../geo/index.js'; +import { isGlobeEngine } from '../globe/contract.js'; /** Taxi threshold: a cached snap answers directly for moves up to this far from * the sampled spot (m). Past it the value is demoted to a held last-known and a @@ -170,7 +171,7 @@ export function firstVisibleTileset(primitives) { /** * Create a per-layer ground-snap cache. * @returns {{ - * heightFor: (viewer: Cesium.Viewer, icao: string, pos: Cesium.Cartesian3, + * heightFor: (target: object, icao: string, pos: Cesium.Cartesian3, * getExclusions?: () => Array) => number|null, * forget: (icao: string) => void, * clear: () => void, @@ -281,7 +282,9 @@ export function createGroundSnap({ groundFloor }) { * A contact that HAS resolved once keeps that answer through a resample outage; * see `heldSnapM`. * - * @param {Cesium.Viewer} viewer - Live viewer (sampling + tiles-ready check). + * @param {object} target - The globe engine (`engine.surface`: sampleHeight + * and tilesLoaded), or a live Cesium viewer from callers that still hold + * one (scene.sampleHeight and the first visible tileset's tilesLoaded). * @param {string} icao - Cache key (per-layer icao space). * @param {Cesium.Cartesian3} pos - Current display position. Cache movement * is measured after projecting this input to the WGS84 ellipsoid, so a @@ -292,7 +295,8 @@ export function createGroundSnap({ groundFloor }) { * otherwise). Only invoked when a sample actually fires. * @returns {number|null} */ - function heightFor(viewer, icao, pos, getExclusions) { + function heightFor(target, icao, pos, getExclusions) { + const engine = isGlobeEngine(target) ? target : null; const surfacePos = geo.ellipsoid.WGS84.scaleToGeodeticSurface( pos, scratchSurfacePos, @@ -344,7 +348,8 @@ export function createGroundSnap({ groundFloor }) { entries.set(icao, next); return heldSnapM(next, surfacePos); }; - if (!_tilesReady(viewer)) return miss(); + if (!(engine ? engine.surface.tilesLoaded() : _tilesReady(target))) + return miss(); windowCount += 1; let sampled; try { @@ -354,10 +359,10 @@ export function createGroundSnap({ groundFloor }) { scratchCarto, ); // sampleHeight throws when unsupported (no depth textures) — that's a miss. - sampled = viewer.scene.sampleHeight( - carto, - getExclusions ? getExclusions() : undefined, - ); + const exclusions = getExclusions ? getExclusions() : undefined; + sampled = engine + ? engine.surface.sampleHeight(carto, exclusions) + : target.scene.sampleHeight(carto, exclusions); } catch { sampled = undefined; } diff --git a/src/services/groundSnap.test.mjs b/src/services/groundSnap.test.mjs new file mode 100644 index 000000000..e8fbfea77 --- /dev/null +++ b/src/services/groundSnap.test.mjs @@ -0,0 +1,53 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as geo from '../geo/index.js'; +import { createFakeEngine } from '../globe/testing/fakeEngine.js'; +import { createGroundSnap } from './groundSnap.js'; + +const groundFloor = (reports = []) => ({ + cachedMeshFloor: () => null, + meshFloorPreferred: () => true, + reportValidatedMeshFloorCell: (...args) => reports.push(args), +}); +const KAUS = geo.cartesian3.fromDegrees(-97.67, 30.19, 0); + +test('on a globe engine a snap samples engine.surface once and caches it', () => { + const reports = []; + const snap = createGroundSnap({ groundFloor: groundFloor(reports) }); + const engine = createFakeEngine(); + engine.fake.script('sampleHeight', () => 138.5); + let exclusions = 0; + const exclude = () => { + exclusions++; + return ['own-model']; + }; + assert.equal(snap.heightFor(engine, 'a1', KAUS, exclude), 138.5); + assert.equal(snap.heightFor(engine, 'a1', KAUS, exclude), 138.5); + const calls = engine.fake.callsOf('surface.sampleHeight'); + assert.equal(calls.length, 1, 'one sample, then the cache'); + assert.deepEqual(calls[0].args[1], ['own-model']); + assert.equal(exclusions, 1); + assert.ok( + Math.abs(geo.math.toDegrees(calls[0].args[0].latitude) - 30.19) < 1e-9, + ); + assert.equal(reports.length, 1, 'the sample feeds the mesh floor'); +}); + +test('on a globe engine loading tiles or a miss return null without a sample cache', () => { + const snap = createGroundSnap({ groundFloor: groundFloor() }); + const engine = createFakeEngine(); + engine.fake.script('tilesLoaded', false); + engine.fake.script('sampleHeight', () => 10); + assert.equal(snap.heightFor(engine, 'b1', KAUS), null); + assert.equal(engine.fake.callsOf('surface.sampleHeight').length, 0); + engine.fake.script('tilesLoaded', true); + engine.fake.script('sampleHeight', () => undefined); + assert.equal(snap.heightFor(engine, 'c1', KAUS), null); + assert.equal(engine.fake.callsOf('surface.sampleHeight').length, 1); + assert.equal( + snap.heightFor(engine, 'c1', KAUS), + null, + 'backoff after a miss', + ); + assert.equal(engine.fake.callsOf('surface.sampleHeight').length, 1); +}); diff --git a/src/services/meshFloorSampler.js b/src/services/meshFloorSampler.js index b1cf65f29..9d62891a9 100644 --- a/src/services/meshFloorSampler.js +++ b/src/services/meshFloorSampler.js @@ -1,4 +1,6 @@ import * as Cesium from 'cesium'; +import * as geo from '../geo/index.js'; +import { isGlobeEngine } from '../globe/contract.js'; /** Construct an instance-owned meshFloorSampler service with explicit dependencies. */ export function createMeshFloorSampler({ @@ -50,6 +52,7 @@ export function createMeshFloorSampler({ const MAX_CAMERA_HEIGHT_M = 25_000; /** @type {Cesium.Cartographic} Scratch for probe coordinates. */ const _scratchProbe = new Cesium.Cartographic(); + const _scratchEngineProbe = geo.cartographic.create(); // Regime tracking: mesh cells only apply while the photoreal (google-3d) // stack renders. main.js re-dispatches MapStackController.onChange as this @@ -95,7 +98,9 @@ export function createMeshFloorSampler({ * layer poll (never per contact, never per frame). Synchronous — Cesium's * sampleHeight is a CPU-side query against loaded tiles. * - * @param {Cesium.Scene|undefined} scene - The scene (skipped when absent/torn down). + * @param {object|undefined} target - The globe engine (engine.surface, + * camera.height, maps.photorealActive), or the Cesium scene from callers + * that still hold one (skipped when absent/torn down). * @param {Array<{lat: number, lon: number}>} points - Contact/waypoint coords. * @param {object} [options] * @param {Array} [options.excludeObjects] - Own billboards/models to @@ -105,21 +110,31 @@ export function createMeshFloorSampler({ * Viewer subpoint (computed once by the caller's poll). */ function sampleMeshFloorCells( - scene, + target, points, { excludeObjects = [], viewerLat, viewerLon } = {}, ) { if (!meshFloorPreferred()) return; - if (!scene || typeof scene.sampleHeight !== 'function') return; + const engine = isGlobeEngine(target) ? target : null; + const scene = engine ? null : target; + if (engine ? !engine.surface.supported : !scene) return; + if (!engine && typeof scene.sampleHeight !== 'function') return; if (!Array.isArray(points) || !points.length) return; // Round-5 hardening (the CCTV projectionTilesReady lesson, ported late): // while tiles are MID-STREAM a probe returns coarse-LOD heights — real // numbers, wildly wrong — and the one-shot latch made them permanent. // Only sample when the visible Google tileset reports tilesLoaded AND the // camera is low enough that the streamed LOD near it is fine-grained. - const camH = scene.camera?.positionCartographic?.height; + const camH = engine + ? engine.camera.height() + : scene.camera?.positionCartographic?.height; if (!Number.isFinite(camH) || camH > MAX_CAMERA_HEIGHT_M) return; - if (!_visibleTilesetLoaded(scene)) return; + if ( + !(engine + ? engine.maps.photorealActive() && engine.surface.tilesLoaded() + : _visibleTilesetLoaded(scene)) + ) + return; let sampled = 0; const attempted = new Set(); for (const p of points) { @@ -139,13 +154,23 @@ export function createMeshFloorSampler({ } let height; try { - const carto = Cesium.Cartographic.fromDegrees( - cell.lon, - cell.lat, - 0, - _scratchProbe, - ); - height = scene.sampleHeight(carto, excludeObjects); + if (engine) { + const carto = geo.cartographic.fromDegrees( + cell.lon, + cell.lat, + 0, + _scratchEngineProbe, + ); + height = engine.surface.sampleHeight(carto, excludeObjects); + } else { + const carto = Cesium.Cartographic.fromDegrees( + cell.lon, + cell.lat, + 0, + _scratchProbe, + ); + height = scene.sampleHeight(carto, excludeObjects); + } sampled += 1; } catch { continue; // scene mid-teardown — try again next poll