diff --git a/CHANGELOG.md b/CHANGELOG.md index b51d8c8e4..9cb230427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md - Unchanged local infrastructure overlays no longer sustain idle rendering. Ground samples wait for visible terrain to settle and cannot place a marker below its loaded surface; roofs and valid below-sea-level heights are retained. + Already sampled markers also follow higher terrain as close-up tiles refine. - Datacenter and dam marker stems use bounded, zoom-dependent active sets with stable selection during camera motion. Close-up stems scale to the actual diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 8e7d65622..1332eaf49 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -12,7 +12,9 @@ Unchanged overlay entries do not request another frame; moved positions, changed membership and re-enabling still publish. Ground sampling waits for visible globe tiles to settle and uses valid loaded terrain as a floor while retaining rooftop and below-sea-level elevations. A hidden globe does not -constrain photoreal geometry, and retry work remains bounded. +constrain photoreal geometry, and retry work remains bounded. Already sampled +nearby markers follow higher settled terrain on existing bounded walks, without +additional GPU samples or timers, when close-up tile detail refines the floor. The submarine-cable renderer is unchanged. These limits bound active stem work; they do not reduce the materialized entity count or establish an FPS gain. diff --git a/src/data/localGeojson.js b/src/data/localGeojson.js index b32fef1f7..7cf7aeb5b 100644 --- a/src/data/localGeojson.js +++ b/src/data/localGeojson.js @@ -807,7 +807,8 @@ export function createLocalGeoJsonLayer({ if (!isActive) continue; const wasGroundSampled = record.groundSampled; - if (refreshStemGeometry) { + const terrainFloorChanged = refreshLocalTerrainFloor(viewer, record); + if (refreshStemGeometry || terrainFloorChanged) { updateLocalStemGeometry(viewer, record, now); } else if (canSampleGround && !record.groundSampled && now - record.lastGroundSampleMs >= GROUND_SAMPLE_RETRY_MS) { @@ -911,6 +912,21 @@ function insertLocalCellContender(contenders, record) { if (contenders.length > LOCAL_OVERLAY_CELL_SURPLUS) contenders.length = LOCAL_OVERLAY_CELL_SURPLUS; } +function refreshLocalTerrainFloor(viewer, record) { + const globe = viewer.scene.globe; + if (!record.groundSampled || !globe?.show || globe.tilesLoaded === false) return false; + if (Cesium.Cartesian3.distance(viewer.camera.positionWC, record.base) + >= GROUND_SAMPLE_MAX_DISTANCE_M) return false; + // Camera motion can refine terrain after the first successful depth sample. + // Reuse the loaded mesh on existing bounded walks; never arm a timer or do + // another GPU readback just to maintain this floor. Keep roof elevations. + const height = globe.getHeight?.(record.carto); + if (!Number.isFinite(height) || Math.abs(height) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M + || height <= record.groundHeight) return false; + setLocalGroundHeight(record, height); + return true; +} + function sampleLocalGroundHeight(viewer, record, now) { if (record.groundSampled || !viewer.scene.sampleHeightSupported) return false; if (now - record.lastGroundSampleMs < GROUND_SAMPLE_RETRY_MS) return false; @@ -933,7 +949,12 @@ function sampleLocalGroundHeight(viewer, record, now) { sampled = Math.max(sampled, terrainHeight); } record.groundSampled = true; - record.groundHeight = sampled; + setLocalGroundHeight(record, sampled); + return true; +} + +function setLocalGroundHeight(record, height) { + record.groundHeight = height; Cesium.Cartesian3.fromRadians( record.carto.longitude, record.carto.latitude, @@ -942,7 +963,6 @@ function sampleLocalGroundHeight(viewer, record, now) { record.base, ); record.entity.__localBaseCartesian = record.base; - return true; } function updateLocalStemGeometry(viewer, record, now, knownDistance = null) { diff --git a/src/data/localGeojson.test.mjs b/src/data/localGeojson.test.mjs index 0fd84f24d..40f09b96d 100644 --- a/src/data/localGeojson.test.mjs +++ b/src/data/localGeojson.test.mjs @@ -497,7 +497,7 @@ test('local infrastructure creates no native labels or per-frame geometry callba assert.match(source, /const stemPositionBuffers = \[\[base, tip\], \[base, tip\]\]/); assert.match(source, /record\.entity\.polyline\.positions\.setValue\(stemPositions\)/); assert.match(source, /viewer\.camera\.moveEnd\.addEventListener/); - assert.match(source, /if \(refreshStemGeometry\)/); + assert.match(source, /if \(refreshStemGeometry \|\| terrainFloorChanged\)/); assert.match(source, /now - _lastVisibilityUpdate < VISIBILITY_UPDATE_MS/); }); @@ -728,6 +728,41 @@ test('ground sampling waits for visible globe tiles before caching a height', as assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01); }); +test('settled terrain refinement lifts an already sampled stem without another GPU sample', async (t) => { + const env = await createRealLocalLayerHarness({ + sampleHeightSupported: true, sampleHeight: () => 117, + }); + const clock = installFakeClock(t); + t.after(() => { env.layer.destroy(env.viewer); env.cleanup(); }); + let terrain = 117; + const globe = { show: true, tilesLoaded: true, getHeight: () => terrain }; + env.viewer.scene.globe = globe; + setCameraAltitude(env, 20_000); + env.preRender.raise(); + assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01); + + terrain = 120; + globe.tilesLoaded = false; + clock.advance(500); + env.preRender.raise(); + assert.ok(Math.abs(baseHeightM(env) - 117) < 0.01, 'wait for the refined mesh to settle'); + globe.tilesLoaded = true; + clock.advance(500); + env.preRender.raise(); + assert.ok(Math.abs(baseHeightM(env) - 120) < 0.01, 'a parked marker must follow the refined floor'); + + terrain = 110; + clock.advance(500); + env.preRender.raise(); + assert.ok(Math.abs(baseHeightM(env) - 120) < 0.01, 'a lower floor must not flatten existing geometry'); + terrain = 140; + globe.show = false; + clock.advance(500); + env.preRender.raise(); + assert.ok(Math.abs(baseHeightM(env) - 120) < 0.01, 'hidden globe terrain must not constrain geometry'); + assert.equal(env.sampleCalls.count, 1, 'terrain maintenance must not repeat the GPU readback'); +}); + for (const [terrainHeight, sampledHeight, expectedHeight, globeShown] of [ [117, -2238, 117, true], [-400, -410, -400, true],