mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +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
@@ -34,7 +34,9 @@
|
|||||||
"src/layers/infrastructure/render/display.js",
|
"src/layers/infrastructure/render/display.js",
|
||||||
"src/layers/shared/color.js",
|
"src/layers/shared/color.js",
|
||||||
"src/sources/requestDeadline.js",
|
"src/sources/requestDeadline.js",
|
||||||
"src/overlays/bindOverlayHost.js"
|
"src/overlays/bindOverlayHost.js",
|
||||||
|
"src/globe/surfaceDetail.js",
|
||||||
|
"src/globe/surfacePlacement.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -978,7 +980,9 @@
|
|||||||
"src/ui/unitLabels.js",
|
"src/ui/unitLabels.js",
|
||||||
"src/geo/",
|
"src/geo/",
|
||||||
"src/globe/cesium/doubleClick.js",
|
"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"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -1273,7 +1277,9 @@
|
|||||||
"src/data/trackedReadout.js",
|
"src/data/trackedReadout.js",
|
||||||
"src/ui/releaseTrackers.js",
|
"src/ui/releaseTrackers.js",
|
||||||
"src/globe/cesium/orbital.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"]
|
"external": ["cesium", "vue", "pinia", "vue-i18n"]
|
||||||
},
|
},
|
||||||
@@ -1944,7 +1950,9 @@
|
|||||||
"src/i18n/locales/zh-CN.json",
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/i18n/locales/zh-Hant.json",
|
"src/i18n/locales/zh-Hant.json",
|
||||||
"src/geo/",
|
"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"]
|
"external": ["pbf", "@mapbox/vector-tile"]
|
||||||
},
|
},
|
||||||
@@ -2111,10 +2119,11 @@
|
|||||||
"src/layers/shared/color.js",
|
"src/layers/shared/color.js",
|
||||||
"src/globe/cameraMath.js",
|
"src/globe/cameraMath.js",
|
||||||
"src/layers/streetLevel/render/display.js",
|
"src/layers/streetLevel/render/display.js",
|
||||||
"src/layers/shared/render/surfacePoints.js",
|
|
||||||
"src/globe/contract.js",
|
"src/globe/contract.js",
|
||||||
"src/time/worldClock.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"]
|
"external": ["pbf", "@mapbox/vector-tile", "mapillary-js"]
|
||||||
},
|
},
|
||||||
@@ -2228,8 +2237,9 @@
|
|||||||
"src/sources/requestDeadline.js",
|
"src/sources/requestDeadline.js",
|
||||||
"src/layers/shared/color.js",
|
"src/layers/shared/color.js",
|
||||||
"src/layers/submarineCables/render/display.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"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -3543,7 +3553,6 @@
|
|||||||
"src/layers/earthquakes/render/display.js",
|
"src/layers/earthquakes/render/display.js",
|
||||||
"src/layers/airQuality/render/display.js",
|
"src/layers/airQuality/render/display.js",
|
||||||
"src/layers/submarineCables/render/display.js",
|
"src/layers/submarineCables/render/display.js",
|
||||||
"src/layers/shared/render/surfacePoints.js",
|
|
||||||
"src/layers/firms/render/sprites.js",
|
"src/layers/firms/render/sprites.js",
|
||||||
"src/layers/cyclones/render/display.js",
|
"src/layers/cyclones/render/display.js",
|
||||||
"src/layers/droneAirspace/render/display.js",
|
"src/layers/droneAirspace/render/display.js",
|
||||||
@@ -3593,7 +3602,8 @@
|
|||||||
"src/globe/cesium/orbital.js",
|
"src/globe/cesium/orbital.js",
|
||||||
"src/globe/cesium/worldTime.js",
|
"src/globe/cesium/worldTime.js",
|
||||||
"src/data/geoImportLimits.js",
|
"src/data/geoImportLimits.js",
|
||||||
"src/data/geoImportPlan.js"
|
"src/data/geoImportPlan.js",
|
||||||
|
"src/globe/surfacePlacement.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -4273,7 +4283,6 @@
|
|||||||
"src/layers/gnss/render/display.js",
|
"src/layers/gnss/render/display.js",
|
||||||
"src/layers/firms/render/sprites.js",
|
"src/layers/firms/render/sprites.js",
|
||||||
"src/layers/submarineCables/render/display.js",
|
"src/layers/submarineCables/render/display.js",
|
||||||
"src/layers/shared/render/surfacePoints.js",
|
|
||||||
"src/layers/perimeters/render/display.js",
|
"src/layers/perimeters/render/display.js",
|
||||||
"src/layers/droneAirspace/render/display.js",
|
"src/layers/droneAirspace/render/display.js",
|
||||||
"src/globe/ground.js",
|
"src/globe/ground.js",
|
||||||
@@ -4306,7 +4315,9 @@
|
|||||||
"src/data/ucsSatellites.js",
|
"src/data/ucsSatellites.js",
|
||||||
"src/data/imagingSensors.js",
|
"src/data/imagingSensors.js",
|
||||||
"src/data/planespottersPhoto.js",
|
"src/data/planespottersPhoto.js",
|
||||||
"src/data/geoImportLimits.js"
|
"src/data/geoImportLimits.js",
|
||||||
|
"src/globe/surfaceDetail.js",
|
||||||
|
"src/globe/surfacePlacement.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
"hls.js",
|
"hls.js",
|
||||||
@@ -4394,7 +4405,8 @@
|
|||||||
"src/data/placeBoundaries.js",
|
"src/data/placeBoundaries.js",
|
||||||
"src/data/placeNames.js",
|
"src/data/placeNames.js",
|
||||||
"src/data/local_data/us_census_places/files.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": [
|
"external": [
|
||||||
"cesium",
|
"cesium",
|
||||||
@@ -4661,7 +4673,9 @@
|
|||||||
"src/geo/",
|
"src/geo/",
|
||||||
"src/layers/directions/render/display.js",
|
"src/layers/directions/render/display.js",
|
||||||
"src/layers/directions/render/style.js",
|
"src/layers/directions/render/style.js",
|
||||||
"src/overlays/bindOverlayHost.js"
|
"src/overlays/bindOverlayHost.js",
|
||||||
|
"src/globe/surfaceDetail.js",
|
||||||
|
"src/globe/surfacePlacement.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -5031,7 +5045,8 @@
|
|||||||
"src/data/trackedReadout.js",
|
"src/data/trackedReadout.js",
|
||||||
"src/data/trackingGesture.js",
|
"src/data/trackingGesture.js",
|
||||||
"src/time/worldClock.js",
|
"src/time/worldClock.js",
|
||||||
"src/time/clock.js"
|
"src/time/clock.js",
|
||||||
|
"src/globe/surfacePlacement.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "egm96-universal", "pbf"]
|
"external": ["cesium", "egm96-universal", "pbf"]
|
||||||
},
|
},
|
||||||
@@ -5592,7 +5607,9 @@
|
|||||||
"src/layers/wildlifeSightings/display.js",
|
"src/layers/wildlifeSightings/display.js",
|
||||||
"src/data/spriteOrder.js",
|
"src/data/spriteOrder.js",
|
||||||
"src/layers/shared/color.js",
|
"src/layers/shared/color.js",
|
||||||
"src/overlays/bindOverlayHost.js"
|
"src/overlays/bindOverlayHost.js",
|
||||||
|
"src/globe/surfaceDetail.js",
|
||||||
|
"src/globe/surfacePlacement.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
// tiles have loaded; a point whose height holds is not written again, so a
|
// tiles have loaded; a point whose height holds is not written again, so a
|
||||||
// parked camera asks for no frames.
|
// 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 SAMPLES_PER_PASS = 16;
|
||||||
const SAMPLE_BUDGET_MS = 4;
|
const SAMPLE_BUDGET_MS = 4;
|
||||||
@@ -21,8 +22,88 @@ const SCREEN_MARGIN = 0.25;
|
|||||||
const plausible = (height) =>
|
const plausible = (height) =>
|
||||||
Number.isFinite(height) && height >= -500 && height <= 9000;
|
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 {object} options
|
||||||
* @param {() => Iterable<{ set: object | null, points: object[], exclude?: object[] }>} options.groups
|
* @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
|
* 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
|
* placement replaces (keeping the height in `surfaceHeight`); one with
|
||||||
* `show === false` is skipped.
|
* `show === false` is skipped.
|
||||||
* @param {() => void} options.request Asks for a frame after a placement.
|
* @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
|
* @param {boolean} [options.estimateGround=false] Screen-test a point not
|
||||||
* measured yet on the loaded terrain (under 3D tiles without it, at the
|
* measured yet on the loaded terrain (under 3D tiles without it, at the
|
||||||
* ground under the camera) rather than where it stands now.
|
* ground under the camera) rather than where it stands now.
|
||||||
* @returns {{ place: () => void, destroy: () => void }} `place()` measures
|
* @param {number} [options.screenMargin=0.25] Extra viewport fraction to measure.
|
||||||
* the points not yet placed on this surface (new points, for example).
|
* @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(
|
export function createSurfacePoints(
|
||||||
engine,
|
engine,
|
||||||
{ groups, request, heightOffset = 0, estimateGround = false },
|
{
|
||||||
|
groups,
|
||||||
|
request,
|
||||||
|
heightOffset = 0,
|
||||||
|
estimateGround = false,
|
||||||
|
screenMargin = SCREEN_MARGIN,
|
||||||
|
prefer,
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
let revision = 0;
|
let revision = 0;
|
||||||
let timer = null;
|
let timer = null;
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
let tilesLoaded = engine.surface.tilesLoaded();
|
|
||||||
let photoreal = engine.maps?.photorealActive?.() === true;
|
|
||||||
|
|
||||||
function onScreen(position, size) {
|
function onScreen(position, size) {
|
||||||
|
if (screenMargin === Infinity) return true;
|
||||||
const screen = engine.projector.worldToWindow(position);
|
const screen = engine.projector.worldToWindow(position);
|
||||||
const x = size.width * SCREEN_MARGIN;
|
const x = size.width * screenMargin;
|
||||||
const y = size.height * SCREEN_MARGIN;
|
const y = size.height * screenMargin;
|
||||||
return (
|
return (
|
||||||
screen &&
|
screen &&
|
||||||
screen.x >= -x &&
|
screen.x >= -x &&
|
||||||
@@ -72,7 +161,8 @@ export function createSurfacePoints(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Points off screen are passed over until the next move or tile load.
|
// Points off screen are passed over until the next move or tile load.
|
||||||
function place() {
|
function place({ all = false } = {}) {
|
||||||
|
if (all) revision++;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = null;
|
timer = null;
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
@@ -80,6 +170,7 @@ export function createSurfacePoints(
|
|||||||
const tiles = engine.maps?.photorealActive?.() === true;
|
const tiles = engine.maps?.photorealActive?.() === true;
|
||||||
const started = performance.now();
|
const started = performance.now();
|
||||||
let samples = 0;
|
let samples = 0;
|
||||||
|
let visited = 0;
|
||||||
let moved = false;
|
let moved = false;
|
||||||
let cameraGround;
|
let cameraGround;
|
||||||
for (const { set, points, exclude = [set] } of groups()) {
|
for (const { set, points, exclude = [set] } of groups()) {
|
||||||
@@ -90,18 +181,23 @@ export function createSurfacePoints(
|
|||||||
if (point.surfaceRevision === revision) continue;
|
if (point.surfaceRevision === revision) continue;
|
||||||
if (
|
if (
|
||||||
samples >= SAMPLES_PER_PASS ||
|
samples >= SAMPLES_PER_PASS ||
|
||||||
performance.now() - started >= SAMPLE_BUDGET_MS
|
(visited > 0 && performance.now() - started >= SAMPLE_BUDGET_MS)
|
||||||
) {
|
) {
|
||||||
if (changed) set.commit();
|
if (changed) set.commit();
|
||||||
if (changed || moved) request();
|
if (changed || moved) request();
|
||||||
timer = setTimeout(place, 0);
|
timer = setTimeout(place, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
visited++;
|
||||||
point.surfaceRevision = revision;
|
point.surfaceRevision = revision;
|
||||||
if (point.show === false) continue;
|
if (point.show === false) continue;
|
||||||
|
const offset =
|
||||||
|
typeof heightOffset === 'function'
|
||||||
|
? heightOffset(point)
|
||||||
|
: heightOffset;
|
||||||
const estimate = estimateGround && point.surfaceHeight === undefined;
|
const estimate = estimateGround && point.surfaceHeight === undefined;
|
||||||
const terrain = estimate
|
const terrain = estimate
|
||||||
? engine.surface.terrainHeight(point.cartographic)
|
? readSurface(engine, point.cartographic, { prefer: 'terrain' })
|
||||||
: undefined;
|
: undefined;
|
||||||
let ground = terrain;
|
let ground = terrain;
|
||||||
if (estimate && tiles && !plausible(ground)) {
|
if (estimate && tiles && !plausible(ground)) {
|
||||||
@@ -115,23 +211,22 @@ export function createSurfacePoints(
|
|||||||
? geo.cartesian3.fromRadians(
|
? geo.cartesian3.fromRadians(
|
||||||
point.cartographic.longitude,
|
point.cartographic.longitude,
|
||||||
point.cartographic.latitude,
|
point.cartographic.latitude,
|
||||||
ground + heightOffset,
|
ground + offset,
|
||||||
)
|
)
|
||||||
: point.position;
|
: point.position;
|
||||||
if (!onScreen(position, size)) continue;
|
if (!onScreen(position, size)) continue;
|
||||||
samples++;
|
samples++;
|
||||||
let height = tiles
|
const height = readSurface(engine, point.cartographic, {
|
||||||
? engine.surface.sampleHeight(point.cartographic, exclude)
|
exclude,
|
||||||
: undefined;
|
prefer: typeof prefer === 'function' ? prefer(point) : prefer,
|
||||||
if (!plausible(height))
|
});
|
||||||
height = terrain ?? engine.surface.terrainHeight(point.cartographic);
|
|
||||||
if (!plausible(height)) continue;
|
if (!plausible(height)) continue;
|
||||||
if (Math.abs(height - point.surfaceHeight) < HEIGHT_EPSILON_M) continue;
|
if (Math.abs(height - point.surfaceHeight) < HEIGHT_EPSILON_M) continue;
|
||||||
point.surfaceHeight = height;
|
point.surfaceHeight = height;
|
||||||
point.position = geo.cartesian3.fromRadians(
|
point.position = geo.cartesian3.fromRadians(
|
||||||
point.cartographic.longitude,
|
point.cartographic.longitude,
|
||||||
point.cartographic.latitude,
|
point.cartographic.latitude,
|
||||||
height + heightOffset,
|
height + offset,
|
||||||
);
|
);
|
||||||
set.set(index, { position: point.position });
|
set.set(index, { position: point.position });
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -142,18 +237,7 @@ export function createSurfacePoints(
|
|||||||
if (moved) request();
|
if (moved) request();
|
||||||
}
|
}
|
||||||
|
|
||||||
function refine() {
|
const removeSurface = surfaceTriggers(engine, () => place({ all: true }));
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
place,
|
place,
|
||||||
@@ -161,8 +245,7 @@ export function createSurfacePoints(
|
|||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
removeMoveEnd();
|
removeSurface();
|
||||||
removeFrame();
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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 { normalizeGroundItem } from '../../../globe/ground.js';
|
||||||
import { rgbaFromCss } from '../../shared/color.js';
|
import { rgbaFromCss } from '../../shared/color.js';
|
||||||
import { imageAtlasLayout } from '../../shared/render/imageAtlas.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 LAYER_ID = 's2-ghostmaps';
|
||||||
const TRANSPARENT = Object.freeze([0, 0, 0, 0]);
|
const TRANSPARENT = Object.freeze([0, 0, 0, 0]);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import * as geo from '../../../geo/index.js';
|
import * as geo from '../../../geo/index.js';
|
||||||
import { rgbaFromCss } from '../../shared/color.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';
|
import { OSM_PLACES_LAYER_ID, OSM_PLACE_COLOR } from '../model.js';
|
||||||
|
|
||||||
// Above the sampled surface, so a point does not sink into it.
|
// 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
|
// 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
|
// terrain, or under Google 3D on the rendered tiles, measured on screen a
|
||||||
// few per pass and again after moves and tile loads
|
// 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 * as geo from '../../../geo/index.js';
|
||||||
import { rgbaFromCss } from '../../shared/color.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. */
|
/** Lines per ground set: smaller sets build, and show, sooner. */
|
||||||
export const SEQUENCE_LINE_BATCH = 120;
|
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.
|
// its measure, and a camera move can bring it into view first.
|
||||||
function locate(row) {
|
function locate(row) {
|
||||||
row.cartographic = geo.cartographic.fromDegrees(row.lon, row.lat);
|
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.position = geo.cartesian3.fromDegrees(
|
||||||
row.lon,
|
row.lon,
|
||||||
row.lat,
|
row.lat,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { bindOverlayHost } from '../../../overlays/bindOverlayHost.js';
|
|||||||
import * as geo from '../../../geo/index.js';
|
import * as geo from '../../../geo/index.js';
|
||||||
import { isPointerFree } from '../../../data/inputOwnership.js';
|
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||||
import { rgbaFromCss } from '../../shared/color.js';
|
import { rgbaFromCss } from '../../shared/color.js';
|
||||||
import { createSurfacePoints } from '../../shared/render/surfacePoints.js';
|
import { createSurfacePoints } from '../../../globe/surfacePlacement.js';
|
||||||
import {
|
import {
|
||||||
featureLabel,
|
featureLabel,
|
||||||
featureReference,
|
featureReference,
|
||||||
|
|||||||
Reference in new issue
Block a user