mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 14:52:19 +00:00
Cut duplicated and dead code across the engines, the layers and the QA scripts
- src/geo drops the ported debug argument checks and the tests that bitExact already covers; package boundaries accept a directory entry, so src/geo/ is listed once per group. - Cesium no longer carries movers and lines nobody draws with: they are optional engine capabilities. The fake engine and the Cesium camera use the shared cameraMath; unused contract tables, clock.ticked, the 'finest' surface detail and Navara click statistics are gone. - ALPR keeps one overlay core and one presentation model for both engines, bikeshare one station bookkeeping, the Navara ground effects one definition helper; image atlases, view centres, mover banks and colours are shared. - The legacy switch route, the clock offset and rate controls and other unused exports are removed. - The acceptance runs the Navara draw, geo import and scope checks; scenarios use the harness switch and page helpers; the main chunk check keeps the size budget only and runs once in CI.
This commit is contained in:
1 parent
c502404e1d
commit
7abf74518f
272 files changed
+4828
-10213
No files matched your search
@@ -14,7 +14,7 @@ const LINE_ID = 'gev-draw-preview';
|
||||
const DOTS_ID = 'gev-draw-vertices';
|
||||
|
||||
/** '#rrggbb' → rgba in 0..1. */
|
||||
export function cssRgba(css, alpha = 1) {
|
||||
function cssRgba(css, alpha = 1) {
|
||||
const hex = Number.parseInt(String(css).slice(1), 16);
|
||||
return [
|
||||
((hex >> 16) & 255) / 255,
|
||||
|
||||
@@ -68,14 +68,3 @@ export function catalogControlServices(catalog) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer roles for controls on a page without data layers: one empty object
|
||||
* per role, so the shell's optional calls
|
||||
* (`layer.method?.()`) do nothing.
|
||||
*/
|
||||
export function absentLayerControlServices() {
|
||||
return Object.fromEntries(
|
||||
Object.keys(CONTROL_LAYER_IDS).map((role) => [role, {}]),
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
absentLayerControlServices,
|
||||
createLayerCatalog,
|
||||
catalogControlServices,
|
||||
} from './catalog.js';
|
||||
import { createLayerCatalog, catalogControlServices } from './catalog.js';
|
||||
import { createApplicationData } from './data.js';
|
||||
|
||||
const metadata = (layers) =>
|
||||
@@ -158,40 +154,6 @@ test('controls bind catalog instances rather than similarly named defaults', ()
|
||||
);
|
||||
});
|
||||
|
||||
test('without data layers the controls get one empty stand-in per layer role', () => {
|
||||
const ids = [
|
||||
'traffic',
|
||||
'flights',
|
||||
'military',
|
||||
'satellites',
|
||||
'cctv',
|
||||
'street-level',
|
||||
'radio',
|
||||
'bikeshare',
|
||||
'transit',
|
||||
'ais-live-vessels',
|
||||
'military-awareness',
|
||||
'military-installations',
|
||||
'rocket-launches',
|
||||
'local-adsb',
|
||||
];
|
||||
const layers = ids.map((id) => ({ id }));
|
||||
const roles = Object.keys(
|
||||
catalogControlServices(createLayerCatalog(layers, metadata(layers))),
|
||||
);
|
||||
const absent = absentLayerControlServices();
|
||||
assert.deepEqual(Object.keys(absent), roles);
|
||||
for (const role of roles) {
|
||||
assert.deepEqual(Object.keys(absent[role]), [], role);
|
||||
assert.equal(absent[role].subscribe, undefined);
|
||||
}
|
||||
assert.notEqual(absent.flightsLayer, absent.trafficLayer);
|
||||
assert.notEqual(
|
||||
absentLayerControlServices().flightsLayer,
|
||||
absent.flightsLayer,
|
||||
);
|
||||
});
|
||||
|
||||
test('data setup without a viewer registers an empty catalog and no sonar scene', async (t) => {
|
||||
const previous = globalThis.document;
|
||||
globalThis.document = { getElementById: () => null };
|
||||
|
||||
@@ -6,17 +6,17 @@ import { createAirQualityCesiumRenderer } from '../../layers/airQuality/render/c
|
||||
import { createFirmsCesiumRenderer } from '../../layers/firms/render/cesium.js';
|
||||
import { createEarthquakesCesiumRenderer } from '../../layers/earthquakes/render/cesium.js';
|
||||
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
|
||||
// Which engine draws which layer: the layer renderer registry (plan, "layer
|
||||
// renderer interface") and the layer availability API built on it.
|
||||
// Which engine draws which layer: the layer renderer registry and the layer
|
||||
// availability API built on it.
|
||||
//
|
||||
// A renderer factory `(engine, options) => renderer` draws one layer on one
|
||||
// engine kind (`render/cesium.js`, `render/navara.js` of a layer). Cesium
|
||||
// layers still draw themselves until their render/cesium.js is extracted
|
||||
// (the *a tasks): they are registered as CESIUM_BUILTIN. Cesium entries are
|
||||
// registered here, statically (main chunk); Navara factories are registered
|
||||
// by the Navara mode after its import() (src/app/navaraMode.js).
|
||||
// layers without a render/cesium.js draw themselves: they are registered as
|
||||
// CESIUM_BUILTIN. Cesium entries are registered here, statically (main
|
||||
// chunk); Navara factories are registered by the Navara mode after its
|
||||
// import() (src/app/navaraMode.js).
|
||||
//
|
||||
// ## Layer availability (stable API, used by phase 4's layer list)
|
||||
// ## Layer availability (stable API, used by the layer list)
|
||||
//
|
||||
// import { createLayerAvailability } from './app/layers/renderers.js';
|
||||
// const availability = createLayerAvailability(engine.kind);
|
||||
@@ -28,8 +28,8 @@ import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesiu
|
||||
// The object is frozen; available() and unavailable() return fresh arrays.
|
||||
// It reads the registry when called, so a Navara factory registered later
|
||||
// (after the Navara mode loads) shows up. `name` is the layer's display name
|
||||
// (src/app/layers/directory.js). Phase 4 injects it as its `availability`
|
||||
// provider (`{ engine, unavailable() }` is the part it reads).
|
||||
// (src/app/layers/directory.js). The layer list takes it as its
|
||||
// `availability` provider (`{ engine, unavailable() }` is the part it reads).
|
||||
|
||||
import { APPLICATION_LAYER_DIRECTORY } from './directory.js';
|
||||
import { createFlightsCesiumRenderer } from '../../layers/flights/render/cesium.js';
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
// drawn(id) what the layer draws per object (glyph, model),
|
||||
// as plain data, or null (layers with qaDrawn)
|
||||
// following() { label, native } of engine.camera.following()
|
||||
// movers() engine.movers.list()
|
||||
// movers() engine.movers.list() ([] without movers: Cesium)
|
||||
// framesDrawn() frames drawn since the probe started
|
||||
|
||||
const plain = (position) =>
|
||||
@@ -83,7 +83,7 @@ export function createQaProbe({ engine, dataManager, availability }) {
|
||||
native: handle.native?.id ?? null,
|
||||
};
|
||||
},
|
||||
movers: () => engine.movers.list(),
|
||||
movers: () => engine.movers?.list() ?? [],
|
||||
framesDrawn: () => frames,
|
||||
destroy: () => removeFrames(),
|
||||
};
|
||||
|
||||
+5
-5
@@ -217,11 +217,11 @@ async function createRenderer({
|
||||
googleTileset: tileset,
|
||||
cesiumToken,
|
||||
initialStack: tileset ? 'photoreal' : 'esri-imagery',
|
||||
// Task 5 (height-datum fix): rebroadcast stack changes as a window
|
||||
// CustomEvent so data layers (CCTV per-regime ground resolution) can
|
||||
// react without coupling MapStackController to layer modules. Fires on
|
||||
// 'switching'/'ready'/'error'; listeners derive the surface regime from
|
||||
// live scene state, so intermediate emissions are harmless.
|
||||
// Rebroadcast stack changes as a window CustomEvent so data layers (CCTV
|
||||
// per-regime ground resolution) can react without coupling
|
||||
// MapStackController to layer modules. Fires on 'switching'/'ready'/
|
||||
// 'error'; listeners derive the surface regime from live scene state, so
|
||||
// intermediate emissions are harmless.
|
||||
onChange: (state) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('gev:map-stack-changed', { detail: state }),
|
||||
|
||||
@@ -9,13 +9,6 @@
|
||||
// a release that runs, in reverse, when the part detaches (before its
|
||||
// detachScene).
|
||||
|
||||
const STATES = Object.freeze([
|
||||
'detached',
|
||||
'attaching',
|
||||
'attached',
|
||||
'detaching',
|
||||
]);
|
||||
|
||||
export class SceneSwitchingError extends Error {
|
||||
constructor(message = 'The globe is switching engines') {
|
||||
super(message);
|
||||
@@ -208,5 +201,3 @@ export function createSceneHost({ onError = null } = {}) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const SCENE_HOST_STATES = STATES;
|
||||
+1
-1
@@ -327,7 +327,7 @@ function attachViewerTools(
|
||||
);
|
||||
|
||||
// Install the explicit scope mask used by the DISPLAY controls.
|
||||
installScopeMask(viewer);
|
||||
installScopeMask(engine);
|
||||
defer(() => destroyScopeMask());
|
||||
|
||||
// The follow camera recomputes the tracked target's dead-reckon position
|
||||
|
||||
@@ -9,7 +9,7 @@ export const PITCH_CLAMP_MIN_HEIGHT_M = 300_000;
|
||||
// with the first Cesium globe, after this module (src/app/loadCesium.js).
|
||||
const RADIANS_PER_DEGREE = Math.PI / 180;
|
||||
/** Look this far below the Earth's limb, so the globe stays on screen. */
|
||||
export const LIMB_MARGIN_RAD = 8 * RADIANS_PER_DEGREE;
|
||||
const LIMB_MARGIN_RAD = 8 * RADIANS_PER_DEGREE;
|
||||
const PITCH_EASE = 0.35;
|
||||
const PITCH_SNAP_RAD = 0.1 * RADIANS_PER_DEGREE;
|
||||
const HEIGHT_EPSILON_M = 0.0001;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Deterministic Launch Library 2 records for phase-3 mission/replay acceptance.
|
||||
import { SATELLITE_FIXTURE_EPOCH } from './satelliteFixture.js';
|
||||
export const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH;
|
||||
const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH;
|
||||
export function launchFixture() {
|
||||
const definitions = [
|
||||
[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Deterministic TLE catalogs for phase 3 browser QA; no live feed requests.
|
||||
// Deterministic TLE catalogs for browser QA; no live feed requests.
|
||||
import { W1_FROZEN_MS } from './w1Feeds.js';
|
||||
|
||||
const checksum = (line) => {
|
||||
@@ -30,8 +30,6 @@ const GROUPS = {
|
||||
starlink: [50000, 6000, 53, 15.2, 'QA STARLINK'],
|
||||
};
|
||||
export const SATELLITE_FIXTURE_EPOCH = W1_FROZEN_MS;
|
||||
export const SATELLITE_FIXTURE_CORE_COUNT = 839;
|
||||
export const SATELLITE_FIXTURE_DENSE_COUNT = 6839;
|
||||
export function satelliteFixture(group) {
|
||||
const spec = GROUPS[group];
|
||||
if (!spec) throw new TypeError('Unknown fixture group');
|
||||
|
||||
@@ -57,6 +57,7 @@ function fakeViewer({ cameraHeightM, surfaceM, canvas = null }) {
|
||||
const position = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, cameraHeightM);
|
||||
const viewer = {
|
||||
flights,
|
||||
canvas,
|
||||
scene: {
|
||||
canvas,
|
||||
sampleHeightSupported: true,
|
||||
|
||||
@@ -400,7 +400,7 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
|
||||
assert.doesNotMatch(source, /camera\.flyTo/);
|
||||
}
|
||||
// Vessels' click rule is a function the engine's selection dispatcher
|
||||
// calls (phase 3, 13a): the same body, out of the old LEFT_CLICK handler.
|
||||
// calls: the same body, out of the old LEFT_CLICK handler.
|
||||
const vesselClick = body(
|
||||
vessels,
|
||||
/function handleClick\(click\) \{([\s\S]*?)\n \}/,
|
||||
|
||||
@@ -2370,17 +2370,3 @@ export function flyRoute(
|
||||
};
|
||||
return typeof runNavigation === 'function' ? runNavigation(start) : start();
|
||||
}
|
||||
|
||||
// QA (development builds only; production builds drop it): the verbs and
|
||||
// their diagnostics, so a browser scenario can drive them on an engine whose
|
||||
// voice tools do not offer them yet.
|
||||
if (import.meta.env?.DEV && typeof window !== 'undefined')
|
||||
window.__gevCameraVerbs = Object.freeze({
|
||||
cameraVerbsSupported,
|
||||
flyRoute,
|
||||
getActiveCameraMotion,
|
||||
initCameraVerbs,
|
||||
interruptCameraMotion,
|
||||
moveCamera,
|
||||
releaseCameraVerbs,
|
||||
});
|
||||
@@ -261,10 +261,8 @@ const TRACKED_RASTER_PX = 192;
|
||||
* Default size serves the fleet; pass `aircraftIcon(kind, TRACKED_ICON_PX)`
|
||||
* (re-exported below) for the tracked billboard. */
|
||||
export const TRACKED_ICON_PX = TRACKED_RASTER_PX;
|
||||
/** The fleet raster size (aircraftIcon's default). */
|
||||
export const FLEET_ICON_PX = FLEET_RASTER_PX;
|
||||
/** Every silhouette kind aircraftIcon draws (an engine's sprite atlas). */
|
||||
export const AIRCRAFT_ICON_KINDS = Object.freeze(Object.keys(BODIES));
|
||||
const AIRCRAFT_ICON_KINDS = Object.freeze(Object.keys(BODIES));
|
||||
export function aircraftIcon(kind, px = FLEET_RASTER_PX) {
|
||||
const k = BODIES[kind] ? kind : 'airliner';
|
||||
const key = `${k}@${px}`;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { engineFromViewer } from '../globe/cesium/engineFromViewer.js';
|
||||
|
||||
/**
|
||||
* Arrival corrections belong to one globe engine and yield to its next camera
|
||||
* owner. Callers may name the engine or (layers until phase 1c) the viewer it
|
||||
* is registered for; both reach the same correction.
|
||||
* owner. Callers may name the engine or (layers that still hold one) the
|
||||
* viewer it is registered for; both reach the same correction.
|
||||
*/
|
||||
const arrivals = new WeakMap();
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ for (const layer of LAYERS) {
|
||||
|
||||
test(`${layer.name}: every GLB creation bypasses the tile-contended frame-spread queue`, () => {
|
||||
// The aircraft renderer loads through its `loadModel`, Cesium.Model.fromGltfAsync
|
||||
// unless a test injects a loader (phase 3).
|
||||
// unless a test injects a loader.
|
||||
assert.match(source, /loadModel = \(options\) => Cesium\.Model\.fromGltfAsync\(options\)/);
|
||||
const calls = [...source.matchAll(/(?:Cesium\.Model\.fromGltfAsync|\bloadModel)\(\{([\s\S]*?)\}\)/g)];
|
||||
assert.ok(calls.length >= 3, `expected fleet, tracked, and preload model calls; found ${calls.length}`);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const OPENMAPTILES_CREDIT = {
|
||||
key: 'openfreemap',
|
||||
html: 'Vector tiles: <a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> © <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>',
|
||||
};
|
||||
export const GOOGLE_PLACES_CREDIT = {
|
||||
const GOOGLE_PLACES_CREDIT = {
|
||||
key: 'google-maps-places',
|
||||
html: 'Google Maps Places: <a href="https://developers.google.com/maps/documentation/places/web-service/policies" target="_blank" rel="noopener">Google Maps</a>',
|
||||
};
|
||||
@@ -45,7 +45,7 @@ const viewerCredits = new WeakMap();
|
||||
|
||||
/**
|
||||
* Credits of a globe engine, or of a Cesium viewer's credit display for
|
||||
* callers that still hold a viewer (layers until phase 1c); null without one.
|
||||
* callers that still hold a viewer (Cesium layers); null without one.
|
||||
* @returns {{ addStatic: Function, request: () => void } | null}
|
||||
*/
|
||||
function creditsOf(target) {
|
||||
|
||||
@@ -143,7 +143,7 @@ test('every scene click handler consults ownership before it picks', () => {
|
||||
'src/layers/submarineCables/interaction.js',
|
||||
'viewer.scene.pick(click.position)',
|
||||
],
|
||||
// Vessels take the engine dispatcher's pick (phase 3, 13a): the guard
|
||||
// Vessels take the engine dispatcher's pick: the guard
|
||||
// comes before the click rule reads it.
|
||||
['src/layers/vessels/selection.js', 'resolvePickId(picked)'],
|
||||
];
|
||||
|
||||
@@ -140,7 +140,7 @@ test('sampler on a globe engine: samples engine.surface and latches against the
|
||||
assert.equal(cachedMeshFloor(cell.lat, cell.lon), 137.0);
|
||||
const [call] = engine.fake.callsOf('surface.sampleHeightDetailed');
|
||||
assert.ok(Math.abs(Cesium.Math.toDegrees(call.args[0].latitude) - cell.lat) < 1e-9);
|
||||
assert.deepEqual(call.args[1], { exclude: ['own'], lod: 'visible' });
|
||||
assert.deepEqual(call.args[1], { exclude: ['own'] });
|
||||
});
|
||||
|
||||
test('sampler: a coarse tile answer is a miss; a fine one latches', async () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Native constants shared by the W1 adapters; keep the SDK import here.
|
||||
// Read on call: the production build's Cesium global loads with the first
|
||||
// Cesium globe, after this module has loaded on a Navara start.
|
||||
import { Axis, ScreenSpaceEventType } from 'cesium';
|
||||
import { ScreenSpaceEventType } from 'cesium';
|
||||
|
||||
export const modelAxis = () => Axis;
|
||||
export const trackingEventTypes = () => ScreenSpaceEventType;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { nearbyContact } from './pickRegistry.js';
|
||||
// One selection dispatcher for the layers that have moved onto the globe
|
||||
// engine (phase 3 Task 4): one click subscription with picks
|
||||
// One selection dispatcher for the layers that draw through the globe
|
||||
// engine: one click subscription with picks
|
||||
// (engine.input.on('click', …, { pick: true, drill: 8 })), the press
|
||||
// gesture and one Escape listener, instead of a ScreenSpaceEventHandler
|
||||
// per layer. Owners register as with pickRegistry (registerPickOwner, so
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
isTrackingSelectionGesture,
|
||||
} from './trackingClickGesture.js';
|
||||
|
||||
export const SELECTION_DRILL = 8;
|
||||
const SELECTION_DRILL = 8;
|
||||
|
||||
/**
|
||||
* @param {import('../globe/contract.js').GlobeEngine} engine
|
||||
|
||||
@@ -109,7 +109,7 @@ test('flights, AIS, and FIRMS enable paths are wired through the shared sprite r
|
||||
assert.deepEqual(calls, [viewer, viewer, viewer]);
|
||||
|
||||
const firmsLayer = createFirmsHeatmapLayer({ id: 'firms', name: 'FIRMS' });
|
||||
// Flights draws through its renderer (phase 3): enable asks the fleet to
|
||||
// Flights draws through its renderer: enable asks the fleet to
|
||||
// restore its order, which the Cesium aircraft renderer wires to the shared
|
||||
// restorer under the flights' sprite slot.
|
||||
assert.match(flightsLayer.enable.toString(), /fleet\.restoreOrderOnEnable\(\)/);
|
||||
|
||||
@@ -114,7 +114,7 @@ test('civilian and military click handlers apply duration only at the deselect b
|
||||
readLayerSource(new URL('./flights.js', import.meta.url)),
|
||||
readLayerSource(new URL('./militaryFlights.js', import.meta.url)),
|
||||
];
|
||||
// Flights and military moved to the selection dispatcher (phase 3), whose
|
||||
// Flights and military use the selection dispatcher, whose
|
||||
// source applies the same two rules: the selection gesture before a
|
||||
// select, the full click classifier before the empty-space deselect, which
|
||||
// each layer answers with a user-origin clear.
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Seeded, deterministic bit-for-bit comparison of src/geo against Cesium.
|
||||
// Seeded, deterministic bit-for-bit comparison of src/geo against Cesium's
|
||||
// release build (Build/Cesium, debug checks stripped, as production ships it).
|
||||
// Every number is compared with Object.is (so -0 and NaN count), a throw on one
|
||||
// side must be a throw on the other, and `result` aliasing is exercised the
|
||||
// same way on both sides.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createRequire } from 'node:module';
|
||||
import * as geo from './index.js';
|
||||
|
||||
const Cesium = createRequire(import.meta.url)('cesium/Build/Cesium/index.cjs');
|
||||
|
||||
const {
|
||||
cartesian3: c3,
|
||||
cartographic,
|
||||
@@ -108,7 +111,7 @@ const cesium = (v) => new C3(v.x, v.y, v.z);
|
||||
|
||||
test('vector operations, including -0 inputs and aliased results, equal Cesium bit for bit', () => {
|
||||
const { scalar, vector } = inputs(1);
|
||||
// Directionless vectors first: normalize must throw on both sides.
|
||||
// Directionless vectors first: normalize returns NaN on both sides.
|
||||
const DEGENERATE = [
|
||||
{ x: 0, y: 0, z: 0 },
|
||||
{ x: -0, y: 0, z: -0 },
|
||||
@@ -397,12 +400,7 @@ test('matrices and quaternions, including aliased results and every fromRotation
|
||||
][i % 4];
|
||||
const angle =
|
||||
i % 8 < 4 ? Math.PI - 0.5 * random() : (random() - 0.5) * 2 * Math.PI;
|
||||
let m;
|
||||
try {
|
||||
m = rotation(axis, angle);
|
||||
} catch {
|
||||
continue; // a zero axis: Cesium throws, nothing to compare
|
||||
}
|
||||
const m = rotation(axis, angle);
|
||||
const trace = m[0] + m[4] + m[8];
|
||||
branches.add(
|
||||
trace > 0
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — BoundingSphere.js subset.
|
||||
// Exact mission framing: Ritter/box fit and the union of two spheres.
|
||||
import * as Cartesian3 from './cartesian3.js';
|
||||
import { Check } from './check.js';
|
||||
import { DEBUG } from './debug.js';
|
||||
const defined = (value) => value !== null && value !== undefined;
|
||||
export function create(center = Cartesian3.ZERO, radius = 0) {
|
||||
return { center: Cartesian3.clone(center), radius };
|
||||
@@ -184,11 +182,6 @@ export function fromPoints(positions, result) {
|
||||
return result;
|
||||
}
|
||||
export function union(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
if (!defined(result)) {
|
||||
result = create();
|
||||
}
|
||||
|
||||
+1
-11
@@ -1,17 +1,12 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartesian2.js
|
||||
//
|
||||
// 2D vectors (screen coordinates) as plain-data {x, y} objects. With DEBUG on
|
||||
// (development and tests; see debug.js) invalid arguments throw the same
|
||||
// DeveloperError as Cesium's debug checks.
|
||||
// 2D vectors (screen coordinates) as plain-data {x, y} objects.
|
||||
//
|
||||
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
|
||||
// copyState and src/director/documentFields.js jsonTree reject them: copy
|
||||
// the fields into a literal before putting one into product state or a
|
||||
// scene document.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
|
||||
// Values are instances of a module-private constructor rather than object
|
||||
// literals: every `{x, y}` literal in the process shares one V8 hidden
|
||||
// class, and code elsewhere that builds such literals would make geo's hot
|
||||
@@ -38,11 +33,6 @@ export function clone(cartesian, result) {
|
||||
|
||||
/** The distance between two points. */
|
||||
export function distance(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
const x = left.x - right.x;
|
||||
const y = left.y - right.y;
|
||||
return Math.sqrt(x * x + y * y);
|
||||
|
||||
+3
-144
@@ -3,19 +3,14 @@
|
||||
// 3D vectors as plain-data {x, y, z} objects. Arithmetic takes a required `result`
|
||||
// (as Cesium does) that may be one of the inputs; creators take an optional
|
||||
// one. The operation order matches Cesium so the outputs are bit-identical.
|
||||
// With DEBUG on (development and tests; see debug.js) invalid arguments throw
|
||||
// the same DeveloperError as Cesium's debug checks, and `normalize` throws on a
|
||||
// zero or non-finite vector; production builds skip the checks and return NaN
|
||||
// components there, as release Cesium does.
|
||||
// As in release Cesium, arguments are not checked and `normalize` returns NaN
|
||||
// components for a zero or non-finite vector.
|
||||
//
|
||||
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
|
||||
// copyState and src/director/documentFields.js jsonTree reject them: copy
|
||||
// the fields into a literal before putting one into product state or a
|
||||
// scene document.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
import { equalsEpsilon as scalarEqualsEpsilon, toRadians } from './math.js';
|
||||
|
||||
// Values are instances of a module-private constructor rather than object
|
||||
@@ -59,10 +54,6 @@ export const UNIT_Y = Object.freeze(create(0.0, 1.0, 0.0));
|
||||
export const UNIT_Z = Object.freeze(create(0.0, 0.0, 1.0));
|
||||
|
||||
export function magnitudeSquared(cartesian) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
}
|
||||
|
||||
return (
|
||||
cartesian.x * cartesian.x +
|
||||
cartesian.y * cartesian.y +
|
||||
@@ -77,60 +68,29 @@ export function magnitude(cartesian) {
|
||||
const distanceScratch = create();
|
||||
|
||||
export function distance(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
subtract(left, right, distanceScratch);
|
||||
return magnitude(distanceScratch);
|
||||
}
|
||||
|
||||
export function distanceSquared(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
subtract(left, right, distanceScratch);
|
||||
return magnitudeSquared(distanceScratch);
|
||||
}
|
||||
|
||||
/** The unit vector; with DEBUG on, throws when the vector has no direction (zero or non-finite). */
|
||||
/** The unit vector; NaN components when the vector has no direction (zero or non-finite). */
|
||||
export function normalize(cartesian, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const length = magnitude(cartesian);
|
||||
result.x = cartesian.x / length;
|
||||
result.y = cartesian.y / length;
|
||||
result.z = cartesian.z / length;
|
||||
if (DEBUG) {
|
||||
if (isNaN(result.x) || isNaN(result.y) || isNaN(result.z)) {
|
||||
throw new DeveloperError('normalized result is not a number');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function dot(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
return left.x * right.x + left.y * right.y + left.z * right.z;
|
||||
}
|
||||
|
||||
export function multiplyComponents(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = left.x * right.x;
|
||||
result.y = left.y * right.y;
|
||||
result.z = left.z * right.z;
|
||||
@@ -138,12 +98,6 @@ export function multiplyComponents(left, right, result) {
|
||||
}
|
||||
|
||||
export function add(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = left.x + right.x;
|
||||
result.y = left.y + right.y;
|
||||
result.z = left.z + right.z;
|
||||
@@ -151,12 +105,6 @@ export function add(left, right, result) {
|
||||
}
|
||||
|
||||
export function subtract(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = left.x - right.x;
|
||||
result.y = left.y - right.y;
|
||||
result.z = left.z - right.z;
|
||||
@@ -164,12 +112,6 @@ export function subtract(left, right, result) {
|
||||
}
|
||||
|
||||
export function multiplyByScalar(cartesian, scalar, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.number('scalar', scalar);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = cartesian.x * scalar;
|
||||
result.y = cartesian.y * scalar;
|
||||
result.z = cartesian.z * scalar;
|
||||
@@ -177,12 +119,6 @@ export function multiplyByScalar(cartesian, scalar, result) {
|
||||
}
|
||||
|
||||
export function divideByScalar(cartesian, scalar, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.number('scalar', scalar);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = cartesian.x / scalar;
|
||||
result.y = cartesian.y / scalar;
|
||||
result.z = cartesian.z / scalar;
|
||||
@@ -190,11 +126,6 @@ export function divideByScalar(cartesian, scalar, result) {
|
||||
}
|
||||
|
||||
export function negate(cartesian, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = -cartesian.x;
|
||||
result.y = -cartesian.y;
|
||||
result.z = -cartesian.z;
|
||||
@@ -204,25 +135,12 @@ export function negate(cartesian, result) {
|
||||
const lerpScratch = create();
|
||||
|
||||
export function lerp(start, end, t, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('start', start);
|
||||
Check.typeOf.object('end', end);
|
||||
Check.typeOf.number('t', t);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
multiplyByScalar(end, t, lerpScratch);
|
||||
result = multiplyByScalar(start, 1.0 - t, result);
|
||||
return add(lerpScratch, result, result);
|
||||
}
|
||||
|
||||
export function cross(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const leftX = left.x;
|
||||
const leftY = left.y;
|
||||
const leftZ = left.z;
|
||||
@@ -241,12 +159,6 @@ export function cross(left, right, result) {
|
||||
}
|
||||
|
||||
export function midpoint(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = (left.x + right.x) * 0.5;
|
||||
result.y = (left.y + right.y) * 0.5;
|
||||
result.z = (left.z + right.z) * 0.5;
|
||||
@@ -292,11 +204,6 @@ let scratchK = create();
|
||||
|
||||
/** The Earth-fixed position of a geodetic one in radians on the ellipsoid (WGS84 by default). */
|
||||
export function fromRadians(longitude, latitude, height, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
height = height ?? 0.0;
|
||||
const radiiSquared =
|
||||
ellipsoid == null ? WGS84_RADII_SQUARED : ellipsoid.radiiSquared;
|
||||
@@ -318,11 +225,6 @@ export function fromRadians(longitude, latitude, height, ellipsoid, result) {
|
||||
|
||||
/** The Earth-fixed position of a geodetic one in degrees on the ellipsoid (WGS84 by default). */
|
||||
export function fromDegrees(longitude, latitude, height, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
longitude = toRadians(longitude);
|
||||
latitude = toRadians(latitude);
|
||||
return fromRadians(longitude, latitude, height, ellipsoid, result);
|
||||
@@ -330,15 +232,6 @@ export function fromDegrees(longitude, latitude, height, ellipsoid, result) {
|
||||
|
||||
/** Positions from a flat [lon, lat, lon, lat, …] list in degrees, at height 0. */
|
||||
export function fromDegreesArray(coordinates, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.defined('coordinates', coordinates);
|
||||
if (coordinates.length < 2 || coordinates.length % 2 !== 0) {
|
||||
throw new DeveloperError(
|
||||
'the number of coordinates must be a multiple of 2 and at least 2',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const length = coordinates.length;
|
||||
if (result == null) {
|
||||
result = new Array(length / 2);
|
||||
@@ -358,43 +251,9 @@ export function fromDegreesArray(coordinates, ellipsoid, result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Positions from a flat [lon, lat, height, …] list in degrees and metres. */
|
||||
export function fromDegreesArrayHeights(coordinates, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.defined('coordinates', coordinates);
|
||||
if (coordinates.length < 3 || coordinates.length % 3 !== 0) {
|
||||
throw new DeveloperError(
|
||||
'the number of coordinates must be a multiple of 3 and at least 3',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const length = coordinates.length;
|
||||
if (result == null) {
|
||||
result = new Array(length / 3);
|
||||
} else {
|
||||
result.length = length / 3;
|
||||
}
|
||||
for (let i = 0; i < length; i += 3) {
|
||||
const index = i / 3;
|
||||
result[index] = fromDegrees(
|
||||
coordinates[i],
|
||||
coordinates[i + 1],
|
||||
coordinates[i + 2],
|
||||
ellipsoid,
|
||||
result[index],
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const angleBetweenScratch = create();
|
||||
const angleBetweenScratch2 = create();
|
||||
export function angleBetween(left, right) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
normalize(left, angleBetweenScratch);
|
||||
normalize(right, angleBetweenScratch2);
|
||||
const cosine = dot(angleBetweenScratch, angleBetweenScratch2);
|
||||
|
||||
@@ -21,78 +21,16 @@ const same = (ours, theirs, label) =>
|
||||
label,
|
||||
);
|
||||
|
||||
test('binary vector ops equal Cesium bit for bit', () => {
|
||||
for (const a of SAMPLES)
|
||||
for (const b of SAMPLES) {
|
||||
for (const op of ['add', 'subtract', 'cross', 'multiplyComponents']) {
|
||||
same(
|
||||
c3[op](asPlain(a), asPlain(b), c3.create()),
|
||||
C[op](asCesium(a), asCesium(b), new C()),
|
||||
`${op}`,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
c3.dot(asPlain(a), asPlain(b)),
|
||||
C.dot(asCesium(a), asCesium(b)),
|
||||
);
|
||||
assert.equal(
|
||||
c3.distance(asPlain(a), asPlain(b)),
|
||||
C.distance(asCesium(a), asCesium(b)),
|
||||
);
|
||||
assert.equal(
|
||||
c3.distanceSquared(asPlain(a), asPlain(b)),
|
||||
C.distanceSquared(asCesium(a), asCesium(b)),
|
||||
);
|
||||
same(
|
||||
c3.lerp(asPlain(a), asPlain(b), 0.3, c3.create()),
|
||||
C.lerp(asCesium(a), asCesium(b), 0.3, new C()),
|
||||
'lerp',
|
||||
);
|
||||
same(
|
||||
c3.midpoint(asPlain(a), asPlain(b), c3.create()),
|
||||
C.midpoint(asCesium(a), asCesium(b), new C()),
|
||||
'midpoint',
|
||||
);
|
||||
assert.equal(
|
||||
c3.equalsEpsilon(asPlain(a), asPlain(b), 1e-7),
|
||||
C.equalsEpsilon(asCesium(a), asCesium(b), 1e-7),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('unary ops and scalars equal Cesium', () => {
|
||||
test('magnitudeSquared equals Cesium', () => {
|
||||
for (const a of SAMPLES) {
|
||||
assert.equal(c3.magnitude(asPlain(a)), C.magnitude(asCesium(a)));
|
||||
assert.equal(
|
||||
c3.magnitudeSquared(asPlain(a)),
|
||||
C.magnitudeSquared(asCesium(a)),
|
||||
);
|
||||
same(
|
||||
c3.negate(asPlain(a), c3.create()),
|
||||
C.negate(asCesium(a), new C()),
|
||||
'negate',
|
||||
);
|
||||
same(
|
||||
c3.multiplyByScalar(asPlain(a), 2.5, c3.create()),
|
||||
C.multiplyByScalar(asCesium(a), 2.5, new C()),
|
||||
'mul',
|
||||
);
|
||||
same(
|
||||
c3.divideByScalar(asPlain(a), 4, c3.create()),
|
||||
C.divideByScalar(asCesium(a), 4, new C()),
|
||||
'div',
|
||||
);
|
||||
if (C.magnitude(asCesium(a)) !== 0) {
|
||||
same(
|
||||
c3.normalize(asPlain(a), c3.create()),
|
||||
C.normalize(asCesium(a), new C()),
|
||||
'normalize',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('geodetic constructors equal Cesium', () => {
|
||||
test('fromRadians and fromDegreesArray equal Cesium', () => {
|
||||
for (const [lon, lat, h] of [
|
||||
[0, 0, 0],
|
||||
[-73.9857, 40.7484, 381],
|
||||
@@ -100,11 +38,6 @@ test('geodetic constructors equal Cesium', () => {
|
||||
[180, -90, 1e4],
|
||||
[12.5, 89.999999, 1],
|
||||
]) {
|
||||
same(
|
||||
c3.fromDegrees(lon, lat, h),
|
||||
C.fromDegrees(lon, lat, h),
|
||||
`fromDegrees ${lon},${lat}`,
|
||||
);
|
||||
const lr = [Cesium.Math.toRadians(lon), Cesium.Math.toRadians(lat), h];
|
||||
same(c3.fromRadians(...lr), C.fromRadians(...lr), 'fromRadians');
|
||||
}
|
||||
@@ -112,10 +45,6 @@ test('geodetic constructors equal Cesium', () => {
|
||||
c3.fromDegreesArray(flat).forEach((p, i) =>
|
||||
same(p, C.fromDegreesArray(flat)[i], `array ${i}`),
|
||||
);
|
||||
const heights = [-74, 40.7, 10, -73.9, 40.8, 20];
|
||||
c3.fromDegreesArrayHeights(heights).forEach((p, i) =>
|
||||
same(p, C.fromDegreesArrayHeights(heights)[i], `heights ${i}`),
|
||||
);
|
||||
});
|
||||
|
||||
test('result aliasing works like Cesium and creators reuse result', () => {
|
||||
@@ -138,20 +67,3 @@ test('plain results are accepted by Cesium functions (duck typing)', () => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('normalize throws on a zero or NaN vector, as Cesium does when run from source', () => {
|
||||
for (const [x, y, z] of [
|
||||
[0, 0, 0],
|
||||
[-0, 0, -0],
|
||||
[Number.NaN, 1, 2],
|
||||
]) {
|
||||
assert.throws(
|
||||
() => C.normalize(new C(x, y, z), new C()),
|
||||
/normalized result is not a number/,
|
||||
);
|
||||
assert.throws(
|
||||
() => c3.normalize({ x, y, z }, c3.create()),
|
||||
/normalized result is not a number/,
|
||||
);
|
||||
}
|
||||
});
|
||||
+1
-19
@@ -1,17 +1,13 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Cartographic.js
|
||||
//
|
||||
// Geodetic positions as plain-data {longitude, latitude, height} objects (radians and
|
||||
// metres). Every function takes an optional `result` and returns it. With
|
||||
// DEBUG on (development and tests; see debug.js) invalid arguments throw the
|
||||
// same DeveloperError as Cesium's debug checks.
|
||||
// metres). Every function takes an optional `result` and returns it.
|
||||
//
|
||||
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
|
||||
// copyState and src/director/documentFields.js jsonTree reject them: copy
|
||||
// the fields into a literal before putting one into product state or a
|
||||
// scene document.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { toRadians } from './math.js';
|
||||
import * as cartesian3 from './cartesian3.js';
|
||||
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
|
||||
@@ -51,11 +47,6 @@ const WGS84_CENTER_TOLERANCE_SQUARED = 0.1;
|
||||
|
||||
/** A geodetic position from radians. */
|
||||
export function fromRadians(longitude, latitude, height, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
height = height ?? 0.0;
|
||||
if (result == null) return create(longitude, latitude, height);
|
||||
result.longitude = longitude;
|
||||
@@ -66,11 +57,6 @@ export function fromRadians(longitude, latitude, height, result) {
|
||||
|
||||
/** A geodetic position from degrees; the result is in radians. */
|
||||
export function fromDegrees(longitude, latitude, height, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('longitude', longitude);
|
||||
Check.typeOf.number('latitude', latitude);
|
||||
}
|
||||
|
||||
longitude = toRadians(longitude);
|
||||
latitude = toRadians(latitude);
|
||||
return fromRadians(longitude, latitude, height, result);
|
||||
@@ -133,10 +119,6 @@ export function fromCartesian(cartesian, ellipsoid, result) {
|
||||
* where Cesium uses Ellipsoid.default; gev never changes it).
|
||||
*/
|
||||
export function toCartesian(cartographic, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
Check.defined('cartographic', cartographic);
|
||||
}
|
||||
|
||||
return cartesian3.fromRadians(
|
||||
cartographic.longitude,
|
||||
cartographic.latitude,
|
||||
|
||||
@@ -4,12 +4,6 @@ import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as cartographic from './cartographic.js';
|
||||
|
||||
const close = (a, b, tol, label) =>
|
||||
assert.ok(
|
||||
Math.abs(a - b) <= tol * Math.max(1, Math.abs(b)),
|
||||
`${label}: ${a} vs ${b}`,
|
||||
);
|
||||
|
||||
test('fromDegrees and fromRadians equal Cesium', () => {
|
||||
for (const [lon, lat, h] of [
|
||||
[0, 0, 0],
|
||||
@@ -30,29 +24,6 @@ test('fromDegrees and fromRadians equal Cesium', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('fromCartesian and toCartesian equal Cesium, including the centre case', () => {
|
||||
for (const [x, y, z] of [
|
||||
[1334762.2, -4654869.1, 4137287.9],
|
||||
[6378137, 0, 0],
|
||||
[0.01, 0, 0],
|
||||
]) {
|
||||
const ours = cartographic.fromCartesian({ x, y, z });
|
||||
const theirs = Cesium.Cartographic.fromCartesian(
|
||||
new Cesium.Cartesian3(x, y, z),
|
||||
);
|
||||
if (theirs === undefined) {
|
||||
assert.equal(ours, undefined);
|
||||
continue;
|
||||
}
|
||||
close(ours.longitude, theirs.longitude, 1e-12, 'lon');
|
||||
close(ours.latitude, theirs.latitude, 1e-12, 'lat');
|
||||
close(ours.height, theirs.height, 1e-9, 'h');
|
||||
const back = cartographic.toCartesian(ours);
|
||||
const cBack = Cesium.Cartographic.toCartesian(theirs);
|
||||
for (const k of ['x', 'y', 'z']) close(back[k], cBack[k], 1e-12, k);
|
||||
}
|
||||
});
|
||||
|
||||
test('result objects are reused', () => {
|
||||
const out = cartographic.create();
|
||||
assert.equal(cartographic.fromDegrees(1, 2, 3, out), out);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Check.js
|
||||
//
|
||||
// Cesium's argument checks with the same conditions and message text. Only the
|
||||
// checks src/geo needs are ported. Callers run them inside `if (DEBUG)` blocks
|
||||
// (see debug.js), as Cesium runs them inside its debug pragmas, so production
|
||||
// builds drop the calls and this module with them.
|
||||
|
||||
import { DeveloperError } from './developerError.js';
|
||||
|
||||
const undefinedMessage = (name) =>
|
||||
`${name} is required, actual value was undefined`;
|
||||
const typeMessage = (actual, expected, name) =>
|
||||
`Expected ${name} to be typeof ${expected}, actual typeof was ${actual}`;
|
||||
|
||||
/** Throws unless `test` is neither undefined nor null. */
|
||||
function defined(name, test) {
|
||||
if (test === undefined || test === null) {
|
||||
throw new DeveloperError(undefinedMessage(name));
|
||||
}
|
||||
}
|
||||
|
||||
/** Throws unless `typeof test` is 'number' (NaN passes, as in Cesium). */
|
||||
function number(name, test) {
|
||||
if (typeof test !== 'number') {
|
||||
throw new DeveloperError(typeMessage(typeof test, 'number', name));
|
||||
}
|
||||
}
|
||||
|
||||
// Attached in a function marked pure, not by top-level assignments, so that a
|
||||
// production build, where no caller runs a check, drops this module entirely.
|
||||
function withBounds(check) {
|
||||
check.lessThanOrEquals = function (name, test, limit) {
|
||||
check(name, test);
|
||||
if (test > limit) {
|
||||
throw new DeveloperError(
|
||||
`Expected ${name} to be less than or equal to ${limit}, actual value was ${test}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
check.greaterThanOrEquals = function (name, test, limit) {
|
||||
check(name, test);
|
||||
if (test < limit) {
|
||||
throw new DeveloperError(
|
||||
`Expected ${name} to be greater than or equal to ${limit}, actual value was ${test}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
return check;
|
||||
}
|
||||
|
||||
/** Throws unless `typeof test` is 'object' (null passes, as in Cesium). */
|
||||
function object(name, test) {
|
||||
if (typeof test !== 'object') {
|
||||
throw new DeveloperError(typeMessage(typeof test, 'object', name));
|
||||
}
|
||||
}
|
||||
|
||||
export const Check = /* @__PURE__ */ Object.freeze({
|
||||
defined,
|
||||
typeOf: Object.freeze({ number: /* @__PURE__ */ withBounds(number), object }),
|
||||
});
|
||||
@@ -1,486 +0,0 @@
|
||||
// Argument-check parity with Cesium. gev runs Cesium from source, where its
|
||||
// debug checks are live, so for any input both sides must throw an error with
|
||||
// the same name and message, or neither may throw. The inputs are seeded and
|
||||
// deterministic: each case builds its arguments twice from the same seed, once
|
||||
// for geo and once for Cesium, so no object is shared between the two sides.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from './index.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
|
||||
const TRIALS = 150;
|
||||
|
||||
function generator(seed) {
|
||||
return () => {
|
||||
seed = (seed + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
// Invalid values every argument may receive. NaN is a number to Check.typeOf.number
|
||||
// and null an object to Check.typeOf.object, so both reach the arithmetic.
|
||||
const INVALID = [undefined, null, Number.NaN, 'abc', 5, true, {}];
|
||||
|
||||
const vectorValues = { x: 1.5, y: -2, z: 3 };
|
||||
const VALID = {
|
||||
num: (side, random) => [0, -0, 0.7, -2.5, 1e6][Math.floor(random() * 5)],
|
||||
index3: (side, random) => Math.floor(random() * 5) - 1,
|
||||
index4: (side, random) => Math.floor(random() * 6) - 1,
|
||||
nonNegative: (side, random) => [0, 3, 6378137, -1][Math.floor(random() * 4)],
|
||||
vec: (side) =>
|
||||
side === 'geo' ? { ...vectorValues } : new Cesium.Cartesian3(1.5, -2, 3),
|
||||
zeroVec: (side) =>
|
||||
side === 'geo' ? { x: 0, y: 0, z: 0 } : new Cesium.Cartesian3(),
|
||||
vec4: (side) =>
|
||||
side === 'geo' ? { x: 0, y: 0, z: 0, w: 0 } : new Cesium.Cartesian4(),
|
||||
carto: (side) =>
|
||||
side === 'geo'
|
||||
? { longitude: 0.3, latitude: -0.2, height: 10 }
|
||||
: new Cesium.Cartographic(0.3, -0.2, 10),
|
||||
partialCarto: (side) =>
|
||||
side === 'geo'
|
||||
? { longitude: 0.3, latitude: -0.2 }
|
||||
: { longitude: 0.3, latitude: -0.2 },
|
||||
quat: (side) =>
|
||||
side === 'geo'
|
||||
? { x: 0.1, y: 0.2, z: 0.3, w: 0.9 }
|
||||
: new Cesium.Quaternion(0.1, 0.2, 0.3, 0.9),
|
||||
mat3: (side) =>
|
||||
side === 'geo'
|
||||
? geo.matrix3.fromRotationZ(0.4)
|
||||
: Cesium.Matrix3.fromRotationZ(0.4, new Cesium.Matrix3()),
|
||||
mat4: (side) =>
|
||||
side === 'geo'
|
||||
? geo.transforms.eastNorthUpToFixedFrame({ x: 6378137, y: 1, z: 2 })
|
||||
: Cesium.Transforms.eastNorthUpToFixedFrame(
|
||||
new Cesium.Cartesian3(6378137, 1, 2),
|
||||
),
|
||||
hpr: (side) =>
|
||||
side === 'geo'
|
||||
? { heading: 0.3, pitch: -0.2, roll: 0.1 }
|
||||
: new Cesium.HeadingPitchRoll(0.3, -0.2, 0.1),
|
||||
partialHpr: () => ({ heading: 0.3 }),
|
||||
ellipsoid: (side) =>
|
||||
side === 'geo' ? geo.ellipsoid.WGS84 : Cesium.Ellipsoid.WGS84,
|
||||
coords2: (side, random) =>
|
||||
[[1, 2], [1, 2, 3], [], [1, 2, 3, 4]][Math.floor(random() * 4)],
|
||||
coords3: (side, random) =>
|
||||
[[1, 2, 3], [1, 2], [], [1, 2, 3, 4, 5, 6], [1, 2, 'h']][
|
||||
Math.floor(random() * 5)
|
||||
],
|
||||
nanVec: (side) =>
|
||||
side === 'geo'
|
||||
? { x: 1, y: Number.NaN, z: 2 }
|
||||
: new Cesium.Cartesian3(1, Number.NaN, 2),
|
||||
partialVec: () => ({ x: 1, y: 2 }),
|
||||
stringHeight: () => '5',
|
||||
};
|
||||
|
||||
/** One argument: usually a valid value of its kind, sometimes an invalid one. */
|
||||
function argument(kinds, side, random) {
|
||||
if (random() < 0.4) return INVALID[Math.floor(random() * INVALID.length)];
|
||||
const kind = kinds[Math.floor(random() * kinds.length)];
|
||||
return VALID[kind](side, random);
|
||||
}
|
||||
|
||||
const W = Cesium.Ellipsoid.WGS84;
|
||||
const C3 = Cesium.Cartesian3;
|
||||
const V = ['vec', 'zeroVec', 'nanVec', 'partialVec'];
|
||||
const N = ['num'];
|
||||
const R = ['vec'];
|
||||
|
||||
// [name, geo function, Cesium function, argument kinds per position]
|
||||
const CASES = [
|
||||
['math.toRadians', geo.math.toRadians, Cesium.Math.toRadians, [N]],
|
||||
['math.toDegrees', geo.math.toDegrees, Cesium.Math.toDegrees, [N]],
|
||||
['math.clamp', geo.math.clamp, Cesium.Math.clamp, [N, N, N]],
|
||||
['math.lerp', geo.math.lerp, Cesium.Math.lerp, [N, N, N]],
|
||||
['math.mod', geo.math.mod, Cesium.Math.mod, [N, ['num', 'nonNegative']]],
|
||||
['math.zeroToTwoPi', geo.math.zeroToTwoPi, Cesium.Math.zeroToTwoPi, [N]],
|
||||
[
|
||||
'math.negativePiToPi',
|
||||
geo.math.negativePiToPi,
|
||||
Cesium.Math.negativePiToPi,
|
||||
[N],
|
||||
],
|
||||
[
|
||||
'math.equalsEpsilon',
|
||||
geo.math.equalsEpsilon,
|
||||
Cesium.Math.equalsEpsilon,
|
||||
[N, N, N, N],
|
||||
],
|
||||
|
||||
['cartesian2.clone', geo.cartesian2.clone, Cesium.Cartesian2.clone, [R, R]],
|
||||
[
|
||||
'cartesian2.distance',
|
||||
geo.cartesian2.distance,
|
||||
Cesium.Cartesian2.distance,
|
||||
[R, R],
|
||||
],
|
||||
|
||||
['cartesian3.clone', geo.cartesian3.clone, C3.clone, [V, R]],
|
||||
[
|
||||
'cartesian3.fromElements',
|
||||
geo.cartesian3.fromElements,
|
||||
C3.fromElements,
|
||||
[N, N, N, R],
|
||||
],
|
||||
['cartesian3.magnitude', geo.cartesian3.magnitude, C3.magnitude, [V]],
|
||||
[
|
||||
'cartesian3.magnitudeSquared',
|
||||
geo.cartesian3.magnitudeSquared,
|
||||
C3.magnitudeSquared,
|
||||
[V],
|
||||
],
|
||||
['cartesian3.distance', geo.cartesian3.distance, C3.distance, [V, V]],
|
||||
[
|
||||
'cartesian3.distanceSquared',
|
||||
geo.cartesian3.distanceSquared,
|
||||
C3.distanceSquared,
|
||||
[V, V],
|
||||
],
|
||||
['cartesian3.normalize', geo.cartesian3.normalize, C3.normalize, [V, R]],
|
||||
['cartesian3.dot', geo.cartesian3.dot, C3.dot, [V, V]],
|
||||
[
|
||||
'cartesian3.multiplyComponents',
|
||||
geo.cartesian3.multiplyComponents,
|
||||
C3.multiplyComponents,
|
||||
[V, V, R],
|
||||
],
|
||||
['cartesian3.add', geo.cartesian3.add, C3.add, [V, V, R]],
|
||||
['cartesian3.subtract', geo.cartesian3.subtract, C3.subtract, [V, V, R]],
|
||||
[
|
||||
'cartesian3.multiplyByScalar',
|
||||
geo.cartesian3.multiplyByScalar,
|
||||
C3.multiplyByScalar,
|
||||
[V, N, R],
|
||||
],
|
||||
[
|
||||
'cartesian3.divideByScalar',
|
||||
geo.cartesian3.divideByScalar,
|
||||
C3.divideByScalar,
|
||||
[V, N, R],
|
||||
],
|
||||
['cartesian3.negate', geo.cartesian3.negate, C3.negate, [V, R]],
|
||||
['cartesian3.lerp', geo.cartesian3.lerp, C3.lerp, [V, V, N, R]],
|
||||
['cartesian3.cross', geo.cartesian3.cross, C3.cross, [V, V, R]],
|
||||
['cartesian3.midpoint', geo.cartesian3.midpoint, C3.midpoint, [V, V, R]],
|
||||
['cartesian3.equals', geo.cartesian3.equals, C3.equals, [V, V]],
|
||||
[
|
||||
'cartesian3.equalsEpsilon',
|
||||
geo.cartesian3.equalsEpsilon,
|
||||
C3.equalsEpsilon,
|
||||
[V, V, N, N],
|
||||
],
|
||||
[
|
||||
'cartesian3.fromRadians',
|
||||
geo.cartesian3.fromRadians,
|
||||
C3.fromRadians,
|
||||
[N, N, ['num', 'stringHeight'], ['ellipsoid'], R],
|
||||
],
|
||||
[
|
||||
'cartesian3.fromDegrees',
|
||||
geo.cartesian3.fromDegrees,
|
||||
C3.fromDegrees,
|
||||
[N, N, ['num', 'stringHeight'], ['ellipsoid'], R],
|
||||
],
|
||||
[
|
||||
'cartesian3.fromDegreesArray',
|
||||
geo.cartesian3.fromDegreesArray,
|
||||
C3.fromDegreesArray,
|
||||
[['coords2'], ['ellipsoid']],
|
||||
],
|
||||
[
|
||||
'cartesian3.fromDegreesArrayHeights',
|
||||
geo.cartesian3.fromDegreesArrayHeights,
|
||||
C3.fromDegreesArrayHeights,
|
||||
[['coords3'], ['ellipsoid']],
|
||||
],
|
||||
|
||||
[
|
||||
'cartographic.fromRadians',
|
||||
geo.cartographic.fromRadians,
|
||||
Cesium.Cartographic.fromRadians,
|
||||
[N, N, N, ['carto']],
|
||||
],
|
||||
[
|
||||
'cartographic.fromDegrees',
|
||||
geo.cartographic.fromDegrees,
|
||||
Cesium.Cartographic.fromDegrees,
|
||||
[N, N, N, ['carto']],
|
||||
],
|
||||
[
|
||||
'cartographic.fromCartesian',
|
||||
geo.cartographic.fromCartesian,
|
||||
Cesium.Cartographic.fromCartesian,
|
||||
[V, ['ellipsoid'], ['carto']],
|
||||
],
|
||||
[
|
||||
'cartographic.toCartesian',
|
||||
geo.cartographic.toCartesian,
|
||||
Cesium.Cartographic.toCartesian,
|
||||
[['carto', 'partialCarto'], ['ellipsoid'], R],
|
||||
],
|
||||
[
|
||||
'cartographic.clone',
|
||||
geo.cartographic.clone,
|
||||
Cesium.Cartographic.clone,
|
||||
[['carto'], ['carto']],
|
||||
],
|
||||
|
||||
[
|
||||
'ellipsoid.create',
|
||||
geo.ellipsoid.create,
|
||||
(x, y, z) => new Cesium.Ellipsoid(x, y, z),
|
||||
[['nonNegative'], ['nonNegative'], ['nonNegative']],
|
||||
],
|
||||
[
|
||||
'WGS84.geodeticSurfaceNormal',
|
||||
geo.ellipsoid.WGS84.geodeticSurfaceNormal,
|
||||
(c, r) => W.geodeticSurfaceNormal(c, r),
|
||||
[V, R],
|
||||
],
|
||||
[
|
||||
'WGS84.geodeticSurfaceNormalCartographic',
|
||||
geo.ellipsoid.WGS84.geodeticSurfaceNormalCartographic,
|
||||
(c, r) => W.geodeticSurfaceNormalCartographic(c, r),
|
||||
[['carto', 'partialCarto'], R],
|
||||
],
|
||||
[
|
||||
'WGS84.scaleToGeodeticSurface',
|
||||
geo.ellipsoid.WGS84.scaleToGeodeticSurface,
|
||||
(c, r) => W.scaleToGeodeticSurface(c, r),
|
||||
[V, R],
|
||||
],
|
||||
[
|
||||
'WGS84.cartographicToCartesian',
|
||||
geo.ellipsoid.WGS84.cartographicToCartesian,
|
||||
(c, r) => W.cartographicToCartesian(c, r),
|
||||
[['carto', 'partialCarto'], R],
|
||||
],
|
||||
[
|
||||
'WGS84.cartesianToCartographic',
|
||||
geo.ellipsoid.WGS84.cartesianToCartographic,
|
||||
(c, r) => W.cartesianToCartographic(c, r),
|
||||
[V, ['carto']],
|
||||
],
|
||||
|
||||
[
|
||||
'matrix3.fromQuaternion',
|
||||
geo.matrix3.fromQuaternion,
|
||||
Cesium.Matrix3.fromQuaternion,
|
||||
[['quat'], ['mat3']],
|
||||
],
|
||||
[
|
||||
'matrix3.fromRotationZ',
|
||||
geo.matrix3.fromRotationZ,
|
||||
Cesium.Matrix3.fromRotationZ,
|
||||
[N, ['mat3']],
|
||||
],
|
||||
[
|
||||
'matrix3.multiplyByVector',
|
||||
geo.matrix3.multiplyByVector,
|
||||
Cesium.Matrix3.multiplyByVector,
|
||||
[['mat3'], V, R],
|
||||
],
|
||||
[
|
||||
'matrix3.getColumn',
|
||||
geo.matrix3.getColumn,
|
||||
Cesium.Matrix3.getColumn,
|
||||
[['mat3'], ['index3'], R],
|
||||
],
|
||||
|
||||
[
|
||||
'matrix4.clone',
|
||||
geo.matrix4.clone,
|
||||
Cesium.Matrix4.clone,
|
||||
[['mat4'], ['mat4']],
|
||||
],
|
||||
[
|
||||
'matrix4.equals',
|
||||
geo.matrix4.equals,
|
||||
Cesium.Matrix4.equals,
|
||||
[['mat4'], ['mat4']],
|
||||
],
|
||||
[
|
||||
'matrix4.multiply',
|
||||
geo.matrix4.multiply,
|
||||
Cesium.Matrix4.multiply,
|
||||
[['mat4'], ['mat4'], ['mat4']],
|
||||
],
|
||||
[
|
||||
'matrix4.multiplyTransformation',
|
||||
geo.matrix4.multiplyTransformation,
|
||||
Cesium.Matrix4.multiplyTransformation,
|
||||
[['mat4'], ['mat4'], ['mat4']],
|
||||
],
|
||||
[
|
||||
'matrix4.multiplyByPoint',
|
||||
geo.matrix4.multiplyByPoint,
|
||||
Cesium.Matrix4.multiplyByPoint,
|
||||
[['mat4'], V, R],
|
||||
],
|
||||
[
|
||||
'matrix4.multiplyByPointAsVector',
|
||||
geo.matrix4.multiplyByPointAsVector,
|
||||
Cesium.Matrix4.multiplyByPointAsVector,
|
||||
[['mat4'], V, R],
|
||||
],
|
||||
[
|
||||
'matrix4.getTranslation',
|
||||
geo.matrix4.getTranslation,
|
||||
Cesium.Matrix4.getTranslation,
|
||||
[['mat4'], R],
|
||||
],
|
||||
[
|
||||
'matrix4.getMatrix3',
|
||||
geo.matrix4.getMatrix3,
|
||||
Cesium.Matrix4.getMatrix3,
|
||||
[['mat4'], ['mat3']],
|
||||
],
|
||||
[
|
||||
'matrix4.getColumn',
|
||||
geo.matrix4.getColumn,
|
||||
Cesium.Matrix4.getColumn,
|
||||
[['mat4'], ['index4'], ['vec4']],
|
||||
],
|
||||
[
|
||||
'matrix4.inverseTransformation',
|
||||
geo.matrix4.inverseTransformation,
|
||||
Cesium.Matrix4.inverseTransformation,
|
||||
[['mat4'], ['mat4']],
|
||||
],
|
||||
[
|
||||
'matrix4.fromRotationTranslation',
|
||||
geo.matrix4.fromRotationTranslation,
|
||||
Cesium.Matrix4.fromRotationTranslation,
|
||||
[['mat3'], R, ['mat4']],
|
||||
],
|
||||
[
|
||||
'matrix4.fromUniformScale',
|
||||
geo.matrix4.fromUniformScale,
|
||||
Cesium.Matrix4.fromUniformScale,
|
||||
[N, ['mat4']],
|
||||
],
|
||||
|
||||
[
|
||||
'quaternion.fromAxisAngle',
|
||||
geo.quaternion.fromAxisAngle,
|
||||
Cesium.Quaternion.fromAxisAngle,
|
||||
[V, N, ['quat']],
|
||||
],
|
||||
[
|
||||
'quaternion.fromRotationMatrix',
|
||||
geo.quaternion.fromRotationMatrix,
|
||||
Cesium.Quaternion.fromRotationMatrix,
|
||||
[['mat3'], ['quat']],
|
||||
],
|
||||
|
||||
[
|
||||
'transforms.eastNorthUpToFixedFrame',
|
||||
geo.transforms.eastNorthUpToFixedFrame,
|
||||
Cesium.Transforms.eastNorthUpToFixedFrame,
|
||||
[V, ['ellipsoid'], ['mat4']],
|
||||
],
|
||||
[
|
||||
'transforms.headingPitchRollToFixedFrame',
|
||||
geo.transforms.headingPitchRollToFixedFrame,
|
||||
Cesium.Transforms.headingPitchRollToFixedFrame,
|
||||
[V, ['hpr', 'partialHpr'], ['ellipsoid'], [], ['mat4']],
|
||||
],
|
||||
];
|
||||
|
||||
function outcome(fn, args) {
|
||||
try {
|
||||
fn(...args);
|
||||
return 'returned';
|
||||
} catch (error) {
|
||||
return `${error?.name}: ${error?.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
test('every ported function throws exactly where Cesium does, with the same name and message', () => {
|
||||
let thrown = 0;
|
||||
let checked = 0;
|
||||
const mismatches = [];
|
||||
CASES.forEach(([name, ours, theirs, kinds], caseIndex) => {
|
||||
for (let trial = 0; trial < TRIALS; trial++) {
|
||||
const seed = caseIndex * 100003 + trial;
|
||||
// Trial 0 passes no arguments at all; the rest draw from the pools.
|
||||
const build = (side) => {
|
||||
if (trial === 0) return [];
|
||||
const random = generator(seed);
|
||||
return kinds.map((k) =>
|
||||
k.length ? argument(k, side, random) : undefined,
|
||||
);
|
||||
};
|
||||
const a = outcome(ours, build('geo'));
|
||||
const b = outcome(theirs, build('cesium'));
|
||||
if (a !== b)
|
||||
mismatches.push(`${name} (seed ${seed}): geo ${a} | Cesium ${b}`);
|
||||
checked++;
|
||||
if (b !== 'returned') thrown++;
|
||||
}
|
||||
});
|
||||
assert.deepEqual(mismatches, []);
|
||||
// The pools must actually reach the checks.
|
||||
assert.ok(thrown > checked / 4, `only ${thrown} of ${checked} cases threw`);
|
||||
});
|
||||
|
||||
test('the checks throw a DeveloperError with Cesium message text', () => {
|
||||
const cases = [
|
||||
[() => geo.math.toRadians(undefined), 'degrees is required.'],
|
||||
[() => geo.math.mod(5, 0), 'divisor cannot be 0.'],
|
||||
[
|
||||
() => geo.math.clamp('1', 0, 1),
|
||||
'Expected value to be typeof number, actual typeof was string',
|
||||
],
|
||||
[
|
||||
() => geo.cartesian3.fromDegrees(undefined, 1),
|
||||
'Expected longitude to be typeof number, actual typeof was undefined',
|
||||
],
|
||||
[
|
||||
() => geo.cartesian3.add({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }),
|
||||
'Expected result to be typeof object, actual typeof was undefined',
|
||||
],
|
||||
[
|
||||
() => geo.cartesian3.fromDegreesArray([1]),
|
||||
'the number of coordinates must be a multiple of 2 and at least 2',
|
||||
],
|
||||
[
|
||||
() => geo.cartesian3.fromDegreesArray(),
|
||||
'coordinates is required, actual value was undefined',
|
||||
],
|
||||
[
|
||||
() => geo.ellipsoid.create(-1, 1, 1),
|
||||
'Expected x to be greater than or equal to 0, actual value was -1',
|
||||
],
|
||||
[
|
||||
() => geo.matrix3.getColumn(geo.matrix3.create(), 3, {}),
|
||||
'Expected index to be less than or equal to 2, actual value was 3',
|
||||
],
|
||||
[
|
||||
() =>
|
||||
geo.transforms.eastNorthUpToFixedFrame({ x: 1, y: Number.NaN, z: 0 }),
|
||||
'origin has a NaN component',
|
||||
],
|
||||
[
|
||||
() => geo.cartographic.fromCartesian({ x: 1, y: 2, z: 3 }, {}),
|
||||
'oneOverRadii is required.',
|
||||
],
|
||||
];
|
||||
for (const [fn, message] of cases) {
|
||||
assert.throws(fn, (error) => {
|
||||
assert.ok(error instanceof DeveloperError);
|
||||
assert.equal(error.name, 'DeveloperError');
|
||||
assert.equal(error.message, message);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
// NaN is typeof number, so Check.typeOf.number lets it through (as in Cesium).
|
||||
assert.ok(Number.isNaN(geo.math.clamp(Number.NaN, 0, 1)));
|
||||
assert.ok(Number.isNaN(Cesium.Math.clamp(Number.NaN, 0, 1)));
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
// Whether src/geo runs Cesium's debug argument checks.
|
||||
//
|
||||
// Cesium wraps its argument checks in `//>>includeStart('debug')` pragmas: they
|
||||
// are live in its Source modules (Node tests, the Vite dev server) and stripped
|
||||
// from Build/Cesium, which vite-plugin-cesium copies into production builds.
|
||||
// src/geo does the same: every Check call and every debug throw sits in an
|
||||
// `if (DEBUG)` block. DEBUG is true in Node (no import.meta.env) and in Vite
|
||||
// dev; `vite build` folds it to false and the minifier drops the blocks, so a
|
||||
// production build returns what release Cesium returns (NaN, not a throw, for
|
||||
// a degenerate input).
|
||||
|
||||
export const DEBUG = import.meta.env?.PROD !== true;
|
||||
@@ -1,13 +0,0 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — DeveloperError.js
|
||||
//
|
||||
// The error Cesium throws for invalid arguments. Its debug argument checks are
|
||||
// live in Cesium's Source modules (Node tests, the Vite dev server) and
|
||||
// stripped from production builds; src/geo throws the same name and message
|
||||
// wherever Cesium does while DEBUG is on (see debug.js).
|
||||
|
||||
export class DeveloperError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'DeveloperError';
|
||||
}
|
||||
}
|
||||
+1
-22
@@ -3,14 +3,9 @@
|
||||
// A quadratic surface x²/a² + y²/b² + z²/c² = 1 as a plain, frozen object.
|
||||
// Every method takes an optional `result` and returns it (a new object when it
|
||||
// is absent); `result` may be the input. The algorithms and the operation order
|
||||
// match Cesium so the outputs are bit-identical. With DEBUG on (development
|
||||
// and tests; see debug.js) invalid arguments throw the same DeveloperError as
|
||||
// Cesium's debug checks.
|
||||
// match Cesium so the outputs are bit-identical.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { create as createCartographic } from './cartographic.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
import { EPSILON14 } from './math.js';
|
||||
import { scaleToGeodeticSurface } from './scaleToGeodeticSurface.js';
|
||||
import {
|
||||
@@ -39,11 +34,6 @@ export function create(x, y, z) {
|
||||
x = x ?? 0.0;
|
||||
y = y ?? 0.0;
|
||||
z = z ?? 0.0;
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number.greaterThanOrEquals('x', x, 0.0);
|
||||
Check.typeOf.number.greaterThanOrEquals('y', y, 0.0);
|
||||
Check.typeOf.number.greaterThanOrEquals('z', z, 0.0);
|
||||
}
|
||||
|
||||
const radii = Object.freeze(vec(x, y, z));
|
||||
const radiiSquared = Object.freeze(vec(x * x, y * y, z * z));
|
||||
@@ -68,10 +58,6 @@ export function create(x, y, z) {
|
||||
|
||||
/** The normal to the ellipsoid surface at a geodetic position. */
|
||||
function geodeticSurfaceNormalCartographic(cartographic, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartographic', cartographic);
|
||||
}
|
||||
|
||||
const longitude = cartographic.longitude;
|
||||
const latitude = cartographic.latitude;
|
||||
const cosLatitude = Math.cos(latitude);
|
||||
@@ -89,13 +75,6 @@ export function create(x, y, z) {
|
||||
|
||||
/** The normal to the ellipsoid surface at a position, or `undefined` at the centre. */
|
||||
function geodeticSurfaceNormal(cartesian, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
if (isNaN(cartesian.x) || isNaN(cartesian.y) || isNaN(cartesian.z)) {
|
||||
throw new DeveloperError('cartesian has a NaN component');
|
||||
}
|
||||
}
|
||||
|
||||
if (equalsEpsilon(cartesian, ZERO, EPSILON14)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -12,18 +12,6 @@ const close = (a, b, tol, label) =>
|
||||
const closeVec = (a, b, tol, label) => {
|
||||
for (const k of ['x', 'y', 'z']) close(a[k], b[k], tol, `${label}.${k}`);
|
||||
};
|
||||
const POINTS = [
|
||||
[6378137, 0, 0],
|
||||
[0, 6378137, 0],
|
||||
[0, 0, 6356752.314245],
|
||||
[1334762.2, -4654869.1, 4137287.9],
|
||||
[-2694045, -4293642, 3857878],
|
||||
[1e3, 1e3, 1e3],
|
||||
[0.01, 0.01, 0.01],
|
||||
[7e6, -2e6, 1e6],
|
||||
[4510000, 4510000, 1],
|
||||
[1, 1, 6400000],
|
||||
];
|
||||
|
||||
test('WGS84 constants equal Cesium', () => {
|
||||
const c = Cesium.Ellipsoid.WGS84;
|
||||
@@ -40,39 +28,6 @@ test('WGS84 constants equal Cesium', () => {
|
||||
assert.equal(WGS84.maximumRadius, c.maximumRadius);
|
||||
});
|
||||
|
||||
test('surface normals, scaling and conversions equal Cesium', () => {
|
||||
const c = Cesium.Ellipsoid.WGS84;
|
||||
for (const [x, y, z] of POINTS) {
|
||||
const p = { x, y, z };
|
||||
const cp = new Cesium.Cartesian3(x, y, z);
|
||||
closeVec(
|
||||
WGS84.geodeticSurfaceNormal(p),
|
||||
c.geodeticSurfaceNormal(cp),
|
||||
1e-12,
|
||||
'normal',
|
||||
);
|
||||
const ours = WGS84.scaleToGeodeticSurface(p);
|
||||
const theirs = c.scaleToGeodeticSurface(cp);
|
||||
if (theirs === undefined) assert.equal(ours, undefined);
|
||||
else closeVec(ours, theirs, 1e-12, 'scale');
|
||||
const carto = WGS84.cartesianToCartographic(p);
|
||||
const cCarto = c.cartesianToCartographic(cp);
|
||||
if (cCarto === undefined) {
|
||||
assert.equal(carto, undefined);
|
||||
} else {
|
||||
close(carto.longitude, cCarto.longitude, 1e-12, 'lon');
|
||||
close(carto.latitude, cCarto.latitude, 1e-12, 'lat');
|
||||
close(carto.height, cCarto.height, 1e-9, 'height');
|
||||
closeVec(
|
||||
WGS84.cartographicToCartesian(carto),
|
||||
c.cartographicToCartesian(cCarto),
|
||||
1e-12,
|
||||
'roundtrip',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('result parameters are filled in place and may alias the input', () => {
|
||||
const p = { x: 1334762.2, y: -4654869.1, z: 4137287.9 };
|
||||
const out = { x: 0, y: 0, z: 0 };
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — EllipsoidGeodesic.js.
|
||||
// Vincenty inverse and surface interpolation, preserving operation order.
|
||||
import * as Cartesian3 from './cartesian3.js';
|
||||
import * as Cartographic from './cartographic.js';
|
||||
import * as Ellipsoid from './ellipsoid.js';
|
||||
import * as CesiumMath from './math.js';
|
||||
import { Check } from './check.js';
|
||||
import { DEBUG } from './debug.js';
|
||||
const defined = (value) => value !== null && value !== undefined;
|
||||
function setConstants(ellipsoidGeodesic) {
|
||||
const uSquared = ellipsoidGeodesic._uSquared;
|
||||
@@ -230,29 +227,7 @@ function vincentyInverseFormula(
|
||||
ellipsoidGeodesic._uSquared = uSquared;
|
||||
}
|
||||
|
||||
const scratchCart1 = Cartesian3.create();
|
||||
const scratchCart2 = Cartesian3.create();
|
||||
function computeProperties(ellipsoidGeodesic, start, end, ellipsoid) {
|
||||
const firstCartesian = Cartesian3.normalize(
|
||||
ellipsoid.cartographicToCartesian(start, scratchCart2),
|
||||
scratchCart1,
|
||||
);
|
||||
const lastCartesian = Cartesian3.normalize(
|
||||
ellipsoid.cartographicToCartesian(end, scratchCart2),
|
||||
scratchCart2,
|
||||
);
|
||||
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number.greaterThanOrEquals(
|
||||
'value',
|
||||
Math.abs(
|
||||
Math.abs(Cartesian3.angleBetween(firstCartesian, lastCartesian)) -
|
||||
Math.PI,
|
||||
),
|
||||
0.0125,
|
||||
);
|
||||
}
|
||||
|
||||
vincentyInverseFormula(
|
||||
ellipsoidGeodesic,
|
||||
ellipsoid.maximumRadius,
|
||||
@@ -322,10 +297,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
*/
|
||||
surfaceDistance: {
|
||||
get: function () {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
return this._distance;
|
||||
},
|
||||
},
|
||||
@@ -362,10 +333,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
*/
|
||||
startHeading: {
|
||||
get: function () {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
return this._startHeading;
|
||||
},
|
||||
},
|
||||
@@ -378,10 +345,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
*/
|
||||
endHeading: {
|
||||
get: function () {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
return this._endHeading;
|
||||
},
|
||||
},
|
||||
@@ -394,11 +357,6 @@ Object.defineProperties(EllipsoidGeodesic.prototype, {
|
||||
* @param {Cartographic} end The final planetodetic point on the path.
|
||||
*/
|
||||
EllipsoidGeodesic.prototype.setEndPoints = function (start, end) {
|
||||
if (DEBUG) {
|
||||
Check.defined('start', start);
|
||||
Check.defined('end', end);
|
||||
}
|
||||
|
||||
computeProperties(this, start, end, this._ellipsoid);
|
||||
};
|
||||
|
||||
@@ -425,17 +383,11 @@ EllipsoidGeodesic.prototype.interpolateUsingFraction = function (
|
||||
* @param {number} distance The distance from the initial point to the point of interest along the geodesic
|
||||
* @param {Cartographic} [result] The object in which to store the result.
|
||||
* @returns {Cartographic} The location of the point along the geodesic.
|
||||
*
|
||||
* @exception {DeveloperError} start and end must be set before calling function interpolateUsingSurfaceDistance
|
||||
*/
|
||||
EllipsoidGeodesic.prototype.interpolateUsingSurfaceDistance = function (
|
||||
distance,
|
||||
result,
|
||||
) {
|
||||
if (DEBUG) {
|
||||
Check.defined('distance', this._distance);
|
||||
}
|
||||
|
||||
const constants = this._constants;
|
||||
|
||||
const s = constants.distanceRatio + distance / constants.b;
|
||||
|
||||
@@ -66,17 +66,3 @@ test('endpoint reset, result reuse, custom sphere and dateline/polar paths retai
|
||||
);
|
||||
}
|
||||
});
|
||||
test('unset and near-antipodal geodesics reject the same debug inputs', () => {
|
||||
assert.throws(() => geo.ellipsoidGeodesic.create().surfaceDistance, {
|
||||
name: 'DeveloperError',
|
||||
});
|
||||
const a = geo.cartographic.fromDegrees(0, 0),
|
||||
b = geo.cartographic.fromDegrees(180, 0);
|
||||
assert.throws(() => geo.ellipsoidGeodesic.create(a, b), {
|
||||
name: 'DeveloperError',
|
||||
});
|
||||
assert.throws(
|
||||
() => geo.ellipsoidGeodesic.create().setEndPoints(undefined, b),
|
||||
{ name: 'DeveloperError' },
|
||||
);
|
||||
});
|
||||
+1
-54
@@ -1,15 +1,8 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Math.js
|
||||
//
|
||||
// Engine-neutral angle maths. Formulas and constants match Cesium.Math bit for
|
||||
// bit. With DEBUG on (development and tests; see debug.js) invalid arguments
|
||||
// throw the same DeveloperError as Cesium's debug checks; production builds
|
||||
// skip the checks, as release Cesium does.
|
||||
// bit.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
|
||||
export const EPSILON7 = 0.0000001;
|
||||
export const EPSILON10 = 0.0000000001;
|
||||
export const EPSILON12 = 0.000000000001;
|
||||
export const EPSILON14 = 0.00000000000001;
|
||||
@@ -20,32 +13,16 @@ export const DEGREES_PER_RADIAN = 180.0 / Math.PI;
|
||||
|
||||
/** Converts degrees to radians. */
|
||||
export function toRadians(degrees) {
|
||||
if (DEBUG) {
|
||||
if (degrees == null) {
|
||||
throw new DeveloperError('degrees is required.');
|
||||
}
|
||||
}
|
||||
return degrees * RADIANS_PER_DEGREE;
|
||||
}
|
||||
|
||||
/** Converts radians to degrees. */
|
||||
export function toDegrees(radians) {
|
||||
if (DEBUG) {
|
||||
if (radians == null) {
|
||||
throw new DeveloperError('radians is required.');
|
||||
}
|
||||
}
|
||||
return radians * DEGREES_PER_RADIAN;
|
||||
}
|
||||
|
||||
/** Constrains a value to lie between two values. */
|
||||
export function clamp(value, min, max) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('value', value);
|
||||
Check.typeOf.number('min', min);
|
||||
Check.typeOf.number('max', max);
|
||||
}
|
||||
|
||||
return value < min ? min : value > max ? max : value;
|
||||
}
|
||||
|
||||
@@ -56,17 +33,6 @@ export function lerp(p, q, time) {
|
||||
|
||||
/** The modulo operation that also works for negative dividends. */
|
||||
export function mod(m, n) {
|
||||
if (DEBUG) {
|
||||
if (m == null) {
|
||||
throw new DeveloperError('m is required.');
|
||||
}
|
||||
if (n == null) {
|
||||
throw new DeveloperError('n is required.');
|
||||
}
|
||||
if (n === 0.0) {
|
||||
throw new DeveloperError('divisor cannot be 0.');
|
||||
}
|
||||
}
|
||||
if (Math.sign(m) === Math.sign(n) && Math.abs(m) < Math.abs(n)) {
|
||||
// Early exit if the input does not need to be modded. This avoids
|
||||
// unnecessary math which could introduce floating point error.
|
||||
@@ -77,11 +43,6 @@ export function mod(m, n) {
|
||||
|
||||
/** Produces an angle in the range 0 <= angle <= 2Pi equivalent to the provided angle. */
|
||||
export function zeroToTwoPi(angle) {
|
||||
if (DEBUG) {
|
||||
if (angle == null) {
|
||||
throw new DeveloperError('angle is required.');
|
||||
}
|
||||
}
|
||||
if (angle >= 0 && angle <= TWO_PI) {
|
||||
// Early exit if the input is already inside the range. This avoids
|
||||
// unnecessary math which could introduce floating point error.
|
||||
@@ -96,11 +57,6 @@ export function zeroToTwoPi(angle) {
|
||||
|
||||
/** Produces an angle in the range -Pi <= angle <= Pi equivalent to the provided angle. */
|
||||
export function negativePiToPi(angle) {
|
||||
if (DEBUG) {
|
||||
if (angle == null) {
|
||||
throw new DeveloperError('angle is required.');
|
||||
}
|
||||
}
|
||||
if (angle >= -Math.PI && angle <= Math.PI) {
|
||||
// Early exit if the input is already inside the range. This avoids
|
||||
// unnecessary math which could introduce floating point error.
|
||||
@@ -114,15 +70,6 @@ export function negativePiToPi(angle) {
|
||||
* (the absolute one defaults to the relative one).
|
||||
*/
|
||||
export function equalsEpsilon(left, right, relativeEpsilon, absoluteEpsilon) {
|
||||
if (DEBUG) {
|
||||
if (left == null) {
|
||||
throw new DeveloperError('left is required.');
|
||||
}
|
||||
if (right == null) {
|
||||
throw new DeveloperError('right is required.');
|
||||
}
|
||||
}
|
||||
|
||||
relativeEpsilon = relativeEpsilon ?? 0.0;
|
||||
absoluteEpsilon = absoluteEpsilon ?? relativeEpsilon;
|
||||
const absDiff = Math.abs(left - right);
|
||||
|
||||
@@ -31,7 +31,6 @@ test('constants equal Cesium.Math', () => {
|
||||
for (const name of [
|
||||
'PI_OVER_TWO',
|
||||
'TWO_PI',
|
||||
'EPSILON7',
|
||||
'EPSILON10',
|
||||
'EPSILON12',
|
||||
'EPSILON14',
|
||||
|
||||
+2
-73
@@ -4,13 +4,10 @@ import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as m3 from './matrix3.js';
|
||||
import * as m4 from './matrix4.js';
|
||||
import * as quat from './quaternion.js';
|
||||
|
||||
const arr = (m, n) => Array.from({ length: n }, (_, i) => m[i]);
|
||||
const sameArr = (ours, theirs, n, label) =>
|
||||
assert.deepEqual(arr(ours, n), arr(theirs, n), label);
|
||||
const close = (a, b, tol, label) =>
|
||||
assert.ok(Math.abs(a - b) <= tol, `${label}: ${a} vs ${b}`);
|
||||
|
||||
const T = Cesium.Matrix4.fromTranslationQuaternionRotationScale(
|
||||
new Cesium.Cartesian3(10, -20, 30),
|
||||
@@ -29,41 +26,7 @@ const U = Cesium.Matrix4.fromRotationTranslation(
|
||||
);
|
||||
const plain4 = (m) => arr(m, 16);
|
||||
|
||||
test('matrix4 products, points and inverses equal Cesium', () => {
|
||||
sameArr(
|
||||
m4.multiply(plain4(T), plain4(U), m4.create()),
|
||||
Cesium.Matrix4.multiply(T, U, new Cesium.Matrix4()),
|
||||
16,
|
||||
'multiply',
|
||||
);
|
||||
sameArr(
|
||||
m4.multiplyTransformation(plain4(T), plain4(U), m4.create()),
|
||||
Cesium.Matrix4.multiplyTransformation(T, U, new Cesium.Matrix4()),
|
||||
16,
|
||||
'multiplyTransformation',
|
||||
);
|
||||
const p = new Cesium.Cartesian3(1.5, -2, 3);
|
||||
const ours = m4.multiplyByPoint(
|
||||
plain4(T),
|
||||
{ x: 1.5, y: -2, z: 3 },
|
||||
{ x: 0, y: 0, z: 0 },
|
||||
);
|
||||
const theirs = Cesium.Matrix4.multiplyByPoint(T, p, new Cesium.Cartesian3());
|
||||
assert.deepEqual({ ...ours }, { x: theirs.x, y: theirs.y, z: theirs.z });
|
||||
const v = m4.multiplyByPointAsVector(
|
||||
plain4(T),
|
||||
{ x: 1.5, y: -2, z: 3 },
|
||||
{ x: 0, y: 0, z: 0 },
|
||||
);
|
||||
const cv = Cesium.Matrix4.multiplyByPointAsVector(
|
||||
T,
|
||||
p,
|
||||
new Cesium.Cartesian3(),
|
||||
);
|
||||
assert.deepEqual({ ...v }, { x: cv.x, y: cv.y, z: cv.z });
|
||||
const inv = m4.inverseTransformation(plain4(T), m4.create());
|
||||
const cinv = Cesium.Matrix4.inverseTransformation(T, new Cesium.Matrix4());
|
||||
for (let i = 0; i < 16; i++) close(inv[i], cinv[i], 1e-12, `inv ${i}`);
|
||||
test('matrix4 getters, equals and IDENTITY equal Cesium', () => {
|
||||
const t = m4.getTranslation(plain4(T), { x: 0, y: 0, z: 0 });
|
||||
assert.deepEqual({ ...t }, { x: 10, y: -20, z: 30 });
|
||||
sameArr(
|
||||
@@ -74,49 +37,15 @@ test('matrix4 products, points and inverses equal Cesium', () => {
|
||||
);
|
||||
assert.ok(m4.equals(m4.clone(plain4(T)), plain4(T)));
|
||||
assert.ok(Object.isFrozen(m4.IDENTITY));
|
||||
sameArr(
|
||||
m4.fromUniformScale(2.5, m4.create()),
|
||||
Cesium.Matrix4.fromUniformScale(2.5, new Cesium.Matrix4()),
|
||||
16,
|
||||
'scale',
|
||||
);
|
||||
});
|
||||
|
||||
test('matrix3, quaternion and rotation-translation equal Cesium', () => {
|
||||
test('matrix3 and rotation-translation equal Cesium', () => {
|
||||
sameArr(
|
||||
m3.fromRotationZ(1.1, m3.create()),
|
||||
Cesium.Matrix3.fromRotationZ(1.1, new Cesium.Matrix3()),
|
||||
9,
|
||||
'rotZ',
|
||||
);
|
||||
const axis = {
|
||||
x: 0.2672612419124244,
|
||||
y: 0.5345224838248488,
|
||||
z: 0.8017837257372732,
|
||||
};
|
||||
const q = quat.fromAxisAngle(axis, 0.7, quat.create());
|
||||
const cq = Cesium.Quaternion.fromAxisAngle(
|
||||
new Cesium.Cartesian3(axis.x, axis.y, axis.z),
|
||||
0.7,
|
||||
new Cesium.Quaternion(),
|
||||
);
|
||||
assert.deepEqual({ ...q }, { x: cq.x, y: cq.y, z: cq.z, w: cq.w });
|
||||
sameArr(
|
||||
m3.fromQuaternion(q, m3.create()),
|
||||
Cesium.Matrix3.fromQuaternion(cq, new Cesium.Matrix3()),
|
||||
9,
|
||||
'fromQuaternion',
|
||||
);
|
||||
const back = quat.fromRotationMatrix(
|
||||
m3.fromQuaternion(q, m3.create()),
|
||||
quat.create(),
|
||||
);
|
||||
const cback = Cesium.Quaternion.fromRotationMatrix(
|
||||
Cesium.Matrix3.fromQuaternion(cq, new Cesium.Matrix3()),
|
||||
new Cesium.Quaternion(),
|
||||
);
|
||||
for (const k of ['x', 'y', 'z', 'w'])
|
||||
close(back[k], cback[k], 1e-15, `q.${k}`);
|
||||
const mv = m3.multiplyByVector(
|
||||
m3.fromRotationZ(1.1, m3.create()),
|
||||
{ x: 1, y: 2, z: 3 },
|
||||
|
||||
+1
-27
@@ -2,12 +2,7 @@
|
||||
//
|
||||
// 3x3 matrices as plain arrays of 9 numbers in Cesium's column-major index
|
||||
// order (m[column * 3 + row]), so Cesium accepts them wherever it reads
|
||||
// m[0]..m[8]. Only the subset gev uses is ported. With DEBUG on (development
|
||||
// and tests; see debug.js) invalid arguments throw the same DeveloperError as
|
||||
// Cesium's debug checks.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
// m[0]..m[8]. Only the subset gev uses is ported.
|
||||
|
||||
/** A matrix of zeros (Cesium's `new Matrix3()`). */
|
||||
export function create() {
|
||||
@@ -16,10 +11,6 @@ export function create() {
|
||||
|
||||
/** The rotation matrix of a unit quaternion. */
|
||||
export function fromQuaternion(quaternion, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('quaternion', quaternion);
|
||||
}
|
||||
|
||||
const x2 = quaternion.x * quaternion.x;
|
||||
const xy = quaternion.x * quaternion.y;
|
||||
const xz = quaternion.x * quaternion.z;
|
||||
@@ -58,10 +49,6 @@ export function fromQuaternion(quaternion, result) {
|
||||
|
||||
/** A rotation by `angle` radians around the z-axis. */
|
||||
export function fromRotationZ(angle, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('angle', angle);
|
||||
}
|
||||
|
||||
const cosAngle = Math.cos(angle);
|
||||
const sinAngle = Math.sin(angle);
|
||||
|
||||
@@ -80,12 +67,6 @@ export function fromRotationZ(angle, result) {
|
||||
|
||||
/** The product of a matrix and a column vector; `result` is required. */
|
||||
export function multiplyByVector(matrix, cartesian, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const vX = cartesian.x;
|
||||
const vY = cartesian.y;
|
||||
const vZ = cartesian.z;
|
||||
@@ -102,13 +83,6 @@ export function multiplyByVector(matrix, cartesian, result) {
|
||||
|
||||
/** The column at `index` (0..2) as {x, y, z}; `result` is required. */
|
||||
export function getColumn(matrix, index, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.number.greaterThanOrEquals('index', index, 0);
|
||||
Check.typeOf.number.lessThanOrEquals('index', index, 2);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const startIndex = index * 3;
|
||||
const x = matrix[startIndex];
|
||||
const y = matrix[startIndex + 1];
|
||||
|
||||
+1
-81
@@ -4,12 +4,8 @@
|
||||
// order (m[column * 4 + row]), so Cesium accepts them wherever it reads
|
||||
// m[0]..m[15] (e.g. the Model.modelMatrix setter clones by index). Products
|
||||
// and getters take a required `result` that may be an input; constructors
|
||||
// take an optional one. Only the subset gev uses is ported. With DEBUG on
|
||||
// (development and tests; see debug.js) invalid arguments throw the same
|
||||
// DeveloperError as Cesium's debug checks.
|
||||
// take an optional one. Only the subset gev uses is ported.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { ZERO } from './cartesian3.js';
|
||||
|
||||
/** A matrix of zeros (Cesium's `new Matrix4()`). */
|
||||
@@ -65,12 +61,6 @@ export function equals(left, right) {
|
||||
|
||||
/** The product of two matrices. */
|
||||
export function multiply(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const left0 = left[0];
|
||||
const left1 = left[1];
|
||||
const left2 = left[2];
|
||||
@@ -162,12 +152,6 @@ export function multiply(left, right, result) {
|
||||
|
||||
/** The product of two affine transformations (bottom rows assumed [0, 0, 0, 1]). */
|
||||
export function multiplyTransformation(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const left0 = left[0];
|
||||
const left1 = left[1];
|
||||
const left2 = left[2];
|
||||
@@ -234,12 +218,6 @@ export function multiplyTransformation(left, right, result) {
|
||||
|
||||
/** Transforms a point (w = 1). */
|
||||
export function multiplyByPoint(matrix, cartesian, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const vX = cartesian.x;
|
||||
const vY = cartesian.y;
|
||||
const vZ = cartesian.z;
|
||||
@@ -256,12 +234,6 @@ export function multiplyByPoint(matrix, cartesian, result) {
|
||||
|
||||
/** Transforms a direction (w = 0), ignoring the translation. */
|
||||
export function multiplyByPointAsVector(matrix, cartesian, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.object('cartesian', cartesian);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const vX = cartesian.x;
|
||||
const vY = cartesian.y;
|
||||
const vZ = cartesian.z;
|
||||
@@ -278,11 +250,6 @@ export function multiplyByPointAsVector(matrix, cartesian, result) {
|
||||
|
||||
/** The translation part as {x, y, z}. */
|
||||
export function getTranslation(matrix, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result.x = matrix[12];
|
||||
result.y = matrix[13];
|
||||
result.z = matrix[14];
|
||||
@@ -291,11 +258,6 @@ export function getTranslation(matrix, result) {
|
||||
|
||||
/** The upper-left 3x3 part. */
|
||||
export function getMatrix3(matrix, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
result[0] = matrix[0];
|
||||
result[1] = matrix[1];
|
||||
result[2] = matrix[2];
|
||||
@@ -310,13 +272,6 @@ export function getMatrix3(matrix, result) {
|
||||
|
||||
/** The column at `index` (0..3) as {x, y, z, w}. */
|
||||
export function getColumn(matrix, index, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.number.greaterThanOrEquals('index', index, 0);
|
||||
Check.typeOf.number.lessThanOrEquals('index', index, 3);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
const startIndex = index * 4;
|
||||
const x = matrix[startIndex];
|
||||
const y = matrix[startIndex + 1];
|
||||
@@ -332,11 +287,6 @@ export function getColumn(matrix, index, result) {
|
||||
|
||||
/** The inverse of a rotation-plus-translation matrix (no scale or shear). */
|
||||
export function inverseTransformation(matrix, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
Check.typeOf.object('result', result);
|
||||
}
|
||||
|
||||
// This function is an optimized version of the below 4 lines.
|
||||
// const rT = Matrix3.transpose(Matrix4.getMatrix3(matrix));
|
||||
// const rTN = Matrix3.negate(rT);
|
||||
@@ -382,10 +332,6 @@ export function inverseTransformation(matrix, result) {
|
||||
|
||||
/** A transformation from a 3x3 rotation and a translation (zero when absent). */
|
||||
export function fromRotationTranslation(rotation, translation, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('rotation', rotation);
|
||||
}
|
||||
|
||||
translation = translation ?? ZERO;
|
||||
if (result == null) result = create();
|
||||
result[0] = rotation[0];
|
||||
@@ -406,29 +352,3 @@ export function fromRotationTranslation(rotation, translation, result) {
|
||||
result[15] = 1.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A uniform scale. */
|
||||
export function fromUniformScale(scale, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.number('scale', scale);
|
||||
}
|
||||
|
||||
if (result == null) result = create();
|
||||
result[0] = scale;
|
||||
result[1] = 0.0;
|
||||
result[2] = 0.0;
|
||||
result[3] = 0.0;
|
||||
result[4] = 0.0;
|
||||
result[5] = scale;
|
||||
result[6] = 0.0;
|
||||
result[7] = 0.0;
|
||||
result[8] = 0.0;
|
||||
result[9] = 0.0;
|
||||
result[10] = scale;
|
||||
result[11] = 0.0;
|
||||
result[12] = 0.0;
|
||||
result[13] = 0.0;
|
||||
result[14] = 0.0;
|
||||
result[15] = 1.0;
|
||||
return result;
|
||||
}
|
||||
+1
-14
@@ -1,16 +1,12 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — Quaternion.js
|
||||
//
|
||||
// Rotations as plain-data {x, y, z, w} objects. Only the subset gev uses is ported.
|
||||
// With DEBUG on (development and tests; see debug.js) invalid arguments throw
|
||||
// the same DeveloperError as Cesium's debug checks.
|
||||
//
|
||||
// Geo values are not Object.prototype objects, so src/app/stateChannel.js
|
||||
// copyState and src/director/documentFields.js jsonTree reject them: copy
|
||||
// the fields into a literal before putting one into product state or a
|
||||
// scene document.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { normalize, create as createCartesian3 } from './cartesian3.js';
|
||||
|
||||
// Values are instances of a module-private constructor rather than object
|
||||
@@ -32,13 +28,8 @@ export function create(x, y, z, w) {
|
||||
|
||||
let fromAxisAngleScratch = createCartesian3();
|
||||
|
||||
/** The rotation by `angle` radians around `axis` (normalised here; with DEBUG on, a zero axis throws). */
|
||||
/** The rotation by `angle` radians around `axis` (normalised here; a zero axis gives NaN). */
|
||||
export function fromAxisAngle(axis, angle, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('axis', axis);
|
||||
Check.typeOf.number('angle', angle);
|
||||
}
|
||||
|
||||
const halfAngle = angle / 2.0;
|
||||
const s = Math.sin(halfAngle);
|
||||
fromAxisAngleScratch = normalize(axis, fromAxisAngleScratch);
|
||||
@@ -63,10 +54,6 @@ const elementIndex = (column, row) => column * 3 + row;
|
||||
|
||||
/** The rotation of a 3x3 rotation matrix. */
|
||||
export function fromRotationMatrix(matrix, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('matrix', matrix);
|
||||
}
|
||||
|
||||
let root;
|
||||
let x;
|
||||
let y;
|
||||
|
||||
@@ -1,36 +1,15 @@
|
||||
// Release parity: with DEBUG off, src/geo returns what release Cesium returns.
|
||||
// Release parity: src/geo returns what release Cesium returns.
|
||||
//
|
||||
// Production builds fold geo's DEBUG to false (see debug.js) and ship Cesium's
|
||||
// Build/Cesium, whose debug checks are stripped. So for a degenerate input that
|
||||
// throws in development (a zero vector to normalize, a NaN origin, a divisor
|
||||
// of 0, an index out of range) both must now return the same values, NaN for
|
||||
// NaN and -0 for -0, or fail with the same error. This file loads a copy of
|
||||
// src/geo whose debug.js says `DEBUG = false`, and the stripped Cesium build.
|
||||
import { test, after } from 'node:test';
|
||||
// Production builds ship Cesium's Build/Cesium, whose debug checks are
|
||||
// stripped, and src/geo has no debug checks either. So for a degenerate input
|
||||
// (a zero vector to normalize, a NaN origin, a divisor of 0, an index out of
|
||||
// range) both must return the same values, NaN for NaN and -0 for -0, or fail
|
||||
// with the same error.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRequire } from 'node:module';
|
||||
import {
|
||||
cpSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import * as geo from './index.js';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const copy = mkdtempSync(path.join(tmpdir(), 'geo-release-'));
|
||||
for (const name of readdirSync(here)) {
|
||||
if (name.endsWith('.js'))
|
||||
cpSync(path.join(here, name), path.join(copy, name));
|
||||
}
|
||||
writeFileSync(path.join(copy, 'debug.js'), 'export const DEBUG = false;\n');
|
||||
after(() => rmSync(copy, { recursive: true, force: true }));
|
||||
|
||||
const geo = await import(pathToFileURL(path.join(copy, 'index.js')).href);
|
||||
const { DEBUG } = await import(pathToFileURL(path.join(copy, 'debug.js')).href);
|
||||
const Cesium = createRequire(import.meta.url)('cesium/Build/Cesium/index.cjs');
|
||||
|
||||
const W = Cesium.Ellipsoid.WGS84;
|
||||
@@ -185,18 +164,6 @@ const CASES = [
|
||||
C3.fromDegreesArray,
|
||||
[list(1, 2, 3)],
|
||||
],
|
||||
[
|
||||
'cartesian3.fromDegreesArrayHeights(4)',
|
||||
geo.cartesian3.fromDegreesArrayHeights,
|
||||
C3.fromDegreesArrayHeights,
|
||||
[list(1, 2, 3, 4)],
|
||||
],
|
||||
[
|
||||
'cartesian3.fromDegreesArrayHeights(str)',
|
||||
geo.cartesian3.fromDegreesArrayHeights,
|
||||
C3.fromDegreesArrayHeights,
|
||||
[list(1, 2, 'h')],
|
||||
],
|
||||
|
||||
[
|
||||
'cartographic.fromRadians(NaN)',
|
||||
@@ -314,12 +281,6 @@ const CASES = [
|
||||
Cesium.Matrix4.multiplyByPoint,
|
||||
[nanMat4, NAN, R],
|
||||
],
|
||||
[
|
||||
'matrix4.fromUniformScale(str)',
|
||||
geo.matrix4.fromUniformScale,
|
||||
Cesium.Matrix4.fromUniformScale,
|
||||
[value('2'), mat4],
|
||||
],
|
||||
|
||||
[
|
||||
'quaternion.fromAxisAngle(zero)',
|
||||
@@ -403,11 +364,6 @@ function outcome(fn, args) {
|
||||
}
|
||||
}
|
||||
|
||||
test('the copy under test has its debug checks off', () => {
|
||||
assert.equal(DEBUG, false);
|
||||
assert.ok(!('normalized result is not a number' in Cesium));
|
||||
});
|
||||
|
||||
for (const [name, geoFn, cesiumFn, builders] of CASES) {
|
||||
test(`release ${name} matches Build/Cesium`, () => {
|
||||
const g = outcome(
|
||||
@@ -432,7 +388,7 @@ for (const [name, geoFn, cesiumFn, builders] of CASES) {
|
||||
});
|
||||
}
|
||||
|
||||
test('release geo returns NaN where development geo throws', () => {
|
||||
test('geo returns NaN for degenerate inputs, as release Cesium does', () => {
|
||||
const r = geo.cartesian3.normalize(
|
||||
geo.cartesian3.create(),
|
||||
geo.cartesian3.create(),
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
// Internal to src/geo (not re-exported by index.js): used by ellipsoid.js and,
|
||||
// as in Cesium, directly by cartographic.fromCartesian.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
import { EPSILON12 } from './math.js';
|
||||
import {
|
||||
clone,
|
||||
@@ -28,21 +26,6 @@ export function scaleToGeodeticSurface(
|
||||
centerToleranceSquared,
|
||||
result,
|
||||
) {
|
||||
if (DEBUG) {
|
||||
if (cartesian == null) {
|
||||
throw new DeveloperError('cartesian is required.');
|
||||
}
|
||||
if (oneOverRadii == null) {
|
||||
throw new DeveloperError('oneOverRadii is required.');
|
||||
}
|
||||
if (oneOverRadiiSquared == null) {
|
||||
throw new DeveloperError('oneOverRadiiSquared is required.');
|
||||
}
|
||||
if (centerToleranceSquared == null) {
|
||||
throw new DeveloperError('centerToleranceSquared is required.');
|
||||
}
|
||||
}
|
||||
|
||||
const positionX = cartesian.x;
|
||||
const positionY = cartesian.y;
|
||||
const positionZ = cartesian.z;
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
// Local east-north-up frames and heading-pitch-roll orientations as 4x4
|
||||
// column-major matrices (see matrix4.js). Outputs match Cesium bit for bit,
|
||||
// including the degenerate frames at the Earth's centre and at the poles.
|
||||
// With DEBUG on (development and tests; see debug.js) invalid arguments throw
|
||||
// the same DeveloperError as Cesium's debug checks.
|
||||
|
||||
import { DEBUG } from './debug.js';
|
||||
import { Check } from './check.js';
|
||||
import { DeveloperError } from './developerError.js';
|
||||
import {
|
||||
EPSILON14,
|
||||
TWO_PI,
|
||||
@@ -52,15 +47,6 @@ function unpack(array, result) {
|
||||
* `Transforms.localFrameToFixedFrameGenerator('east', 'north')`).
|
||||
*/
|
||||
export function eastNorthUpToFixedFrame(origin, ellipsoid, result) {
|
||||
if (DEBUG) {
|
||||
if (origin == null) {
|
||||
throw new DeveloperError('origin is required.');
|
||||
}
|
||||
if (isNaN(origin.x) || isNaN(origin.y) || isNaN(origin.z)) {
|
||||
throw new DeveloperError('origin has a NaN component');
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null) result = createMatrix4();
|
||||
|
||||
const east = scratchEast;
|
||||
@@ -228,12 +214,6 @@ export function headingPitchRollToFixedFrame(
|
||||
fixedFrameTransform,
|
||||
result,
|
||||
) {
|
||||
// The private helpers below skip Cesium's checks on their internal
|
||||
// arguments (scratch objects and constants), which can never fail.
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('HeadingPitchRoll', headingPitchRoll);
|
||||
}
|
||||
|
||||
fixedFrameTransform = fixedFrameTransform ?? eastNorthUpToFixedFrame;
|
||||
const hprQuaternion = quaternionFromHeadingPitchRoll(
|
||||
headingPitchRoll,
|
||||
|
||||
@@ -4,63 +4,6 @@ import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as transforms from './transforms.js';
|
||||
|
||||
const close = (a, b, tol, label) =>
|
||||
assert.ok(Math.abs(a - b) <= tol, `${label}: ${a} vs ${b}`);
|
||||
const ORIGINS = [
|
||||
[-73.9857, 40.7484, 381],
|
||||
[0, 0, 0],
|
||||
[179.9, -45, 10],
|
||||
[10, 89.9999999, 0],
|
||||
[10, -90, 0],
|
||||
[0, 90, 0],
|
||||
];
|
||||
|
||||
test('eastNorthUpToFixedFrame equals Cesium, including the poles', () => {
|
||||
for (const [lon, lat, h] of ORIGINS) {
|
||||
const origin = Cesium.Cartesian3.fromDegrees(lon, lat, h);
|
||||
const ours = transforms.eastNorthUpToFixedFrame({
|
||||
x: origin.x,
|
||||
y: origin.y,
|
||||
z: origin.z,
|
||||
});
|
||||
const theirs = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
|
||||
for (let i = 0; i < 16; i++)
|
||||
close(
|
||||
ours[i],
|
||||
theirs[i],
|
||||
1e-9 * Math.max(1, Math.abs(theirs[i])),
|
||||
`${lon},${lat} [${i}]`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('headingPitchRollToFixedFrame equals Cesium', () => {
|
||||
for (const [lon, lat, h] of ORIGINS.slice(0, 3)) {
|
||||
const origin = Cesium.Cartesian3.fromDegrees(lon, lat, h);
|
||||
for (const [hd, p, r] of [
|
||||
[0, 0, 0],
|
||||
[0.7, -0.2, 0.1],
|
||||
[-2.5, 1.2, -0.4],
|
||||
]) {
|
||||
const ours = transforms.headingPitchRollToFixedFrame(
|
||||
{ x: origin.x, y: origin.y, z: origin.z },
|
||||
{ heading: hd, pitch: p, roll: r },
|
||||
);
|
||||
const theirs = Cesium.Transforms.headingPitchRollToFixedFrame(
|
||||
origin,
|
||||
new Cesium.HeadingPitchRoll(hd, p, r),
|
||||
);
|
||||
for (let i = 0; i < 16; i++)
|
||||
close(
|
||||
ours[i],
|
||||
theirs[i],
|
||||
1e-9 * Math.max(1, Math.abs(theirs[i])),
|
||||
`hpr [${i}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('headingPitchRollToFixedFrame keeps Cesium argument order (result is the fifth argument)', () => {
|
||||
const origin = Cesium.Cartesian3.fromDegrees(-73.9857, 40.7484, 381);
|
||||
const hpr = new Cesium.HeadingPitchRoll(0.7, -0.2, 0.1);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Camera math for the Navara engine, on src/geo only: Cesium's heading/pitch/
|
||||
// roll convention, the perspective projection and picks of a camera given as
|
||||
// { position, direction, up, fovy, width, height } (Earth-fixed metres, unit
|
||||
// vectors, vertical fov in radians, CSS pixels), Cesium's view rectangle and
|
||||
// horizon test, and the Navara setCamera pose (degrees). Ported from the S4
|
||||
// spike (src/bench/cameraConvention.js) and the contract's fake engine; the
|
||||
// projection matches what Navara draws because the engine keeps Navara's
|
||||
// camera on the same pose and fov.
|
||||
// Camera math for the Navara and fake engines, on src/geo only: Cesium's
|
||||
// heading/pitch/roll convention, the perspective projection and picks of a
|
||||
// camera given as { position, direction, up, fovy, width, height }
|
||||
// (Earth-fixed metres, unit vectors, vertical fov in radians, CSS pixels),
|
||||
// Cesium's view rectangle and horizon test, and the Navara setCamera pose
|
||||
// (degrees). The projection matches what Navara draws because the engine
|
||||
// keeps Navara's camera on the same pose and fov.
|
||||
|
||||
import * as geo from '../../geo/index.js';
|
||||
import * as geo from '../geo/index.js';
|
||||
|
||||
const { cartesian2, cartesian3, cartographic, math, matrix4 } = geo;
|
||||
const WGS84 = geo.ellipsoid.WGS84;
|
||||
@@ -109,7 +108,7 @@ export function headingPitchRoll(
|
||||
/**
|
||||
* The Navara setCamera/flyTo pose for a camera pose: the eye's longitude,
|
||||
* latitude and ellipsoid height, and the same heading/pitch/roll in degrees
|
||||
* (Navara 0.2.1 uses Cesium's convention, S4), heading in [0, 360) and roll
|
||||
* (Navara 0.2.1 uses Cesium's convention), heading in [0, 360) and roll
|
||||
* in (−180, 180].
|
||||
*/
|
||||
export function navaraPose(position, direction, up) {
|
||||
@@ -206,14 +205,16 @@ export function pickRay(camera, screen) {
|
||||
return { origin: copy3(camera.position), direction: unit(direction) };
|
||||
}
|
||||
|
||||
/** Nearest intersection of a ray with the WGS84 ellipsoid, if any. */
|
||||
export function intersectEllipsoid(origin, direction) {
|
||||
const o = [origin.x / RADII.x, origin.y / RADII.y, origin.z / RADII.z];
|
||||
const d = [
|
||||
direction.x / RADII.x,
|
||||
direction.y / RADII.y,
|
||||
direction.z / RADII.z,
|
||||
];
|
||||
/**
|
||||
* Nearest intersection of a ray with the WGS84 ellipsoid raised by `height`
|
||||
* metres (each radius grown by it), if any.
|
||||
*/
|
||||
export function intersectEllipsoid(origin, direction, height = 0) {
|
||||
const rx = RADII.x + height;
|
||||
const ry = RADII.y + height;
|
||||
const rz = RADII.z + height;
|
||||
const o = [origin.x / rx, origin.y / ry, origin.z / rz];
|
||||
const d = [direction.x / rx, direction.y / ry, direction.z / rz];
|
||||
const a = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
|
||||
const b = 2 * (o[0] * d[0] + o[1] * d[1] + o[2] * d[2]);
|
||||
const c = o[0] * o[0] + o[1] * o[1] + o[2] * o[2] - 1;
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import * as geo from '../geo/index.js';
|
||||
import {
|
||||
cesiumFovy,
|
||||
headingPitchRoll,
|
||||
@@ -194,8 +194,8 @@ test('horizonVisible: the nadir is visible and the antipode hidden', () => {
|
||||
assert.equal(horizonVisible(eye, cartesian3.fromDegrees(-70, 10, 2e7)), true);
|
||||
});
|
||||
|
||||
test("viewRectangle matches Cesium's computeViewRectangle (S4 reference poses)", () => {
|
||||
// S4 measured these on Cesium at 1280×720 with Cesium's fov.
|
||||
test("viewRectangle matches Cesium's computeViewRectangle (reference poses)", () => {
|
||||
// Measured on Cesium at 1280×720 with Cesium's fov.
|
||||
const deg = (rectangle) =>
|
||||
['west', 'south', 'east', 'north'].map((side) =>
|
||||
math.toDegrees(rectangle[side]),
|
||||
+30
-70
@@ -3,24 +3,17 @@
|
||||
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { createEngineEvent } from '../contract.js';
|
||||
import { wrapAngle } from '../cameraMath.js';
|
||||
import { bridgeEvent } from './events.js';
|
||||
import { toCesium2, toCesium3, toGeo3 } from './convert.js';
|
||||
import { createCesiumFollow } from './follow.js';
|
||||
|
||||
const { math } = geo;
|
||||
const easings = () => ({
|
||||
cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
sinusoidalInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
||||
linear: Cesium.EasingFunction.LINEAR_NONE,
|
||||
});
|
||||
|
||||
// Cesium reports heading and roll in [0, 2π]; the contract wants [0, 2π).
|
||||
function wrapAngle(angle) {
|
||||
const value = math.zeroToTwoPi(angle);
|
||||
return value < 1e-12 || math.TWO_PI - value < 1e-12 ? 0 : value;
|
||||
}
|
||||
|
||||
function cesiumOrientation(orientation) {
|
||||
if (!orientation) return undefined;
|
||||
if (orientation.direction)
|
||||
@@ -65,70 +58,38 @@ const COMMANDS = Object.freeze([
|
||||
/**
|
||||
* camera.commanded: raised before any COMMANDS call on the Cesium camera and
|
||||
* when the viewer's tracked entity changes. The hooks exist only while the
|
||||
* event has listeners and then restore each method exactly (unless something
|
||||
* newer replaced it), so an idle engine leaves the camera untouched.
|
||||
* event has listeners (bridgeEvent) and then restore each method exactly
|
||||
* (unless something newer replaced it), so an idle engine leaves the camera
|
||||
* untouched.
|
||||
*/
|
||||
function commandedEvent(viewer, cesium) {
|
||||
const event = createEngineEvent();
|
||||
let unhook = null;
|
||||
const hook = () => {
|
||||
const removers = [];
|
||||
for (const name of COMMANDS) {
|
||||
const original = cesium[name];
|
||||
if (typeof original !== 'function') continue;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(cesium, name);
|
||||
const wrapped = function (...args) {
|
||||
event.raiseEvent();
|
||||
return original.apply(this, args);
|
||||
return bridgeEvent({
|
||||
addEventListener(raise) {
|
||||
const removers = [];
|
||||
for (const name of COMMANDS) {
|
||||
const original = cesium[name];
|
||||
if (typeof original !== 'function') continue;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(cesium, name);
|
||||
const wrapped = function (...args) {
|
||||
raise();
|
||||
return original.apply(this, args);
|
||||
};
|
||||
cesium[name] = wrapped;
|
||||
removers.push(() => {
|
||||
if (cesium[name] !== wrapped) return;
|
||||
if (descriptor) Object.defineProperty(cesium, name, descriptor);
|
||||
else delete cesium[name];
|
||||
});
|
||||
}
|
||||
const removeTracking = viewer.trackedEntityChanged?.addEventListener(() =>
|
||||
raise(),
|
||||
);
|
||||
if (removeTracking) removers.push(removeTracking);
|
||||
return () => {
|
||||
for (const remove of removers) remove();
|
||||
};
|
||||
cesium[name] = wrapped;
|
||||
removers.push(() => {
|
||||
if (cesium[name] !== wrapped) return;
|
||||
if (descriptor) Object.defineProperty(cesium, name, descriptor);
|
||||
else delete cesium[name];
|
||||
});
|
||||
}
|
||||
const removeTracking = viewer.trackedEntityChanged?.addEventListener(() =>
|
||||
event.raiseEvent(),
|
||||
);
|
||||
if (removeTracking) removers.push(removeTracking);
|
||||
return () => {
|
||||
for (const remove of removers) remove();
|
||||
};
|
||||
};
|
||||
let detached = false;
|
||||
const sync = () => {
|
||||
const wanted = !detached && event.numberOfListeners > 0;
|
||||
if (wanted === (unhook !== null)) return;
|
||||
if (wanted) unhook = hook();
|
||||
else {
|
||||
const remove = unhook;
|
||||
unhook = null;
|
||||
remove();
|
||||
}
|
||||
};
|
||||
const add = event.addEventListener;
|
||||
const remove = event.removeEventListener;
|
||||
event.addEventListener = (listener) => {
|
||||
const removeListener = add(listener);
|
||||
sync();
|
||||
return () => {
|
||||
removeListener();
|
||||
sync();
|
||||
};
|
||||
};
|
||||
event.removeEventListener = (listener) => {
|
||||
const removed = remove(listener);
|
||||
sync();
|
||||
return removed;
|
||||
};
|
||||
return {
|
||||
event,
|
||||
detach() {
|
||||
detached = true;
|
||||
sync();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,8 +97,7 @@ function commandedEvent(viewer, cesium) {
|
||||
* @returns {{ camera: import('../contract.js').GlobeCamera, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumCamera(viewer) {
|
||||
// Viewer.camera is scene.camera; the public alias is what shell callers read.
|
||||
const cesium = viewer.camera ?? viewer.scene.camera;
|
||||
const cesium = viewer.camera;
|
||||
const bridges = {
|
||||
changed: bridgeEvent(cesium.changed),
|
||||
moveStart: bridgeEvent(cesium.moveStart),
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
// preUpdate instead, attached on first use so creating the engine adds no
|
||||
// scene listener.
|
||||
|
||||
import { createEngineEvent } from '../contract.js';
|
||||
import { appClock as pageClock } from '../../time/clock.js';
|
||||
|
||||
/**
|
||||
@@ -28,21 +27,16 @@ export function createCesiumClock(viewer, { appClock = pageClock } = {}) {
|
||||
// A viewer-shaped object without a clock (tests, a bare scene): the
|
||||
// scene's preUpdate opens the frame instead.
|
||||
const tickSource = viewer.clock?.onTick ?? viewer.scene.preUpdate;
|
||||
const ticked = createEngineEvent();
|
||||
let removeTick = null;
|
||||
let destroyed = false;
|
||||
const onTick = () => ticked.raiseValue(appClock.beginFrame());
|
||||
const onTick = () => {
|
||||
appClock.beginFrame();
|
||||
};
|
||||
const attach = () => {
|
||||
if (!removeTick && !destroyed)
|
||||
removeTick = tickSource.addEventListener(onTick);
|
||||
};
|
||||
if (viewer.clock?.onTick) attach();
|
||||
const add = ticked.addEventListener;
|
||||
ticked.addEventListener = (listener) => {
|
||||
const remove = add(listener);
|
||||
attach();
|
||||
return remove;
|
||||
};
|
||||
return {
|
||||
clock: {
|
||||
now: () => appClock.now(),
|
||||
@@ -50,7 +44,6 @@ export function createCesiumClock(viewer, { appClock = pageClock } = {}) {
|
||||
attach();
|
||||
return appClock.frameTime();
|
||||
},
|
||||
ticked,
|
||||
},
|
||||
destroy() {
|
||||
destroyed = true;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { isEngineEvent } from '../contract.js';
|
||||
import { createAppClock } from '../../time/clock.js';
|
||||
import { createCesiumClock } from './clock.js';
|
||||
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
||||
@@ -32,7 +31,6 @@ test('the clock reads the app clock and latches on the viewer clock from creatio
|
||||
const viewer = createFakeViewer();
|
||||
const wall = manualAppClock();
|
||||
const { clock } = createCesiumClock(viewer, { appClock: wall.appClock });
|
||||
assert.ok(isEngineEvent(clock.ticked));
|
||||
// Attached at creation, so it runs before onTick listeners added later
|
||||
// (cameraVerbs installs its own after the engine exists).
|
||||
assert.equal(viewer.clock.onTick.numberOfListeners, 1);
|
||||
@@ -51,7 +49,9 @@ test('frameTime is latched on the viewer clock tick, before preUpdate', () => {
|
||||
const wall = manualAppClock();
|
||||
const { clock } = createCesiumClock(viewer, { appClock: wall.appClock });
|
||||
const seen = [];
|
||||
clock.ticked.addEventListener((time) => seen.push(['ticked', time]));
|
||||
viewer.clock.onTick.addEventListener(() =>
|
||||
seen.push(['tick', clock.frameTime()]),
|
||||
);
|
||||
// A preUpdate listener the app added before the engine existed still
|
||||
// reads this frame's time: the latch is on the tick, not on preUpdate.
|
||||
viewer.scene.preUpdate.addEventListener(() =>
|
||||
@@ -66,10 +66,10 @@ test('frameTime is latched on the viewer clock tick, before preUpdate', () => {
|
||||
wall.advance(16);
|
||||
cesiumFrame(viewer, { draw: false });
|
||||
assert.deepEqual(seen, [
|
||||
['ticked', t0],
|
||||
['tick', t0],
|
||||
['preUpdate', t0],
|
||||
['preRender', t0],
|
||||
['ticked', t0 + 19],
|
||||
['tick', t0 + 19],
|
||||
['preUpdate', t0 + 19],
|
||||
]);
|
||||
});
|
||||
@@ -99,11 +99,14 @@ test('a viewer without a clock latches on preUpdate', () => {
|
||||
const { clock, destroy } = createCesiumClock(viewer, {
|
||||
appClock: wall.appClock,
|
||||
});
|
||||
const seen = [];
|
||||
clock.ticked.addEventListener((time) => seen.push(time));
|
||||
// Attached on first use: creating the clock adds no scene listener.
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 0);
|
||||
const before = clock.frameTime();
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 1);
|
||||
wall.advance(7);
|
||||
viewer.scene.preUpdate.raiseEvent(viewer.scene);
|
||||
assert.deepEqual(seen, [wall.appClock.now()]);
|
||||
wall.advance(2);
|
||||
assert.equal(clock.frameTime(), before + 7);
|
||||
destroy();
|
||||
assert.equal(viewer.scene.preUpdate.numberOfListeners, 0);
|
||||
});
|
||||
@@ -116,7 +119,7 @@ test('the viewer clock itself is left alone', () => {
|
||||
const { clock } = createCesiumClock(viewer, {
|
||||
appClock: manualAppClock().appClock,
|
||||
});
|
||||
clock.ticked.addEventListener(() => {});
|
||||
clock.frameTime();
|
||||
cesiumFrame(viewer);
|
||||
assert.equal(viewer.clock.shouldAnimate, false);
|
||||
assert.equal(viewer.clock.multiplier, 1);
|
||||
|
||||
@@ -18,6 +18,6 @@ export const toCesium3 = (value) =>
|
||||
|
||||
/**
|
||||
* A new Cesium.Cartesian2 for a window position. Always a new instance: Cesium
|
||||
* caches pickPosition by windowPosition.toString() (phase 1a rule 9).
|
||||
* caches pickPosition by windowPosition.toString().
|
||||
*/
|
||||
export const toCesium2 = (value) => new Cesium.Cartesian2(value.x, value.y);
|
||||
@@ -10,9 +10,7 @@ import { createCesiumClock } from './clock.js';
|
||||
import { createCesiumCredits } from './credits.js';
|
||||
import { createCesiumFrames } from './frames.js';
|
||||
import { createCesiumInput } from './input.js';
|
||||
import { createCesiumLines } from './lines.js';
|
||||
import { createCesiumMaps } from './maps.js';
|
||||
import { createCesiumMovers } from './movers.js';
|
||||
import { createCesiumPicker } from './picker.js';
|
||||
import { createCesiumProjector } from './projector.js';
|
||||
import { createCesiumRender } from './render.js';
|
||||
@@ -37,44 +35,18 @@ import { createCesiumSurface } from './surface.js';
|
||||
*/
|
||||
export function createCesiumEngine(viewer, options = {}) {
|
||||
if (!viewer?.scene) throw new TypeError('createCesiumEngine needs a viewer');
|
||||
// Viewer.canvas is scene.canvas; the engine reads the public alias.
|
||||
const canvas = viewer.canvas ?? viewer.scene.canvas;
|
||||
const { canvas } = viewer;
|
||||
const render = createCesiumRender(viewer, options);
|
||||
const credits = createCesiumCredits(viewer);
|
||||
// Picks name the mover set or line they hit (movers and lines exist
|
||||
// below; the picker asks them when it picks).
|
||||
let lines = null;
|
||||
let movers = null;
|
||||
let projectorPart = null;
|
||||
const picker = createCesiumPicker(viewer, {
|
||||
identify: (picked) =>
|
||||
movers?.identify(picked) ?? lines?.identify(picked) ?? null,
|
||||
// A mover instance behind the globe is not picked (billboards drawn
|
||||
// without a depth test reach Cesium's GPU pick through it).
|
||||
visible: ({ owner, instance }) => {
|
||||
const position = movers?.instancePosition(owner, instance);
|
||||
return position ? projectorPart.projector.horizonVisible(position) : true;
|
||||
},
|
||||
});
|
||||
const picker = createCesiumPicker(viewer);
|
||||
const parts = {
|
||||
camera: createCesiumCamera(viewer),
|
||||
picker,
|
||||
projector: (projectorPart = createCesiumProjector(viewer)),
|
||||
projector: createCesiumProjector(viewer),
|
||||
frames: createCesiumFrames(viewer),
|
||||
clock: createCesiumClock(viewer, options),
|
||||
lines: (lines = createCesiumLines(viewer, {
|
||||
requestRender: (reason) => render.render.request(reason),
|
||||
...(options.lineFactories ? { factories: options.lineFactories } : {}),
|
||||
})),
|
||||
movers: (movers = createCesiumMovers(viewer, {
|
||||
requestRender: (reason) => render.render.request(reason),
|
||||
...(options.moverFactories ? { factories: options.moverFactories } : {}),
|
||||
})),
|
||||
render,
|
||||
// Mover sets in a surface `exclude` are hidden for the sample.
|
||||
surface: createCesiumSurface(viewer, {
|
||||
engineExclude: (entry) => movers?.primitiveOf(entry) ?? null,
|
||||
}),
|
||||
surface: createCesiumSurface(viewer),
|
||||
input: createCesiumInput(viewer, { ...options, picker: picker.picker }),
|
||||
credits,
|
||||
maps: createCesiumMaps(viewer, {
|
||||
|
||||
@@ -146,57 +146,11 @@ test('the engine clock is the app clock given, latched on the viewer clock tick'
|
||||
const viewer = createFakeViewer();
|
||||
const engine = createCesiumEngine(viewer, { appClock });
|
||||
assert.equal(engine.clock.now(), 1_000_000);
|
||||
const seen = [];
|
||||
engine.clock.ticked.addEventListener((time) => seen.push(time));
|
||||
assert.equal(viewer.clock.onTick.numberOfListeners, 1);
|
||||
wall += 16;
|
||||
viewer.clock.onTick.raiseEvent(viewer.clock);
|
||||
assert.deepEqual(seen, [1_000_016]);
|
||||
wall += 5;
|
||||
assert.equal(engine.clock.frameTime(), 1_000_016);
|
||||
engine.destroy();
|
||||
assert.equal(viewer.clock.onTick.numberOfListeners, 0);
|
||||
});
|
||||
|
||||
test('picks leave out mover instances behind the horizon and the follow proxy', async () => {
|
||||
const geo = await import('../../geo/index.js');
|
||||
const viewer = createFakeViewer();
|
||||
const engine = createCesiumEngine(viewer, { requestRender: () => {} });
|
||||
try {
|
||||
const set = engine.movers.createSet({
|
||||
id: 'horizon-points',
|
||||
pickable: true,
|
||||
kind: 'point',
|
||||
capacity: 2,
|
||||
point: { sizePx: 8 },
|
||||
});
|
||||
// The fake camera is at x = 7e6 (over 0°, 0°): 0°E is in front, 180° behind.
|
||||
set.set(0, { position: geo.cartesian3.fromDegrees(0, 1, 0) });
|
||||
set.set(1, { position: geo.cartesian3.fromDegrees(180, 1, 0) });
|
||||
set.setCount(2);
|
||||
set.commit();
|
||||
const [front, back] = viewer.primitiveList.flatMap((root) =>
|
||||
collectPoints(root),
|
||||
);
|
||||
viewer.scene.drillPick = () => [
|
||||
{ primitive: back, id: 'back' },
|
||||
{ primitive: front, id: 'front' },
|
||||
];
|
||||
assert.deepEqual(
|
||||
engine.picker.drillPick({ x: 1, y: 1 }).map(({ id }) => id),
|
||||
['front'],
|
||||
);
|
||||
} finally {
|
||||
engine.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// The PointPrimitives under a primitive collection tree.
|
||||
function collectPoints(collection) {
|
||||
const out = [];
|
||||
for (let i = 0; i < (collection.length ?? 0); i++) {
|
||||
const item = collection.get(i);
|
||||
if (item instanceof Cesium.PointPrimitive) out.push(item);
|
||||
else if (item?.length !== undefined) out.push(...collectPoints(item));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// The bridge for shell callers that still hold only a Cesium viewer (layers
|
||||
// until phase 1c): the engine registered for that viewer. The app registers
|
||||
// The bridge for shell callers that hold only a Cesium viewer: the engine
|
||||
// registered for that viewer. The app registers
|
||||
// its scene engine (src/app/globeEngine.js) and unregisters it before
|
||||
// destroying it. Nothing is ever built here, so no engine can outlive its
|
||||
// viewer without a destroy path: an unregistered or torn-down viewer has none.
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
// engine.lines on Cesium: each line is one Primitive of a PolylineGeometry
|
||||
// (built synchronously, rebuilt when its positions change) with a
|
||||
// PolylineMaterialAppearance, inside one engine-owned PrimitiveCollection
|
||||
// added to the scene with the first line. depthFailAlpha adds Cesium's
|
||||
// depthFailAppearance (the same material, its alpha scaled) for the part
|
||||
// the scene hides; dashes in pixels are PolylineDash; the tactical dash is
|
||||
// the launches' selected-orbit pattern along the whole line.
|
||||
|
||||
import * as Cesium from 'cesium';
|
||||
import { createLineRegistry } from '../lines.js';
|
||||
|
||||
const TACTICAL_TYPE = 'GevLineTactical';
|
||||
let tacticalRegistered = false;
|
||||
|
||||
// The launches' GevMissionOrbitTactical shader (layers/launches/
|
||||
// orbitRendering.js), under the engine's own type until Task 15a moves
|
||||
// launches onto engine.lines.
|
||||
function registerTactical() {
|
||||
if (tacticalRegistered) return;
|
||||
new Cesium.Material({
|
||||
fabric: {
|
||||
type: TACTICAL_TYPE,
|
||||
uniforms: { color: Cesium.Color.CYAN, groupCount: 4, dashCount: 100 },
|
||||
source: `
|
||||
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||
float groupPosition = fract(materialInput.st.s * groupCount);
|
||||
float markPosition = groupPosition * (dashCount + 1.0);
|
||||
float markIndex = floor(markPosition);
|
||||
float localPosition = fract(markPosition);
|
||||
float centerDistance = abs(localPosition - 0.5);
|
||||
float edge = max(fwidth(localPosition) * 1.35, 0.012);
|
||||
float dashAlong = 1.0 - smoothstep(0.27 - edge, 0.27 + edge, centerDistance);
|
||||
float dashAcross = 1.0 - smoothstep(0.12, 0.24, abs(materialInput.st.t - 0.5));
|
||||
float dash = dashAlong * dashAcross;
|
||||
float dotAlong = (localPosition - 0.5) / 0.32;
|
||||
float dotAcross = (materialInput.st.t - 0.5) / 0.5;
|
||||
float dot = 1.0 - smoothstep(0.78, 1.0, length(vec2(dotAlong, dotAcross)));
|
||||
float isDot = 1.0 - step(0.5, markIndex);
|
||||
float visible = mix(dash, dot, isDot);
|
||||
material.diffuse = color.rgb;
|
||||
material.emission = color.rgb * mix(0.07, 0.65, isDot);
|
||||
material.alpha = color.a * visible * mix(0.58, 1.0, isDot);
|
||||
return material;
|
||||
}`,
|
||||
},
|
||||
});
|
||||
tacticalRegistered = true;
|
||||
}
|
||||
|
||||
/** PolylineDash's 16-bit pattern: the dash first, then the gap. */
|
||||
export function dashPattern(lengthPx, gapPx) {
|
||||
const on = Math.max(
|
||||
1,
|
||||
Math.min(16, Math.round((16 * lengthPx) / (lengthPx + gapPx))),
|
||||
);
|
||||
return (0xffff << (16 - on)) & 0xffff;
|
||||
}
|
||||
|
||||
const toColor = ([r, g, b, a], alphaScale = 1) =>
|
||||
new Cesium.Color(r, g, b, a * alphaScale);
|
||||
|
||||
/** The Cesium material type and uniforms of a line (pure, for tests). */
|
||||
export function materialSpec(options, color, alphaScale = 1) {
|
||||
if (options.arrow)
|
||||
return {
|
||||
type: 'PolylineArrow',
|
||||
uniforms: { color: toColor(color, alphaScale) },
|
||||
};
|
||||
const { dash } = options;
|
||||
if (dash?.kind === 'pixels')
|
||||
return {
|
||||
type: 'PolylineDash',
|
||||
uniforms: {
|
||||
color: toColor(color, alphaScale),
|
||||
gapColor: Cesium.Color.TRANSPARENT,
|
||||
dashLength: dash.lengthPx + dash.gapPx,
|
||||
dashPattern: dashPattern(dash.lengthPx, dash.gapPx),
|
||||
},
|
||||
};
|
||||
if (dash?.kind === 'tactical')
|
||||
return {
|
||||
type: TACTICAL_TYPE,
|
||||
uniforms: {
|
||||
color: toColor(color, alphaScale),
|
||||
groupCount: dash.groups,
|
||||
dashCount: dash.dashes,
|
||||
},
|
||||
};
|
||||
return { type: 'Color', uniforms: { color: toColor(color, alphaScale) } };
|
||||
}
|
||||
|
||||
function cesiumMaterial({ type, uniforms }) {
|
||||
if (type === TACTICAL_TYPE) registerTactical();
|
||||
return Cesium.Material.fromType(type, uniforms);
|
||||
}
|
||||
|
||||
const DEFAULT_FACTORIES = {
|
||||
primitive: (options) => new Cesium.Primitive(options),
|
||||
appearance: (spec) =>
|
||||
new Cesium.PolylineMaterialAppearance({ material: cesiumMaterial(spec) }),
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Cesium.Viewer} viewer
|
||||
* @param {{ requestRender?: (reason: string) => void, factories?: object }} [options]
|
||||
* @returns {{ lines: import('../contract.js').GlobeLines, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumLines(
|
||||
viewer,
|
||||
{ requestRender = () => viewer.scene.requestRender(), factories = {} } = {},
|
||||
) {
|
||||
const make = { ...DEFAULT_FACTORIES, ...factories };
|
||||
const { scene } = viewer;
|
||||
let root = null;
|
||||
const rootCollection = () => {
|
||||
if (!root) {
|
||||
root = new Cesium.PrimitiveCollection({ destroyPrimitives: true });
|
||||
scene.primitives.add(root);
|
||||
}
|
||||
return root;
|
||||
};
|
||||
const registry = createLineRegistry((options) => {
|
||||
rootCollection();
|
||||
let color = options.color;
|
||||
let modelMatrix = options.modelMatrix
|
||||
? Cesium.Matrix4.fromArray(options.modelMatrix)
|
||||
: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY);
|
||||
let shown = true;
|
||||
let primitive = null;
|
||||
let appearance = null;
|
||||
let depthFail = null;
|
||||
const drop = () => {
|
||||
if (primitive) root.remove(primitive);
|
||||
primitive = null;
|
||||
};
|
||||
return {
|
||||
setPositions(flat) {
|
||||
drop();
|
||||
if (flat.length < 6) {
|
||||
requestRender('lines');
|
||||
return;
|
||||
}
|
||||
const positions = [];
|
||||
for (let index = 0; index < flat.length; index += 3)
|
||||
positions.push(
|
||||
new Cesium.Cartesian3(
|
||||
flat[index],
|
||||
flat[index + 1],
|
||||
flat[index + 2],
|
||||
),
|
||||
);
|
||||
appearance = make.appearance(materialSpec(options, color));
|
||||
depthFail =
|
||||
options.depthFailAlpha > 0
|
||||
? make.appearance(
|
||||
materialSpec(options, color, options.depthFailAlpha),
|
||||
)
|
||||
: undefined;
|
||||
primitive = make.primitive({
|
||||
geometryInstances: new Cesium.GeometryInstance({
|
||||
geometry: new Cesium.PolylineGeometry({
|
||||
positions,
|
||||
width: options.widthPx,
|
||||
vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT,
|
||||
arcType:
|
||||
options.arc === 'geodesic'
|
||||
? Cesium.ArcType.GEODESIC
|
||||
: Cesium.ArcType.NONE,
|
||||
}),
|
||||
id: options.pickId,
|
||||
}),
|
||||
appearance,
|
||||
depthFailAppearance: depthFail,
|
||||
modelMatrix: Cesium.Matrix4.clone(modelMatrix),
|
||||
show: shown,
|
||||
asynchronous: false,
|
||||
});
|
||||
primitive.gevLine = options.id;
|
||||
root.add(primitive);
|
||||
requestRender('lines');
|
||||
},
|
||||
setColor(rgba) {
|
||||
color = rgba;
|
||||
if (appearance) appearance.material.uniforms.color = toColor(rgba);
|
||||
if (depthFail)
|
||||
depthFail.material.uniforms.color = toColor(
|
||||
rgba,
|
||||
options.depthFailAlpha,
|
||||
);
|
||||
requestRender('lines');
|
||||
},
|
||||
setModelMatrix(matrix) {
|
||||
modelMatrix = Cesium.Matrix4.fromArray(matrix);
|
||||
if (primitive)
|
||||
primitive.modelMatrix = Cesium.Matrix4.clone(modelMatrix);
|
||||
requestRender('lines');
|
||||
},
|
||||
show(visible) {
|
||||
shown = visible;
|
||||
if (primitive) primitive.show = visible;
|
||||
requestRender('lines');
|
||||
},
|
||||
destroy() {
|
||||
drop();
|
||||
requestRender('lines');
|
||||
},
|
||||
};
|
||||
});
|
||||
return {
|
||||
lines: registry.lines,
|
||||
/** The line a Cesium pick result belongs to, or null. */
|
||||
identify(picked) {
|
||||
const owner = picked?.primitive?.gevLine;
|
||||
return owner !== undefined && registry.live.has(owner) ? { owner } : null;
|
||||
},
|
||||
destroy() {
|
||||
registry.destroyAll();
|
||||
if (root) {
|
||||
scene.primitives.remove(root);
|
||||
root = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createCesiumLines, dashPattern } from './lines.js';
|
||||
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
||||
|
||||
function fakePrimitives() {
|
||||
const made = [];
|
||||
return {
|
||||
made,
|
||||
factories: {
|
||||
primitive: (options) => {
|
||||
const primitive = {
|
||||
...options,
|
||||
isDestroyed: () => false,
|
||||
destroy() {
|
||||
primitive.destroyed = true;
|
||||
},
|
||||
};
|
||||
made.push(primitive);
|
||||
return primitive;
|
||||
},
|
||||
appearance: (spec) => ({
|
||||
material: { type: spec.type, uniforms: { ...spec.uniforms } },
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const points = [
|
||||
{ x: 6_378_137, y: 0, z: 0 },
|
||||
{ x: 6_378_137, y: 1000, z: 0 },
|
||||
{ x: 6_378_137, y: 2000, z: 1000 },
|
||||
];
|
||||
|
||||
test('a dash in pixels becomes PolylineDash with a 16-bit pattern', () => {
|
||||
assert.equal(dashPattern(16, 16), 0xff00);
|
||||
assert.equal(dashPattern(12, 4), 0xfff0);
|
||||
assert.equal(dashPattern(1, 0), 0xffff);
|
||||
});
|
||||
|
||||
test('positions build one polyline primitive; a change rebuilds it', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories, made } = fakePrimitives();
|
||||
const renders = [];
|
||||
const { lines, destroy } = createCesiumLines(viewer, {
|
||||
factories,
|
||||
requestRender: (reason) => renders.push(reason),
|
||||
});
|
||||
assert.equal(viewer.scene.primitives.length, 0, 'nothing at creation');
|
||||
const line = lines.createLine({
|
||||
id: 'trail',
|
||||
widthPx: 3,
|
||||
color: [1, 0, 0, 1],
|
||||
depthFailAlpha: 0.4,
|
||||
arc: 'geodesic',
|
||||
pickId: 'trail-1',
|
||||
});
|
||||
assert.equal(viewer.scene.primitives.length, 1);
|
||||
assert.equal(made.length, 0, 'no primitive without positions');
|
||||
line.setPositions(points);
|
||||
assert.equal(made.length, 1);
|
||||
const [first] = made;
|
||||
const geometry = first.geometryInstances.geometry;
|
||||
assert.equal(geometry._width, 3);
|
||||
assert.equal(geometry._arcType, Cesium.ArcType.GEODESIC);
|
||||
assert.equal(geometry._positions.length, 3);
|
||||
assert.equal(first.geometryInstances.id, 'trail-1');
|
||||
assert.equal(first.asynchronous, false);
|
||||
assert.ok(first.depthFailAppearance, 'the dimmer copy behind the scene');
|
||||
assert.ok(
|
||||
Cesium.Color.equals(
|
||||
first.depthFailAppearance.material.uniforms.color,
|
||||
new Cesium.Color(1, 0, 0, 0.4),
|
||||
),
|
||||
);
|
||||
line.setPositions(points.slice(0, 2));
|
||||
assert.equal(made.length, 2);
|
||||
assert.equal(first.destroyed, true, 'the old primitive goes');
|
||||
line.setColor([0, 1, 0, 1]);
|
||||
assert.ok(
|
||||
Cesium.Color.equals(
|
||||
made[1].appearance.material.uniforms.color,
|
||||
new Cesium.Color(0, 1, 0, 1),
|
||||
),
|
||||
);
|
||||
const turn = Cesium.Matrix4.fromRotation(Cesium.Matrix3.fromRotationZ(0.5));
|
||||
line.setModelMatrix(Cesium.Matrix4.toArray(turn));
|
||||
assert.ok(Cesium.Matrix4.equals(made[1].modelMatrix, turn));
|
||||
line.show(false);
|
||||
assert.equal(made[1].show, false);
|
||||
line.setPositions(points);
|
||||
assert.equal(made[2].show, false, 'a rebuild keeps show and matrix');
|
||||
assert.ok(Cesium.Matrix4.equals(made[2].modelMatrix, turn));
|
||||
assert.ok(renders.length >= 4);
|
||||
line.destroy();
|
||||
assert.equal(made[2].destroyed, true);
|
||||
destroy();
|
||||
assert.equal(viewer.scene.primitives.length, 0);
|
||||
});
|
||||
|
||||
test('dash kinds pick their materials', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories, made } = fakePrimitives();
|
||||
const { lines } = createCesiumLines(viewer, { factories });
|
||||
lines
|
||||
.createLine({
|
||||
id: 'd',
|
||||
widthPx: 2,
|
||||
color: [1, 1, 1, 1],
|
||||
dash: { lengthPx: 12, gapPx: 4 },
|
||||
})
|
||||
.setPositions(points);
|
||||
lines
|
||||
.createLine({ id: 't', widthPx: 3, color: [0, 1, 1, 1], dash: 'tactical' })
|
||||
.setPositions(points);
|
||||
lines
|
||||
.createLine({ id: 's', widthPx: 1, color: [1, 1, 1, 1] })
|
||||
.setPositions(points);
|
||||
assert.equal(made[0].appearance.material.type, 'PolylineDash');
|
||||
assert.equal(made[0].appearance.material.uniforms.dashLength, 16);
|
||||
assert.equal(made[0].appearance.material.uniforms.dashPattern, 0xfff0);
|
||||
assert.equal(made[1].appearance.material.type, 'GevLineTactical');
|
||||
assert.equal(made[1].appearance.material.uniforms.groupCount, 4);
|
||||
assert.equal(made[2].appearance.material.type, 'Color');
|
||||
assert.equal(made[2].depthFailAppearance, undefined);
|
||||
});
|
||||
|
||||
test('a pick of a line primitive names the line', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories, made } = fakePrimitives();
|
||||
const { lines, identify } = createCesiumLines(viewer, { factories });
|
||||
lines
|
||||
.createLine({
|
||||
id: 'orbit-25544',
|
||||
widthPx: 2,
|
||||
color: [1, 1, 1, 1],
|
||||
pickId: 'o',
|
||||
})
|
||||
.setPositions(points);
|
||||
assert.deepEqual(identify({ primitive: made[0], id: 'o' }), {
|
||||
owner: 'orbit-25544',
|
||||
});
|
||||
assert.equal(identify({ primitive: {}, id: 'o' }), null);
|
||||
});
|
||||
|
||||
test('arrow lines retain the native arrow material in both depth passes', () => {
|
||||
const { factories, made } = fakePrimitives();
|
||||
const { lines, destroy } = createCesiumLines(createFakeViewer(), {
|
||||
factories,
|
||||
});
|
||||
lines
|
||||
.createLine({
|
||||
id: 'arrow',
|
||||
widthPx: 8,
|
||||
color: [1, 0, 0, 1],
|
||||
arrow: true,
|
||||
depthFailAlpha: 0.45,
|
||||
})
|
||||
.setPositions(points);
|
||||
assert.equal(made[0].appearance.material.type, 'PolylineArrow');
|
||||
assert.equal(made[0].depthFailAppearance.material.type, 'PolylineArrow');
|
||||
assert.equal(made[0].depthFailAppearance.material.uniforms.color.alpha, 0.45);
|
||||
destroy();
|
||||
});
|
||||
@@ -1,365 +0,0 @@
|
||||
// engine.movers on Cesium: one primitive per set inside one engine-owned
|
||||
// PrimitiveCollection (added to the scene with the first set, in `order`):
|
||||
// sprites are a BillboardCollection (screen-aligned, rotation in screen
|
||||
// space, an image per atlas cell), points a PointPrimitiveCollection,
|
||||
// models one Model per instance in a PrimitiveCollection. commit() writes
|
||||
// the changed instances and requests a render; nothing else renders.
|
||||
|
||||
import * as Cesium from 'cesium';
|
||||
import { createMoverRegistry } from '../movers.js';
|
||||
|
||||
let scratchPosition;
|
||||
let scratchColor;
|
||||
let scratchQuaternion;
|
||||
let scratchRotation;
|
||||
// Model.modelMatrix and Model.color copy what they are given.
|
||||
let scratchModelMatrix;
|
||||
|
||||
/** The atlas cells as canvases, by key (a DOM canvas or image source). */
|
||||
async function cropAtlasCells(atlas, keys) {
|
||||
let image = atlas.canvas;
|
||||
if (!image) {
|
||||
image = new Image();
|
||||
image.crossOrigin = 'anonymous';
|
||||
image.src = atlas.url;
|
||||
await image.decode();
|
||||
}
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
const cells = new Map();
|
||||
for (const key of keys) {
|
||||
const [u0, v0, u1, v1] = atlas.cells[key];
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round((u1 - u0) * width));
|
||||
canvas.height = Math.max(1, Math.round((v1 - v0) * height));
|
||||
canvas
|
||||
.getContext('2d')
|
||||
.drawImage(
|
||||
image,
|
||||
u0 * width,
|
||||
v0 * height,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
);
|
||||
cells.set(key, canvas);
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
const DEFAULT_FACTORIES = {
|
||||
billboards: (scene) => new Cesium.BillboardCollection({ scene }),
|
||||
points: () => new Cesium.PointPrimitiveCollection(),
|
||||
models: () => new Cesium.PrimitiveCollection({ destroyPrimitives: true }),
|
||||
loadModel: (options) => Cesium.Model.fromGltfAsync(options),
|
||||
cropCells: cropAtlasCells,
|
||||
};
|
||||
|
||||
const depthTestDistance = (depthTest) =>
|
||||
depthTest === 'always'
|
||||
? Number.POSITIVE_INFINITY
|
||||
: depthTest === 'normal'
|
||||
? 0
|
||||
: depthTest.disableWithinM;
|
||||
|
||||
const nearFar = (value) =>
|
||||
value ? new Cesium.NearFarScalar(...value) : undefined;
|
||||
|
||||
function readPosition(store, index) {
|
||||
const { positions } = store;
|
||||
return Cesium.Cartesian3.fromElements(
|
||||
positions[index * 3],
|
||||
positions[index * 3 + 1],
|
||||
positions[index * 3 + 2],
|
||||
scratchPosition,
|
||||
);
|
||||
}
|
||||
|
||||
function readColor(store, index) {
|
||||
const { colors } = store;
|
||||
scratchColor.red = colors[index * 4];
|
||||
scratchColor.green = colors[index * 4 + 1];
|
||||
scratchColor.blue = colors[index * 4 + 2];
|
||||
scratchColor.alpha = colors[index * 4 + 3];
|
||||
return scratchColor;
|
||||
}
|
||||
|
||||
function spritePart(scene, options, store, factories, requestRender, owners) {
|
||||
const collection = factories.billboards(scene);
|
||||
const { sprite, depthTest, id } = options;
|
||||
const style = {
|
||||
width: sprite.sizePx,
|
||||
height: sprite.sizePx,
|
||||
scaleByDistance: nearFar(sprite.scaleByDistance),
|
||||
disableDepthTestDistance: depthTestDistance(depthTest),
|
||||
};
|
||||
const billboards = [];
|
||||
const shownCell = [];
|
||||
let cells = null;
|
||||
let destroyed = false;
|
||||
const apply = (from, to) => {
|
||||
for (let index = from; index < to; index++) {
|
||||
let billboard = billboards[index];
|
||||
if (index >= store.count) {
|
||||
if (billboard) billboard.show = false;
|
||||
continue;
|
||||
}
|
||||
if (!billboard) {
|
||||
billboard = collection.add({ ...style });
|
||||
billboards[index] = billboard;
|
||||
owners.set(billboard, { owner: id, instance: index });
|
||||
shownCell[index] = -1;
|
||||
}
|
||||
billboard.position = readPosition(store, index);
|
||||
billboard.rotation = store.rotations[index];
|
||||
billboard.scale = store.scales[index];
|
||||
billboard.color = readColor(store, index);
|
||||
billboard.show = store.shows[index] === 1;
|
||||
billboard.id = store.pickIds[index];
|
||||
const cell = store.cells[index];
|
||||
if (shownCell[index] !== cell) {
|
||||
const key = store.cellKeys[cell];
|
||||
billboard.setImage(`${id}:${key}`, cells.get(key));
|
||||
shownCell[index] = cell;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Instances wait for the atlas; then everything drawn so far is applied.
|
||||
factories
|
||||
.cropCells(sprite.atlas, store.cellKeys)
|
||||
.then((ready) => {
|
||||
if (destroyed) return;
|
||||
cells = ready;
|
||||
apply(0, Math.max(store.count, billboards.length));
|
||||
requestRender('movers');
|
||||
})
|
||||
.catch((error) => console.warn(`[movers] ${id}: atlas failed`, error));
|
||||
return {
|
||||
primitive: collection,
|
||||
ready: () => cells !== null,
|
||||
commit(range) {
|
||||
if (cells && range) apply(range.from, range.to);
|
||||
requestRender('movers');
|
||||
},
|
||||
destroy() {
|
||||
destroyed = true;
|
||||
billboards.length = 0;
|
||||
shownCell.length = 0;
|
||||
cells = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function pointPart(options, store, factories, requestRender, owners) {
|
||||
const collection = factories.points();
|
||||
const { point, depthTest } = options;
|
||||
const style = {
|
||||
outlineWidth: point.outlinePx,
|
||||
outlineColor: new Cesium.Color(...point.outlineColor),
|
||||
scaleByDistance: nearFar(point.scaleByDistance),
|
||||
translucencyByDistance: nearFar(point.translucencyByDistance),
|
||||
disableDepthTestDistance: depthTestDistance(depthTest),
|
||||
};
|
||||
const points = [];
|
||||
return {
|
||||
primitive: collection,
|
||||
commit(range) {
|
||||
if (range)
|
||||
for (let index = range.from; index < range.to; index++) {
|
||||
let item = points[index];
|
||||
if (index >= store.count) {
|
||||
if (item) item.show = false;
|
||||
continue;
|
||||
}
|
||||
if (!item) {
|
||||
item = collection.add({ ...style });
|
||||
points[index] = item;
|
||||
owners.set(item, { owner: options.id, instance: index });
|
||||
}
|
||||
item.position = readPosition(store, index);
|
||||
item.pixelSize = point.sizePx * store.scales[index];
|
||||
item.color = readColor(store, index);
|
||||
item.show = store.shows[index] === 1;
|
||||
item.id = store.pickIds[index];
|
||||
}
|
||||
requestRender('movers');
|
||||
},
|
||||
destroy() {
|
||||
points.length = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function modelPart(options, store, factories, requestRender, owners) {
|
||||
const collection = factories.models();
|
||||
const { model } = options;
|
||||
const blendMode =
|
||||
model.colorBlend?.mode === 'replace'
|
||||
? Cesium.ColorBlendMode.REPLACE
|
||||
: Cesium.ColorBlendMode.MIX;
|
||||
const models = [];
|
||||
const loading = [];
|
||||
let destroyed = false;
|
||||
const place = (item, index) => {
|
||||
const { orientations } = store;
|
||||
scratchQuaternion.x = orientations[index * 4];
|
||||
scratchQuaternion.y = orientations[index * 4 + 1];
|
||||
scratchQuaternion.z = orientations[index * 4 + 2];
|
||||
scratchQuaternion.w = orientations[index * 4 + 3];
|
||||
item.modelMatrix = Cesium.Matrix4.fromRotationTranslation(
|
||||
Cesium.Matrix3.fromQuaternion(scratchQuaternion, scratchRotation),
|
||||
readPosition(store, index),
|
||||
scratchModelMatrix,
|
||||
);
|
||||
item.scale = store.scales[index];
|
||||
item.show = index < store.count && store.shows[index] === 1;
|
||||
item.id = store.pickIds[index];
|
||||
if (model.colorBlend) item.color = readColor(store, index);
|
||||
};
|
||||
const ensure = (index) => {
|
||||
if (models[index] || loading[index]) return;
|
||||
loading[index] = true;
|
||||
factories
|
||||
.loadModel({
|
||||
url: model.url,
|
||||
minimumPixelSize: model.minimumPixelSize,
|
||||
maximumScale: Number.isFinite(model.maximumScale)
|
||||
? model.maximumScale
|
||||
: undefined,
|
||||
colorBlendMode: blendMode,
|
||||
colorBlendAmount: model.colorBlend?.amount ?? 0.5,
|
||||
...(model.unlit
|
||||
? {
|
||||
customShader: new Cesium.CustomShader({
|
||||
lightingModel: Cesium.LightingModel.UNLIT,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.then((item) => {
|
||||
if (destroyed) {
|
||||
item.destroy?.();
|
||||
return;
|
||||
}
|
||||
models[index] = item;
|
||||
owners.set(item, { owner: options.id, instance: index });
|
||||
place(item, index);
|
||||
collection.add(item);
|
||||
requestRender('movers');
|
||||
})
|
||||
.catch((error) =>
|
||||
console.warn(`[movers] ${options.id}: model failed`, error),
|
||||
);
|
||||
};
|
||||
return {
|
||||
primitive: collection,
|
||||
ready() {
|
||||
for (let index = 0; index < store.count; index++)
|
||||
if (!models[index]?.ready) return false;
|
||||
return true;
|
||||
},
|
||||
commit(range) {
|
||||
if (range)
|
||||
for (let index = range.from; index < range.to; index++) {
|
||||
if (index < store.count) ensure(index);
|
||||
if (models[index]) place(models[index], index);
|
||||
}
|
||||
requestRender('movers');
|
||||
},
|
||||
destroy() {
|
||||
destroyed = true;
|
||||
models.length = 0;
|
||||
loading.length = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Cesium.Viewer} viewer
|
||||
* @param {{ requestRender?: (reason: string) => void, factories?: object }} [options]
|
||||
* `factories` (collections, model loading, atlas cropping) is a test seam.
|
||||
* @returns {{ movers: import('../contract.js').GlobeMovers, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumMovers(
|
||||
viewer,
|
||||
{ requestRender = () => viewer.scene.requestRender(), factories = {} } = {},
|
||||
) {
|
||||
scratchPosition ??= new Cesium.Cartesian3();
|
||||
scratchColor ??= new Cesium.Color();
|
||||
scratchQuaternion ??= new Cesium.Quaternion();
|
||||
scratchRotation ??= new Cesium.Matrix3();
|
||||
scratchModelMatrix ??= new Cesium.Matrix4();
|
||||
const make = { ...DEFAULT_FACTORIES, ...factories };
|
||||
const { scene } = viewer;
|
||||
let root = null;
|
||||
const ordered = [];
|
||||
// Billboard, point or model → its set and instance, for picks.
|
||||
const owners = new WeakMap();
|
||||
const rootCollection = () => {
|
||||
if (!root) {
|
||||
root = new Cesium.PrimitiveCollection({ destroyPrimitives: true });
|
||||
scene.primitives.add(root);
|
||||
}
|
||||
return root;
|
||||
};
|
||||
const registry = createMoverRegistry((options, store) => {
|
||||
const part =
|
||||
options.kind === 'sprite'
|
||||
? spritePart(scene, options, store, make, requestRender, owners)
|
||||
: options.kind === 'point'
|
||||
? pointPart(options, store, make, requestRender, owners)
|
||||
: modelPart(options, store, make, requestRender, owners);
|
||||
const { primitive } = part;
|
||||
primitive.gevMoverSet = options.id;
|
||||
// Later (higher) order draws later: after every set of lower or equal order.
|
||||
const at = ordered.filter((entry) => entry.order <= options.order).length;
|
||||
ordered.splice(at, 0, { order: options.order, primitive });
|
||||
rootCollection().add(primitive, at);
|
||||
return {
|
||||
primitive,
|
||||
ready: part.ready,
|
||||
commit: () => part.commit(store.dirty()),
|
||||
destroy() {
|
||||
part.destroy();
|
||||
ordered.splice(
|
||||
ordered.findIndex((entry) => entry.primitive === primitive),
|
||||
1,
|
||||
);
|
||||
root.remove(primitive);
|
||||
requestRender('movers');
|
||||
},
|
||||
};
|
||||
});
|
||||
return {
|
||||
movers: registry.movers,
|
||||
/** Where an instance of a set is (a geo Cartesian3), or null. */
|
||||
instancePosition(owner, instance) {
|
||||
const entry = registry.sets.get(owner);
|
||||
if (!entry || !(instance < entry.store.count)) return null;
|
||||
const view = entry.store.positionsView();
|
||||
const at = 3 * instance;
|
||||
return { x: view[at], y: view[at + 1], z: view[at + 2] };
|
||||
},
|
||||
/** The primitive drawing a live mover set of this engine, or null. */
|
||||
primitiveOf(set) {
|
||||
const entry = set?.id ? registry.sets.get(set.id) : null;
|
||||
return entry && entry.set === set ? entry.part.primitive : null;
|
||||
},
|
||||
/** The set and instance a Cesium pick result belongs to, or null. */
|
||||
identify(picked) {
|
||||
const known = picked?.primitive ? owners.get(picked.primitive) : null;
|
||||
const entry = known && registry.sets.get(known.owner);
|
||||
return entry ? { ...known, pickable: entry.options.pickable } : null;
|
||||
},
|
||||
destroy() {
|
||||
registry.destroyAll();
|
||||
if (root) {
|
||||
scene.primitives.remove(root);
|
||||
root = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { createCesiumMovers } from './movers.js';
|
||||
import { createFakeViewer } from './testing/fakeViewer.mjs';
|
||||
|
||||
// Collections that keep what the engine gives them (Cesium's own need a
|
||||
// WebGL scene to add billboards).
|
||||
function fakeFactories() {
|
||||
const made = [];
|
||||
const collection = (kind) => {
|
||||
const items = [];
|
||||
const value = {
|
||||
kind,
|
||||
items,
|
||||
show: true,
|
||||
add(options) {
|
||||
// Cesium's billboards and points copy positions and colours.
|
||||
let position;
|
||||
let color;
|
||||
const item = {
|
||||
...options,
|
||||
get position() {
|
||||
return position;
|
||||
},
|
||||
set position(value) {
|
||||
position = Cesium.Cartesian3.clone(value);
|
||||
},
|
||||
get color() {
|
||||
return color;
|
||||
},
|
||||
set color(value) {
|
||||
color = Cesium.Color.clone(value);
|
||||
},
|
||||
images: [],
|
||||
setImage(id, image) {
|
||||
this.images.push(id);
|
||||
this.image = image;
|
||||
},
|
||||
};
|
||||
items.push(item);
|
||||
return item;
|
||||
},
|
||||
remove(item) {
|
||||
items.splice(items.indexOf(item), 1);
|
||||
return true;
|
||||
},
|
||||
get length() {
|
||||
return items.length;
|
||||
},
|
||||
isDestroyed: () => false,
|
||||
destroy() {
|
||||
value.destroyed = true;
|
||||
},
|
||||
};
|
||||
made.push(value);
|
||||
return value;
|
||||
};
|
||||
return {
|
||||
made,
|
||||
factories: {
|
||||
billboards: () => collection('billboards'),
|
||||
points: () => collection('points'),
|
||||
models: () => collection('models'),
|
||||
loadModel: async (options) => ({ ...options, loaded: true }),
|
||||
cropCells: async (atlas, keys) =>
|
||||
new Map(keys.map((key) => [key, { cell: key }])),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const atlas = {
|
||||
canvas: { width: 64, height: 32 },
|
||||
cells: { jet: [0, 0, 0.5, 1], heli: [0.5, 0, 1, 1] },
|
||||
};
|
||||
const at = (x) => ({ x, y: 0, z: 6_400_000 });
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
test('creating the movers changes nothing; the first set adds one root collection', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories } = fakeFactories();
|
||||
const { movers, destroy } = createCesiumMovers(viewer, { factories });
|
||||
assert.equal(viewer.scene.primitives.length, 0);
|
||||
movers.createSet({
|
||||
id: 'a',
|
||||
kind: 'point',
|
||||
capacity: 2,
|
||||
point: { sizePx: 4 },
|
||||
});
|
||||
assert.equal(viewer.scene.primitives.length, 1);
|
||||
destroy();
|
||||
assert.equal(viewer.scene.primitives.length, 0);
|
||||
});
|
||||
|
||||
test('sprites become billboards with the set style once the atlas cells are ready', async () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories, made } = fakeFactories();
|
||||
const requests = [];
|
||||
const { movers } = createCesiumMovers(viewer, {
|
||||
factories,
|
||||
requestRender: (reason) => requests.push(reason),
|
||||
});
|
||||
const set = movers.createSet({
|
||||
id: 'flights',
|
||||
kind: 'sprite',
|
||||
capacity: 3,
|
||||
depthTest: 'always',
|
||||
sprite: { atlas, sizePx: 20, scaleByDistance: [1e3, 1, 1e6, 0.5] },
|
||||
});
|
||||
set.set(0, { position: at(1), rotation: 0.25, cell: 'heli', pickId: 'x1' });
|
||||
set.set(1, { position: at(2), color: [1, 0, 0, 0.5], scale: 2, show: false });
|
||||
set.setCount(2);
|
||||
set.commit();
|
||||
const billboards = made.find(
|
||||
(collection) => collection.kind === 'billboards',
|
||||
);
|
||||
assert.equal(billboards.items.length, 0, 'waits for the atlas');
|
||||
await settle();
|
||||
assert.equal(billboards.items.length, 2);
|
||||
const [first, second] = billboards.items;
|
||||
assert.ok(first.position instanceof Cesium.Cartesian3);
|
||||
assert.equal(first.position.x, 1);
|
||||
assert.equal(first.width, 20);
|
||||
assert.equal(first.height, 20);
|
||||
assert.equal(first.rotation, 0.25);
|
||||
assert.equal(first.id, 'x1');
|
||||
assert.equal(first.disableDepthTestDistance, Number.POSITIVE_INFINITY);
|
||||
assert.ok(first.scaleByDistance instanceof Cesium.NearFarScalar);
|
||||
assert.deepEqual(first.images, ['flights:heli']);
|
||||
assert.equal(first.show, true);
|
||||
assert.equal(second.show, false);
|
||||
assert.equal(second.scale, 2);
|
||||
assert.ok(Cesium.Color.equals(second.color, new Cesium.Color(1, 0, 0, 0.5)));
|
||||
assert.ok(requests.length >= 1);
|
||||
// Lowering the count hides the instances past it.
|
||||
set.setCount(1);
|
||||
set.commit();
|
||||
assert.equal(second.show, false);
|
||||
set.set(0, { cell: 'heli' });
|
||||
set.commit();
|
||||
assert.deepEqual(
|
||||
first.images,
|
||||
['flights:heli'],
|
||||
'the same cell is not re-sent',
|
||||
);
|
||||
});
|
||||
|
||||
test('points take the size, outline and depth test of the set', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories, made } = fakeFactories();
|
||||
const { movers } = createCesiumMovers(viewer, { factories });
|
||||
const set = movers.createSet({
|
||||
id: 'sats',
|
||||
kind: 'point',
|
||||
capacity: 2,
|
||||
depthTest: { disableWithinM: 5000 },
|
||||
point: { sizePx: 4, outlinePx: 1 },
|
||||
});
|
||||
set.set(0, { position: at(3), scale: 1.5, color: [0, 1, 0, 1] });
|
||||
set.setCount(1);
|
||||
set.commit();
|
||||
const [point] = made.find((c) => c.kind === 'points').items;
|
||||
assert.equal(point.pixelSize, 6);
|
||||
assert.equal(point.outlineWidth, 1);
|
||||
assert.equal(point.disableDepthTestDistance, 5000);
|
||||
assert.ok(Cesium.Color.equals(point.color, new Cesium.Color(0, 1, 0, 1)));
|
||||
});
|
||||
|
||||
test('sets are ordered in the root by their order', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories } = fakeFactories();
|
||||
const { movers } = createCesiumMovers(viewer, { factories });
|
||||
const make = (id, order) =>
|
||||
movers.createSet({
|
||||
id,
|
||||
kind: 'point',
|
||||
capacity: 1,
|
||||
order,
|
||||
point: { sizePx: 2 },
|
||||
});
|
||||
make('top', 5);
|
||||
make('bottom', -1);
|
||||
make('middle', 0);
|
||||
const root = viewer.scene.primitives.get(0);
|
||||
assert.deepEqual(
|
||||
[0, 1, 2].map((index) => root.get(index).gevMoverSet),
|
||||
['bottom', 'middle', 'top'],
|
||||
);
|
||||
});
|
||||
|
||||
test('models load per instance and follow position and orientation', async () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories, made } = fakeFactories();
|
||||
const { movers } = createCesiumMovers(viewer, { factories });
|
||||
const set = movers.createSet({
|
||||
id: 'models',
|
||||
kind: 'model',
|
||||
capacity: 2,
|
||||
model: {
|
||||
url: '/models/airplane.glb',
|
||||
minimumPixelSize: 24,
|
||||
colorBlend: { mode: 'mix', amount: 0.4 },
|
||||
},
|
||||
});
|
||||
set.set(0, {
|
||||
position: at(4),
|
||||
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
||||
color: [1, 0.5, 0, 1],
|
||||
});
|
||||
set.setCount(1);
|
||||
set.commit();
|
||||
await settle();
|
||||
const models = made.find((c) => c.kind === 'models');
|
||||
assert.equal(models.items.length, 1);
|
||||
const [model] = models.items;
|
||||
assert.equal(model.url, '/models/airplane.glb');
|
||||
assert.equal(model.minimumPixelSize, 24);
|
||||
assert.equal(model.colorBlendMode, Cesium.ColorBlendMode.MIX);
|
||||
assert.equal(model.colorBlendAmount, 0.4);
|
||||
assert.equal(model.modelMatrix[12], 4);
|
||||
set.destroy();
|
||||
assert.equal(models.destroyed, true);
|
||||
});
|
||||
|
||||
test('a pick of a billboard or point names its set and instance', async () => {
|
||||
const viewer = createFakeViewer();
|
||||
const { factories, made } = fakeFactories();
|
||||
const { movers, identify } = createCesiumMovers(viewer, { factories });
|
||||
const set = movers.createSet({
|
||||
id: 'sats',
|
||||
kind: 'point',
|
||||
capacity: 3,
|
||||
point: { sizePx: 4 },
|
||||
});
|
||||
set.set(0, { position: at(1) });
|
||||
set.set(1, { position: at(2), pickId: 25544 });
|
||||
set.setCount(2);
|
||||
set.commit();
|
||||
const [, second] = made.find((c) => c.kind === 'points').items;
|
||||
assert.deepEqual(identify({ primitive: second, id: 25544 }), {
|
||||
owner: 'sats',
|
||||
instance: 1,
|
||||
pickable: false,
|
||||
});
|
||||
assert.equal(identify({ primitive: {}, id: 'x' }), null);
|
||||
set.destroy();
|
||||
assert.equal(identify({ primitive: second, id: 25544 }), null);
|
||||
});
|
||||
@@ -6,44 +6,18 @@ import { toCesium2, toGeo3 } from './convert.js';
|
||||
|
||||
/**
|
||||
* @param {Cesium.Viewer} viewer
|
||||
* @param {{
|
||||
* identify?: (picked: object) => ({ owner: string, instance?: number, pickable?: boolean } | null),
|
||||
* visible?: (known: { owner: string, instance?: number }) => boolean,
|
||||
* }} [options]
|
||||
* `identify` names the mover set or line (and instance) a pick belongs to;
|
||||
* `visible` says whether that instance is in front of the horizon
|
||||
* (Cesium's GPU pick sees billboards drawn without a depth test through
|
||||
* the globe).
|
||||
* @returns {{ picker: object, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumPicker(
|
||||
viewer,
|
||||
{ identify = () => null, visible = () => true } = {},
|
||||
) {
|
||||
export function createCesiumPicker(viewer) {
|
||||
const normalise = (picked) => {
|
||||
if (picked == null) return undefined;
|
||||
// The follow camera's proxy entity (follow.js) is not an object.
|
||||
if (picked.id?.gevFollowProxy) return undefined;
|
||||
const known = identify(picked);
|
||||
// A mover set created with pickable: false is not picked (Cesium's GPU
|
||||
// pick sees every primitive; Navara's only the pickable ones), nor an
|
||||
// instance behind the horizon.
|
||||
if (known?.pickable === false) return undefined;
|
||||
if (known && known.instance !== undefined && !visible(known))
|
||||
return undefined;
|
||||
return {
|
||||
id: picked.id,
|
||||
primitive: picked.primitive,
|
||||
content: picked.content,
|
||||
object: picked,
|
||||
...(known
|
||||
? {
|
||||
owner: known.owner,
|
||||
...(known.instance !== undefined
|
||||
? { instance: known.instance }
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
const { scene } = viewer;
|
||||
@@ -58,8 +32,8 @@ export function createCesiumPicker(
|
||||
if (top == null) return undefined;
|
||||
const picked = normalise(top);
|
||||
if (picked) return picked;
|
||||
// The top result is filtered: what lies under it, the first with an
|
||||
// id (an object), else the first kept (map tiles).
|
||||
// The top result is the follow proxy: what lies under it, the first
|
||||
// with an id (an object), else the first kept (map tiles).
|
||||
const under = drill(screen, undefined, width, height);
|
||||
return under.find((entry) => entry.id !== undefined) ?? under[0];
|
||||
},
|
||||
@@ -81,8 +55,7 @@ export function createCesiumPicker(
|
||||
pickGlobe(screen) {
|
||||
if (!scene.globe) return undefined;
|
||||
try {
|
||||
const camera = viewer.camera ?? scene.camera;
|
||||
const ray = camera.getPickRay(toCesium2(screen));
|
||||
const ray = viewer.camera.getPickRay(toCesium2(screen));
|
||||
return ray ? toGeo3(scene.globe.pick(ray, scene)) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
@@ -114,97 +114,23 @@ test('pickGlobe asks the globe along the camera pick ray and returns geo', () =>
|
||||
assert.equal(picker.pickGlobe(geo.cartesian2.create(1, 1)), undefined);
|
||||
});
|
||||
|
||||
test('picks of movers and lines name their owner and instance', () => {
|
||||
test('a follow-proxy top pick falls through to the first pick under it with an id', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const billboard = { id: 'ae01ce' };
|
||||
viewer.scene.pick = () => ({ primitive: billboard, id: 'ae01ce' });
|
||||
viewer.scene.drillPick = () => [
|
||||
{ primitive: billboard, id: 'ae01ce' },
|
||||
{ primitive: {}, id: 'other' },
|
||||
];
|
||||
const { picker } = createCesiumPicker(viewer, {
|
||||
identify: (raw) =>
|
||||
raw.primitive === billboard ? { owner: 'flights', instance: 7 } : null,
|
||||
});
|
||||
const picked = picker.pick({ x: 1, y: 2 });
|
||||
assert.equal(picked.id, 'ae01ce');
|
||||
assert.equal(picked.owner, 'flights');
|
||||
assert.equal(picked.instance, 7);
|
||||
const drilled = picker.drillPick({ x: 1, y: 2 }, 5);
|
||||
assert.equal(drilled[0].owner, 'flights');
|
||||
assert.equal(drilled[1].owner, undefined);
|
||||
});
|
||||
|
||||
test('picks of a mover set made with pickable: false are dropped', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const quiet = { id: 'q' };
|
||||
viewer.scene.pick = () => ({ primitive: quiet, id: 'q' });
|
||||
viewer.scene.drillPick = () => [
|
||||
{ primitive: quiet, id: 'q' },
|
||||
{ primitive: {}, id: 'other' },
|
||||
];
|
||||
const { picker } = createCesiumPicker(viewer, {
|
||||
identify: (raw) =>
|
||||
raw.primitive === quiet
|
||||
? { owner: 'quiet', instance: 0, pickable: false }
|
||||
: null,
|
||||
});
|
||||
// The pick falls through to what is under the quiet set.
|
||||
assert.equal(picker.pick({ x: 1, y: 2 }).id, 'other');
|
||||
assert.deepEqual(
|
||||
picker.drillPick({ x: 1, y: 2 }, 5).map(({ id }) => id),
|
||||
['other'],
|
||||
);
|
||||
});
|
||||
|
||||
test('a filtered top pick falls through to the first pick under it with an id', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const quiet = { primitive: 'quiet-billboard', id: 'q' };
|
||||
const proxy = { primitive: 'proxy-point', id: { gevFollowProxy: true } };
|
||||
const tile = { primitive: 'tileset', content: {} };
|
||||
const flight = { primitive: 'flight-billboard', id: 'ae01ce' };
|
||||
const { picker } = createCesiumPicker(viewer, {
|
||||
identify: (picked) =>
|
||||
picked === quiet
|
||||
? { owner: 'quiet', instance: 0, pickable: false }
|
||||
: picked === flight
|
||||
? { owner: 'flights', instance: 3, pickable: true }
|
||||
: null,
|
||||
});
|
||||
viewer.scene.pick = () => quiet;
|
||||
viewer.scene.drillPick = () => [quiet, proxy, tile, flight];
|
||||
const picked = picker.pick(geo.cartesian2.create(1, 1));
|
||||
assert.equal(picked.id, 'ae01ce');
|
||||
assert.equal(picked.owner, 'flights');
|
||||
const { picker } = createCesiumPicker(viewer);
|
||||
viewer.scene.pick = () => proxy;
|
||||
viewer.scene.drillPick = () => [proxy, tile, flight];
|
||||
assert.equal(picker.pick(geo.cartesian2.create(1, 1)).id, 'ae01ce');
|
||||
assert.deepEqual(
|
||||
picker.drillPick(geo.cartesian2.create(1, 1)).map((p) => p.primitive),
|
||||
['tileset', 'flight-billboard'],
|
||||
'the follow proxy and pickable: false sets are never picked',
|
||||
'the follow proxy is never picked',
|
||||
);
|
||||
// Nothing under it with an id: the first kept result (a tile).
|
||||
viewer.scene.drillPick = () => [quiet, tile];
|
||||
viewer.scene.drillPick = () => [proxy, tile];
|
||||
assert.equal(picker.pick(geo.cartesian2.create(1, 1)).primitive, 'tileset');
|
||||
viewer.scene.pick = () => proxy;
|
||||
viewer.scene.drillPick = () => [proxy];
|
||||
assert.equal(picker.pick(geo.cartesian2.create(1, 1)), undefined);
|
||||
});
|
||||
|
||||
test('drills leave out mover instances behind the horizon', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const near = { primitive: 'near', id: 'n' };
|
||||
const far = { primitive: 'far', id: 'f' };
|
||||
const { picker } = createCesiumPicker(viewer, {
|
||||
identify: (picked) => ({
|
||||
owner: 'flights',
|
||||
instance: picked === near ? 0 : 1,
|
||||
}),
|
||||
visible: (known) => known.instance === 0,
|
||||
});
|
||||
viewer.scene.drillPick = () => [far, near];
|
||||
assert.deepEqual(
|
||||
picker.drillPick(geo.cartesian2.create(1, 1)).map((p) => p.id),
|
||||
['n'],
|
||||
);
|
||||
viewer.scene.pick = () => far;
|
||||
assert.equal(picker.pick(geo.cartesian2.create(1, 1)).id, 'n');
|
||||
});
|
||||
@@ -15,9 +15,7 @@ export function createCesiumProjector(
|
||||
worldToWindowCoordinates = Cesium.SceneTransforms.worldToWindowCoordinates,
|
||||
} = {},
|
||||
) {
|
||||
const { scene } = viewer;
|
||||
// Viewer.camera is scene.camera; the engine reads the public alias.
|
||||
const camera = viewer.camera ?? scene.camera;
|
||||
const { scene, camera } = viewer;
|
||||
const scratchWindow = new Cesium.Cartesian2();
|
||||
const scratchMatrix = new Cesium.Matrix4();
|
||||
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// GlobeSurface on a Cesium scene: the rendered-surface queries that
|
||||
// services/groundSnap and services/meshFloorSampler make.
|
||||
//
|
||||
// `exclude` takes the engine's own mover sets (engine.movers) as well as
|
||||
// Cesium objects: Cesium's sampleHeight matches excluded objects by the
|
||||
// picked primitive, which for a set is each billboard, point or model, so a
|
||||
// set is hidden for the sample instead (synchronous: nothing draws in
|
||||
// between) and shown again after it.
|
||||
|
||||
import { firstVisibleTileset } from '../../services/groundSnap.js';
|
||||
import { normalizeSurfaceOptions } from '../surfaceDetail.js';
|
||||
@@ -15,46 +9,16 @@ const finite = (value) => (Number.isFinite(value) ? value : undefined);
|
||||
|
||||
/**
|
||||
* @param {Cesium.Viewer} viewer
|
||||
* @param {{ engineExclude?: (entry: any) => ({ show: boolean } | null) }} [options]
|
||||
* `engineExclude` maps an engine object (a mover set) to the Cesium
|
||||
* primitive that draws it, or null for a Cesium object.
|
||||
* @returns {{ surface: import('../contract.js').GlobeSurface, destroy: () => void }}
|
||||
*/
|
||||
export function createCesiumSurface(
|
||||
viewer,
|
||||
{ engineExclude = () => null } = {},
|
||||
) {
|
||||
export function createCesiumSurface(viewer) {
|
||||
const { scene } = viewer;
|
||||
|
||||
// Runs `query(nativeExclude)` with the engine objects in `exclude` hidden.
|
||||
function excluding(exclude, query) {
|
||||
if (!exclude?.length) return query(exclude);
|
||||
let native = exclude;
|
||||
const hidden = [];
|
||||
for (const entry of exclude) {
|
||||
const primitive = engineExclude(entry);
|
||||
if (!primitive) continue;
|
||||
if (native === exclude)
|
||||
native = exclude.filter((other) => !engineExclude(other));
|
||||
if (primitive.show) {
|
||||
primitive.show = false;
|
||||
hidden.push(primitive);
|
||||
}
|
||||
}
|
||||
try {
|
||||
return query(native);
|
||||
} finally {
|
||||
for (const primitive of hidden) primitive.show = true;
|
||||
}
|
||||
}
|
||||
|
||||
const sampleHeight = (cartographic, exclude) => {
|
||||
if (!scene.sampleHeightSupported) return undefined;
|
||||
try {
|
||||
// Throws when unsupported (no depth textures); that is a miss.
|
||||
return excluding(exclude, (native) =>
|
||||
finite(scene.sampleHeight(cartographic, native)),
|
||||
);
|
||||
return finite(scene.sampleHeight(cartographic, exclude));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -64,8 +28,8 @@ export function createCesiumSurface(
|
||||
|
||||
const surface = {
|
||||
sampleHeight,
|
||||
// Cesium samples the tiles it has loaded (lod 'finest' cannot do better
|
||||
// synchronously) and does not say which tile answered.
|
||||
// Cesium samples the tiles it has loaded and does not say which tile
|
||||
// answered.
|
||||
sampleHeightDetailed(cartographic, options) {
|
||||
const { exclude } = normalizeSurfaceOptions(options);
|
||||
const height = sampleHeight(cartographic, exclude);
|
||||
@@ -85,9 +49,7 @@ export function createCesiumSurface(
|
||||
clampToHeight(position, exclude) {
|
||||
if (!scene.clampToHeightSupported) return undefined;
|
||||
try {
|
||||
return excluding(exclude, (native) =>
|
||||
toGeo3(scene.clampToHeight(position, native)),
|
||||
);
|
||||
return toGeo3(scene.clampToHeight(position, exclude));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -93,35 +93,12 @@ test('sampleHeightDetailed: tiles from sampleHeight, else terrain; no tile size'
|
||||
source: 'tiles',
|
||||
});
|
||||
scene.sampleHeight = () => undefined;
|
||||
assert.deepEqual(surface.sampleHeightDetailed(place, { lod: 'finest' }), {
|
||||
assert.deepEqual(surface.sampleHeightDetailed(place), {
|
||||
height: 12.5,
|
||||
source: 'terrain',
|
||||
});
|
||||
assert.throws(
|
||||
() => surface.sampleHeightDetailed(place, { lod: 2 }),
|
||||
() => surface.sampleHeightDetailed(place, { exclude: 'x' }),
|
||||
TypeError,
|
||||
);
|
||||
});
|
||||
|
||||
test('an engine mover set in exclude is hidden for the sample, native objects pass', () => {
|
||||
const viewer = createFakeViewer();
|
||||
const collection = { show: true };
|
||||
const set = { id: 'flights' };
|
||||
const { surface } = createCesiumSurface(viewer, {
|
||||
engineExclude: (entry) => (entry === set ? collection : null),
|
||||
});
|
||||
const shownDuring = [];
|
||||
viewer.scene.sampleHeight = (position, excluded) => {
|
||||
shownDuring.push(collection.show, excluded);
|
||||
return 30;
|
||||
};
|
||||
const native = { primitive: 1 };
|
||||
const place = geo.cartographic.fromDegrees(20, 10, 0);
|
||||
assert.equal(surface.sampleHeight(place, [set, native]), 30);
|
||||
assert.deepEqual(shownDuring, [false, [native]]);
|
||||
assert.equal(collection.show, true, 'shown again after the sample');
|
||||
shownDuring.length = 0;
|
||||
surface.sampleHeightDetailed(place, { exclude: [set] });
|
||||
assert.deepEqual(shownDuring, [false, []]);
|
||||
assert.equal(collection.show, true);
|
||||
});
|
||||
+35
-62
@@ -189,12 +189,12 @@
|
||||
* @typedef {object} GlobeSurface
|
||||
* @property {(cartographic: Cartographic, exclude?: any[]) => (number | undefined)} sampleHeight
|
||||
* Height of the rendered scene (tiles, models) under a point.
|
||||
* @property {(cartographic: Cartographic, options?: { exclude?: any[], lod?: 'visible' | 'finest' }) => (SurfaceHeight | undefined)} sampleHeightDetailed
|
||||
* @property {(cartographic: Cartographic, options?: { exclude?: any[] }) => (SurfaceHeight | undefined)} sampleHeightDetailed
|
||||
* sampleHeight with the answer's quality (src/globe/surfaceDetail.js):
|
||||
* lod 'visible' (default) answers from the tiles drawn now, 'finest' from
|
||||
* the finest loaded ones (Navara; Cesium samples what it has loaded).
|
||||
* Where no tile answers, the terrain. `exclude` takes this engine's mover
|
||||
* sets as well as engine objects.
|
||||
* from the tiles drawn now (Navara: where they miss, the finest loaded
|
||||
* ones; Cesium samples what it has loaded). Where no tile answers, the
|
||||
* terrain. `exclude` takes this engine's mover sets as well as engine
|
||||
* objects.
|
||||
* @property {boolean} supported Whether sampleHeight works.
|
||||
* @property {(cartographic: Cartographic) => (number | undefined)} terrainHeight
|
||||
* Height of the terrain/globe surface only (Cesium's globe.getHeight).
|
||||
@@ -203,7 +203,6 @@
|
||||
* The position moved onto the rendered scene along the geodetic normal.
|
||||
* @property {boolean} clampToHeightSupported
|
||||
* @property {() => boolean} tilesLoaded
|
||||
* @property {boolean} [raycastSupported] Optional visible-tile obstruction probe.
|
||||
* @property {(ray: { origin: Cartesian3, direction: Cartesian3, far: number, exclude?: any[] }) => ({ position: Cartesian3, distance: number, object: any } | undefined)} [raycast]
|
||||
* Nearest visible tile triangle within finite positive `far` metres. Direction
|
||||
* is normalized by the adapter. Excluded roots/meshes and hidden objects do
|
||||
@@ -339,28 +338,23 @@
|
||||
* epoch: the one time source for layers and the engine's own time-driven
|
||||
* drawing (Navara's sun). By default it is the wall clock (Date.now); QA
|
||||
* and tests may freeze, offset or speed it up on the app clock: now()
|
||||
* follows at once, frameTime() and ticked from the engine's next frame, and
|
||||
* in request-render mode nothing draws until a render is requested, so a
|
||||
* QA helper that moves the clock calls render.request() after it.
|
||||
* Not on this clock yet (each moves when its layer or code moves):
|
||||
* follows at once, frameTime() from the engine's next frame, and in
|
||||
* request-render mode nothing draws until a render is requested, so a QA
|
||||
* helper that moves the clock calls render.request() after it.
|
||||
* Not on this clock:
|
||||
* - Cesium's viewer.clock: the app never animates it (shouldAnimate false,
|
||||
* its time frozen at boot); Cesium's sun and lighting and every entity
|
||||
* property read at viewer.clock.currentTime stay on that time;
|
||||
* - wall-clock throttles and feed receipt stamps everywhere (they measure
|
||||
* real elapsed time, not scene time).
|
||||
* Launch replay, satellite propagation and orbit GMST now use this clock.
|
||||
* - wall-clock throttles and feed receipt stamps (they measure real
|
||||
* elapsed time, not scene time).
|
||||
* @property {() => number} now The current app time.
|
||||
* @property {() => number} frameTime The app time latched at the start of the
|
||||
* engine's current frame: every reader in one frame gets the same value; it
|
||||
* does not decrease from frame to frame while the app clock runs forward.
|
||||
* Before the first frame, the current time.
|
||||
* @property {EngineEvent} ticked Raised once per engine frame, with the
|
||||
* frame time, before that frame's preUpdate (and so before preRender). A
|
||||
* frame is one turn of the engine's update loop, also when it draws
|
||||
* nothing: Cesium raises it on the viewer clock's tick, right before
|
||||
* scene.render (which raises preUpdate on every animation frame, drawn or
|
||||
* not); Navara on the view's preUpdate, which it emits on every turn of
|
||||
* its loop.
|
||||
* Before the first frame, the current time. A frame is one turn of the
|
||||
* engine's update loop, also when it draws nothing; its time is latched
|
||||
* before its preUpdate (Cesium: on the viewer clock's tick, right before
|
||||
* scene.render; Navara: on the view's preUpdate).
|
||||
*
|
||||
* @typedef {object} MoverSetOptions
|
||||
* A set of moving objects drawn as one batch (src/globe/movers.js checks
|
||||
@@ -383,14 +377,14 @@
|
||||
* @property {{ sizePx: number, outlinePx?: number, outlineColor?: number[], scaleByDistance?: number[], translucencyByDistance?: number[] }} [point]
|
||||
* A filled disc of sizePx CSS px diameter plus outlinePx on each side;
|
||||
* outlineColor is rgba in 0..1 (opaque black by default).
|
||||
* @property {{ url: string, minimumPixelSize?: number, maximumScale?: number,
|
||||
* @property {{ url: string, minimumPixelSize?: number,
|
||||
* colorBlend?: { mode: 'mix'|'replace', amount: number }, unlit?: boolean }} [model]
|
||||
* A glTF drawn per instance in metres, at least minimumPixelSize px.
|
||||
* @property {boolean} [pickable]
|
||||
* @property {boolean} [sharpen] Sprites/points: include the glyph in the
|
||||
* sharpen/AA stage after tone mapping, for translucent fields such as FIRMS.
|
||||
* Navara defaults to the later display stage used by W1; Cesium retains its
|
||||
* native billboard stage. Does not enable sharpening or change its amount.
|
||||
* sharpen/AA stage after tone mapping, for translucent fields such as FIRMS
|
||||
* (by default Navara draws them in the later display stage). Does not
|
||||
* enable sharpening or change its amount.
|
||||
*
|
||||
* @typedef {object} MoverFields
|
||||
* One instance; omitted fields keep their value (defaults: white, scale 1,
|
||||
@@ -413,10 +407,8 @@
|
||||
* @property {() => number} count
|
||||
* @property {() => void} commit Uploads what changed (one buffer write) and
|
||||
* requests a render; nothing else draws a frame.
|
||||
* @property {() => Float64Array} positionsView x, y, z of the drawn
|
||||
* instances (a view on the set's own array; read-only).
|
||||
* @property {() => boolean} ready Whether what the set draws has loaded: a
|
||||
* model set's glTF (Cesium: every instance's model), a sprite set's atlas.
|
||||
* model set's glTF, a sprite set's atlas.
|
||||
* A layer keeps its fallback visual until then.
|
||||
* @property {() => void} destroy
|
||||
*
|
||||
@@ -475,8 +467,11 @@
|
||||
* @property {{ addStatic: (html: string, options?: { onScreen?: boolean }) => () => void }} credits
|
||||
* @property {GlobeMaps} maps
|
||||
* @property {GlobeClock} clock
|
||||
* @property {GlobeMovers} movers
|
||||
* @property {GlobeLines} lines
|
||||
* @property {GlobeMovers} [movers] Optional, with lines (capabilities
|
||||
* `movers` and `lines`): the engine draws the layers' moving objects and
|
||||
* polylines (Navara). Cesium has none: in Cesium mode the layers draw
|
||||
* their own Cesium primitives.
|
||||
* @property {GlobeLines} [lines]
|
||||
* @property {() => void} destroy
|
||||
* @property {() => boolean} isDestroyed True once destroy() has run.
|
||||
*/
|
||||
@@ -491,7 +486,7 @@ export const INPUT_TYPES = Object.freeze([
|
||||
]);
|
||||
export const MODIFIERS = Object.freeze(['ctrl', 'shift', 'alt']);
|
||||
/** Input types whose listeners may ask for picks. */
|
||||
export const PICK_INPUT_TYPES = Object.freeze(['click', 'doubleClick', 'move']);
|
||||
const PICK_INPUT_TYPES = Object.freeze(['click', 'doubleClick', 'move']);
|
||||
|
||||
/**
|
||||
* input.on options checked and defaulted: { modifier, pick, drill } (drill
|
||||
@@ -512,11 +507,6 @@ export function normalizeInputOptions(type, options = {}) {
|
||||
if (drill && type === 'move') throw new TypeError('A move cannot drill');
|
||||
return { modifier, pick: pick || drill > 0, drill };
|
||||
}
|
||||
export const FLIGHT_EASINGS = Object.freeze([
|
||||
'cubicInOut',
|
||||
'sinusoidalInOut',
|
||||
'linear',
|
||||
]);
|
||||
|
||||
const ENGINE_EVENT = Symbol('gev.globe.engineEvent');
|
||||
|
||||
@@ -681,9 +671,7 @@ const SHAPE = {
|
||||
},
|
||||
input: { on: isFunction, setCameraInputs: isFunction },
|
||||
credits: { addStatic: isFunction },
|
||||
clock: { now: isFunction, frameTime: isFunction, ticked: isEngineEvent },
|
||||
movers: { createSet: isFunction, list: isFunction },
|
||||
lines: { createLine: isFunction, list: isFunction },
|
||||
clock: { now: isFunction, frameTime: isFunction },
|
||||
maps: {
|
||||
setBaseImagery: isFunction,
|
||||
setTerrain: isFunction,
|
||||
@@ -720,21 +708,16 @@ const OPTIONAL_SHAPE = {
|
||||
'render.setContactSonar': isFunction,
|
||||
'render.contactSonar': isFunction,
|
||||
},
|
||||
movers: {
|
||||
'movers.createSet': isFunction,
|
||||
'movers.list': isFunction,
|
||||
},
|
||||
lines: {
|
||||
'lines.createLine': isFunction,
|
||||
'lines.list': isFunction,
|
||||
},
|
||||
};
|
||||
|
||||
/** The optional capability flags, e.g. 'visualStyles'. */
|
||||
export const ENGINE_CAPABILITIES = Object.freeze(Object.keys(OPTIONAL_SHAPE));
|
||||
|
||||
/** The member paths of each optional capability. */
|
||||
export const OPTIONAL_ENGINE_MEMBERS = Object.freeze(
|
||||
Object.fromEntries(
|
||||
Object.entries(OPTIONAL_SHAPE).map(([flag, members]) => [
|
||||
flag,
|
||||
Object.freeze(Object.keys(members)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const memberAt = (engine, path) =>
|
||||
path.split('.').reduce((value, key) => value?.[key], engine);
|
||||
|
||||
@@ -756,16 +739,6 @@ export function engineCapabilities(engine) {
|
||||
);
|
||||
}
|
||||
|
||||
/** The capability paths the contract requires, e.g. 'camera.flyTo'. */
|
||||
export const GLOBE_ENGINE_MEMBERS = Object.freeze([
|
||||
'kind',
|
||||
...Object.entries(SHAPE).flatMap(([capability, members]) =>
|
||||
Object.keys(members).map((member) => `${capability}.${member}`),
|
||||
),
|
||||
'destroy',
|
||||
'isDestroyed',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Whether a value is a globe engine rather than, say, a Cesium viewer (a
|
||||
* cheap structural test; assertGlobeEngine checks every member).
|
||||
|
||||
+21
-108
@@ -2,14 +2,11 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
ENGINE_KINDS,
|
||||
GLOBE_ENGINE_MEMBERS,
|
||||
INPUT_TYPES,
|
||||
MODIFIERS,
|
||||
BLOOM_INITIAL,
|
||||
CLOUD_COVER_INITIAL,
|
||||
CONTACT_SONAR_INITIAL,
|
||||
ENGINE_CAPABILITIES,
|
||||
OPTIONAL_ENGINE_MEMBERS,
|
||||
SHARPEN_INITIAL,
|
||||
assertGlobeEngine,
|
||||
createEngineEvent,
|
||||
@@ -198,42 +195,6 @@ test('assertGlobeEngine accepts a complete engine and names every missing capabi
|
||||
assert.throws(() => assertGlobeEngine(null), TypeError);
|
||||
});
|
||||
|
||||
test('the contract carries what the shell infrastructure needs', () => {
|
||||
for (const member of [
|
||||
'camera.livePosition',
|
||||
'camera.height',
|
||||
'maps.releasePhotorealTiles',
|
||||
'maps.current',
|
||||
])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
const engine = createFakeEngine();
|
||||
const broken = {
|
||||
...engine,
|
||||
camera: { ...engine.camera, livePosition: undefined, height: 1 },
|
||||
maps: { ...engine.maps, releasePhotorealTiles: undefined },
|
||||
};
|
||||
assert.throws(
|
||||
() => assertGlobeEngine(broken),
|
||||
/camera\.livePosition.*camera\.height.*maps\.releasePhotorealTiles/,
|
||||
);
|
||||
});
|
||||
|
||||
test('the contract carries what shell navigation needs', () => {
|
||||
for (const member of ['camera.commanded', 'picker.pickGlobe', 'isDestroyed'])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
const engine = createFakeEngine();
|
||||
const broken = {
|
||||
...engine,
|
||||
camera: { ...engine.camera, commanded: {} },
|
||||
picker: { ...engine.picker, pickGlobe: undefined },
|
||||
isDestroyed: false,
|
||||
};
|
||||
assert.throws(
|
||||
() => assertGlobeEngine(broken),
|
||||
/camera\.commanded.*picker\.pickGlobe.*isDestroyed/,
|
||||
);
|
||||
});
|
||||
|
||||
test('the fake engine passes the engine contract suite', async (t) => {
|
||||
const engine = createFakeEngine();
|
||||
const checks = createContractSuite(engine, {
|
||||
@@ -314,9 +275,7 @@ test('raiseValue raises exactly one argument with the event semantics', () => {
|
||||
assert.equal(event.numberOfListeners, 1);
|
||||
});
|
||||
|
||||
test('the contract carries the sharpening and checks its values', () => {
|
||||
for (const member of ['render.setSharpen', 'render.sharpen'])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
test('mergeSharpen keeps omitted fields and rejects bad values', () => {
|
||||
assert.deepEqual(SHARPEN_INITIAL, { enabled: false, amount: 1 });
|
||||
const on = mergeSharpen(SHARPEN_INITIAL, { enabled: true });
|
||||
assert.deepEqual(on, { enabled: true, amount: 1 });
|
||||
@@ -334,68 +293,34 @@ test('the contract carries the sharpening and checks its values', () => {
|
||||
assert.throws(() => mergeSharpen(on, bad), TypeError);
|
||||
});
|
||||
|
||||
test('the contract carries the app clock', () => {
|
||||
for (const member of ['clock.now', 'clock.frameTime', 'clock.ticked'])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
test('movers and lines are optional capabilities, whole or absent', () => {
|
||||
const engine = createFakeEngine();
|
||||
assert.equal(assertGlobeEngine(engine), engine);
|
||||
const broken = {
|
||||
...engine,
|
||||
clock: { ...engine.clock, frameTime: 1, ticked: {} },
|
||||
};
|
||||
assert.equal(engineCapabilities(engine).movers, true);
|
||||
assert.equal(engineCapabilities(engine).lines, true);
|
||||
const without = { ...engine };
|
||||
delete without.movers;
|
||||
delete without.lines;
|
||||
assert.equal(assertGlobeEngine(without), without);
|
||||
assert.equal(engineCapabilities(without).movers, false);
|
||||
const half = { ...engine, movers: { createSet: 1 } };
|
||||
assert.throws(
|
||||
() => assertGlobeEngine(broken),
|
||||
/clock\.frameTime.*clock\.ticked/,
|
||||
);
|
||||
const missing = { ...engine };
|
||||
delete missing.clock;
|
||||
assert.throws(() => assertGlobeEngine(missing), /\bclock\b/);
|
||||
});
|
||||
|
||||
test('the contract carries instanced movers', () => {
|
||||
for (const member of ['movers.createSet', 'movers.list'])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
const engine = createFakeEngine();
|
||||
const broken = { ...engine, movers: { createSet: 1 } };
|
||||
assert.throws(
|
||||
() => assertGlobeEngine(broken),
|
||||
() => assertGlobeEngine(half),
|
||||
/movers\.createSet.*movers\.list/,
|
||||
);
|
||||
});
|
||||
|
||||
test('the contract carries dynamic lines', () => {
|
||||
for (const member of ['lines.createLine', 'lines.list'])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
const engine = createFakeEngine();
|
||||
assert.equal(assertGlobeEngine(engine), engine);
|
||||
const broken = { ...engine, lines: {} };
|
||||
assert.throws(
|
||||
() => assertGlobeEngine(broken),
|
||||
/lines\.createLine.*lines\.list/,
|
||||
);
|
||||
});
|
||||
|
||||
test('the contract carries the follow camera', () => {
|
||||
for (const member of [
|
||||
'camera.follow',
|
||||
'camera.following',
|
||||
'camera.followChanged',
|
||||
])
|
||||
assert.ok(GLOBE_ENGINE_MEMBERS.includes(member), member);
|
||||
const engine = createFakeEngine();
|
||||
const broken = { ...engine, camera: { ...engine.camera, followChanged: {} } };
|
||||
assert.throws(() => assertGlobeEngine(broken), /camera\.followChanged/);
|
||||
// Without them the suite leaves out the checks that draw movers and lines.
|
||||
const names = (target) =>
|
||||
createContractSuite(target, { waitFrames: async () => {} }).map(
|
||||
({ name }) => name,
|
||||
);
|
||||
assert.ok(names(engine).some((name) => name.startsWith('movers:')));
|
||||
const plain = names(without);
|
||||
assert.ok(!plain.some((name) => /^(movers|lines|colours):/.test(name)));
|
||||
assert.ok(plain.some((name) => name.startsWith('camera.')));
|
||||
});
|
||||
|
||||
test('engineCapabilities names the optional capabilities an engine has whole', () => {
|
||||
assert.ok(ENGINE_CAPABILITIES.includes('visualStyles'));
|
||||
assert.deepEqual(OPTIONAL_ENGINE_MEMBERS.visualStyles, [
|
||||
'render.visualStyles',
|
||||
'render.setStyle',
|
||||
'render.style',
|
||||
]);
|
||||
const plain = createFakeEngine();
|
||||
for (const flag of ENGINE_CAPABILITIES)
|
||||
for (const flag of ['visualStyles', 'bloom', 'cloudCover', 'contactSonar'])
|
||||
assert.equal(engineCapabilities(plain)[flag], false, flag);
|
||||
const styled = createFakeEngine({
|
||||
visualStyles: { thermal: ['palette', 'time'] },
|
||||
@@ -457,11 +382,6 @@ test('a fake engine with visual styles passes the optional style check', async (
|
||||
});
|
||||
|
||||
test('the bloom capability: whole or absent, merged and checked', async (t) => {
|
||||
assert.ok(ENGINE_CAPABILITIES.includes('bloom'));
|
||||
assert.deepEqual(OPTIONAL_ENGINE_MEMBERS.bloom, [
|
||||
'render.setBloom',
|
||||
'render.bloom',
|
||||
]);
|
||||
const engine = createFakeEngine({ bloom: true });
|
||||
assert.equal(engineCapabilities(engine).bloom, true);
|
||||
assert.equal(engineCapabilities(engine).visualStyles, false);
|
||||
@@ -482,7 +402,6 @@ test('the bloom capability: whole or absent, merged and checked', async (t) => {
|
||||
});
|
||||
|
||||
test('the cloudCover capability: whole or absent, null for the engine own', async (t) => {
|
||||
assert.ok(ENGINE_CAPABILITIES.includes('cloudCover'));
|
||||
const engine = createFakeEngine({ cloudCover: true });
|
||||
assert.equal(engineCapabilities(engine).cloudCover, true);
|
||||
assert.equal(engineCapabilities(createFakeEngine()).cloudCover, false);
|
||||
@@ -506,12 +425,6 @@ test('the cloudCover capability: whole or absent, null for the engine own', asyn
|
||||
});
|
||||
|
||||
test('the contactSonar capability: whole or absent, merged and checked', async (t) => {
|
||||
assert.deepEqual(ENGINE_CAPABILITIES, [
|
||||
'visualStyles',
|
||||
'bloom',
|
||||
'cloudCover',
|
||||
'contactSonar',
|
||||
]);
|
||||
const engine = createFakeEngine({ contactSonar: true });
|
||||
assert.equal(engineCapabilities(engine).contactSonar, true);
|
||||
assert.deepEqual(engine.render.contactSonar(), CONTACT_SONAR_INITIAL);
|
||||
|
||||
@@ -16,9 +16,9 @@ const scratchB = geo.cartesian3.create();
|
||||
const scratchScreen = { x: 0, y: 0 };
|
||||
const scratchScreenB = { x: 0, y: 0 };
|
||||
|
||||
export const PICK_RADIUS_PX = 3;
|
||||
const PICK_RADIUS_PX = 3;
|
||||
/** A line is picked within 8 px of its drawn segments (thin trails too). */
|
||||
export const LINE_PICK_PX = 8;
|
||||
const LINE_PICK_PX = 8;
|
||||
|
||||
/** Distance from point p to segment ab (2D). */
|
||||
export function distanceToSegment(p, a, b) {
|
||||
|
||||
+1
-6
@@ -193,12 +193,7 @@ export function orbitOffset(
|
||||
}
|
||||
|
||||
/** The ENU offset scaled by `factor`, never inside `minRange`. */
|
||||
export function zoomOffset(
|
||||
offset,
|
||||
factor,
|
||||
minRange,
|
||||
result = cartesian3.create(),
|
||||
) {
|
||||
function zoomOffset(offset, factor, minRange, result = cartesian3.create()) {
|
||||
const range = cartesian3.magnitude(offset);
|
||||
if (!(range > 0) || !(factor > 0)) return cartesian3.clone(offset, result);
|
||||
const next = Math.max(minRange, range * factor);
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ function fail(message) {
|
||||
*/
|
||||
export const TACTICAL_DASH = Object.freeze({ groups: 4, dashes: 100 });
|
||||
|
||||
export const LINE_ARCS = Object.freeze(['none', 'geodesic']);
|
||||
const LINE_ARCS = Object.freeze(['none', 'geodesic']);
|
||||
|
||||
const isColor = (value) =>
|
||||
(Array.isArray(value) || ArrayBuffer.isView(value)) &&
|
||||
|
||||
+6
-9
@@ -5,7 +5,7 @@
|
||||
// metres), colours rgba in 0..1, rotations radians. No engine imports.
|
||||
|
||||
export const MOVER_KINDS = Object.freeze(['sprite', 'point', 'model']);
|
||||
export const MOVER_DEPTH_TESTS = Object.freeze(['normal', 'always']);
|
||||
const MOVER_DEPTH_TESTS = Object.freeze(['normal', 'always']);
|
||||
|
||||
const isFiniteNumber = (value) =>
|
||||
typeof value === 'number' && Number.isFinite(value);
|
||||
@@ -146,10 +146,6 @@ export function normalizeMoverSetOptions(options) {
|
||||
'model.minimumPixelSize',
|
||||
0,
|
||||
),
|
||||
maximumScale:
|
||||
model.maximumScale === undefined
|
||||
? Infinity
|
||||
: positive(model.maximumScale, 'model.maximumScale'),
|
||||
colorBlend: blend
|
||||
? { mode: blend.mode, amount: blend.amount ?? 0.5 }
|
||||
: null,
|
||||
@@ -305,8 +301,10 @@ export function createMoverStore(options) {
|
||||
});
|
||||
this.clean();
|
||||
},
|
||||
/** Indices changed since clean(), as [from, to), or null. */
|
||||
/** The changed range (one object, reused: read it at once), or null. */
|
||||
/**
|
||||
* Indices changed since clean(), as [from, to) (one object, reused: read
|
||||
* it at once), or null.
|
||||
*/
|
||||
dirty() {
|
||||
if (dirtyFrom === Infinity) return null;
|
||||
range.from = dirtyFrom;
|
||||
@@ -350,14 +348,13 @@ export function createMoverSet(
|
||||
commit();
|
||||
store.clean();
|
||||
},
|
||||
positionsView: () => store.positionsView(),
|
||||
// Whether what it draws has loaded (a model's glTF, an atlas image).
|
||||
ready: () => !destroyed && ready() === true,
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
destroy();
|
||||
store.release?.();
|
||||
store.release();
|
||||
},
|
||||
isDestroyed: () => destroyed,
|
||||
};
|
||||
|
||||
@@ -43,7 +43,6 @@ test('set options are normalised with the contract defaults', () => {
|
||||
model: { url: '/models/airplane.glb' },
|
||||
});
|
||||
assert.equal(model.model.minimumPixelSize, 0);
|
||||
assert.equal(model.model.maximumScale, Infinity);
|
||||
assert.equal(model.model.colorBlend, null);
|
||||
});
|
||||
|
||||
@@ -177,7 +176,7 @@ test('the set facade forwards to the store and an engine commit, once destroyed
|
||||
set.set(0, { position: { x: 1, y: 2, z: 3 } });
|
||||
set.setCount(1);
|
||||
assert.equal(set.count(), 1);
|
||||
assert.deepEqual([...set.positionsView()], [1, 2, 3]);
|
||||
assert.deepEqual([...store.positionsView()], [1, 2, 3]);
|
||||
set.commit();
|
||||
assert.deepEqual(calls, [['commit', { from: 0, to: 1 }]]);
|
||||
assert.equal(store.dirty(), null, 'commit cleans the store');
|
||||
@@ -205,7 +204,7 @@ test('a destroyed set releases its instance arrays', () => {
|
||||
assert.equal(store.count, 0);
|
||||
assert.equal(store.positions.length, 0);
|
||||
assert.equal(store.pickIds.length, 0);
|
||||
assert.equal(set.positionsView().length, 0);
|
||||
assert.equal(store.positionsView().length, 0);
|
||||
assert.equal(store.dirty(), null);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { createBloomPort } from './bloomSettings.js';
|
||||
import { pipelinePassNames } from './internals.js';
|
||||
import { withPassDisposal } from './frameDescriptor.js';
|
||||
|
||||
export const BLOOM_EFFECT_KEY = 'gevBloom';
|
||||
const BLOOM_EFFECT_KEY = 'gevBloom';
|
||||
|
||||
function apply(effect, settings) {
|
||||
effect.luminanceMaterial.threshold = settings.luminanceThreshold;
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
sphereRange,
|
||||
viewProjection,
|
||||
viewRectangle,
|
||||
} from './cameraMath.js';
|
||||
} from '../cameraMath.js';
|
||||
|
||||
const { cartesian3, cartographic, math, matrix4 } = geo;
|
||||
const WGS84 = geo.ellipsoid.WGS84;
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { isEngineEvent } from '../contract.js';
|
||||
import { orientationVectors } from './cameraMath.js';
|
||||
import { orientationVectors } from '../cameraMath.js';
|
||||
import { createCameraModel } from './cameraModel.js';
|
||||
|
||||
const { cartesian2, cartesian3, math, matrix4 } = geo;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Navara 0.2.1 leak fix (phase 3 review): Navara's transparent pass keeps a
|
||||
// Navara 0.2.1 leak fix: Navara's transparent pass keeps a
|
||||
// copy of each scene light and refreshes it every frame with
|
||||
// `copy.copy(light, true)`. For the sun's cascaded shadow lights
|
||||
// (@navaramap/three-csm CascadedDirectionalLights) that copy clones every
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// contract's preUpdate; every listener of the frame then reads that time.
|
||||
// The look (look.js) reads the same clock for the sun.
|
||||
|
||||
import { createEngineEvent } from '../contract.js';
|
||||
import { appClock as pageClock } from '../../time/clock.js';
|
||||
|
||||
/**
|
||||
@@ -14,15 +13,13 @@ import { appClock as pageClock } from '../../time/clock.js';
|
||||
* @returns {{ clock: import('../contract.js').GlobeClock, beginFrame: () => void }}
|
||||
*/
|
||||
export function createNavaraClock({ appClock = pageClock } = {}) {
|
||||
const ticked = createEngineEvent();
|
||||
return {
|
||||
clock: {
|
||||
now: () => appClock.now(),
|
||||
frameTime: () => appClock.frameTime(),
|
||||
ticked,
|
||||
},
|
||||
beginFrame() {
|
||||
ticked.raiseValue(appClock.beginFrame());
|
||||
appClock.beginFrame();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isEngineEvent } from '../contract.js';
|
||||
import { createAppClock } from '../../time/clock.js';
|
||||
import { createNavaraClock } from './clock.js';
|
||||
|
||||
test('beginFrame latches the app clock and raises ticked with the frame time', () => {
|
||||
test('beginFrame latches the app clock', () => {
|
||||
let wall = 10_000;
|
||||
const appClock = createAppClock({ now: () => wall });
|
||||
const { clock, beginFrame } = createNavaraClock({ appClock });
|
||||
assert.ok(isEngineEvent(clock.ticked));
|
||||
const seen = [];
|
||||
clock.ticked.addEventListener((time) => seen.push(time));
|
||||
assert.equal(clock.frameTime(), 10_000);
|
||||
wall = 10_016;
|
||||
beginFrame();
|
||||
wall = 10_020;
|
||||
assert.deepEqual(seen, [10_016]);
|
||||
assert.equal(clock.frameTime(), 10_016);
|
||||
assert.equal(clock.now(), 10_020);
|
||||
});
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
mergeSharpen,
|
||||
mergeStyle,
|
||||
} from '../contract.js';
|
||||
import { cesiumFovy } from './cameraMath.js';
|
||||
import { cesiumFovy, intersectEllipsoid } from '../cameraMath.js';
|
||||
import { createCameraModel } from './cameraModel.js';
|
||||
import { createNavaraClock } from './clock.js';
|
||||
import { createNavaraCredits } from './credits.js';
|
||||
@@ -62,31 +62,6 @@ function readCamera(raw) {
|
||||
};
|
||||
}
|
||||
|
||||
// Nearest hit of a ray with the WGS84 ellipsoid raised by `height` metres
|
||||
// (each radius grown by it), or undefined.
|
||||
function rayRaisedEllipsoid({ origin, direction }, height) {
|
||||
const radii = geo.ellipsoid.WGS84.radii;
|
||||
const rx = radii.x + height;
|
||||
const ry = radii.y + height;
|
||||
const rz = radii.z + height;
|
||||
const o = [origin.x / rx, origin.y / ry, origin.z / rz];
|
||||
const d = [direction.x / rx, direction.y / ry, direction.z / rz];
|
||||
const a = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
|
||||
const b = 2 * (o[0] * d[0] + o[1] * d[1] + o[2] * d[2]);
|
||||
const c = o[0] * o[0] + o[1] * o[1] + o[2] * o[2] - 1;
|
||||
const discriminant = b * b - 4 * a * c;
|
||||
if (discriminant < 0) return undefined;
|
||||
const root = Math.sqrt(discriminant);
|
||||
const near = (-b - root) / (2 * a);
|
||||
const t = near > 0 ? near : (-b + root) / (2 * a);
|
||||
if (!(t > 0)) return undefined;
|
||||
return cartesian3.add(
|
||||
origin,
|
||||
cartesian3.multiplyByScalar(direction, t, cartesian3.create()),
|
||||
cartesian3.create(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} view An initialised Navara ThreeView.
|
||||
* @param {{
|
||||
@@ -156,7 +131,7 @@ export function createNavaraEngine(
|
||||
read: () => readCamera(view.camera.raw),
|
||||
fly: (pose, options) => view.flyTo(pose, options),
|
||||
// Navara 0.2.1 has no cancel call: any camera change interrupts a
|
||||
// flight, and an empty setCamera changes nothing else (S4).
|
||||
// flight, and an empty setCamera changes nothing else.
|
||||
stop: () => view.setCamera({}),
|
||||
size,
|
||||
requestFrame: () => view.forceUpdate(),
|
||||
@@ -170,8 +145,7 @@ export function createNavaraEngine(
|
||||
};
|
||||
const engineClock = createNavaraClock(appClock ? { appClock } : {});
|
||||
// camera.follow (src/globe/follow.js): Navara's own cameraFollow takes
|
||||
// no user rotation or zoom (phase 3 Task 5 probe), so the engine steps
|
||||
// its own follow each frame.
|
||||
// no user rotation or zoom, so the engine steps its own follow each frame.
|
||||
const followStep = createEngineEvent();
|
||||
const follow = createFollowController({
|
||||
camera: model.camera,
|
||||
@@ -259,8 +233,6 @@ export function createNavaraEngine(
|
||||
// Clicks with { pick } get Navara's GPU pick of the gesture instead.
|
||||
pick: (screen) => picking.pick(screen),
|
||||
drillPick: (screen, limit) => picking.drillPick(screen, limit),
|
||||
// Navara-only diagnostics: how clicks with { pick } were answered.
|
||||
clickStats: () => picking.clickStats(),
|
||||
pickPosition(screen) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const hit = view.pickDepthPosition(
|
||||
@@ -282,7 +254,7 @@ export function createNavaraEngine(
|
||||
geo.cartographic.fromCartesian(hit, geo.ellipsoid.WGS84),
|
||||
);
|
||||
if (!Number.isFinite(height)) break;
|
||||
const next = rayRaisedEllipsoid(ray, height);
|
||||
const next = intersectEllipsoid(ray.origin, ray.direction, height);
|
||||
if (!next) break;
|
||||
const moved = cartesian3.distance(next, hit);
|
||||
hit = next;
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { assertGlobeEngine, engineCapabilities } from '../contract.js';
|
||||
import { cesiumFovy, orientationVectors } from './cameraMath.js';
|
||||
import { cesiumFovy, orientationVectors } from '../cameraMath.js';
|
||||
import { createNavaraEngine } from './createNavaraEngine.js';
|
||||
|
||||
const { cartesian2, cartesian3, math } = geo;
|
||||
@@ -138,7 +138,6 @@ test('the engine clock is the app clock, latched on the view preUpdate before fr
|
||||
const engine = createNavaraEngine(view, { hooks, appClock });
|
||||
assert.equal(engine.clock.now(), 2_000_000);
|
||||
const seen = [];
|
||||
engine.clock.ticked.addEventListener((time) => seen.push(['ticked', time]));
|
||||
for (const name of ['preUpdate', 'preRender', 'postRender'])
|
||||
engine.frames[name].addEventListener(() =>
|
||||
seen.push([name, engine.clock.frameTime()]),
|
||||
@@ -149,7 +148,6 @@ test('the engine clock is the app clock, latched on the view preUpdate before fr
|
||||
view.emit('preRender', 2);
|
||||
view.emit('postRender', 3);
|
||||
assert.deepEqual(seen, [
|
||||
['ticked', 2_000_016],
|
||||
['preUpdate', 2_000_016],
|
||||
['preRender', 2_000_016],
|
||||
['postRender', 2_000_016],
|
||||
@@ -158,7 +156,7 @@ test('the engine clock is the app clock, latched on the view preUpdate before fr
|
||||
engine.destroy();
|
||||
wall += 16;
|
||||
view.emit('preUpdate', 4);
|
||||
assert.equal(seen.length, 4, 'no tick after destroy');
|
||||
assert.equal(seen.length, 3, 'no frame after destroy');
|
||||
});
|
||||
|
||||
test('render requests are forceUpdate and continuous mode is Navara animation', () => {
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
} from './viewOptions.js';
|
||||
|
||||
// Navara 0.2.1 has no preserveDrawingBuffer option but takes a canvas whose
|
||||
// WebGL2 context is already created (S4); three then uses that context.
|
||||
// WebGL2 context is already created; three then uses that context.
|
||||
function preservedCanvas(container) {
|
||||
const canvas = document.createElement('canvas');
|
||||
Object.assign(canvas.style, {
|
||||
@@ -107,24 +107,13 @@ function groundInView(engine) {
|
||||
/**
|
||||
* @param {{
|
||||
* container: HTMLElement,
|
||||
* effects?: 'minimal' | 'photoreal' | 'cinematic',
|
||||
* search?: string,
|
||||
* pixelRatio?: number,
|
||||
* appClock?: import('../../time/clock.js').AppClock,
|
||||
* }} options `effects` names the look (cinematic by default); `search`, the
|
||||
* page's query string, picks it from `?look=` instead (the app's way).
|
||||
* `pixelRatio` only for measurements; the product uses the device's.
|
||||
* `appClock` (the page's by default) is the engine clock's time and so the
|
||||
* sun's: the look reads the engine clock's frame time.
|
||||
* }} options `search`, the page's query string, picks the look from
|
||||
* `?look=` (cinematic by default). The sun follows the engine clock (the
|
||||
* page's app clock): the look reads its frame time.
|
||||
* @returns {Promise<{ view: object, engine: import('../contract.js').GlobeEngine, destroy: () => void }>}
|
||||
*/
|
||||
export async function createNavaraGlobe({
|
||||
container,
|
||||
effects,
|
||||
search,
|
||||
pixelRatio,
|
||||
appClock,
|
||||
}) {
|
||||
export async function createNavaraGlobe({ container, search }) {
|
||||
if (!container) throw new TypeError('createNavaraGlobe needs a container');
|
||||
const canvas = preservedCanvas(container);
|
||||
let view;
|
||||
@@ -136,13 +125,13 @@ export async function createNavaraGlobe({
|
||||
// Parked for a switch back: its render loop is off (setRenderLoop).
|
||||
let parked = false;
|
||||
try {
|
||||
view = new ThreeView(navaraViewOptions({ container, canvas, pixelRatio }));
|
||||
view = new ThreeView(navaraViewOptions({ container, canvas }));
|
||||
const plugin = new DefaultPlugin();
|
||||
view.addPlugin(plugin);
|
||||
await view.init();
|
||||
const automaticLandscape =
|
||||
!effects && new URLSearchParams(search).get('sun') !== 'live';
|
||||
const plan = navaraScenePlan(effects ?? resolveNavaraLook(search), {
|
||||
new URLSearchParams(search).get('sun') !== 'live';
|
||||
const plan = navaraScenePlan(resolveNavaraLook(search), {
|
||||
mobile: Boolean(view.isMobileOptimized?.()),
|
||||
});
|
||||
// The sun follows the engine clock (one time source for layers and the
|
||||
@@ -158,7 +147,6 @@ export async function createNavaraGlobe({
|
||||
engine = createNavaraEngine(view, {
|
||||
hooks: navaraHooks(view),
|
||||
container,
|
||||
...(appClock ? { appClock } : {}),
|
||||
sharpen: createNavaraSharpen(view),
|
||||
bloom: createNavaraBloom(view),
|
||||
clouds: createNavaraCloudCover(look),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// canvas's top-left corner, and a listener with a modifier fires only while
|
||||
// that key is held (shift over ctrl over alt), one without only while none is.
|
||||
//
|
||||
// Camera inputs map to Navara's controller flags (view.camera.options, S4).
|
||||
// Camera inputs map to Navara's controller flags (view.camera.options).
|
||||
// Navara 0.2.1 lets a flyTo run on under user input, where Cesium cancels it;
|
||||
// `onCameraInput` reports user camera input so the engine can do the same.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Every private Navara member the engine relies on, in one place. Written
|
||||
// against @navaramap/three 0.2.1 (package.json pins it exactly; the test in
|
||||
// internals.test.mjs fails if the two drift apart): re-check each access below
|
||||
// before moving to another Navara version. Each names the parity spike that
|
||||
// found it and the public API that would replace it upstream.
|
||||
// before moving to another Navara version. Each names the public API that
|
||||
// would replace it upstream.
|
||||
|
||||
import { ModelMesh } from '@navaramap/three';
|
||||
import { patchCascadedLightCopy } from './cascadedLightCopy.js';
|
||||
@@ -21,7 +21,7 @@ export function setRenderLoopEnabled(view, enabled) {
|
||||
let restoreRelease = null;
|
||||
|
||||
/**
|
||||
* S2: keep 3D-tile positions and indices on the CPU after upload so the
|
||||
* Keeps 3D-tile positions and indices on the CPU after upload so the
|
||||
* surface ray can hit them. Private: ModelMesh.prototype._releaseGeometryArrays,
|
||||
* which frees every attribute array once the tile is on the GPU. Patched on
|
||||
* the prototype, for all views; idempotent. Upstream ask: an option to retain
|
||||
@@ -56,7 +56,7 @@ export function retainTileGeometry() {
|
||||
}
|
||||
|
||||
/**
|
||||
* S2: the loaded 3D-tile roots (ModelMesh objects). Private: view._scenes.mrt,
|
||||
* The loaded 3D-tile roots (ModelMesh objects). Private: view._scenes.mrt,
|
||||
* the scene Navara renders tiles into. Upstream ask: as for
|
||||
* retainTileGeometry.
|
||||
*/
|
||||
@@ -67,7 +67,7 @@ export function tileRoots(view) {
|
||||
}
|
||||
|
||||
/**
|
||||
* S2/S5: whether Navara has stopped updating (tiles loaded, camera still).
|
||||
* Whether Navara has stopped updating (tiles loaded, camera still).
|
||||
* Private: view._isIdle, which the public `idle` event reports only as an
|
||||
* edge. Upstream ask: a public isIdle/tilesLoaded getter.
|
||||
*/
|
||||
@@ -88,7 +88,7 @@ export function isIdle(view) {
|
||||
* throws in the dev server and warns once in a build. Upstream ask: a sky
|
||||
* luminance or atmosphere exposure that leaves unlit content alone.
|
||||
*/
|
||||
export const LUMINANCE_UNIFORMS = Object.freeze([
|
||||
const LUMINANCE_UNIFORMS = Object.freeze([
|
||||
'SUN_SPECTRAL_RADIANCE_TO_LUMINANCE',
|
||||
'SKY_SPECTRAL_RADIANCE_TO_LUMINANCE',
|
||||
]);
|
||||
@@ -141,7 +141,7 @@ export function forEachLuminanceUniform(handles, visit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3 review (leak): the sun's cascaded shadow lights are re-cloned by
|
||||
* A leak: the sun's cascaded shadow lights are re-cloned by
|
||||
* the transparent pass every frame (cascadedLightCopy.js). Finds the
|
||||
* CascadedDirectionalLights class among the view's scene lights (private:
|
||||
* view._scenes.light) and patches its copy to update in place. Returns the
|
||||
@@ -189,7 +189,7 @@ export const navaraHooks = (view) => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* Stage 6 (QA diagnostics only): the names of the passes in the view's
|
||||
* QA diagnostics only: the names of the passes in the view's
|
||||
* post-processing pipeline, in draw order, or null when the path is gone.
|
||||
* Private: ctx.renderPassOrchestrator.passMap.list ({ name, pass }).
|
||||
* Upstream ask: a public read of the pipeline order.
|
||||
|
||||
@@ -242,7 +242,7 @@ export function buildNavaraLook(
|
||||
}
|
||||
|
||||
/** Navara's own stars intensity, for a look that does not set one. */
|
||||
export const NAVARA_STARS_INTENSITY = 10;
|
||||
const NAVARA_STARS_INTENSITY = 10;
|
||||
|
||||
const IRRADIANCE_ON = Object.freeze({
|
||||
aerialPerspective: Object.freeze({ irradiance: true }),
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
// - terrain: the keyless Re:Earth quantized mesh, Cesium World Terrain through
|
||||
// the ion endpoint with an explicit token (Navara sends it as a Bearer
|
||||
// token), or none (the ellipsoid). Navara 0.2.1 panics on a quantized-mesh
|
||||
// URL without the {z}/{x}/{y}.terrain template (S2), so URLs always carry it.
|
||||
// - photoreal: Google 3D tiles with a browser key only in this phase (the
|
||||
// server proxy of S3 comes later), with Google's logo and the tiles' own
|
||||
// URL without the {z}/{x}/{y}.terrain template, so URLs always carry it.
|
||||
// - photoreal: Google 3D tiles with a browser key only (no server proxy),
|
||||
// with Google's logo and the tiles' own
|
||||
// copyright lines (creditLayerId) on screen. Showing them hides the base
|
||||
// imagery, as MapSourceController removes it; clearing them does not bring
|
||||
// it back until setBaseImagery. Hidden, not deleted: deleting the last
|
||||
@@ -42,7 +42,7 @@ const ION_WORLD_TERRAIN_ENDPOINT =
|
||||
const GOOGLE_TILES_ROOT = 'https://tile.googleapis.com/v1/3dtiles/root.json';
|
||||
// The mark Cesium shows for Google tiles (GoogleMaps.getDefaultCredit).
|
||||
const GOOGLE_LOGO = 'https://assets.ion.cesium.com/google-credit.png';
|
||||
// Cesium3DTileset's default maximumScreenSpaceError, as the S1–S5 benches use.
|
||||
// Cesium3DTileset's default maximumScreenSpaceError.
|
||||
const PHOTOREAL_MAX_SSE = 16;
|
||||
|
||||
const kindOf = (source) => (source?.descriptor ?? source)?.kind;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// PickableInstancedMeshWrapper so Navara's pick pass sees each instance;
|
||||
// pickIds.js turns its batch ids back into instances. commit() uploads and
|
||||
// asks for one frame (view.forceUpdate); per-frame work runs only on frames
|
||||
// the view draws anyway (S5), so an idle view stays idle.
|
||||
// the view draws anyway, so an idle view stays idle.
|
||||
|
||||
import { withFrameUpdates } from '../frameDescriptor.js';
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// The three side of a Navara model set: the glTF loaded once, one
|
||||
// InstancedMesh per mesh in it (all instances share the model's meshes),
|
||||
// each instance placed by position, orientation and scale relative to an
|
||||
// origin near the camera. Cesium's minimumPixelSize/maximumScale are kept on
|
||||
// the CPU once per drawn frame (models are few: tracked and proximity
|
||||
// aircraft), and the instance colour is mixed into the surface colour
|
||||
// (colorBlend), or the model drawn unlit.
|
||||
// origin near the camera. Cesium's minimumPixelSize is kept on the CPU once
|
||||
// per drawn frame (models are few: tracked and proximity aircraft), and the
|
||||
// instance colour is mixed into the surface colour (colorBlend), or the model
|
||||
// drawn unlit.
|
||||
|
||||
import {
|
||||
Box3,
|
||||
@@ -25,7 +25,7 @@ import { createModelTint } from './modelTint.js';
|
||||
* besides the sun. Navara's sky light alone left it grey-brown at dawn.
|
||||
* The instance colour is applied after lighting, independently of this share.
|
||||
*/
|
||||
export const MODEL_AMBIENT = 0.5;
|
||||
const MODEL_AMBIENT = 0.5;
|
||||
|
||||
/**
|
||||
* The distance Cesium sizes a model's minimumPixelSize at (Camera
|
||||
@@ -159,7 +159,6 @@ export function createModelMover(
|
||||
const pixels = (2 * radius * scale) / (footprint * distance);
|
||||
if (pixels < model.minimumPixelSize)
|
||||
scale *= model.minimumPixelSize / pixels;
|
||||
scale = Math.min(scale, model.maximumScale);
|
||||
drawnPx[index] = (pixels * scale) / scales[index];
|
||||
}
|
||||
scratchTranslation.set(
|
||||
|
||||
@@ -23,7 +23,7 @@ export function atlasUvRects(cells, keys) {
|
||||
}
|
||||
|
||||
/** The camera distance from the origin past which the origin moves. */
|
||||
export const recentreDistance = (cameraHeight) =>
|
||||
const recentreDistance = (cameraHeight) =>
|
||||
Math.max(1_000, 0.1 * Math.abs(cameraHeight));
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
// depth; their lit display colour is resolved here so HDR effects and MSAA
|
||||
// cannot spread a tint into neighbouring pixels.
|
||||
// FIRMS opts into a separate native-stage overlay before sharpening/AA.
|
||||
// Only that stage owns an encoded-RGBA8 blend target; W1 stays in place.
|
||||
// Only that stage owns an encoded-RGBA8 blend target; this overlay stays in
|
||||
// place.
|
||||
|
||||
import { withFrameUpdates, withPassDisposal } from './frameDescriptor.js';
|
||||
import { EffectDesc, Pass as NavaraPass } from '@navaramap/three';
|
||||
@@ -18,7 +19,7 @@ import { GevOverlayPass } from './overlayPass.js';
|
||||
import { sonarUniformsFor } from './sonar.js';
|
||||
import { RGBADepthPacking, Scene } from 'three';
|
||||
|
||||
export const OVERLAY_EFFECT_KEY = 'gevOverlay';
|
||||
const OVERLAY_EFFECT_KEY = 'gevOverlay';
|
||||
const PRE_SHARPEN_KEY = 'gevPreSharpenOverlay';
|
||||
const registeredViews = new WeakMap();
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ test('empty display overlay does not swap an unwritten frame buffer', () => {
|
||||
h.pass.dispose();
|
||||
});
|
||||
|
||||
test('ordinary W1 overlays keep the direct pass without a display copy or target', () => {
|
||||
test('ordinary overlays keep the direct pass without a display copy or target', () => {
|
||||
const h = harness(),
|
||||
input = { width: 1280, height: 800, samples: 4, texture: {} };
|
||||
h.overlay.displayBlend = false;
|
||||
|
||||
@@ -52,13 +52,6 @@ export function createNavaraPicking({
|
||||
|
||||
// Clicks waiting for Navara's GPU answer, oldest first.
|
||||
const waiting = [];
|
||||
// Diagnostics: how clicks were answered and how long it took (ms).
|
||||
const stats = { gpu: 0, timeout: 0, stale: 0, latencies: [] };
|
||||
const answered = (entry, how) => {
|
||||
stats[how]++;
|
||||
stats.latencies.push(now() - entry.startedAt);
|
||||
if (stats.latencies.length > 200) stats.latencies.shift();
|
||||
};
|
||||
// Navara 0.2.1 answers clicks in quick succession (within a frame or so)
|
||||
// with the first click's pick: a GPU answer whose instance is not drawn
|
||||
// near the click is stale, and the CPU pick answers instead.
|
||||
@@ -70,7 +63,7 @@ export function createNavaraPicking({
|
||||
// the CPU sees must be drawn under the click (its pick radius, no slack)
|
||||
// and be the one the CPU finds on top. A stale answer from the previous
|
||||
// gesture (the followed aircraft at the centre) lay within twice its
|
||||
// radius of a neighbour's click and won (10b/11b flake). A model hit is
|
||||
// radius of a neighbour's click and won. A model hit is
|
||||
// checked against its projected position within its drawn radius
|
||||
// (pickRadiusPx); the CPU's top hit (drawn order first) is not asked of
|
||||
// it, as a model is drawn depth-tested among the glyphs.
|
||||
@@ -83,10 +76,7 @@ export function createNavaraPicking({
|
||||
(top !== undefined &&
|
||||
!(top.owner === hit.set && top.instance === hit.instance) &&
|
||||
(!isModelSet(hit.set) || top.instance === undefined));
|
||||
if (stale) {
|
||||
stats.stale++;
|
||||
return drillPick(at, 1)[0];
|
||||
}
|
||||
if (stale) return drillPick(at, 1)[0];
|
||||
}
|
||||
return hit
|
||||
? { id: hit.pickId, owner: hit.set, instance: hit.instance }
|
||||
@@ -103,7 +93,6 @@ export function createNavaraPicking({
|
||||
return;
|
||||
}
|
||||
cancel(next.timer);
|
||||
answered(next, 'gpu');
|
||||
next.resolve(answerFrom(resolveInfo(info), next.at));
|
||||
};
|
||||
view.on('featureClick', onFeatureClick);
|
||||
@@ -119,29 +108,22 @@ export function createNavaraPicking({
|
||||
clickPick(at) {
|
||||
const gesture = early;
|
||||
early = null;
|
||||
if (gesture && now() - gesture.at <= EARLY_ANSWER_MS) {
|
||||
const entry = { startedAt: gesture.at };
|
||||
answered(entry, 'gpu');
|
||||
if (gesture && now() - gesture.at <= EARLY_ANSWER_MS)
|
||||
return Promise.resolve(answerFrom(gesture.hit, at));
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const entry = {
|
||||
at: { x: at.x, y: at.y },
|
||||
resolve,
|
||||
timer: null,
|
||||
startedAt: now(),
|
||||
};
|
||||
entry.timer = later(() => {
|
||||
const index = waiting.indexOf(entry);
|
||||
if (index >= 0) waiting.splice(index, 1);
|
||||
answered(entry, 'timeout');
|
||||
resolve(drillPick(entry.at, 1)[0]);
|
||||
}, timeoutMs);
|
||||
waiting.push(entry);
|
||||
});
|
||||
},
|
||||
/** How clicks were answered: GPU picks, timeouts, recent latencies (ms). */
|
||||
clickStats: () => ({ ...stats, latencies: [...stats.latencies] }),
|
||||
destroy() {
|
||||
meshPickers.clear();
|
||||
view.off('featureClick', onFeatureClick);
|
||||
|
||||
@@ -162,36 +162,6 @@ test('without a GPU answer in time the click falls back to the CPU pick', async
|
||||
assert.equal(view.handlers.size, 0);
|
||||
});
|
||||
|
||||
test('click stats count GPU answers and timeouts with their latency', async () => {
|
||||
const view = fakeView();
|
||||
const clock = timers();
|
||||
let time = 0;
|
||||
const picking = createNavaraPicking({
|
||||
view,
|
||||
projector,
|
||||
camera,
|
||||
movers: () => [],
|
||||
lines: () => [],
|
||||
resolveBatch: () => null,
|
||||
now: () => time,
|
||||
...clock,
|
||||
});
|
||||
const first = picking.clickPick({ x: 0, y: 0 });
|
||||
time = 30;
|
||||
view.emit('featureClick', null);
|
||||
await first;
|
||||
const second = picking.clickPick({ x: 0, y: 0 });
|
||||
time = 280;
|
||||
clock.pending[0].fn();
|
||||
await second;
|
||||
assert.deepEqual(picking.clickStats(), {
|
||||
gpu: 1,
|
||||
timeout: 1,
|
||||
stale: 0,
|
||||
latencies: [30, 250],
|
||||
});
|
||||
});
|
||||
|
||||
test("Navara's answer may come before the engine's click of the same gesture", async () => {
|
||||
const view = fakeView();
|
||||
const clock = timers();
|
||||
@@ -247,7 +217,6 @@ test('a GPU answer naming an instance drawn far from the click is stale', async
|
||||
const answer = picking.clickPick({ x: 10, y: 10 });
|
||||
view.emit('featureClick', { batchId: 2 });
|
||||
assert.equal((await answer).id, 'drawn-here');
|
||||
assert.equal(picking.clickStats().stale, 1);
|
||||
});
|
||||
|
||||
test('a thin line is picked within 8 px of its segments', () => {
|
||||
@@ -328,7 +297,6 @@ test('a stale GPU answer for a neighbour of the click never wins over the instan
|
||||
view.emit('featureClick', { batchId: 1 }); // stale: A again
|
||||
assert.equal((await onB).id, 'b', `gesture ${gesture}: b, not the stale a`);
|
||||
}
|
||||
assert.equal(picking.clickStats().stale, 20);
|
||||
});
|
||||
|
||||
function modelSet(id, rows, radiusPx) {
|
||||
@@ -406,7 +374,6 @@ test('a model hit is checked against its projected position within its drawn rad
|
||||
const outside = picking.clickPick({ x: 160, y: 100 });
|
||||
view.emit('featureClick', { batchId: 1 });
|
||||
assert.equal(await outside, undefined);
|
||||
assert.equal(picking.clickStats().stale, 1);
|
||||
// The CPU pick sees the model at its drawn radius, not the 24 px minimum.
|
||||
assert.equal(picking.drillPick({ x: 121, y: 100 })[0]?.id, 'jet');
|
||||
assert.equal(picking.drillPick({ x: 125, y: 100 }).length, 0);
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
// center + (center − blur) × amount, clamped to [0, 1].
|
||||
//
|
||||
// After tone mapping and native-stage fields such as FIRMS, before AA and
|
||||
// the ordinary W1 display overlay. Native SHARPEN_SHADER filters encoded
|
||||
// display RGB. Navara buffers stay linear, so use the exact sRGB transfer
|
||||
// before the box mask and its inverse afterward. A gamma-2.2 approximation
|
||||
// changes the colors of small, high-contrast glows even at the same amount.
|
||||
// the ordinary display overlay (overlay.js). Native SHARPEN_SHADER filters
|
||||
// encoded display RGB. Navara buffers stay linear, so use the exact sRGB
|
||||
// transfer before the box mask and its inverse afterward. A gamma-2.2
|
||||
// approximation changes the colors of small, high-contrast glows even at the
|
||||
// same amount.
|
||||
//
|
||||
// The sky is left alone: Navara's clouds are dithered (a fine grain at the
|
||||
// `low` quality the look uses), and the mask turned that grain into visible
|
||||
// noise. A pixel whose scene depth (the MRT pass's packed depth buffer, as
|
||||
// the S1 drape reads it) is the far plane is sky or cloud; it keeps its
|
||||
// the drape reads it) is the far plane is sky or cloud; it keeps its
|
||||
// colour, and costs one texture read instead of nine.
|
||||
//
|
||||
// Its own pass: a convolution reads the pass input, so merged into the tone
|
||||
@@ -25,7 +26,7 @@ import { Effect as NavaraEffect, EffectDesc } from '@navaramap/three';
|
||||
import { Effect, EffectAttribute } from 'postprocessing';
|
||||
import { RGBADepthPacking, Uniform } from 'three';
|
||||
|
||||
export const SHARPEN_EFFECT_KEY = 'gevSharpen';
|
||||
const SHARPEN_EFFECT_KEY = 'gevSharpen';
|
||||
|
||||
const fragmentShader = /* glsl */ `
|
||||
uniform float amount;
|
||||
@@ -87,9 +88,7 @@ class UnsharpMaskEffect extends Effect {
|
||||
}
|
||||
|
||||
/** config.gevSharpen: { amount } (Cesium's `amount` uniform). */
|
||||
export class SharpenEffectDesc extends withPassDisposal(
|
||||
withFrameUpdates(EffectDesc),
|
||||
) {
|
||||
class SharpenEffectDesc extends withPassDisposal(withFrameUpdates(EffectDesc)) {
|
||||
static key = SHARPEN_EFFECT_KEY;
|
||||
static insertAfter = ['toneMapping'];
|
||||
static insertBefore = ['smaa', 'fxaa', 'gevOverlay', 'final'];
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
|
||||
export const STYLES_EFFECT_KEY = 'gevStyles';
|
||||
/** Intensity under which a style draws nothing (as VisualEffects disables a stage). */
|
||||
export const STYLE_VISIBLE_INTENSITY = 0.001;
|
||||
const STYLE_VISIBLE_INTENSITY = 0.001;
|
||||
|
||||
const VERTEX_SHADER = /* glsl */ `precision highp float;
|
||||
in vec3 position;
|
||||
|
||||
+21
-27
@@ -1,4 +1,4 @@
|
||||
// GlobeSurface on Navara (S2): the height of the rendered scene is a CPU ray
|
||||
// GlobeSurface on Navara: the height of the rendered scene is a CPU ray
|
||||
// straight down the ellipsoid normal against the 3D-tile meshes Navara shows.
|
||||
// Where the shown tiles do not reach (outside the frustum), the loaded
|
||||
// tiles Navara hides answer, the finest first. Tile positions are float32
|
||||
@@ -187,15 +187,14 @@ const NO_EXCLUDE = new Set();
|
||||
// the top one among them. Tiles the ray passes are tried finest first, and
|
||||
// coarser ones stop being tried once a hit sets the limit, so the coarse
|
||||
// parents' triangles are never walked.
|
||||
// With `includeShown` (lod 'finest') the shown tiles take part too.
|
||||
const hiddenTiles = [];
|
||||
function finestHiddenHit(roots, exclude, ray, includeShown = false) {
|
||||
function finestHiddenHit(roots, exclude, ray) {
|
||||
worldRay.origin.set(ray.origin.x, ray.origin.y, ray.origin.z);
|
||||
worldRay.direction.set(ray.direction.x, ray.direction.y, ray.direction.z);
|
||||
hiddenTiles.length = 0;
|
||||
for (let i = 0; i < roots.length; i++) {
|
||||
const root = roots[i];
|
||||
if ((root.visible && !includeShown) || exclude.has(root)) continue;
|
||||
if (root.visible || exclude.has(root)) continue;
|
||||
const extent = tileExtent(root);
|
||||
if (extent && worldRay.intersectsSphere(extent)) hiddenTiles.push(root);
|
||||
}
|
||||
@@ -257,33 +256,29 @@ export function createNavaraSurface(view, hooks) {
|
||||
return height === undefined ? undefined : { height, source: 'terrain' };
|
||||
};
|
||||
const sampleHeightDetailed = (place, options) => {
|
||||
const { exclude, lod } = normalizeSurfaceOptions(options);
|
||||
const { exclude } = normalizeSurfaceOptions(options);
|
||||
if (!supported) return terrainDetail(place);
|
||||
const ray = surfaceRay(place);
|
||||
const roots = hooks.tileRoots();
|
||||
const excluded = exclude.length ? new Set(exclude) : NO_EXCLUDE;
|
||||
let hit = { distance: Infinity, root: null };
|
||||
if (lod === 'visible') {
|
||||
// The tiles on screen first; where they do not reach (the ground
|
||||
// under a camera looking ahead is outside the frustum), the hidden
|
||||
// ones.
|
||||
shownMeshes.length = 0;
|
||||
shownRoots.length = 0;
|
||||
for (let i = 0; i < roots.length; i++)
|
||||
collectMeshes(
|
||||
roots[i],
|
||||
excluded,
|
||||
true,
|
||||
shownMeshes,
|
||||
shownRoots,
|
||||
roots[i],
|
||||
);
|
||||
hit = nearestHit(shownMeshes, shownRoots, ray);
|
||||
shownMeshes.length = 0;
|
||||
shownRoots.length = 0;
|
||||
}
|
||||
// The tiles on screen first; where they do not reach (the ground under
|
||||
// a camera looking ahead is outside the frustum), the hidden ones.
|
||||
shownMeshes.length = 0;
|
||||
shownRoots.length = 0;
|
||||
for (let i = 0; i < roots.length; i++)
|
||||
collectMeshes(
|
||||
roots[i],
|
||||
excluded,
|
||||
true,
|
||||
shownMeshes,
|
||||
shownRoots,
|
||||
roots[i],
|
||||
);
|
||||
let hit = nearestHit(shownMeshes, shownRoots, ray);
|
||||
shownMeshes.length = 0;
|
||||
shownRoots.length = 0;
|
||||
if (!Number.isFinite(hit.distance))
|
||||
hit = finestHiddenHit(roots, excluded, ray, lod === 'finest');
|
||||
hit = finestHiddenHit(roots, excluded, ray);
|
||||
const height = heightAlongRay(ray, hit.distance);
|
||||
if (height === undefined) return terrainDetail(place);
|
||||
return { height, source: 'tiles', tileSizeM: tileSizeOf(hit.root) };
|
||||
@@ -342,7 +337,6 @@ export function createNavaraSurface(view, hooks) {
|
||||
return {
|
||||
surface: {
|
||||
raycast,
|
||||
raycastSupported: supported,
|
||||
sampleHeight,
|
||||
sampleHeightDetailed,
|
||||
supported,
|
||||
|
||||
@@ -230,15 +230,12 @@ test('sampleHeightDetailed names the source and the size of the answering tile',
|
||||
assert.equal(shown.source, 'tiles');
|
||||
near(shown.height, 43, 0.05, 'the shown tile');
|
||||
near(shown.tileSizeM, 600 * Math.SQRT2, 1, 'its size');
|
||||
const finest = surface.sampleHeightDetailed(place, { lod: 'finest' });
|
||||
near(finest.height, 40, 0.05, 'the finest loaded tile');
|
||||
near(finest.tileSizeM, 200 * Math.SQRT2, 1, 'its size');
|
||||
assert.deepEqual(
|
||||
surface.sampleHeightDetailed(place, { exclude: [coarse.tile, fine.tile] }),
|
||||
{ height: 7, source: 'terrain' },
|
||||
);
|
||||
assert.throws(
|
||||
() => surface.sampleHeightDetailed(place, { lod: 'best' }),
|
||||
() => surface.sampleHeightDetailed(place, { exclude: 'x' }),
|
||||
TypeError,
|
||||
);
|
||||
const bare = setup({ tiles: [] }).surface;
|
||||
|
||||
@@ -97,7 +97,6 @@ test('invalid and unsupported surface rays never invent terrain intersections',
|
||||
undefined,
|
||||
);
|
||||
const missing = fixture(t, { supported: false });
|
||||
assert.equal(missing.surface.raycastSupported, false);
|
||||
assert.equal(missing.surface.raycast(missing.ray), undefined);
|
||||
assert.equal(missing.terrainQueries(), 0);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// How the product builds its Navara view (S4, S5): render on demand (the
|
||||
// engine's render.setContinuous turns animation on), MSAA 4 as Cesium's
|
||||
// msaaSamples, the device pixel ratio unless a measurement pins one, a canvas
|
||||
// whose WebGL2 context keeps its drawing buffer (Cesium's
|
||||
// contextOptions.webgl.preserveDrawingBuffer, for captures), and a look:
|
||||
// `cinematic` by default (navaraScenePlan below), Navara's own `photoreal`
|
||||
// scene (aerial perspective, tone mapping and SMAA, about 5 ms a frame, S5)
|
||||
// with `?look=photoreal`, and `minimal` (sky, stars, sky light and sun
|
||||
// How the product builds its Navara view: render on demand (the engine's
|
||||
// render.setContinuous turns animation on), MSAA 4 as Cesium's msaaSamples,
|
||||
// the device pixel ratio, a canvas whose WebGL2 context keeps its drawing
|
||||
// buffer (Cesium's contextOptions.webgl.preserveDrawingBuffer, for captures),
|
||||
// and a look: `cinematic` by default (navaraScenePlan below), Navara's own
|
||||
// `photoreal` scene (aerial perspective, tone mapping and SMAA, about 5 ms a
|
||||
// frame) with `?look=photoreal`, and `minimal` (sky, stars, sky light and sun
|
||||
// without post effects) for benches.
|
||||
|
||||
/** WebGL2 attributes Navara 0.2.1 asks for, plus preserveDrawingBuffer. */
|
||||
@@ -21,14 +20,13 @@ export const NAVARA_CONTEXT_ATTRIBUTES = Object.freeze({
|
||||
|
||||
const MSAA_SAMPLES = 4;
|
||||
|
||||
/** ThreeView options; `pixelRatio` only when a measurement pins it. */
|
||||
export function navaraViewOptions({ container, canvas, pixelRatio } = {}) {
|
||||
/** ThreeView options. */
|
||||
export function navaraViewOptions({ container, canvas } = {}) {
|
||||
return {
|
||||
container,
|
||||
...(canvas ? { canvas } : {}),
|
||||
animation: false,
|
||||
multisampling: MSAA_SAMPLES,
|
||||
...(pixelRatio !== undefined ? { pixelRatio } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user