From ef104aaac36eca721c54b06644490711ff0bca6b Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 10 Nov 2026 16:13:06 +0400 Subject: [PATCH] Move surface placement into the globe and give it the fourth trigger src/globe/surfacePlacement.js takes over the shared surface placer and adds readSurface (tiles through sampleHeight, otherwise terrain, with a plausibility range), surfaceTriggers (one subscription to camera moves, tile loads, the photoreal switch and terrain revisions) and a forced full pass. Points now re-place when the terrain provider changes, which they did not before. GhostMaps, cables, OSM Places and Street Level move to the new module unchanged. --- scripts/package-boundaries.json | 49 ++-- .../surfacePlacement.js} | 151 ++++++++--- src/globe/surfacePlacement.test.mjs | 249 ++++++++++++++++++ src/layers/ghostMaps/render/display.js | 2 +- src/layers/osmPlaces/render/display.js | 2 +- .../shared/render/surfacePoints.test.mjs | 75 ------ src/layers/streetLevel/render/display.js | 11 +- src/layers/submarineCables/render/display.js | 2 +- 8 files changed, 410 insertions(+), 131 deletions(-) rename src/{layers/shared/render/surfacePoints.js => globe/surfacePlacement.js} (53%) create mode 100644 src/globe/surfacePlacement.test.mjs delete mode 100644 src/layers/shared/render/surfacePoints.test.mjs diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 33b529d69..ddde315dd 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -34,7 +34,9 @@ "src/layers/infrastructure/render/display.js", "src/layers/shared/color.js", "src/sources/requestDeadline.js", - "src/overlays/bindOverlayHost.js" + "src/overlays/bindOverlayHost.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": [] }, @@ -978,7 +980,9 @@ "src/ui/unitLabels.js", "src/geo/", "src/globe/cesium/doubleClick.js", - "src/globe/cesium/orbital.js" + "src/globe/cesium/orbital.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": ["cesium"] }, @@ -1273,7 +1277,9 @@ "src/data/trackedReadout.js", "src/ui/releaseTrackers.js", "src/globe/cesium/orbital.js", - "src/time/clock.js" + "src/time/clock.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": ["cesium", "vue", "pinia", "vue-i18n"] }, @@ -1944,7 +1950,9 @@ "src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-Hant.json", "src/geo/", - "src/layers/traffic/render/display.js" + "src/layers/traffic/render/display.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": ["pbf", "@mapbox/vector-tile"] }, @@ -2111,10 +2119,11 @@ "src/layers/shared/color.js", "src/globe/cameraMath.js", "src/layers/streetLevel/render/display.js", - "src/layers/shared/render/surfacePoints.js", "src/globe/contract.js", "src/time/worldClock.js", - "src/time/clock.js" + "src/time/clock.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": ["pbf", "@mapbox/vector-tile", "mapillary-js"] }, @@ -2228,8 +2237,9 @@ "src/sources/requestDeadline.js", "src/layers/shared/color.js", "src/layers/submarineCables/render/display.js", - "src/layers/shared/render/surfacePoints.js", - "src/overlays/bindOverlayHost.js" + "src/overlays/bindOverlayHost.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": ["cesium"] }, @@ -3543,7 +3553,6 @@ "src/layers/earthquakes/render/display.js", "src/layers/airQuality/render/display.js", "src/layers/submarineCables/render/display.js", - "src/layers/shared/render/surfacePoints.js", "src/layers/firms/render/sprites.js", "src/layers/cyclones/render/display.js", "src/layers/droneAirspace/render/display.js", @@ -3593,7 +3602,8 @@ "src/globe/cesium/orbital.js", "src/globe/cesium/worldTime.js", "src/data/geoImportLimits.js", - "src/data/geoImportPlan.js" + "src/data/geoImportPlan.js", + "src/globe/surfacePlacement.js" ], "external": [ "hls.js", @@ -4273,7 +4283,6 @@ "src/layers/gnss/render/display.js", "src/layers/firms/render/sprites.js", "src/layers/submarineCables/render/display.js", - "src/layers/shared/render/surfacePoints.js", "src/layers/perimeters/render/display.js", "src/layers/droneAirspace/render/display.js", "src/globe/ground.js", @@ -4306,7 +4315,9 @@ "src/data/ucsSatellites.js", "src/data/imagingSensors.js", "src/data/planespottersPhoto.js", - "src/data/geoImportLimits.js" + "src/data/geoImportLimits.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": [ "hls.js", @@ -4394,7 +4405,8 @@ "src/data/placeBoundaries.js", "src/data/placeNames.js", "src/data/local_data/us_census_places/files.js", - "src/data/local_data/wof_neighborhoods/files.js" + "src/data/local_data/wof_neighborhoods/files.js", + "src/globe/surfacePlacement.js" ], "external": [ "cesium", @@ -4661,7 +4673,9 @@ "src/geo/", "src/layers/directions/render/display.js", "src/layers/directions/render/style.js", - "src/overlays/bindOverlayHost.js" + "src/overlays/bindOverlayHost.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": [] }, @@ -5031,7 +5045,8 @@ "src/data/trackedReadout.js", "src/data/trackingGesture.js", "src/time/worldClock.js", - "src/time/clock.js" + "src/time/clock.js", + "src/globe/surfacePlacement.js" ], "external": ["cesium", "egm96-universal", "pbf"] }, @@ -5592,7 +5607,9 @@ "src/layers/wildlifeSightings/display.js", "src/data/spriteOrder.js", "src/layers/shared/color.js", - "src/overlays/bindOverlayHost.js" + "src/overlays/bindOverlayHost.js", + "src/globe/surfaceDetail.js", + "src/globe/surfacePlacement.js" ], "external": ["cesium"] }, diff --git a/src/layers/shared/render/surfacePoints.js b/src/globe/surfacePlacement.js similarity index 53% rename from src/layers/shared/render/surfacePoints.js rename to src/globe/surfacePlacement.js index 867bdf711..c505c98d8 100644 --- a/src/layers/shared/render/surfacePoints.js +++ b/src/globe/surfacePlacement.js @@ -7,7 +7,8 @@ // tiles have loaded; a point whose height holds is not written again, so a // parked camera asks for no frames. -import * as geo from '../../../geo/index.js'; +import * as geo from '../geo/index.js'; +import { acceptedSurfaceHeight } from './surfaceDetail.js'; const SAMPLES_PER_PASS = 16; const SAMPLE_BUDGET_MS = 4; @@ -21,8 +22,88 @@ const SCREEN_MARGIN = 0.25; const plausible = (height) => Number.isFinite(height) && height >= -500 && height <= 9000; +/** Read the visible surface, or an explicitly requested terrain/mesh floor. + * `scene` retains callers that include roofs even on a terrain map; `tiles` + * requires a mesh hit. `sceneFloor` keeps the terrain below a scene hit. + * A finite tile-size limit also rejects coarse mesh LOD. `minHeight` preserves + * consumers that intentionally admit coarse below-sea-level terrain. + */ +export function readSurface( + engine, + cartographic, + { exclude = [], prefer, maxTileSizeM = Infinity, minHeight = -500 } = {}, +) { + const valid = (height) => + Number.isFinite(height) && height >= minHeight && height <= 9000; + const surface = engine?.surface; + if (!surface || !cartographic) return undefined; + const tiles = engine.maps?.photorealActive?.() === true; + if ( + prefer !== 'terrain' && + (tiles || + prefer === 'scene' || + prefer === 'sceneFloor' || + prefer === 'tiles') + ) { + let height; + try { + if (Number.isFinite(maxTileSizeM)) + height = acceptedSurfaceHeight( + surface.sampleHeightDetailed(cartographic, { exclude }), + { maxTileSizeM }, + ); + else if (surface.supported !== false) + height = surface.sampleHeight(cartographic, exclude); + } catch { + // A streaming or retiring surface may not answer yet. + } + if (valid(height)) { + if (prefer === 'sceneFloor' && !tiles) { + const terrain = readSurface(engine, cartographic, { + prefer: 'terrain', + }); + return terrain === undefined ? height : Math.max(height, terrain); + } + return height; + } + if (prefer === 'tiles') return undefined; + } + try { + const height = + surface.terrainHeightSupported !== false + ? surface.terrainHeight(cartographic) + : undefined; + return valid(height) ? height : undefined; + } catch { + return undefined; + } +} + +/** Subscribe once to camera rest and changes to the rendered ground. */ +export function surfaceTriggers(engine, onChange) { + let loaded = engine.surface.tilesLoaded(); + let tiles = engine.maps?.photorealActive?.() === true; + let terrain = engine.maps?.terrainRevision?.(); + const removeMove = engine.camera.moveEnd.addEventListener(onChange); + const removeFrame = engine.frames.preRender.addEventListener(() => { + const nextLoaded = engine.surface.tilesLoaded(); + const nextTiles = engine.maps?.photorealActive?.() === true; + const nextTerrain = engine.maps?.terrainRevision?.(); + const changed = + (nextLoaded && !loaded) || nextTiles !== tiles || nextTerrain !== terrain; + loaded = nextLoaded; + tiles = nextTiles; + terrain = nextTerrain; + if (changed) onChange(); + }); + return () => { + removeMove(); + removeFrame(); + }; +} + /** - * @param {import('../../../globe/contract.js').GlobeEngine} engine + * @param {import('./contract.js').GlobeEngine} engine * @param {object} options * @param {() => Iterable<{ set: object | null, points: object[], exclude?: object[] }>} options.groups * Mover sets with the points they draw, point i as instance i, and what @@ -31,27 +112,35 @@ const plausible = (height) => * placement replaces (keeping the height in `surfaceHeight`); one with * `show === false` is skipped. * @param {() => void} options.request Asks for a frame after a placement. - * @param {number} [options.heightOffset=0] Metres above the surface. + * @param {number | ((point: object) => number)} [options.heightOffset=0] Metres above the surface. * @param {boolean} [options.estimateGround=false] Screen-test a point not * measured yet on the loaded terrain (under 3D tiles without it, at the * ground under the camera) rather than where it stands now. - * @returns {{ place: () => void, destroy: () => void }} `place()` measures - * the points not yet placed on this surface (new points, for example). + * @param {number} [options.screenMargin=0.25] Extra viewport fraction to measure. + * @param {string | ((point: object) => string | undefined)} [options.prefer] Explicit height policy for readSurface. + * @returns {{ place: (options?: { all?: boolean }) => void, destroy: () => void }} + * `place()` measures new points; `place({ all: true })` revisits every point. */ export function createSurfacePoints( engine, - { groups, request, heightOffset = 0, estimateGround = false }, + { + groups, + request, + heightOffset = 0, + estimateGround = false, + screenMargin = SCREEN_MARGIN, + prefer, + }, ) { let revision = 0; let timer = null; let destroyed = false; - let tilesLoaded = engine.surface.tilesLoaded(); - let photoreal = engine.maps?.photorealActive?.() === true; function onScreen(position, size) { + if (screenMargin === Infinity) return true; const screen = engine.projector.worldToWindow(position); - const x = size.width * SCREEN_MARGIN; - const y = size.height * SCREEN_MARGIN; + const x = size.width * screenMargin; + const y = size.height * screenMargin; return ( screen && screen.x >= -x && @@ -72,7 +161,8 @@ export function createSurfacePoints( } // Points off screen are passed over until the next move or tile load. - function place() { + function place({ all = false } = {}) { + if (all) revision++; clearTimeout(timer); timer = null; if (destroyed) return; @@ -80,6 +170,7 @@ export function createSurfacePoints( const tiles = engine.maps?.photorealActive?.() === true; const started = performance.now(); let samples = 0; + let visited = 0; let moved = false; let cameraGround; for (const { set, points, exclude = [set] } of groups()) { @@ -90,18 +181,23 @@ export function createSurfacePoints( if (point.surfaceRevision === revision) continue; if ( samples >= SAMPLES_PER_PASS || - performance.now() - started >= SAMPLE_BUDGET_MS + (visited > 0 && performance.now() - started >= SAMPLE_BUDGET_MS) ) { if (changed) set.commit(); if (changed || moved) request(); timer = setTimeout(place, 0); return; } + visited++; point.surfaceRevision = revision; if (point.show === false) continue; + const offset = + typeof heightOffset === 'function' + ? heightOffset(point) + : heightOffset; const estimate = estimateGround && point.surfaceHeight === undefined; const terrain = estimate - ? engine.surface.terrainHeight(point.cartographic) + ? readSurface(engine, point.cartographic, { prefer: 'terrain' }) : undefined; let ground = terrain; if (estimate && tiles && !plausible(ground)) { @@ -115,23 +211,22 @@ export function createSurfacePoints( ? geo.cartesian3.fromRadians( point.cartographic.longitude, point.cartographic.latitude, - ground + heightOffset, + ground + offset, ) : point.position; if (!onScreen(position, size)) continue; samples++; - let height = tiles - ? engine.surface.sampleHeight(point.cartographic, exclude) - : undefined; - if (!plausible(height)) - height = terrain ?? engine.surface.terrainHeight(point.cartographic); + const height = readSurface(engine, point.cartographic, { + exclude, + prefer: typeof prefer === 'function' ? prefer(point) : prefer, + }); if (!plausible(height)) continue; if (Math.abs(height - point.surfaceHeight) < HEIGHT_EPSILON_M) continue; point.surfaceHeight = height; point.position = geo.cartesian3.fromRadians( point.cartographic.longitude, point.cartographic.latitude, - height + heightOffset, + height + offset, ); set.set(index, { position: point.position }); changed = true; @@ -142,18 +237,7 @@ export function createSurfacePoints( if (moved) request(); } - function refine() { - revision++; - place(); - } - const removeMoveEnd = engine.camera.moveEnd.addEventListener(refine); - const removeFrame = engine.frames.preRender.addEventListener(() => { - const loaded = engine.surface.tilesLoaded(); - const tiles = engine.maps?.photorealActive?.() === true; - if ((loaded && !tilesLoaded) || tiles !== photoreal) refine(); - tilesLoaded = loaded; - photoreal = tiles; - }); + const removeSurface = surfaceTriggers(engine, () => place({ all: true })); return { place, @@ -161,8 +245,7 @@ export function createSurfacePoints( if (destroyed) return; destroyed = true; clearTimeout(timer); - removeMoveEnd(); - removeFrame(); + removeSurface(); }, }; } diff --git a/src/globe/surfacePlacement.test.mjs b/src/globe/surfacePlacement.test.mjs new file mode 100644 index 000000000..0ae8cf831 --- /dev/null +++ b/src/globe/surfacePlacement.test.mjs @@ -0,0 +1,249 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as geo from '../geo/index.js'; +import { createFakeEngine } from './testing/fakeEngine.js'; +import { + createSurfacePoints, + readSurface, + surfaceTriggers, +} from './surfacePlacement.js'; + +function setup(t, count = 3, options = {}) { + t.mock.method(performance, 'now', () => 0); + const engine = createFakeEngine(); + t.after(() => engine.destroy()); + engine.projector.worldToWindow = () => ({ x: 100, y: 100 }); + const set = engine.movers.createSet({ + id: 'points', + kind: 'point', + capacity: count, + point: { sizePx: 6 }, + }); + const points = Array.from({ length: count }, (_, index) => ({ + cartographic: geo.cartographic.fromDegrees(index, 0), + position: geo.cartesian3.fromDegrees(index, 0), + })); + points.forEach((point, index) => + set.set(index, { position: point.position, show: true }), + ); + set.setCount(count); + set.commit(); + const surface = createSurfacePoints(engine, { + groups: () => [{ set, points }], + request: () => engine.render.request('points'), + ...options, + }); + t.after(() => surface.destroy()); + const heights = () => + points.map( + (point) => geo.cartographic.fromCartesian(point.position).height, + ); + return { engine, surface, heights }; +} + +test('on the globe points stand on the terrain without a pick sample', (t) => { + const { engine, surface, heights } = setup(t); + let picks = 0; + engine.fake.script('sampleHeight', () => { + picks++; + return 900; + }); + engine.fake.script('terrainHeight', () => 40); + surface.place(); + assert.equal(picks, 0); + for (const height of heights()) assert.ok(Math.abs(height - 40) < 0.01); +}); + +test('under 3D tiles points stand on the sampled tiles', async (t) => { + const { engine, surface, heights } = setup(t); + await engine.maps.setPhotorealTiles({ id: 'photoreal' }); + engine.fake.script('sampleHeight', () => 55); + engine.fake.script('terrainHeight', () => 40); + surface.place(); + for (const height of heights()) assert.ok(Math.abs(height - 55) < 0.01); +}); + +test('a parked camera asks for no frames once the points stand', (t) => { + const { engine, heights } = setup(t); + let terrain = 40; + engine.fake.script('terrainHeight', () => terrain); + engine.fake.script('tilesLoaded', () => true); + engine.camera.moveEnd.raiseEvent(); + assert.ok(Math.abs(heights()[0] - 40) < 0.01); + const before = engine.fake.renderRequests; + engine.camera.moveEnd.raiseEvent(); + engine.fake.frames(3); + assert.equal(engine.fake.renderRequests, before, 'same heights, no frame'); + terrain = 46; + engine.camera.moveEnd.raiseEvent(); + assert.ok(engine.fake.renderRequests > before, 'a changed height draws'); + assert.ok(Math.abs(heights()[0] - 46) < 0.01); +}); + +for (const trigger of [ + 'moveEnd', + 'tilesLoaded', + 'photorealActive', + 'terrainRevision', +]) { + for (const source of ['terrain', 'tiles', 'missing', 'implausible']) { + test(`${trigger} refreshes points on ${source}`, async (t) => { + const { engine, surface, heights } = setup(t); + let terrain = 40; + let tiles = 50; + let loaded = true; + engine.fake.script('terrainHeight', () => terrain); + engine.fake.script('sampleHeight', () => + source === 'missing' + ? undefined + : source === 'implausible' + ? 9001 + : tiles, + ); + engine.fake.script('tilesLoaded', () => loaded); + if (source !== 'terrain') await engine.maps.setPhotorealTiles({}); + engine.fake.frames(1); + surface.place(); + assert.ok(Math.abs(heights()[0] - (source === 'tiles' ? 50 : 40)) < 0.01); + if (trigger === 'photorealActive') { + await engine.maps.setPhotorealTiles(source === 'terrain' ? {} : null); + engine.fake.frames(1); + } + terrain = 70; + tiles = 80; + if (trigger === 'moveEnd') engine.camera.moveEnd.raiseEvent(); + if (trigger === 'terrainRevision') { + await engine.maps.setTerrain({ kind: 'ellipsoid' }); + engine.fake.frames(1); + } + if (trigger === 'photorealActive') { + await engine.maps.setPhotorealTiles(source === 'terrain' ? null : {}); + engine.fake.frames(1); + } + if (trigger === 'tilesLoaded') { + loaded = false; + engine.fake.frames(1); + loaded = true; + engine.fake.frames(1); + } + assert.ok(Math.abs(heights()[0] - (source === 'tiles' ? 80 : 70)) < 0.01); + const requests = engine.fake.renderRequests; + engine.fake.frames(3); + assert.equal(engine.fake.renderRequests, requests); + surface.destroy(); + terrain = 90; + engine.camera.moveEnd.raiseEvent(); + engine.fake.frames(1); + assert.equal(engine.fake.renderRequests, requests); + }); + } +} + +test('forced placement revisits measured points and invalid heights retain the last position', (t) => { + const { engine, surface, heights } = setup(t); + engine.fake.script('terrainHeight', 80); + surface.place(); + engine.fake.script('terrainHeight', 100); + surface.place(); + assert.ok(Math.abs(heights()[0] - 80) < 0.01); + surface.place({ all: true }); + assert.ok(Math.abs(heights()[0] - 100) < 0.01); + for (const height of [-501, 9001, NaN, Infinity, undefined]) { + engine.fake.script('terrainHeight', height); + surface.place({ all: true }); + assert.ok(Math.abs(heights()[0] - 100) < 0.01); + } +}); + +test('surface readers retain explicit terrain and fine-mesh policies', async (t) => { + const engine = createFakeEngine(); + t.after(() => engine.destroy()); + const point = geo.cartographic.fromDegrees(0, 0); + engine.fake.script('terrainHeight', -500); + engine.fake.script('sampleHeight', 9000); + assert.equal(readSurface(engine, point), -500); + await engine.maps.setPhotorealTiles({}); + assert.equal(readSurface(engine, point), 9000); + assert.equal(readSurface(engine, point, { prefer: 'terrain' }), -500); + engine.fake.script('sampleHeightDetailed', { + height: 50, + source: 'tiles', + tileSizeM: 200, + }); + assert.equal( + readSurface(engine, point, { prefer: 'tiles', maxTileSizeM: 100 }), + undefined, + ); + engine.fake.script('sampleHeightDetailed', { height: 50, source: 'terrain' }); + assert.equal( + readSurface(engine, point, { prefer: 'tiles', maxTileSizeM: 100 }), + undefined, + ); +}); + +test('surface subscriptions coalesce frame changes and release both listeners', async (t) => { + const engine = createFakeEngine(); + t.after(() => engine.destroy()); + let changes = 0; + const remove = surfaceTriggers(engine, () => changes++); + await engine.maps.setTerrain({ kind: 'ellipsoid' }); + await engine.maps.setPhotorealTiles({}); + engine.fake.frames(1); + assert.equal(changes, 1); + remove(); + engine.camera.moveEnd.raiseEvent(); + await engine.maps.setTerrain({ kind: 'ellipsoid' }); + engine.fake.frames(1); + assert.equal(changes, 1); +}); + +test('initial placement projects high ground before testing the screen and retains its offset', (t) => { + const { engine, surface, heights } = setup(t, 1, { + estimateGround: true, + heightOffset: 2, + }); + engine.fake.script('terrainHeight', 2500); + engine.projector.worldToWindow = (position) => + geo.cartographic.fromCartesian(position).height > 2400 + ? { x: 100, y: 100 } + : undefined; + surface.place(); + assert.ok(Math.abs(heights()[0] - 2502) < 0.01); +}); + +test('a consumer with its own visibility gate can place a ground vertical whose base does not project', (t) => { + const { engine, surface, heights } = setup(t, 1, { screenMargin: Infinity }); + engine.fake.script('terrainHeight', 2500); + engine.projector.worldToWindow = () => undefined; + surface.place(); + assert.ok(Math.abs(heights()[0] - 2500) < 0.01); +}); + +test('placement makes progress when preparing a group exhausts the time budget', (t) => { + let now = 0; + const engine = createFakeEngine(); + t.after(() => engine.destroy()); + t.mock.method(performance, 'now', () => now); + engine.projector.worldToWindow = () => ({ x: 1, y: 1 }); + engine.fake.script('terrainHeight', 50); + const set = engine.movers.createSet({ + id: 'slow-group', + kind: 'point', + capacity: 1, + point: { sizePx: 6 }, + }); + const point = { + cartographic: geo.cartographic.fromDegrees(0, 0), + position: geo.cartesian3.fromDegrees(0, 0), + }; + const surface = createSurfacePoints(engine, { + groups() { + now += 5; + return [{ set, points: [point] }]; + }, + request() {}, + }); + t.after(() => surface.destroy()); + surface.place(); + assert.equal(point.surfaceHeight, 50); +}); diff --git a/src/layers/ghostMaps/render/display.js b/src/layers/ghostMaps/render/display.js index 6dbe3bbbb..256142c30 100644 --- a/src/layers/ghostMaps/render/display.js +++ b/src/layers/ghostMaps/render/display.js @@ -14,7 +14,7 @@ import * as geo from '../../../geo/index.js'; import { normalizeGroundItem } from '../../../globe/ground.js'; import { rgbaFromCss } from '../../shared/color.js'; import { imageAtlasLayout } from '../../shared/render/imageAtlas.js'; -import { createSurfacePoints } from '../../shared/render/surfacePoints.js'; +import { createSurfacePoints } from '../../../globe/surfacePlacement.js'; const LAYER_ID = 's2-ghostmaps'; const TRANSPARENT = Object.freeze([0, 0, 0, 0]); diff --git a/src/layers/osmPlaces/render/display.js b/src/layers/osmPlaces/render/display.js index c9b2c60a7..c0648704e 100644 --- a/src/layers/osmPlaces/render/display.js +++ b/src/layers/osmPlaces/render/display.js @@ -6,7 +6,7 @@ import * as geo from '../../../geo/index.js'; import { rgbaFromCss } from '../../shared/color.js'; -import { createSurfacePoints } from '../../shared/render/surfacePoints.js'; +import { createSurfacePoints } from '../../../globe/surfacePlacement.js'; import { OSM_PLACES_LAYER_ID, OSM_PLACE_COLOR } from '../model.js'; // Above the sampled surface, so a point does not sink into it. diff --git a/src/layers/shared/render/surfacePoints.test.mjs b/src/layers/shared/render/surfacePoints.test.mjs deleted file mode 100644 index d04f254a2..000000000 --- a/src/layers/shared/render/surfacePoints.test.mjs +++ /dev/null @@ -1,75 +0,0 @@ -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 { createSurfacePoints } from './surfacePoints.js'; - -function setup(t, count = 3) { - const engine = createFakeEngine(); - t.after(() => engine.destroy()); - engine.projector.worldToWindow = () => ({ x: 100, y: 100 }); - const set = engine.movers.createSet({ - id: 'points', - kind: 'point', - capacity: count, - point: { sizePx: 6 }, - }); - const points = Array.from({ length: count }, (_, index) => ({ - cartographic: geo.cartographic.fromDegrees(index, 0), - position: geo.cartesian3.fromDegrees(index, 0), - })); - points.forEach((point, index) => - set.set(index, { position: point.position, show: true }), - ); - set.setCount(count); - set.commit(); - const surface = createSurfacePoints(engine, { - groups: () => [{ set, points }], - request: () => engine.render.request('points'), - }); - t.after(() => surface.destroy()); - const heights = () => - points.map( - (point) => geo.cartographic.fromCartesian(point.position).height, - ); - return { engine, surface, heights }; -} - -test('on the globe points stand on the terrain without a pick sample', (t) => { - const { engine, surface, heights } = setup(t); - let picks = 0; - engine.fake.script('sampleHeight', () => { - picks++; - return 900; - }); - engine.fake.script('terrainHeight', () => 40); - surface.place(); - assert.equal(picks, 0); - for (const height of heights()) assert.ok(Math.abs(height - 40) < 0.01); -}); - -test('under 3D tiles points stand on the sampled tiles', async (t) => { - const { engine, surface, heights } = setup(t); - await engine.maps.setPhotorealTiles({ id: 'photoreal' }); - engine.fake.script('sampleHeight', () => 55); - engine.fake.script('terrainHeight', () => 40); - surface.place(); - for (const height of heights()) assert.ok(Math.abs(height - 55) < 0.01); -}); - -test('a parked camera asks for no frames once the points stand', (t) => { - const { engine, heights } = setup(t); - let terrain = 40; - engine.fake.script('terrainHeight', () => terrain); - engine.fake.script('tilesLoaded', () => true); - engine.camera.moveEnd.raiseEvent(); - assert.ok(Math.abs(heights()[0] - 40) < 0.01); - const before = engine.fake.renderRequests; - engine.camera.moveEnd.raiseEvent(); - engine.fake.frames(3); - assert.equal(engine.fake.renderRequests, before, 'same heights, no frame'); - terrain = 46; - engine.camera.moveEnd.raiseEvent(); - assert.ok(engine.fake.renderRequests > before, 'a changed height draws'); - assert.ok(Math.abs(heights()[0] - 46) < 0.01); -}); diff --git a/src/layers/streetLevel/render/display.js b/src/layers/streetLevel/render/display.js index c6c68fabe..f50d4b411 100644 --- a/src/layers/streetLevel/render/display.js +++ b/src/layers/streetLevel/render/display.js @@ -15,11 +15,14 @@ // The contract has no ground clamping for sprites: a sprite sits on the // terrain, or under Google 3D on the rendered tiles, measured on screen a // few per pass and again after moves and tile loads -// (../../shared/render/surfacePoints.js). +// (../../../globe/surfacePlacement.js). import * as geo from '../../../geo/index.js'; import { rgbaFromCss } from '../../shared/color.js'; -import { createSurfacePoints } from '../../shared/render/surfacePoints.js'; +import { + createSurfacePoints, + readSurface, +} from '../../../globe/surfacePlacement.js'; /** Lines per ground set: smaller sets build, and show, sooner. */ export const SEQUENCE_LINE_BATCH = 120; @@ -127,7 +130,9 @@ export function createStreetLevelDisplay(engine) { // its measure, and a camera move can bring it into view first. function locate(row) { row.cartographic = geo.cartographic.fromDegrees(row.lon, row.lat); - const terrain = engine.surface.terrainHeight(row.cartographic); + const terrain = readSurface(engine, row.cartographic, { + prefer: 'terrain', + }); row.position = geo.cartesian3.fromDegrees( row.lon, row.lat, diff --git a/src/layers/submarineCables/render/display.js b/src/layers/submarineCables/render/display.js index 4bb99c10b..e0bf92520 100644 --- a/src/layers/submarineCables/render/display.js +++ b/src/layers/submarineCables/render/display.js @@ -14,7 +14,7 @@ import { bindOverlayHost } from '../../../overlays/bindOverlayHost.js'; import * as geo from '../../../geo/index.js'; import { isPointerFree } from '../../../data/inputOwnership.js'; import { rgbaFromCss } from '../../shared/color.js'; -import { createSurfacePoints } from '../../shared/render/surfacePoints.js'; +import { createSurfacePoints } from '../../../globe/surfacePlacement.js'; import { featureLabel, featureReference,