Files
gods-eye-view/src/layers/shared/render/surfacePoints.js
T
German Khokhlov 60fb8fee07 Fix the follow-up review findings in import, layers, startup and engines
- KML/KMZ import: a per-file canvas budget and cache for ground overlays,
  limits on rings and vertices of raised polygons, overlays across 360° and
  near the poles, degenerate holes, polar and single-latitude fills,
  absolute overlays at their altitude, overlay order by drawOrder as on
  Cesium, and pins on tiles keep their measured height.
- Points on the surface: OSM Places and Street Level cones share the
  surface sampler and sit on the terrain before they are measured, within
  a sampling budget per pass; Cockpit's contact-dot bank keeps the dot
  atlas, and a failed fleet model loads again after a pause.
- Startup: the loading screen lifts after the share-link view is restored,
  the retry screen names the failed engine and takes focus, and the
  fallback toast stays for 6 s.
- Engines: an engine switch lands a running camera flight at its
  destination (camera.completeFlight on both engines), the sighting card
  leaves focus on the engine picker after a switch, and a Cesium model
  error stays with the instance that failed.
- The MCP panel builds again: its wind-mesh alias resolves to the stub.
- QA: the wildlife switch waits for the camera to rest, GBFS --w2 says it
  needs the dev server, and test fixtures are shared instead of copied.
2026-11-10 01:57:11 +04:00

169 lines
6.1 KiB
JavaScript

// Stands map points on the rendered surface (tiles, else terrain), the same
// on every engine: the contract has no ground clamp. On the globe that
// surface is the terrain, read from its loaded tiles; only 3D tiles, which
// hide the globe, need sampleHeight, a pick render on Cesium. Only points
// on screen are measured, a few per pass, as a layer can hold thousands.
// They are measured again after each camera move and once the surface's
// 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';
const SAMPLES_PER_PASS = 16;
const SAMPLE_BUDGET_MS = 4;
const HEIGHT_EPSILON_M = 0.01;
// A point not placed yet projects from the ellipsoid, away from where it
// will stand: the screen test takes this much of the view around it too.
const SCREEN_MARGIN = 0.25;
// Dead Sea to Everest, with geoid slack: Cesium samples kilometres
// underground while tiles stream.
const plausible = (height) =>
Number.isFinite(height) && height >= -500 && height <= 9000;
/**
* @param {import('../../../globe/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
* the samples skip (the set by default; a layer adds the rest it draws
* over the points). A point has `cartographic` and `position`, which a
* 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 {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).
*/
export function createSurfacePoints(
engine,
{ groups, request, heightOffset = 0, estimateGround = false },
) {
let revision = 0;
let timer = null;
let destroyed = false;
let tilesLoaded = engine.surface.tilesLoaded();
let photoreal = engine.maps?.photorealActive?.() === true;
function onScreen(position, size) {
const screen = engine.projector.worldToWindow(position);
const x = size.width * SCREEN_MARGIN;
const y = size.height * SCREEN_MARGIN;
return (
screen &&
screen.x >= -x &&
screen.y >= -y &&
screen.x < size.width + x &&
screen.y < size.height + y
);
}
// Under 3D tiles Cesium hides the globe, which then loads no terrain: the
// ground under the camera stands in for it in the screen test.
function groundUnderCamera(exclude) {
const height = engine.surface.sampleHeight(
geo.cartographic.fromCartesian(engine.camera.livePosition),
exclude,
);
return plausible(height) ? height : Number.NaN;
}
// Points off screen are passed over until the next move or tile load.
function place() {
clearTimeout(timer);
timer = null;
if (destroyed) return;
const size = engine.host.size();
const tiles = engine.maps?.photorealActive?.() === true;
const started = performance.now();
let samples = 0;
let moved = false;
let cameraGround;
for (const { set, points, exclude = [set] } of groups()) {
if (!set) continue;
let changed = false;
for (let index = 0; index < points.length; index++) {
const point = points[index];
if (point.surfaceRevision === revision) continue;
if (
samples >= SAMPLES_PER_PASS ||
performance.now() - started >= SAMPLE_BUDGET_MS
) {
if (changed) set.commit();
if (changed || moved) request();
timer = setTimeout(place, 0);
return;
}
point.surfaceRevision = revision;
if (point.show === false) continue;
const estimate = estimateGround && point.surfaceHeight === undefined;
const terrain = estimate
? engine.surface.terrainHeight(point.cartographic)
: undefined;
let ground = terrain;
if (estimate && tiles && !plausible(ground)) {
if (cameraGround === undefined) {
samples++;
cameraGround = groundUnderCamera(exclude);
}
ground = cameraGround;
}
const position = plausible(ground)
? geo.cartesian3.fromRadians(
point.cartographic.longitude,
point.cartographic.latitude,
ground + heightOffset,
)
: 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);
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,
);
set.set(index, { position: point.position });
changed = true;
}
if (changed) set.commit();
moved ||= changed;
}
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;
});
return {
place,
destroy() {
if (destroyed) return;
destroyed = true;
clearTimeout(timer);
removeMoveEnd();
removeFrame();
},
};
}