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.
This commit is contained in:
German Khokhlov committed 2026-11-10 16:13:06 +04:00
1 parent a90407d547
commit ef104aaac3
8 files changed
+410 -131

No files matched your search

+33 -16
View File
@@ -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"]
},
@@ -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();
},
};
}
+249
View File
@@ -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);
});
+1 -1
View File
@@ -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]);
+1 -1
View File
@@ -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.
@@ -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);
});
+8 -3
View File
@@ -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,
+1 -1
View File
@@ -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,