From 04b8c856ad8d155cc1f60bfbf61f23ebd6fe22fb Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 9 Nov 2026 01:25:43 -0800 Subject: [PATCH 1/3] Keep vehicles off rooftops and bridge decks: probe the mesh where the vehicle is On the photoreal stack the rendered-mesh floor took one probe at each coarse cell's centre (0.001 degrees) and kept it for the session. A centre on a roof passed the +80 m DEM window and lifted every vehicle in the cell to roof height (a Haymarket bus drew 23.5 m above the street, on the garage roof); a centre in the river put Red Line trains on the Longfellow Bridge 6-9 m under the deck. The sampler now probes at the caller's point. A cell with a reading is probed again only at spots about 17 m apart from earlier ones, eight at most, and a spot refused only because the cell's DEM prior was cold is tried again later. The floor keeps each reading that passes the gate at the point it was taken (two within about 5 m keep the lower; sixteen at most per cell) and answers a read from the reading nearest the asked point, so a viaduct and the street beside it in one cell keep their own heights. --- docs/CURRENT-STATE.md | 23 ++- src/data/groundFloor.js | 1 + src/data/meshFloorSampler.test.mjs | 179 +++++++++++++++++++++++- src/layers/cctv/geometry.js | 2 +- src/layers/flights/snapshotRenderer.js | 4 +- src/layers/military/snapshotRenderer.js | 4 +- src/services/groundFloor.js | 81 +++++++++-- src/services/groundSnap.js | 2 +- src/services/meshFloorSampler.js | 61 ++++++-- 9 files changed, 329 insertions(+), 28 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 05240d922..5e7c36be2 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1347,6 +1347,27 @@ and snapshot source. The standalone adapter keeps the existing USGS daily feed, M2.5+ filtering, static discs, magnitude labels and analyst records. Disabling or destroying the layer cancels pending work and ignores late results. +## Rendered-mesh floor readings — November 9, 2026 + +On the photoreal stack the shared floor (`cachedGroundFloor`) answers from +rendered-mesh readings per coarse cell (0.001°, about 110 × 80 m), sampled by +`src/services/meshFloorSampler.js` and gated against the DEM by +`reportValidatedMeshFloorCell` in `src/services/groundFloor.js`. The probe goes +where the caller's point is (a vehicle or contact), not to the cell's centre; +the sampler reads a cell at up to eight spots about 12–17 m or more apart, and +each reading that passes the gate is kept at its point (ground snaps add theirs +the same way; a cell keeps at most sixteen). A read answers from the cell's +reading nearest the asked point, so a viaduct and the street beside it in one +cell keep their own heights; two readings within about 5 m of each other keep +the lower. A spot refused only because the cell's DEM prior was cold is tried +again later. Before this, one probe at the cell's centre latched for the +session, so a centre on a roof lifted everything in the cell (a Haymarket bus +drew 23.5 m above the street, on the garage roof) and a centre in the river put +Red Line trains on the Longfellow Bridge 6–9 m under the deck. Residual: the +probe is vertical, so a vehicle directly under a deck or a roof overhang reads +the structure above it, and a vehicle between readings takes the nearest one, so +its floor steps where the nearest reading changes. + ## Vessel components and sources `src/data/aisLiveVessels.js` assembles `createVesselLayer` from the @@ -3148,7 +3169,7 @@ This is the current runtime/source-of-truth snapshot for the project. > `HELD_SNAP_CONTRADICTION_M` (5 m) ABOVE the held value drops the hold and the > contact is COLD again, back to the floored 2D billboard. The rule is > ONE-SIDED on purpose — it targets BURIAL. A cell reading BELOW a real sample -> is the floor chain's expected under-read (one-shot latch over ~111 m, +> is the floor chain's expected under-read (a few point readings over ~111 m, > neighbours lean lowest, `displayFloorHeightM` only ever raises), not evidence > of anything, and a two-sided cut was disproved on the track rig by a planted > cell 66.7 m below a real sample at the same spot. Accepted residual, stated diff --git a/src/data/groundFloor.js b/src/data/groundFloor.js index fed7a6cd6..978f30218 100644 --- a/src/data/groundFloor.js +++ b/src/data/groundFloor.js @@ -22,6 +22,7 @@ export const { reportMeshFloorCell, meshFloorSampleWithinPrior, reportValidatedMeshFloorCell, + meshFloorPriorReady, cachedMeshFloor, _clearMeshFloorCellsForTest, cachedGroundFloor, diff --git a/src/data/meshFloorSampler.test.mjs b/src/data/meshFloorSampler.test.mjs index f651d72f8..d33c4873d 100644 --- a/src/data/meshFloorSampler.test.mjs +++ b/src/data/meshFloorSampler.test.mjs @@ -24,7 +24,7 @@ import { sampleMeshFloorCells } from './meshFloorSampler.js'; import { resolveEllipsoidalGround } from './terrainHeights.js'; import { cachedGroundFloor, cachedMeshFloor, setMeshFloorPreferred, - _clearMeshFloorCellsForTest, + _clearMeshFloorCellsForTest, reportValidatedMeshFloorCell, } from './groundFloor.js'; /** Distinct site per test — terrainHeights' cache is module-global and warm @@ -137,3 +137,180 @@ test('sampler: a cell just across the antimeridian is still near the viewer', () sampleMeshFloorCells(scene, [{ lat: -16.81, lon: 179.999 }], { viewerLat: -16.81, viewerLon: -179.999 }); assert.equal(calls, 2, 'about 200 m apart, so both cells are probed'); }); + +test('sampler: the probe goes where the point is, not to the cell centre', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + const asked = []; + const scene = fakeScene((carto) => { + asked.push(Cesium.Math.toDegrees(carto.longitude)); + return 122.0; + }); + const point = { lat: cell.lat + 0.0003, lon: cell.lon + 0.0004 }; + sampleMeshFloorCells(scene, [point], { viewerLat: cell.lat, viewerLon: cell.lon }); + assert.equal(asked.length, 1); + assert.ok(Math.abs(asked[0] - point.lon) < 1e-9, `probed at ${asked[0]}, not ${point.lon}`); +}); + +test('sampler: a viaduct and the street beside it in one cell keep their own heights', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + // A viaduct deck 9 m up along one line of the cell, the street elsewhere. + const deckLat = cell.lat + 0.0003; + let calls = 0; + const scene = fakeScene((carto) => { + calls += 1; + return Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0; + }); + const opts = { viewerLat: cell.lat, viewerLon: cell.lon }; + const onDeck = { lat: deckLat, lon: cell.lon }; + const street = { lat: cell.lat - 0.0003, lon: cell.lon }; + sampleMeshFloorCells(scene, [onDeck], opts); + sampleMeshFloorCells(scene, [street], opts); + assert.equal(calls, 2); + assert.equal(cachedMeshFloor(onDeck.lat, onDeck.lon + 0.0001), 129.0, 'the train stays on the deck'); + assert.equal(cachedMeshFloor(street.lat, street.lon - 0.0001), 121.0, 'the bus stays on the street'); + assert.equal(cachedGroundFloor(street.lat, street.lon), 121.0); + sampleMeshFloorCells(scene, [{ lat: deckLat, lon: cell.lon + 0.00001 }], opts); + assert.equal(calls, 2, 'a point next to one already probed is not probed again'); +}); + +test('sampler: a roof over the cell centre does not lift a bus on the street', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + // A roof 22 m up covering the cell's centre; the street 30 m west of it. + const scene = fakeScene((carto) => + Math.abs(Cesium.Math.toDegrees(carto.longitude) - cell.lon) < 0.0001 ? 142.0 : 121.0); + const bus = { lat: cell.lat, lon: cell.lon - 0.0003 }; + sampleMeshFloorCells(scene, [bus], { viewerLat: cell.lat, viewerLon: cell.lon }); + assert.equal(cachedGroundFloor(bus.lat, bus.lon), 121.0); +}); + +test('sampler: a second reading at the same spot keeps the lower one', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + assert.equal(reportValidatedMeshFloorCell(cell.lat, cell.lon, 131.0), true); + assert.equal(reportValidatedMeshFloorCell(cell.lat + 0.00002, cell.lon, 122.0), true); + assert.equal(cachedMeshFloor(cell.lat, cell.lon), 122.0); + assert.equal(reportValidatedMeshFloorCell(cell.lat, cell.lon + 0.00002, 140.0), true); + assert.equal(cachedMeshFloor(cell.lat, cell.lon), 122.0, 'a higher one never raises it'); +}); + +test('sampler: a cell with a reading is probed a few times at most', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + let calls = 0; + const scene = fakeScene(() => { + calls += 1; + return 121.0; + }); + const opts = { viewerLat: cell.lat, viewerLon: cell.lon }; + for (let i = 0; i < 20; i++) + sampleMeshFloorCells( + scene, + [{ lat: cell.lat - 0.0004 + (i % 5) * 0.0002, lon: cell.lon - 0.00045 + Math.floor(i / 5) * 0.0003 }], + opts, + ); + assert.equal(calls, 8); +}); + +test('sampler: a cell with no accepted reading is probed again on every call', async () => { + reset(); + const cell = nextSite(); + let calls = 0; + const scene = fakeScene(() => { + calls += 1; + return 138.0; + }); + const opts = { viewerLat: cell.lat, viewerLon: cell.lon }; + for (let i = 0; i < 3; i++) sampleMeshFloorCells(scene, [cell], opts); + assert.equal(calls, 3, 'no DEM prior yet, so nothing is kept and the cell is retried'); + await seedDem(cell, 120.0); + sampleMeshFloorCells(scene, [cell], opts); + assert.equal(cachedMeshFloor(cell.lat, cell.lon), 138.0, 'and lands once the prior is warm'); +}); + +test('sampler: a spot whose reading is refused does not use up the cell', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + // A tower 260 m up where the first vehicle waits; the street elsewhere and + // a deck 9 m up along one line of the cell. + const towerLon = cell.lon + 0.0004; + const deckLat = cell.lat + 0.0003; + const scene = fakeScene((carto) => { + if (Math.abs(Cesium.Math.toDegrees(carto.longitude) - towerLon) < 1e-7) return 380.0; + return Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0; + }); + const opts = { viewerLat: cell.lat, viewerLon: cell.lon }; + for (let i = 0; i < 10; i++) sampleMeshFloorCells(scene, [{ lat: cell.lat, lon: towerLon }], opts); + assert.equal(cachedMeshFloor(cell.lat, cell.lon), null, 'the tower is refused every time'); + sampleMeshFloorCells(scene, [{ lat: cell.lat - 0.0003, lon: cell.lon }], opts); + sampleMeshFloorCells(scene, [{ lat: deckLat, lon: cell.lon - 0.0003 }], opts); + assert.equal(cachedMeshFloor(deckLat, cell.lon - 0.0003), 129.0, 'the deck is still probed'); +}); + +test('ground floor: a cell keeps at most sixteen readings', async () => { + reset(); + const cell = nextSite(); + await seedDem(cell, 120.0); + for (let i = 0; i < 40; i++) { + const lat = cell.lat - 0.00045 + (i % 8) * 0.0001; + const lon = cell.lon - 0.00045 + Math.floor(i / 8) * 0.0002; + reportValidatedMeshFloorCell(lat, lon, 121.0 + i); + } + // Readings 0..15 are kept; reading 39 (at the far corner) is not, so that + // spot answers from the nearest kept one. + assert.equal(cachedMeshFloor(cell.lat - 0.00045, cell.lon - 0.00045), 121.0); + assert.equal(cachedMeshFloor(cell.lat + 0.00025, cell.lon + 0.00035), 121.0 + 15); +}); + +test('sampler: a spot refused while the DEM was cold is probed again once it is warm', async () => { + reset(); + const cell = nextSite(); + const deckLat = cell.lat + 0.0003; + let calls = 0; + const scene = fakeScene((carto) => { + calls += 1; + return Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0; + }); + const opts = { viewerLat: cell.lat, viewerLon: cell.lon }; + const onDeck = { lat: deckLat, lon: cell.lon }; + sampleMeshFloorCells(scene, [onDeck], opts); + assert.equal(cachedMeshFloor(onDeck.lat, onDeck.lon), null, 'no prior, nothing kept'); + await seedDem(cell, 120.0); + sampleMeshFloorCells(scene, [{ lat: cell.lat - 0.0003, lon: cell.lon }], opts); + sampleMeshFloorCells(scene, [onDeck], opts); + assert.equal(calls, 3); + assert.equal(cachedMeshFloor(onDeck.lat, onDeck.lon), 129.0); +}); + +test('sampler: a spot refused while the DEM prior was evicted is probed again', async () => { + const { createGroundFloor } = await import('../services/groundFloor.js'); + const { createMeshFloorSampler } = await import('../services/meshFloorSampler.js'); + let prior = 120.0; + const terrain = { + cachedRealEllipsoidalGround: () => prior, + resolveEllipsoidalGround: async () => {}, + }; + const groundFloor = createGroundFloor({ terrain }); + const sampler = createMeshFloorSampler({ groundFloor }); + const cell = { lat: 41.5, lon: -97.66 }; + const deckLat = cell.lat + 0.0003; + const scene = fakeScene((carto) => + Math.abs(Cesium.Math.toDegrees(carto.latitude) - deckLat) < 1e-7 ? 129.0 : 121.0); + const opts = { viewerLat: cell.lat, viewerLon: cell.lon }; + const onDeck = { lat: deckLat, lon: cell.lon }; + sampler.sampleMeshFloorCells(scene, [{ lat: cell.lat - 0.0003, lon: cell.lon }], opts); + prior = null; // the cell's DEM entry is evicted; the street reading stays + sampler.sampleMeshFloorCells(scene, [onDeck], opts); + assert.equal(groundFloor.cachedMeshFloor(onDeck.lat, onDeck.lon), 121.0); + prior = 120.0; + sampler.sampleMeshFloorCells(scene, [onDeck], opts); + assert.equal(groundFloor.cachedMeshFloor(onDeck.lat, onDeck.lon), 129.0); +}); diff --git a/src/layers/cctv/geometry.js b/src/layers/cctv/geometry.js index 362ad4789..a1402c6c2 100644 --- a/src/layers/cctv/geometry.js +++ b/src/layers/cctv/geometry.js @@ -448,7 +448,7 @@ export function createGeometry({ state: layerState, services, parts, source }) { } // Photoreal regime. Sampling is delegated to the shared coarse-cell - // sampler. It remains event-driven, one-shot per cell, and keeps its + // sampler. It remains event-driven, a few probes per cell, and keeps its // existing acceptance window; CCTV adds no rooftop rejection policy. if (sampleGround && parts.model.projectionTilesReady()) { record.groundMeshSampleRequestCount = diff --git a/src/layers/flights/snapshotRenderer.js b/src/layers/flights/snapshotRenderer.js index ec6c2933b..d48d12774 100644 --- a/src/layers/flights/snapshotRenderer.js +++ b/src/layers/flights/snapshotRenderer.js @@ -300,8 +300,8 @@ export function createFlightSnapshotRenderer({ // floor cells collected in the loop (fire-and-forget, single-flight; // read synchronously by NEXT poll's clamp — the military-layer pattern). warmGroundFloor(floorWarmPoints); - // Round 4: sample the RENDERED mesh for those same cells (one-shot per - // cell, budget-capped, viewer-proximate, google-3d regime only). Own + // Round 4: sample the RENDERED mesh for those same cells (a few probes + // per cell, budget-capped, viewer-proximate, google-3d regime only). Own // billboards/models are excluded so a vertical probe can't land on an // aircraft instead of the pavement. sampleMeshFloorCells(flightState._viewer?.scene, floorWarmPoints, { diff --git a/src/layers/military/snapshotRenderer.js b/src/layers/military/snapshotRenderer.js index 7ed2b1c1f..eb3a341f8 100644 --- a/src/layers/military/snapshotRenderer.js +++ b/src/layers/military/snapshotRenderer.js @@ -228,8 +228,8 @@ export function createMilitarySnapshotRenderer({ const bbStale = flightState._billboards.get(icao24); if (bbStale) bbStale.position = liftedPos; } - // Round 4: sample the RENDERED mesh for those cells (one-shot per cell, - // budget-capped, viewer-proximate, google-3d regime only), excluding + // Round 4: sample the RENDERED mesh for those cells (a few probes per + // cell, budget-capped, viewer-proximate, google-3d regime only), excluding // this layer's own billboards/models from the probe. { const viewerCarto = diff --git a/src/services/groundFloor.js b/src/services/groundFloor.js index 27b7dc5e4..3b4939fc5 100644 --- a/src/services/groundFloor.js +++ b/src/services/groundFloor.js @@ -418,13 +418,26 @@ export function createGroundFloor({ terrain, signal }) { // is the photogrammetric MESH, which sits above it (measured ~17 m at the // Austin airport apron). DEM-flooring therefore still buried sprites/trails // inside the mesh. These cells hold the RENDERED surface height, sampled - // one-shot per coarse cell near the viewer (scene.sampleHeight — the same + // at the callers' points, a few per coarse cell near the viewer, each kept + // where it was taken and read back from the nearest (scene.sampleHeight — the same // source groundSnap uses, which is why 3D models always looked right) and // reported here by meshFloorSampler.js. Design principle (owner): NEVER // below the visible surface; slightly above is always fine. The DEM stays // the instant global prior/fallback and the sanity gate for samples. - /** @type {Map} coarse cell key -> rendered-surface height (m, ellipsoidal). */ + /** @type {Map>} + * coarse cell key -> rendered-surface readings (m, ellipsoidal), each kept + * at the point it was probed. A read answers from the reading nearest the + * asked point, so a viaduct and the street beside it in one cell keep their + * own heights. */ const _meshCells = new Map(); + /** @constant {number} Readings closer than this (degrees, about 5 m) are + * one spot: the lower one is kept there. */ + const MESH_READING_SAME_SPOT_DEG = 0.00005; + /** @constant {number} Readings kept per cell. The sampler probes a cell at + * most eight times, but ground snaps report at each aircraft's position; + * past this many, a reading away from the others is not kept, so a busy + * apron cell stays a short list to scan. */ + const MESH_READINGS_PER_CELL = 16; /** @type {boolean} Whether mesh cells apply — true only in the google-3d * regime (photoreal stack). On globe stacks the rendered surface IS the * terrain provider (Re:Earth on keyless), so the DEM is already correct @@ -444,7 +457,8 @@ export function createGroundFloor({ terrain, signal }) { /** * Records a sampled rendered-surface height for the cell containing the - * coordinate (one-shot latch — first accepted sample wins). + * coordinate, unchecked, when the cell has no reading yet. The sampler goes + * through `reportValidatedMeshFloorCell`, which keeps a reading per point. * @param {number} lat @param {number} lon @param {number} heightM */ function reportMeshFloorCell(lat, lon, heightM) { @@ -456,7 +470,7 @@ export function createGroundFloor({ terrain, signal }) { return; const c = coarseFloorCoord(lat, lon); const key = `${c.lat},${c.lon}`; - if (!_meshCells.has(key)) _meshCells.set(key, heightM); + if (!_meshCells.has(key)) _meshCells.set(key, [{ lat, lon, h: heightM }]); } /** Return whether a rendered-mesh sample is plausible against a real DEM prior. */ @@ -471,9 +485,11 @@ export function createGroundFloor({ terrain, signal }) { /** * Validate and record a rendered-mesh floor through the single shared gate. - * Callers must not bypass this with raw `reportMeshFloorCell`: a coarse-LOD, - * rooftop, or aircraft hit would otherwise win the session-long first-write - * latch and poison every floor consumer in the cell. + * Callers must not bypass this with raw `reportMeshFloorCell`: a coarse-LOD + * or aircraft hit would otherwise poison every floor consumer in the cell. + * The reading is kept at the point it was taken; a second reading at the + * same spot (within about 5 m) keeps the lower of the two. A cell keeps at + * most MESH_READINGS_PER_CELL readings. * @returns {boolean} Whether the sample passed and was reported. */ function reportValidatedMeshFloorCell(lat, lon, heightM) { @@ -481,13 +497,41 @@ export function createGroundFloor({ terrain, signal }) { const cell = coarseFloorCoord(lat, lon); const prior = cachedRealEllipsoidalGround(cell.lat, cell.lon); if (!meshFloorSampleWithinPrior(heightM, prior)) return false; - reportMeshFloorCell(cell.lat, cell.lon, heightM); + const key = `${cell.lat},${cell.lon}`; + const readings = _meshCells.get(key); + if (!readings) { + _meshCells.set(key, [{ lat, lon, h: heightM }]); + return true; + } + const same = readings.find( + (r) => + Math.abs(r.lat - lat) < MESH_READING_SAME_SPOT_DEG && + Math.abs(r.lon - lon) < MESH_READING_SAME_SPOT_DEG, + ); + if (same) { + if (heightM < same.h) same.h = heightM; + } else if (readings.length < MESH_READINGS_PER_CELL) { + readings.push({ lat, lon, h: heightM }); + } return true; } /** - * Synchronous mesh-cell read — null when the cell is unsampled OR the - * google-3d regime is inactive (globe stacks must floor on the DEM). + * Whether the cell holding the point has the real DEM prior that + * `reportValidatedMeshFloorCell` judges a reading against. + * @param {number} lat @param {number} lon + * @returns {boolean} + */ + function meshFloorPriorReady(lat, lon) { + if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false; + const c = coarseFloorCoord(lat, lon); + return cachedRealEllipsoidalGround(c.lat, c.lon) != null; + } + + /** + * Synchronous mesh-cell read — the cell's reading nearest the point, or + * null when the cell is unsampled OR the google-3d regime is inactive (globe + * stacks must floor on the DEM). * @param {number} lat @param {number} lon * @returns {number|null} */ @@ -495,8 +539,20 @@ export function createGroundFloor({ terrain, signal }) { if (!_meshPreferred) return null; if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; const c = coarseFloorCoord(lat, lon); - const h = _meshCells.get(`${c.lat},${c.lon}`); - return h != null ? h : null; + const readings = _meshCells.get(`${c.lat},${c.lon}`); + if (!readings) return null; + if (readings.length === 1) return readings[0].h; + const k = Math.cos((lat * Math.PI) / 180) ** 2; + let best = readings[0], + bestD = Infinity; + for (const r of readings) { + const d = (r.lat - lat) ** 2 + k * (r.lon - lon) ** 2; + if (d < bestD) { + bestD = d; + best = r; + } + } + return best.h; } /** Test hook: clears mesh cells (never called at runtime — cells are valid all session). */ @@ -662,6 +718,7 @@ export function createGroundFloor({ terrain, signal }) { reportMeshFloorCell, meshFloorSampleWithinPrior, reportValidatedMeshFloorCell, + meshFloorPriorReady, cachedMeshFloor, _clearMeshFloorCellsForTest, cachedGroundFloor, diff --git a/src/services/groundSnap.js b/src/services/groundSnap.js index eecb1fa34..b1d75cfbb 100644 --- a/src/services/groundSnap.js +++ b/src/services/groundSnap.js @@ -101,7 +101,7 @@ const HELD_SNAP_MAX_DRIFT_M = 250; * alone, and the asymmetry is not tidiness: * * - A cell reading LOW against a real sample is the EXPECTED error here, not a - * contradiction. Cells are a one-shot first-write latch over ~111 m, the + * contradiction. Cells hold a few point readings over ~111 m, the * borrowed-neighbour path leans to the LOWEST on purpose (`neighborFloorM`), * and `displayFloorHeightM` only ever RAISES — the whole floor chain is * built to under-read rather than to invent. Releasing on that sign would diff --git a/src/services/meshFloorSampler.js b/src/services/meshFloorSampler.js index c967f80c8..083789ea9 100644 --- a/src/services/meshFloorSampler.js +++ b/src/services/meshFloorSampler.js @@ -11,6 +11,7 @@ export function createMeshFloorSampler({ coarseFloorCoord, cachedMeshFloor, reportValidatedMeshFloorCell, + meshFloorPriorReady, setMeshFloorPreferred, meshFloorPreferred, cachedGroundFloor, @@ -23,13 +24,16 @@ export function createMeshFloorSampler({ // which sits above the Re:Earth bare-earth DEM (measured ~17 m at the Austin // airport apron) — so DEM-floored sprites/trails still buried themselves in // the mesh while 3D models (which groundSnap against the mesh) looked right. - // This module samples the mesh height ONE-SHOT per coarse cell and reports it - // to groundFloor.js, whose cachedGroundFloor() then answers mesh-first. + // This module samples the mesh height at the callers' points, a few probes + // per coarse cell, and reports them to groundFloor.js, which keeps each + // reading that passes its gate at its own point; cachedGroundFloor() then + // answers mesh-first, from the reading nearest the asked point. // // Rationing (the CCTV loading-jitter lesson): scene.sampleHeight forces // tile-load prioritization at the probe point, so sampling is (a) gated to // cells NEAR the viewer — where tiles are streamed and where an error is - // visible at all, (b) capped per call, (c) one-shot per cell for the session, + // visible at all, (b) capped per call, (c) at most MAX_PROBES_PER_CELL + // readings per cell for the session, each at a point apart from the others, // (d) skipped entirely outside the google-3d regime. A returned-undefined // probe (tiles not streamed yet) is NOT latched — it retries on a later call. // @@ -49,6 +53,16 @@ export function createMeshFloorSampler({ * streamed LOD under a high camera is coarse everywhere, so every probe * would latch junk. */ const MAX_CAMERA_HEIGHT_M = 25_000; + /** @constant {number} Readings per cell: a cell is probed at most this + * many times, each at a point apart from the others, and groundFloor keeps + * each reading at its point (a read answers from the nearest one). */ + const MAX_PROBES_PER_CELL = 8; + /** @constant {number} How far apart (degrees, about 17 m) a new probe must + * be from the cell's earlier ones to be worth taking. */ + const PROBE_APART_DEG = 0.00015; + /** @type {Map>} Points probed + * per cell that returned a reading. */ + const _probedPoints = new Map(); /** @type {Cesium.Cartographic} Scratch for probe coordinates. */ const _scratchProbe = new Cesium.Cartographic(); @@ -82,7 +96,7 @@ export function createMeshFloorSampler({ /** * Samples the rendered mesh height for the coarse cells containing `points`, - * one-shot per cell, budget-capped, viewer-proximity-gated. Call ONCE per + * a few probes per cell, budget-capped, viewer-proximity-gated. Call ONCE per * layer poll (never per contact, never per frame). Synchronous — Cesium's * sampleHeight is a CPU-side query against loaded tiles. * @@ -119,8 +133,21 @@ export function createMeshFloorSampler({ const cell = coarseFloorCoord(p.lat, p.lon); const key = `${cell.lat},${cell.lon}`; if (attempted.has(key)) continue; + // A cell with a reading is probed again only at a point well apart + // from the ones already probed, a few times at most, so the cell's + // readings follow where its vehicles actually are. + const probes = _probedPoints.get(key); + if ( + cachedMeshFloor(cell.lat, cell.lon) != null && + (probes?.length >= MAX_PROBES_PER_CELL || + probes?.some( + (q) => + Math.abs(q.lat - p.lat) < PROBE_APART_DEG && + Math.abs(q.lon - p.lon) < PROBE_APART_DEG, + )) + ) + continue; attempted.add(key); - if (cachedMeshFloor(cell.lat, cell.lon) != null) continue; // one-shot latch if ( Number.isFinite(viewerLat) && Number.isFinite(viewerLon) && @@ -131,9 +158,11 @@ export function createMeshFloorSampler({ } let height; try { + // Probe where the caller's point is, not at the cell's centre: a + // centre that falls on a roof would lift everything in the cell. const carto = Cesium.Cartographic.fromDegrees( - cell.lon, - cell.lat, + p.lon, + p.lat, 0, _scratchProbe, ); @@ -171,7 +200,23 @@ export function createMeshFloorSampler({ // feasible and deliberately deferred — see the P2 in // the project roadmap, which bundles it with the fly_route probe so // one post-launch pass settles rendered-mesh sampling everywhere. - reportValidatedMeshFloorCell(cell.lat, cell.lon, height); + const accepted = reportValidatedMeshFloorCell(p.lat, p.lon, height); + // A spot counts as probed once the gate has judged it against a real + // prior: accepted, or refused with the cell's DEM warm. A spot refused + // only because the DEM was still cold is tried again later. Each spot is + // recorded once, so a cell retried while it has no reading does not fill + // its probe allowance with the same spot. + if (!accepted && !meshFloorPriorReady(cell.lat, cell.lon)) continue; + if (!probes) _probedPoints.set(key, [{ lat: p.lat, lon: p.lon }]); + else if ( + probes.length < MAX_PROBES_PER_CELL && + !probes.some( + (q) => + Math.abs(q.lat - p.lat) < PROBE_APART_DEG && + Math.abs(q.lon - p.lon) < PROBE_APART_DEG, + ) + ) + probes.push({ lat: p.lat, lon: p.lon }); } } From 9bce41d3c6a83125ac496ad3b6abeea6ca200f3b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 9 Nov 2026 01:26:01 -0800 Subject: [PATCH 2/3] Read the mesh floor where transit vehicles and grounded flights actually are With readings kept per point, the floor a vehicle gets depends on which point is probed and where it is read. Transit offered the sampler the first point of each vehicle's path in a cell, usually its oldest fix there, so a vehicle that stayed in one cell never had its current spot probed. It now offers the latest point of the path in each cell, and the newest fix in the vehicle's own cell (the selected vehicle's history is walked newest first). The grounded-flight display clamp read the floor at its sticky cell's centre, which now answers from the reading nearest the centre, not the contact. It reads at the contact's own position, held inside the sticky cell so boundary hysteresis still applies. --- docs/CURRENT-STATE.md | 17 ++-- src/data/flights.test.mjs | 35 +++++++++ src/layers/flights/motion.js | 29 +++++-- src/layers/transit/height.js | 16 +++- src/layers/transit/lifecycle.test.mjs | 108 ++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 13 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 5e7c36be2..0739cf5e1 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1360,13 +1360,16 @@ the same way; a cell keeps at most sixteen). A read answers from the cell's reading nearest the asked point, so a viaduct and the street beside it in one cell keep their own heights; two readings within about 5 m of each other keep the lower. A spot refused only because the cell's DEM prior was cold is tried -again later. Before this, one probe at the cell's centre latched for the -session, so a centre on a roof lifted everything in the cell (a Haymarket bus -drew 23.5 m above the street, on the garage roof) and a centre in the river put -Red Line trains on the Longfellow Bridge 6–9 m under the deck. Residual: the -probe is vertical, so a vehicle directly under a deck or a roof overhang reads -the structure above it, and a vehicle between readings takes the nearest one, so -its floor steps where the nearest reading changes. +again later. Transit offers the sampler the latest point of each vehicle's path +in a cell (its newest fix in its own cell), and the grounded-flight display +clamp reads at the contact, held inside its sticky cell. Before this, one probe +at the cell's centre latched for the session, so a centre on a roof lifted +everything in the cell (a Haymarket bus drew 23.5 m above the street, on the +garage roof) and a centre in the river put Red Line trains on the Longfellow +Bridge 6–9 m under the deck. Residual: the probe is vertical, so a vehicle +directly under a deck or a roof overhang reads the structure above it, and a +vehicle between readings takes the nearest one, so its floor steps where the +nearest reading changes. ## Vessel components and sources diff --git a/src/data/flights.test.mjs b/src/data/flights.test.mjs index 7fe197c30..1d6007e8f 100644 --- a/src/data/flights.test.mjs +++ b/src/data/flights.test.mjs @@ -20,9 +20,11 @@ import { setMilitaryLayerActive } from './militaryRegistry.js'; import { GROUND_FLOOR_LIFT_M, reportMeshFloorCell, + reportValidatedMeshFloorCell, setMeshFloorPreferred, _clearMeshFloorCellsForTest, } from './groundFloor.js'; +import { resolveEllipsoidalGround } from './terrainHeights.js'; import { _setTrackedOverlayHostForTest, destroyTrackedReadout, @@ -1181,6 +1183,39 @@ test('display floor: a grounded contact that drifted onto a higher cell is lifte assert.ok(Math.abs(Cesium.Math.toDegrees(c.longitude) + 97.6604) < 1e-6); }); +test('display floor: reads the reading nearest the contact, not the cell centre', async () => { + _clearDisplayFloorStateForTest(); + _clearMeshFloorCellsForTest(); + setMeshFloorPreferred(true); + const realFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response(JSON.stringify({ results: [{ ellipsoid: 120 }] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + try { + await resolveEllipsoidalGround([{ lat: 30.317, lon: -97.717 }]); + } finally { + globalThis.fetch = realFetch; + } + // A hangar roof over the cell's centre; the apron where the contact is. + assert.equal(reportValidatedMeshFloorCell(30.317, -97.717, 142), true); + assert.equal(reportValidatedMeshFloorCell(30.3173, -97.7173, 121), true); + const pos = Cesium.Cartesian3.fromDegrees(-97.7173, 30.3173, 100); + const out = _floorGroundedDisplayPositionForTest( + { onGround: true }, + pos, + false, + ); + const c = _floorCarto(out); + assert.ok( + Math.abs(c.height - (121 + GROUND_FLOOR_LIFT_M)) < 0.05, + `height ${c.height} should be the apron's 121 + lift`, + ); + _clearMeshFloorCellsForTest(); + _clearDisplayFloorStateForTest(); +}); + test('display floor: an AIRBORNE contact over the same cell is never lifted', () => { _clearDisplayFloorStateForTest(); _clearMeshFloorCellsForTest(); diff --git a/src/layers/flights/motion.js b/src/layers/flights/motion.js index a14c8c4ac..c09522153 100644 --- a/src/layers/flights/motion.js +++ b/src/layers/flights/motion.js @@ -37,6 +37,22 @@ import { DISPLAY_CORRIDOR_FAIR_SHARE, } from './policy.js'; +/** Half a coarse floor cell (0.001°), less a margin so rounding keeps a + * clamped coordinate inside the cell. */ +const HALF_CELL_DEG = 0.00049; + +/** + * A coordinate held within half a cell of the cell's centre (the short way + * round for longitude), so a read lands in the given cell. + * @param {number} deg @param {number} centreDeg + * @returns {number} + */ +function clampToCell(deg, centreDeg) { + let d = deg - centreDeg; + d -= 360 * Math.round(d / 360); + return centreDeg + Math.max(-HALF_CELL_DEG, Math.min(HALF_CELL_DEG, d)); +} + export function createMotion({ flightState, services, @@ -865,12 +881,15 @@ export function createMotion({ ); // Boundary hysteresis: a position jittering across a cell edge would flip // floors at fleet-tick rate (see stickyFloorCell). - const cell = stickyFloorCell( - Cesium.Math.toDegrees(carto.latitude), - Cesium.Math.toDegrees(carto.longitude), - state?.cell, + const latDeg = Cesium.Math.toDegrees(carto.latitude); + const lonDeg = Cesium.Math.toDegrees(carto.longitude); + const cell = stickyFloorCell(latDeg, lonDeg, state?.cell); + // A cell holds rendered-surface readings at the points they were taken, + // so the floor is read where the contact is, held inside its sticky cell. + const floor = cachedGroundFloor( + clampToCell(latDeg, cell.lat), + clampToCell(lonDeg, cell.lon), ); - const floor = cachedGroundFloor(cell.lat, cell.lon); const next = state || { cell, in: new Cesium.Cartesian3(), diff --git a/src/layers/transit/height.js b/src/layers/transit/height.js index 2e2945a88..027dc7d41 100644 --- a/src/layers/transit/height.js +++ b/src/layers/transit/height.js @@ -124,7 +124,14 @@ export function createHeight({ state, services, parts }) { const cells = new Map(); const collect = (point, answer) => { if (!onGround) return; - if (!cells.has(answer.cell)) + // A cell keeps the last point offered in it: the path is walked + // oldest first, so that is where the vehicle is now or about to be, + // and the mesh sampler reads the surface there. + const held = cells.get(answer.cell); + if (held) { + held.lat = point.lat; + held.lon = point.lon; + } else cells.set(answer.cell, { lat: point.lat, lon: point.lon, @@ -136,6 +143,13 @@ export function createHeight({ state, services, parts }) { const newest = entry.fixes.at(-1); const answer = requestHeight(newest.lat, newest.lon, entry); entry.heightCell = answer.cell; + // The selected vehicle's path also offers its history, newest segment + // first; the vehicle's own cell is read where its newest fix is. + const own = cells.get(answer.cell); + if (own) { + own.lat = newest.lat; + own.lon = newest.lon; + } if (answer.height !== null) { entry.heightM = answer.height; entry.heightResolved = true; diff --git a/src/layers/transit/lifecycle.test.mjs b/src/layers/transit/lifecycle.test.mjs index e36d87de8..fee9a60ed 100644 --- a/src/layers/transit/lifecycle.test.mjs +++ b/src/layers/transit/lifecycle.test.mjs @@ -2938,6 +2938,114 @@ test('mesh refinement reaches all 100 retained DEM cells within five poll budget assert.equal(probed.size, 100); }); +test('mesh refinement probes the latest point the path offers in a cell', async (t) => { + const { createHeight } = await import('./height.js'); + // Three points along one cell's stretch of path, oldest first. + const path = [ + { lat: 42.3611, lon: -71.0751 }, + { lat: 42.3612, lon: -71.0753 }, + { lat: 42.3614, lon: -71.0754 }, + ]; + const entry = { key: 'train', to: path[0], fixes: [path.at(-1)] }; + const probed = []; + const state = { + _enabled: true, + _selectedKey: null, + _visible: new Set([entry]), + _vehicles: new Map([[entry.key, entry]]), + _floorCursor: 0, + _heightDirty: new Set(), + _viewer: { camera: { positionCartographic: { height: 100 } } }, + }; + const height = createHeight({ + state, + services: { + render: { governorRequestRender() {} }, + ground: { + coarseFloorCoord: (lat, lon) => ({ + lat: Number(lat.toFixed(3)), + lon: Number(lon.toFixed(3)), + }), + cachedGroundFloor: () => 10, + GROUND_FLOOR_LIFT_M: 1.5, + }, + mesh: { + sampleMeshFloorCells(scene, points) { + probed.push(...points.map((p) => [p.lat, p.lon])); + }, + }, + }, + parts: { + trails: { + prepareEntry(e, collect) { + if (collect) + for (const point of path) + collect(point, height.requestHeight(point.lat, point.lon)); + }, + }, + rendering: { requestVisibility() {} }, + }, + }); + t.after(() => height.clear()); + height.anchorFloors(); + assert.deepEqual(probed, [[42.3614, -71.0754]]); +}); + +test('the selected vehicle is probed at its newest fix in its own cell', async (t) => { + const { createHeight } = await import('./height.js'); + // The selected path offers its active corridor first, then its history + // newest segment first, so the last offer in the cell is an older point. + const newest = { lat: 42.3614, lon: -71.0754 }; + const offers = [ + newest, + { lat: 42.3612, lon: -71.0753 }, + { lat: 42.3611, lon: -71.0751 }, + ]; + const entry = { key: 'train', to: offers[0], fixes: [newest] }; + const probed = []; + const state = { + _enabled: true, + _selectedKey: entry.key, + _visible: new Set([entry]), + _vehicles: new Map([[entry.key, entry]]), + _floorCursor: 0, + _heightDirty: new Set(), + _viewer: { camera: { positionCartographic: { height: 100 } } }, + }; + const height = createHeight({ + state, + services: { + render: { governorRequestRender() {} }, + ground: { + coarseFloorCoord: (lat, lon) => ({ + lat: Number(lat.toFixed(3)), + lon: Number(lon.toFixed(3)), + }), + cachedGroundFloor: () => 10, + GROUND_FLOOR_LIFT_M: 1.5, + }, + mesh: { + sampleMeshFloorCells(scene, points) { + probed.push(...points.map((p) => [p.lat, p.lon])); + }, + }, + }, + parts: { + trails: { + prepareEntry(e, collect) { + if (collect) + for (const point of offers) + collect(point, height.requestHeight(point.lat, point.lon)); + }, + }, + rendering: { requestVisibility() {} }, + }, + }); + t.after(() => height.clear()); + height.anchorFloors(); + assert.deepEqual(probed, [[42.3614, -71.0754]]); +}); + test('selection churn releases past geometry on every retained vehicle', async (t) => { const app = harness(t, { floorAt: () => 10 }); const { mergeHistory, seek } = await import('../../data/contactPlayback.js'); From 09520bc34ce262ad208295fc9843146f6dce3330 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 9 Nov 2026 14:54:51 -0800 Subject: [PATCH 3/3] Changelog fragment for #1642 --- changelog.d/1642-transit-floor-per-point.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog.d/1642-transit-floor-per-point.md diff --git a/changelog.d/1642-transit-floor-per-point.md b/changelog.d/1642-transit-floor-per-point.md new file mode 100644 index 000000000..04dd59760 --- /dev/null +++ b/changelog.d/1642-transit-floor-per-point.md @@ -0,0 +1,7 @@ +- Keep buses and trains off rooftops and out of bridge decks on the photoreal + 3D tiles: the ground floor probes the rendered mesh where each vehicle is, + keeps each reading at the point it was taken (up to sixteen a cell) and + answers from the reading nearest the vehicle, instead of one probe at each + 110 m cell's centre that lifted every vehicle in the cell onto whatever lay + there. Transit probes each vehicle's latest point, and grounded flights read + the floor at their own position (Sameh Khamis, @samehkhamis, #1642).