mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
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:
1 parent
a90407d547
commit
ef104aaac3
8 files changed
+410
-131
No files matched your search
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user