refactor(camera,data): take vector maths from src/geo

Move the Cartesian3/Cartographic/Matrix/Quaternion/Transforms/Math calls of
the first batch (src/data motion, anchor, tracked-camera and icon helpers;
the root camera, locations, HUD, share-link, world-focus, ground-guard,
orbit and cloud modules; services/groundSnap) to src/geo. Behaviour is
unchanged: same argument order, same `result` semantics.

Kept on Cesium: out-of-scope classes (JulianDate, Axis, HeadingPitchRange,
BoundingSphere, Rectangle, EasingFunction, EllipsoidalOccluder with its
WGS84 and seed vector, SceneTransforms) and Matrix4.IDENTITY wherever it is
handed straight to a Cesium camera call (locations.test pins its identity).

synthesizeForwardKinematicsFix now returns a plain position, so the two
snapshot renderers clone it with the static Cesium.Cartesian3.clone instead
of the instance method. The package-boundary groups that reach these
modules now own the src/geo files.
This commit is contained in:
German Khokhlov committed 2026-10-15 00:15:59 +04:00
1 parent 4a92ed8377
commit d886be68a8
17 files changed
+481 -285

No files matched your search

+210 -14
View File
@@ -668,7 +668,21 @@
"src/ui/localSdrControls.js", "src/ui/localSdrControls.js",
"src/layers/localAdsb/feedNames.js", "src/layers/localAdsb/feedNames.js",
"src/ui/localSdrPresentation.js", "src/ui/localSdrPresentation.js",
"src/data/cameraArrival.js" "src/data/cameraArrival.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -828,7 +842,21 @@
"src/layers/flights/snapshotRenderer.js", "src/layers/flights/snapshotRenderer.js",
"src/layers/flights/modelSpec.js", "src/layers/flights/modelSpec.js",
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/data/modelEligibility.js" "src/data/modelEligibility.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -867,7 +895,21 @@
"src/layers/military/recordPolicy.js", "src/layers/military/recordPolicy.js",
"src/layers/military/snapshotRenderer.js", "src/layers/military/snapshotRenderer.js",
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/data/modelEligibility.js" "src/data/modelEligibility.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -894,7 +936,21 @@
"src/data/trackingClickGesture.js", "src/data/trackingClickGesture.js",
"src/layers/aircraft/classification.js", "src/layers/aircraft/classification.js",
"src/layers/aircraft/index.js", "src/layers/aircraft/index.js",
"src/data/inputOwnership.js" "src/data/inputOwnership.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -920,7 +976,21 @@
"src/layers/vessels/records.js", "src/layers/vessels/records.js",
"src/layers/vessels/recordPolicy.js", "src/layers/vessels/recordPolicy.js",
"src/layers/vessels/snapshotRenderer.js", "src/layers/vessels/snapshotRenderer.js",
"src/data/inputOwnership.js" "src/data/inputOwnership.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -966,7 +1036,21 @@
"src/layers/firms/source.js", "src/layers/firms/source.js",
"src/layers/firms/state.js", "src/layers/firms/state.js",
"src/layers/firms/viewport.js", "src/layers/firms/viewport.js",
"src/data/inputOwnership.js" "src/data/inputOwnership.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1081,7 +1165,21 @@
"src/layers/awareness/state.js", "src/layers/awareness/state.js",
"src/layers/awareness/subject.js", "src/layers/awareness/subject.js",
"src/layers/awareness/testing.js", "src/layers/awareness/testing.js",
"src/ui/panelRailGeometry.js" "src/ui/panelRailGeometry.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -1186,7 +1284,21 @@
"src/overlays/worldOverlayTokens.js", "src/overlays/worldOverlayTokens.js",
"src/data/cctvFootprint.js", "src/data/cctvFootprint.js",
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/layers/cctv/sourcePolicy.js" "src/layers/cctv/sourcePolicy.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["hls.js", "cesium"] "external": ["hls.js", "cesium"]
}, },
@@ -1287,7 +1399,21 @@
"src/layers/radio/tuningNoise.js", "src/layers/radio/tuningNoise.js",
"src/layers/radio/volume.js", "src/layers/radio/volume.js",
"src/ui/radioTunerModel.js", "src/ui/radioTunerModel.js",
"src/data/inputOwnership.js" "src/data/inputOwnership.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium"] "external": ["cesium"]
}, },
@@ -2209,7 +2335,21 @@
"src/tools/functions.js", "src/tools/functions.js",
"src/voice/actionSchemas.js", "src/voice/actionSchemas.js",
"src/view/index.js", "src/view/index.js",
"src/app/embed.js" "src/app/embed.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": [ "external": [
"hls.js", "hls.js",
@@ -2736,7 +2876,21 @@
"src/data/local_data/osm_military_names/names.json", "src/data/local_data/osm_military_names/names.json",
"src/layers/installations/namedMarkers.js", "src/layers/installations/namedMarkers.js",
"src/data/infrastructureOverlayEntry.js", "src/data/infrastructureOverlayEntry.js",
"src/sources/infrastructureData.js" "src/sources/infrastructureData.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": [ "external": [
"hls.js", "hls.js",
@@ -2801,7 +2955,21 @@
"src/sources/overpassFeatures.js", "src/sources/overpassFeatures.js",
"src/sources/featureSource.js", "src/sources/featureSource.js",
"src/sources/capability.js", "src/sources/capability.js",
"src/data/cameraArrival.js" "src/data/cameraArrival.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": [ "external": [
"cesium", "cesium",
@@ -3238,7 +3406,21 @@
"src/sources/overpassFeaturesRecords.js", "src/sources/overpassFeaturesRecords.js",
"src/sources/overpassFeatures.js", "src/sources/overpassFeatures.js",
"src/sources/featureSource.js", "src/sources/featureSource.js",
"src/sources/capability.js" "src/sources/capability.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium", "egm96-universal", "pbf"] "external": ["cesium", "egm96-universal", "pbf"]
}, },
@@ -3275,7 +3457,21 @@
"scripts/qa-transit-controls.mjs", "scripts/qa-transit-controls.mjs",
"scripts/qa-transit-browser.mjs", "scripts/qa-transit-browser.mjs",
"src/layers/transit/source.js", "src/layers/transit/source.js",
"src/sources/transitHistory.js" "src/sources/transitHistory.js",
"src/geo/index.js",
"src/geo/math.js",
"src/geo/ellipsoid.js",
"src/geo/cartographic.js",
"src/geo/cartesian2.js",
"src/geo/cartesian3.js",
"src/geo/matrix3.js",
"src/geo/matrix4.js",
"src/geo/quaternion.js",
"src/geo/transforms.js",
"src/geo/scaleToGeodeticSurface.js",
"src/geo/check.js",
"src/geo/debug.js",
"src/geo/developerError.js"
], ],
"external": ["cesium", "pbf"] "external": ["cesium", "pbf"]
}, },
+16 -15
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from './geo/index.js';
/** /**
* Camera presets for notable locations. * Camera presets for notable locations.
@@ -6,26 +7,26 @@ import * as Cesium from 'cesium';
*/ */
export const CAMERA_PRESETS = { export const CAMERA_PRESETS = {
austin: { austin: {
destination: Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 800), destination: geo.cartesian3.fromDegrees(-97.7431, 30.2672, 800),
orientation: { orientation: {
heading: Cesium.Math.toRadians(0), heading: geo.math.toRadians(0),
pitch: Cesium.Math.toRadians(-35), pitch: geo.math.toRadians(-35),
roll: 0.0, roll: 0.0,
}, },
}, },
sf: { sf: {
destination: Cesium.Cartesian3.fromDegrees(-122.4194, 37.7749, 1000), destination: geo.cartesian3.fromDegrees(-122.4194, 37.7749, 1000),
orientation: { orientation: {
heading: Cesium.Math.toRadians(30), heading: geo.math.toRadians(30),
pitch: Cesium.Math.toRadians(-30), pitch: geo.math.toRadians(-30),
roll: 0.0, roll: 0.0,
}, },
}, },
nyc: { nyc: {
destination: Cesium.Cartesian3.fromDegrees(-73.9857, 40.7484, 1200), destination: geo.cartesian3.fromDegrees(-73.9857, 40.7484, 1200),
orientation: { orientation: {
heading: Cesium.Math.toRadians(-20), heading: geo.math.toRadians(-20),
pitch: Cesium.Math.toRadians(-30), pitch: geo.math.toRadians(-30),
roll: 0.0, roll: 0.0,
}, },
}, },
@@ -53,10 +54,10 @@ export function flyToPreset(viewer, presetName, duration = 3.0) {
export function flyToAustin(viewer) { export function flyToAustin(viewer) {
// Start from a high altitude, then fly down // Start from a high altitude, then fly down
viewer.camera.setView({ viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 25000), destination: geo.cartesian3.fromDegrees(-97.7431, 30.2672, 25000),
orientation: { orientation: {
heading: Cesium.Math.toRadians(0), heading: geo.math.toRadians(0),
pitch: Cesium.Math.toRadians(-90), pitch: geo.math.toRadians(-90),
roll: 0.0, roll: 0.0,
}, },
}); });
@@ -65,10 +66,10 @@ export function flyToAustin(viewer) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (viewer.isDestroyed()) return; if (viewer.isDestroyed()) return;
viewer.camera.flyTo({ viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(-97.7431, 30.2672, 600), destination: geo.cartesian3.fromDegrees(-97.7431, 30.2672, 600),
orientation: { orientation: {
heading: Cesium.Math.toRadians(15), heading: geo.math.toRadians(15),
pitch: Cesium.Math.toRadians(-30), pitch: geo.math.toRadians(-30),
roll: 0.0, roll: 0.0,
}, },
duration: 4.0, duration: 4.0,
+6 -5
View File
@@ -15,6 +15,7 @@
* held, so the correction reads as the shot settling. * held, so the correction reads as the shot settling.
*/ */
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from './geo/index.js';
import { ownCameraArrival } from './data/cameraArrival.js'; import { ownCameraArrival } from './data/cameraArrival.js';
/** Minimum eye height above the rendered surface for a usable view. */ /** Minimum eye height above the rendered surface for a usable view. */
@@ -25,7 +26,7 @@ export const GUARD_INTERVAL_MS = 600;
/** Ignore sub-metre noise rather than nudging the camera forever. */ /** Ignore sub-metre noise rather than nudging the camera forever. */
const GUARD_EPSILON_M = 2; const GUARD_EPSILON_M = 2;
const probeScratch = new Cesium.Cartographic(); const probeScratch = geo.cartographic.create();
/** /**
* Highest rendered surface across a few cells, with how many cells answered. * Highest rendered surface across a few cells, with how many cells answered.
@@ -41,7 +42,7 @@ function sampleSurfaceM(scene, cells) {
for (const cell of cells) { for (const cell of cells) {
try { try {
const height = scene.sampleHeight( const height = scene.sampleHeight(
Cesium.Cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch), geo.cartographic.fromDegrees(cell.lon, cell.lat, 0, probeScratch),
); );
if (!Number.isFinite(height)) continue; if (!Number.isFinite(height)) continue;
sampled += 1; sampled += 1;
@@ -152,8 +153,8 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
const carto = camera.positionCartographic; const carto = camera.positionCartographic;
// The eye must clear both the framed target and the ground beneath it. // The eye must clear both the framed target and the ground beneath it.
const below = { const below = {
lat: Cesium.Math.toDegrees(carto.latitude), lat: geo.math.toDegrees(carto.latitude),
lon: Cesium.Math.toDegrees(carto.longitude), lon: geo.math.toDegrees(carto.longitude),
}; };
const { heightM, sampled } = sampleSurfaceM(viewer.scene, [ const { heightM, sampled } = sampleSurfaceM(viewer.scene, [
{ lat: at.lat, lon: at.lon }, { lat: at.lat, lon: at.lon },
@@ -168,7 +169,7 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
if (deficit > 0) { if (deficit > 0) {
stop(); stop();
camera.flyTo({ camera.flyTo({
destination: Cesium.Cartesian3.fromRadians( destination: geo.cartesian3.fromRadians(
carto.longitude, carto.longitude,
carto.latitude, carto.latitude,
carto.height + deficit, carto.height + deficit,
+97 -105
View File
@@ -16,6 +16,7 @@
*/ */
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from './geo/index.js';
import { import {
holdContinuousRender, holdContinuousRender,
releaseContinuousRender, releaseContinuousRender,
@@ -36,8 +37,8 @@ const ONCE = {
panViewFraction: 0.25, panViewFraction: 0.25,
durationS: 0.9, durationS: 0.9,
}; };
const PITCH_MIN = Cesium.Math.toRadians(-89); const PITCH_MIN = geo.math.toRadians(-89);
const PITCH_MAX = Cesium.Math.toRadians(-5); const PITCH_MAX = geo.math.toRadians(-5);
/* ── Route dolly: cinematic tuning ────────────────────────────────────────── /* ── Route dolly: cinematic tuning ──────────────────────────────────────────
* Every knob an owner may want to retune lives in this block. The shaping is * Every knob an owner may want to retune lives in this block. The shaping is
@@ -296,30 +297,30 @@ export const ROUTE_CINEMA = Object.freeze({
/* ── Route dolly geometry ────────────────────────────────────────────────── */ /* ── Route dolly geometry ────────────────────────────────────────────────── */
const _arcA = new Cesium.Cartesian3(); const _arcA = geo.cartesian3.create();
const _arcB = new Cesium.Cartesian3(); const _arcB = geo.cartesian3.create();
const _chordVertical = new Cesium.Cartesian3(); const _chordVertical = geo.cartesian3.create();
const _turnCross = new Cesium.Cartesian3(); const _turnCross = geo.cartesian3.create();
const _framePos = new Cesium.Cartesian3(); const _framePos = geo.cartesian3.create();
const _frameAheadPos = new Cesium.Cartesian3(); const _frameAheadPos = geo.cartesian3.create();
const _frameUp = new Cesium.Cartesian3(); const _frameUp = geo.cartesian3.create();
const _frameIn = new Cesium.Cartesian3(); const _frameIn = geo.cartesian3.create();
const _frameOut = new Cesium.Cartesian3(); const _frameOut = geo.cartesian3.create();
const _frameGaze = new Cesium.Cartesian3(); const _frameGaze = geo.cartesian3.create();
const _frameRight = new Cesium.Cartesian3(); const _frameRight = geo.cartesian3.create();
const _frameLevelUp = new Cesium.Cartesian3(); const _frameLevelUp = geo.cartesian3.create();
const _frameDir = new Cesium.Cartesian3(); const _frameDir = geo.cartesian3.create();
const _frameEye = new Cesium.Cartesian3(); const _frameEye = geo.cartesian3.create();
const _frameBankedUp = new Cesium.Cartesian3(); const _frameBankedUp = geo.cartesian3.create();
const _frameCarto = new Cesium.Cartographic(); const _frameCarto = geo.cartographic.create();
const _frameAheadCarto = new Cesium.Cartographic(); const _frameAheadCarto = geo.cartographic.create();
const _bankQuat = new Cesium.Quaternion(); const _bankQuat = geo.quaternion.create();
const _bankMatrix = new Cesium.Matrix3(); const _bankMatrix = geo.matrix3.create();
const _headingQuat = new Cesium.Quaternion(); const _headingQuat = geo.quaternion.create();
const _headingMatrix = new Cesium.Matrix3(); const _headingMatrix = geo.matrix3.create();
const _warmPos = new Cesium.Cartesian3(); const _warmPos = geo.cartesian3.create();
const _warmCarto = new Cesium.Cartographic(); const _warmCarto = geo.cartographic.create();
const _probeCarto = new Cesium.Cartographic(); const _probeCarto = geo.cartographic.create();
/** Point at arc length `s` along the polyline (binary search — queries jump /** Point at arc length `s` along the polyline (binary search — queries jump
* backwards for the inbound chord, so a forward cursor would not do). */ * backwards for the inbound chord, so a forward cursor would not do). */
@@ -336,23 +337,23 @@ function arcPoint(state, s, result) {
const segLen = cumM[lo + 1] - cumM[lo]; const segLen = cumM[lo + 1] - cumM[lo];
const t = const t =
segLen > 1e-6 ? Math.min(1, Math.max(0, (clamped - cumM[lo]) / segLen)) : 0; segLen > 1e-6 ? Math.min(1, Math.max(0, (clamped - cumM[lo]) / segLen)) : 0;
return Cesium.Cartesian3.lerp(pts[lo], pts[lo + 1], t, result); return geo.cartesian3.lerp(pts[lo], pts[lo + 1], t, result);
} }
/** Unit horizontal direction from arc `sA` to arc `sB`, or null when degenerate. */ /** Unit horizontal direction from arc `sA` to arc `sB`, or null when degenerate. */
function horizontalChord(state, sA, sB, up, result) { function horizontalChord(state, sA, sB, up, result) {
const a = arcPoint(state, sA, _arcA); const a = arcPoint(state, sA, _arcA);
const b = arcPoint(state, sB, _arcB); const b = arcPoint(state, sB, _arcB);
Cesium.Cartesian3.subtract(b, a, result); geo.cartesian3.subtract(b, a, result);
const vertical = Cesium.Cartesian3.multiplyByScalar( const vertical = geo.cartesian3.multiplyByScalar(
up, up,
Cesium.Cartesian3.dot(result, up), geo.cartesian3.dot(result, up),
_chordVertical, _chordVertical,
); );
Cesium.Cartesian3.subtract(result, vertical, result); geo.cartesian3.subtract(result, vertical, result);
const len = Cesium.Cartesian3.magnitude(result); const len = geo.cartesian3.magnitude(result);
if (!(len > 1e-3)) return null; if (!(len > 1e-3)) return null;
return Cesium.Cartesian3.divideByScalar(result, len, result); return geo.cartesian3.divideByScalar(result, len, result);
} }
/** /**
@@ -368,9 +369,9 @@ function horizontalChord(state, sA, sB, up, result) {
* @returns {number} Radians, + = right. * @returns {number} Radians, + = right.
*/ */
export function signedTurnRad(a, b, up) { export function signedTurnRad(a, b, up) {
const cross = Cesium.Cartesian3.cross(a, b, _turnCross); const cross = geo.cartesian3.cross(a, b, _turnCross);
const sinLeft = Cesium.Cartesian3.dot(cross, up); const sinLeft = geo.cartesian3.dot(cross, up);
const cosTurn = Cesium.Cartesian3.dot(a, b); const cosTurn = geo.cartesian3.dot(a, b);
if (cosTurn <= -1 + ANTIPODAL_EPS && Math.abs(sinLeft) < ANTIPODAL_EPS) if (cosTurn <= -1 + ANTIPODAL_EPS && Math.abs(sinLeft) < ANTIPODAL_EPS)
return Math.PI; return Math.PI;
return -Math.atan2(sinLeft, cosTurn); return -Math.atan2(sinLeft, cosTurn);
@@ -403,13 +404,13 @@ export function routeCorridorCells(
s += ROUTE_WARM_STEP_M s += ROUTE_WARM_STEP_M
) { ) {
const point = arcPoint(state, s, _warmPos); const point = arcPoint(state, s, _warmPos);
const carto = Cesium.Cartographic.fromCartesian( const carto = geo.cartographic.fromCartesian(
point, point,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_warmCarto, _warmCarto,
); );
const lat = Number(Cesium.Math.toDegrees(carto.latitude).toFixed(3)); const lat = Number(geo.math.toDegrees(carto.latitude).toFixed(3));
const lon = Number(Cesium.Math.toDegrees(carto.longitude).toFixed(3)); const lon = Number(geo.math.toDegrees(carto.longitude).toFixed(3));
const key = `${lat},${lon}`; const key = `${lat},${lon}`;
if (seen.has(key)) continue; if (seen.has(key)) continue;
seen.add(key); seen.add(key);
@@ -555,7 +556,7 @@ export function probeMeshFloorM(scene, cells) {
let sampled = 0; let sampled = 0;
for (const cell of cells) { for (const cell of cells) {
try { try {
const carto = Cesium.Cartographic.fromDegrees( const carto = geo.cartographic.fromDegrees(
cell.lon, cell.lon,
cell.lat, cell.lat,
0, 0,
@@ -604,9 +605,9 @@ export function createRouteFlight({
// the mean: routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly slower // the mean: routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly slower
// than asked. Those are shorter than the camera is tall — nothing to see. // than asked. Those are shorter than the camera is tall — nothing to see.
const durationS = Math.max(0.5, totalM / cruiseMps); const durationS = Math.max(0.5, totalM / cruiseMps);
const pathHeightM = Cesium.Cartographic.fromCartesian( const pathHeightM = geo.cartographic.fromCartesian(
pts[0], pts[0],
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_warmCarto, _warmCarto,
).height; ).height;
const state = { const state = {
@@ -702,12 +703,12 @@ export function advanceRouteFlight(state, dt) {
state.groundSpeedMps = cruiseMps * profile.speed; state.groundSpeedMps = cruiseMps * profile.speed;
const pos = arcPoint(state, traveled, _framePos); const pos = arcPoint(state, traveled, _framePos);
const carto = Cesium.Cartographic.fromCartesian( const carto = geo.cartographic.fromCartesian(
pos, pos,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_frameCarto, _frameCarto,
); );
const up = Cesium.Ellipsoid.WGS84.geodeticSurfaceNormalCartographic( const up = geo.ellipsoid.WGS84.geodeticSurfaceNormalCartographic(
carto, carto,
_frameUp, _frameUp,
); );
@@ -731,7 +732,7 @@ export function advanceRouteFlight(state, dt) {
); );
const turnRateDegS = const turnRateDegS =
inbound && outbound inbound && outbound
? Cesium.Math.toDegrees(signedTurnRad(inbound, outbound, up)) / ? geo.math.toDegrees(signedTurnRad(inbound, outbound, up)) /
ROUTE_BANK_WINDOW_S ROUTE_BANK_WINDOW_S
: 0; : 0;
const bankTarget = routeBankTargetDeg(turnRateDegS, state.reducedMotion); const bankTarget = routeBankTargetDeg(turnRateDegS, state.reducedMotion);
@@ -763,8 +764,7 @@ export function advanceRouteFlight(state, dt) {
horizontalChord(state, traveled, traveled + lookaheadM, up, _frameGaze) || horizontalChord(state, traveled, traveled + lookaheadM, up, _frameGaze) ||
outbound || outbound ||
inbound; inbound;
if (gaze && !state.headingDir) if (gaze && !state.headingDir) state.headingDir = geo.cartesian3.clone(gaze);
state.headingDir = Cesium.Cartesian3.clone(gaze);
else if (gaze) { else if (gaze) {
// Rotate the heading TOWARD the gaze about the local up, rather than // Rotate the heading TOWARD the gaze about the local up, rather than
// lerping the two vectors. A Cartesian lerp cannot cross an antipodal pair // lerping the two vectors. A Cartesian lerp cannot cross an antipodal pair
@@ -775,66 +775,62 @@ export function advanceRouteFlight(state, dt) {
const turnToGaze = signedTurnRad(state.headingDir, gaze, up); const turnToGaze = signedTurnRad(state.headingDir, gaze, up);
// signedTurnRad is + for a RIGHT turn; a positive rotation about up is a // signedTurnRad is + for a RIGHT turn; a positive rotation about up is a
// LEFT turn, so the step is negated. // LEFT turn, so the step is negated.
const rotation = Cesium.Matrix3.fromQuaternion( const rotation = geo.matrix3.fromQuaternion(
Cesium.Quaternion.fromAxisAngle(up, -turnToGaze * k, _headingQuat), geo.quaternion.fromAxisAngle(up, -turnToGaze * k, _headingQuat),
_headingMatrix, _headingMatrix,
); );
const turned = Cesium.Matrix3.multiplyByVector( const turned = geo.matrix3.multiplyByVector(
rotation, rotation,
state.headingDir, state.headingDir,
_frameDir, _frameDir,
); );
const vertical = Cesium.Cartesian3.multiplyByScalar( const vertical = geo.cartesian3.multiplyByScalar(
up, up,
Cesium.Cartesian3.dot(turned, up), geo.cartesian3.dot(turned, up),
_chordVertical, _chordVertical,
); );
Cesium.Cartesian3.subtract(turned, vertical, turned); geo.cartesian3.subtract(turned, vertical, turned);
if (Cesium.Cartesian3.magnitude(turned) > 1e-6) { if (geo.cartesian3.magnitude(turned) > 1e-6) {
Cesium.Cartesian3.normalize(turned, state.headingDir); geo.cartesian3.normalize(turned, state.headingDir);
} }
} }
// A path with no horizontal extent (identical lat/lon, differing heights) // A path with no horizontal extent (identical lat/lon, differing heights)
// has no azimuth to fly: point east so the frame stays orthonormal. // has no azimuth to fly: point east so the frame stays orthonormal.
if (!state.headingDir) { if (!state.headingDir) {
const east = Cesium.Cartesian3.fromElements(-pos.y, pos.x, 0, _frameGaze); const east = geo.cartesian3.fromElements(-pos.y, pos.x, 0, _frameGaze);
state.headingDir = state.headingDir =
Cesium.Cartesian3.magnitude(east) > 1e-6 geo.cartesian3.magnitude(east) > 1e-6
? Cesium.Cartesian3.normalize(east, new Cesium.Cartesian3()) ? geo.cartesian3.normalize(east, geo.cartesian3.create())
: Cesium.Cartesian3.fromElements(1, 0, 0, new Cesium.Cartesian3()); : geo.cartesian3.fromElements(1, 0, 0, geo.cartesian3.create());
} }
const heading = state.headingDir; const heading = state.headingDir;
// Locked pitch: heading gives the azimuth, the pitch never wanders. // Locked pitch: heading gives the azimuth, the pitch never wanders.
const pitch = Cesium.Math.toRadians(ROUTE_PITCH_DEG); const pitch = geo.math.toRadians(ROUTE_PITCH_DEG);
const direction = Cesium.Cartesian3.normalize( const direction = geo.cartesian3.normalize(
Cesium.Cartesian3.add( geo.cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(heading, Math.cos(pitch), _frameDir), geo.cartesian3.multiplyByScalar(heading, Math.cos(pitch), _frameDir),
Cesium.Cartesian3.multiplyByScalar(up, Math.sin(pitch), _frameLevelUp), geo.cartesian3.multiplyByScalar(up, Math.sin(pitch), _frameLevelUp),
_frameDir, _frameDir,
), ),
_frameDir, _frameDir,
); );
// Orthonormal camera frame, then roll it about its own forward axis: a // Orthonormal camera frame, then roll it about its own forward axis: a
// positive rotation drops the right wing, which is what a right turn wants. // positive rotation drops the right wing, which is what a right turn wants.
const right = Cesium.Cartesian3.normalize( const right = geo.cartesian3.normalize(
Cesium.Cartesian3.cross(direction, up, _frameRight), geo.cartesian3.cross(direction, up, _frameRight),
_frameRight, _frameRight,
); );
const levelUp = Cesium.Cartesian3.normalize( const levelUp = geo.cartesian3.normalize(
Cesium.Cartesian3.cross(right, direction, _frameLevelUp), geo.cartesian3.cross(right, direction, _frameLevelUp),
_frameLevelUp, _frameLevelUp,
); );
const bankRad = Cesium.Math.toRadians(appliedBankDeg); const bankRad = geo.math.toRadians(appliedBankDeg);
let bankedUp = levelUp; let bankedUp = levelUp;
if (Math.abs(bankRad) > 1e-6) { if (Math.abs(bankRad) > 1e-6) {
const quat = Cesium.Quaternion.fromAxisAngle(direction, bankRad, _bankQuat); const quat = geo.quaternion.fromAxisAngle(direction, bankRad, _bankQuat);
const rotation = Cesium.Matrix3.fromQuaternion(quat, _bankMatrix); const rotation = geo.matrix3.fromQuaternion(quat, _bankMatrix);
bankedUp = Cesium.Matrix3.multiplyByVector( bankedUp = geo.matrix3.multiplyByVector(rotation, levelUp, _frameBankedUp);
rotation,
levelUp,
_frameBankedUp,
);
} }
// Keep the corridor ahead of the camera warm. The batch dedupes to coarse // Keep the corridor ahead of the camera warm. The batch dedupes to coarse
@@ -850,19 +846,19 @@ export function advanceRouteFlight(state, dt) {
let sampledFloor = Number.NaN; let sampledFloor = Number.NaN;
if (typeof state.floorFn === 'function') { if (typeof state.floorFn === 'function') {
const aheadPos = arcPoint(state, traveled + lookaheadM / 2, _frameAheadPos); const aheadPos = arcPoint(state, traveled + lookaheadM / 2, _frameAheadPos);
const aheadCarto = Cesium.Cartographic.fromCartesian( const aheadCarto = geo.cartographic.fromCartesian(
aheadPos, aheadPos,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_frameAheadCarto, _frameAheadCarto,
); );
try { try {
const here = state.floorFn( const here = state.floorFn(
Cesium.Math.toDegrees(carto.latitude), geo.math.toDegrees(carto.latitude),
Cesium.Math.toDegrees(carto.longitude), geo.math.toDegrees(carto.longitude),
); );
const ahead = state.floorFn( const ahead = state.floorFn(
Cesium.Math.toDegrees(aheadCarto.latitude), geo.math.toDegrees(aheadCarto.latitude),
Cesium.Math.toDegrees(aheadCarto.longitude), geo.math.toDegrees(aheadCarto.longitude),
); );
const warm = [here, ahead].filter((value) => Number.isFinite(value)); const warm = [here, ahead].filter((value) => Number.isFinite(value));
if (warm.length) sampledFloor = Math.max(...warm); if (warm.length) sampledFloor = Math.max(...warm);
@@ -926,11 +922,11 @@ export function advanceRouteFlight(state, dt) {
); );
} }
state.appliedHeightM = heightM; state.appliedHeightM = heightM;
const eye = Cesium.Cartesian3.fromRadians( const eye = geo.cartesian3.fromRadians(
carto.longitude, carto.longitude,
carto.latitude, carto.latitude,
heightM, heightM,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_frameEye, _frameEye,
); );
@@ -1019,7 +1015,7 @@ function levelCameraRoll() {
try { try {
const cam = _viewer?.camera; const cam = _viewer?.camera;
if (!cam || typeof cam.setView !== 'function') return false; if (!cam || typeof cam.setView !== 'function') return false;
const roll = Cesium.Math.negativePiToPi(cam.roll || 0); const roll = geo.math.negativePiToPi(cam.roll || 0);
if (Math.abs(roll) < 1e-5) return false; if (Math.abs(roll) < 1e-5) return false;
cam.setView({ cam.setView({
orientation: { heading: cam.heading, pitch: cam.pitch, roll: 0 }, orientation: { heading: cam.heading, pitch: cam.pitch, roll: 0 },
@@ -1123,9 +1119,9 @@ function onTick() {
const target = _getTarget?.(_viewer); const target = _getTarget?.(_viewer);
if (!target) return; if (!target) return;
const cam2 = _viewer.camera; const cam2 = _viewer.camera;
const range = Cesium.Cartesian3.distance(cam2.positionWC, target); const range = geo.cartesian3.distance(cam2.positionWC, target);
const carto2 = Cesium.Cartographic.fromCartesian(cam2.positionWC); const carto2 = geo.cartographic.fromCartesian(cam2.positionWC);
const tCarto2 = Cesium.Cartographic.fromCartesian(target); const tCarto2 = geo.cartographic.fromCartesian(target);
const dh2 = carto2.height - tCarto2.height; const dh2 = carto2.height - tCarto2.height;
m.target = target; m.target = target;
m.hpr = new Cesium.HeadingPitchRange( m.hpr = new Cesium.HeadingPitchRange(
@@ -1140,12 +1136,11 @@ function onTick() {
} }
if (m.kind === 'orbit') { if (m.kind === 'orbit') {
const rate = const rate =
Cesium.Math.toRadians(ORBIT_DEG_S[m.speed]) * geo.math.toRadians(ORBIT_DEG_S[m.speed]) *
(m.direction === 'left' ? -1 : 1); (m.direction === 'left' ? -1 : 1);
if (m.mode === 'once') { if (m.mode === 'once') {
const total = const total =
Cesium.Math.toRadians(ONCE.orbitDeg) * geo.math.toRadians(ONCE.orbitDeg) * (m.direction === 'left' ? -1 : 1);
(m.direction === 'left' ? -1 : 1);
m.hpr.heading = m.hprStartHeading + total * scale; m.hpr.heading = m.hprStartHeading + total * scale;
} else { } else {
m.hpr.heading += rate * dt; m.hpr.heading += rate * dt;
@@ -1166,11 +1161,11 @@ function onTick() {
const degS = TILT_DEG_S[m.speed]; const degS = TILT_DEG_S[m.speed];
const stepRad = const stepRad =
m.mode === 'once' m.mode === 'once'
? Cesium.Math.toRadians( ? geo.math.toRadians(
m.kind === 'tilt' ? ONCE.tiltDeg : ONCE.rotateDeg, m.kind === 'tilt' ? ONCE.tiltDeg : ONCE.rotateDeg,
) * ) *
(scale - (m.lastScale || 0)) (scale - (m.lastScale || 0))
: Cesium.Math.toRadians(degS) * dt; : geo.math.toRadians(degS) * dt;
m.lastScale = scale; m.lastScale = scale;
if (m.kind === 'tilt') { if (m.kind === 'tilt') {
const up = m.direction === 'up'; const up = m.direction === 'up';
@@ -1298,7 +1293,7 @@ export function moveCamera(args = {}, runNavigation = null) {
// Honest limits: at the clamp a tilt is a no-op — say so instead of // Honest limits: at the clamp a tilt is a no-op — say so instead of
// reporting success and letting the model gaslight itself (field test). // reporting success and letting the model gaslight itself (field test).
const pitch = _viewer.camera.pitch; const pitch = _viewer.camera.pitch;
if (direction === 'up' && pitch >= PITCH_MAX - Cesium.Math.toRadians(0.5)) { if (direction === 'up' && pitch >= PITCH_MAX - geo.math.toRadians(0.5)) {
return { return {
ok: false, ok: false,
action: 'move_camera', action: 'move_camera',
@@ -1306,10 +1301,7 @@ export function moveCamera(args = {}, runNavigation = null) {
'Already at the upper tilt limit (near the horizon) — tilt down, or zoom/pan instead.', 'Already at the upper tilt limit (near the horizon) — tilt down, or zoom/pan instead.',
}; };
} }
if ( if (direction === 'down' && pitch <= PITCH_MIN + geo.math.toRadians(0.5)) {
direction === 'down' &&
pitch <= PITCH_MIN + Cesium.Math.toRadians(0.5)
) {
return { return {
ok: false, ok: false,
action: 'move_camera', action: 'move_camera',
@@ -1389,10 +1381,10 @@ export function moveCamera(args = {}, runNavigation = null) {
}; };
} }
const cam = _viewer.camera; const cam = _viewer.camera;
const range = Cesium.Cartesian3.distance(cam.positionWC, target); const range = geo.cartesian3.distance(cam.positionWC, target);
// Pitch from geometry so lookAt reproduces the CURRENT framing exactly. // Pitch from geometry so lookAt reproduces the CURRENT framing exactly.
const carto = Cesium.Cartographic.fromCartesian(cam.positionWC); const carto = geo.cartographic.fromCartesian(cam.positionWC);
const tCarto = Cesium.Cartographic.fromCartesian(target); const tCarto = geo.cartographic.fromCartesian(target);
const dh = carto.height - tCarto.height; const dh = carto.height - tCarto.height;
const pitch = -Math.asin( const pitch = -Math.asin(
Math.min(1, Math.max(-1, dh / Math.max(1, range))), Math.min(1, Math.max(-1, dh / Math.max(1, range))),
@@ -1505,11 +1497,11 @@ export function flyRoute(
}; };
} }
const pts = route.path.map((p) => const pts = route.path.map((p) =>
Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height || 0), geo.cartesian3.fromDegrees(p.lon, p.lat, p.height || 0),
); );
const cumM = [0]; const cumM = [0];
for (let i = 1; i < pts.length; i += 1) { for (let i = 1; i < pts.length; i += 1) {
cumM.push(cumM[i - 1] + Cesium.Cartesian3.distance(pts[i - 1], pts[i])); cumM.push(cumM[i - 1] + geo.cartesian3.distance(pts[i - 1], pts[i]));
} }
if (!Number.isFinite(cumM.at(-1)) || cumM.at(-1) <= 0) { if (!Number.isFinite(cumM.at(-1)) || cumM.at(-1) <= 0) {
return { return {
+8 -8
View File
@@ -1,5 +1,5 @@
import { applicationServices } from './services/application.js'; import { applicationServices } from './services/application.js';
import * as Cesium from 'cesium'; import * as geo from './geo/index.js';
import { import {
deriveWeatherEffectProfile, deriveWeatherEffectProfile,
weatherAltitudeFactors, weatherAltitudeFactors,
@@ -133,10 +133,10 @@ function greatCircleM(a, b) {
) )
) )
return Infinity; return Infinity;
const latitudeA = Cesium.Math.toRadians(a.latitude); const latitudeA = geo.math.toRadians(a.latitude);
const latitudeB = Cesium.Math.toRadians(b.latitude); const latitudeB = geo.math.toRadians(b.latitude);
const latitudeDelta = Cesium.Math.toRadians(b.latitude - a.latitude); const latitudeDelta = geo.math.toRadians(b.latitude - a.latitude);
const longitudeDelta = Cesium.Math.toRadians(b.longitude - a.longitude); const longitudeDelta = geo.math.toRadians(b.longitude - a.longitude);
const haversine = const haversine =
Math.sin(latitudeDelta / 2) ** 2 + Math.sin(latitudeDelta / 2) ** 2 +
Math.cos(latitudeA) * Math.cos(latitudeA) *
@@ -342,8 +342,8 @@ export class CockpitCloudEffectsController {
cameraPoint() { cameraPoint() {
const cartographic = this.viewer?.camera?.positionCartographic; const cartographic = this.viewer?.camera?.positionCartographic;
if (!cartographic) return null; if (!cartographic) return null;
const latitude = Cesium.Math.toDegrees(cartographic.latitude); const latitude = geo.math.toDegrees(cartographic.latitude);
const longitude = Cesium.Math.toDegrees(cartographic.longitude); const longitude = geo.math.toDegrees(cartographic.longitude);
if (![latitude, longitude].every(Number.isFinite)) return null; if (![latitude, longitude].every(Number.isFinite)) return null;
return { return {
latitude, latitude,
@@ -541,7 +541,7 @@ export class CockpitCloudEffectsController {
render(timeSec) { render(timeSec) {
const gl = this.gl; const gl = this.gl;
if (!gl || !this.program || !this.locations) return; if (!gl || !this.program || !this.locations) return;
const windRadians = Cesium.Math.toRadians(this.windDirectionDeg); const windRadians = geo.math.toRadians(this.windDirectionDeg);
const windScale = 0.008 + this.windStrength * 0.022; const windScale = 0.008 + this.windStrength * 0.022;
gl.useProgram(this.program); gl.useProgram(this.program);
gl.uniform2f( gl.uniform2f(
+22 -21
View File
@@ -22,6 +22,7 @@
* unverified math swap, decides whether exact projection becomes preferred. * unverified math swap, decides whether exact projection becomes preferred.
*/ */
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from '../geo/index.js';
/** Meters used to give the course vector a stable projection magnitude. */ /** Meters used to give the course vector a stable projection magnitude. */
const FORWARD_PROBE_M = 2000; const FORWARD_PROBE_M = 2000;
@@ -31,11 +32,11 @@ const FORWARD_PROBE_M = 2000;
*/ */
const MIN_SCREEN_COMPONENT_M = 0.5; const MIN_SCREEN_COMPONENT_M = 0.5;
/** Ignore sub-degree projection noise while retaining deliberate camera-orbit rotation. */ /** Ignore sub-degree projection noise while retaining deliberate camera-orbit rotation. */
const ROTATION_DEADBAND_RAD = Cesium.Math.toRadians(0.5); const ROTATION_DEADBAND_RAD = geo.math.toRadians(0.5);
const _scratchEnu = new Cesium.Matrix4(); const _scratchEnu = geo.matrix4.create();
const _scratchForward = new Cesium.Cartesian3(); const _scratchForward = geo.cartesian3.create();
const _scratchWorldForward = new Cesium.Cartesian3(); const _scratchWorldForward = geo.cartesian3.create();
/** /**
* Computes the billboard rotation (radians, CCW-positive, for * Computes the billboard rotation (radians, CCW-positive, for
@@ -58,19 +59,19 @@ export function screenProjectedRotation(
const camera = scene?.camera; const camera = scene?.camera;
if (!camera?.rightWC || !camera?.upWC || !position) return previous; if (!camera?.rightWC || !camera?.upWC || !position) return previous;
const courseRad = Cesium.Math.toRadians(courseDeg || 0); const courseRad = geo.math.toRadians(courseDeg || 0);
Cesium.Cartesian3.fromElements( geo.cartesian3.fromElements(
Math.sin(courseRad) * FORWARD_PROBE_M, Math.sin(courseRad) * FORWARD_PROBE_M,
Math.cos(courseRad) * FORWARD_PROBE_M, Math.cos(courseRad) * FORWARD_PROBE_M,
0, 0,
_scratchForward, _scratchForward,
); );
const enu = Cesium.Transforms.eastNorthUpToFixedFrame( const enu = geo.transforms.eastNorthUpToFixedFrame(
position, position,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_scratchEnu, _scratchEnu,
); );
Cesium.Matrix4.multiplyByPointAsVector( geo.matrix4.multiplyByPointAsVector(
enu, enu,
_scratchForward, _scratchForward,
_scratchWorldForward, _scratchWorldForward,
@@ -79,8 +80,8 @@ export function screenProjectedRotation(
// Screen x follows camera-right. Window y grows downward, the opposite of // Screen x follows camera-right. Window y grows downward, the opposite of
// camera-up. Projecting the vector itself avoids clipping/behind-camera // camera-up. Projecting the vector itself avoids clipping/behind-camera
// failure modes from the old forward-point worldToWindowCoordinates probe. // failure modes from the old forward-point worldToWindowCoordinates probe.
const dx = Cesium.Cartesian3.dot(_scratchWorldForward, camera.rightWC); const dx = geo.cartesian3.dot(_scratchWorldForward, camera.rightWC);
const dy = -Cesium.Cartesian3.dot(_scratchWorldForward, camera.upWC); const dy = -geo.cartesian3.dot(_scratchWorldForward, camera.upWC);
if (dx * dx + dy * dy < MIN_SCREEN_COMPONENT_M * MIN_SCREEN_COMPONENT_M) if (dx * dx + dy * dy < MIN_SCREEN_COMPONENT_M * MIN_SCREEN_COMPONENT_M)
return previous; return previous;
@@ -129,7 +130,7 @@ const _occluder = new Cesium.EllipsoidalOccluder(
*/ */
export const HORIZON_FEATHER_RAD = 0.019; export const HORIZON_FEATHER_RAD = 0.019;
const _wgs84OneOverRadii = Cesium.Ellipsoid.WGS84.oneOverRadii; const _wgs84OneOverRadii = geo.ellipsoid.WGS84.oneOverRadii;
/** /**
* How much SKY sits behind a world point from this camera: 0 (planet behind) * How much SKY sits behind a world point from this camera: 0 (planet behind)
@@ -271,9 +272,9 @@ export function cameraPoseSignature(camera) {
* frustum the vehicle is nowhere near. * frustum the vehicle is nowhere near.
*/ */
const NEAR_PROBE_M = 30; const NEAR_PROBE_M = 30;
const _scratchProbe = new Cesium.Cartesian3(); const _scratchProbe = geo.cartesian3.create();
const _scratchWindowAt = new Cesium.Cartesian2(); const _scratchWindowAt = geo.cartesian2.create();
const _scratchWindowAhead = new Cesium.Cartesian2(); const _scratchWindowAhead = geo.cartesian2.create();
/** /**
* Billboard rotation for a SURFACE contact, projected exactly — perspective * Billboard rotation for a SURFACE contact, projected exactly — perspective
@@ -311,24 +312,24 @@ export function perspectiveProjectedRotation(
if (!scene?.camera || !position || !Number.isFinite(courseDeg)) { if (!scene?.camera || !position || !Number.isFinite(courseDeg)) {
return previous; return previous;
} }
const courseRad = Cesium.Math.toRadians(courseDeg); const courseRad = geo.math.toRadians(courseDeg);
Cesium.Cartesian3.fromElements( geo.cartesian3.fromElements(
Math.sin(courseRad) * NEAR_PROBE_M, Math.sin(courseRad) * NEAR_PROBE_M,
Math.cos(courseRad) * NEAR_PROBE_M, Math.cos(courseRad) * NEAR_PROBE_M,
0, 0,
_scratchForward, _scratchForward,
); );
const enu = Cesium.Transforms.eastNorthUpToFixedFrame( const enu = geo.transforms.eastNorthUpToFixedFrame(
position, position,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_scratchEnu, _scratchEnu,
); );
Cesium.Matrix4.multiplyByPointAsVector( geo.matrix4.multiplyByPointAsVector(
enu, enu,
_scratchForward, _scratchForward,
_scratchWorldForward, _scratchWorldForward,
); );
Cesium.Cartesian3.add(position, _scratchWorldForward, _scratchProbe); geo.cartesian3.add(position, _scratchWorldForward, _scratchProbe);
const at = Cesium.SceneTransforms.worldToWindowCoordinates( const at = Cesium.SceneTransforms.worldToWindowCoordinates(
scene, scene,
+9 -8
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from '../geo/index.js';
/** /**
* Model-space visual centres measured from the shipped GLBs' scene-space AABBs. * Model-space visual centres measured from the shipped GLBs' scene-space AABBs.
* *
@@ -76,13 +77,13 @@ export const MODEL_TRAIL_ANCHOR_NATIVE = Object.freeze({
* renders with `modelMatrix x components.transform x axisCorrection`, and the * renders with `modelMatrix x components.transform x axisCorrection`, and the
* trail anchor has to ride the same chain or it lands on a different axis than * trail anchor has to ride the same chain or it lands on a different axis than
* the aircraft it is supposed to be attached to. */ * the aircraft it is supposed to be attached to. */
const AXIS_CORRECTION = Cesium.Matrix4.multiplyTransformation( const AXIS_CORRECTION = geo.matrix4.multiplyTransformation(
Cesium.Axis.Y_UP_TO_Z_UP, Cesium.Axis.Y_UP_TO_Z_UP,
Cesium.Axis.Z_UP_TO_X_UP, Cesium.Axis.Z_UP_TO_X_UP,
new Cesium.Matrix4(), geo.matrix4.create(),
); );
const _anchorScratch = new Cesium.Cartesian3(); const _anchorScratch = geo.cartesian3.create();
const _chainScratch = new Cesium.Matrix4(); const _chainScratch = geo.matrix4.create();
/** /**
* World position of a model-local anchor, through the SAME transform chain * World position of a model-local anchor, through the SAME transform chain
@@ -121,17 +122,17 @@ export function modelAnchorWorld(model, nativeAnchor, result) {
// correction. Read from the instance rather than assumed, so an asset with a // correction. Read from the instance rather than assumed, so an asset with a
// baked root transform is carried too. // baked root transform is carried too.
const components = model?.sceneGraph?.components?.transform; const components = model?.sceneGraph?.components?.transform;
Cesium.Matrix4.multiplyTransformation( geo.matrix4.multiplyTransformation(
model.modelMatrix, model.modelMatrix,
components || Cesium.Matrix4.IDENTITY, components || geo.matrix4.IDENTITY,
_chainScratch, _chainScratch,
); );
Cesium.Matrix4.multiplyTransformation( geo.matrix4.multiplyTransformation(
_chainScratch, _chainScratch,
AXIS_CORRECTION, AXIS_CORRECTION,
_chainScratch, _chainScratch,
); );
return Cesium.Matrix4.multiplyByPoint(_chainScratch, _anchorScratch, result); return geo.matrix4.multiplyByPoint(_chainScratch, _anchorScratch, result);
} }
/** /**
+18 -17
View File
@@ -7,10 +7,11 @@
* skylight (https://github.com/cpaczek/skylight, MIT) shared/src/aim.ts. * skylight (https://github.com/cpaczek/skylight, MIT) shared/src/aim.ts.
*/ */
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from '../geo/index.js';
const DEG = Math.PI / 180; const DEG = Math.PI / 180;
const _mmCarto = new Cesium.Cartographic(); const _mmCarto = geo.cartographic.create();
const _mmCandidateCarto = new Cesium.Cartographic(); const _mmCandidateCarto = geo.cartographic.create();
export function norm360(deg) { export function norm360(deg) {
return ((deg % 360) + 360) % 360; return ((deg % 360) + 360) % 360;
@@ -28,22 +29,22 @@ export function norm360(deg) {
*/ */
export function liftRepeatedGroundFix(newest, candidatePosition, onGround) { export function liftRepeatedGroundFix(newest, candidatePosition, onGround) {
if (!onGround || !newest?.position || !candidatePosition) return false; if (!onGround || !newest?.position || !candidatePosition) return false;
const stored = Cesium.Cartographic.fromCartesian( const stored = geo.cartographic.fromCartesian(
newest.position, newest.position,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_mmCarto, _mmCarto,
); );
const candidate = Cesium.Cartographic.fromCartesian( const candidate = geo.cartographic.fromCartesian(
candidatePosition, candidatePosition,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_mmCandidateCarto, _mmCandidateCarto,
); );
if (!stored || !candidate || candidate.height <= stored.height) return false; if (!stored || !candidate || candidate.height <= stored.height) return false;
Cesium.Cartesian3.fromRadians( geo.cartesian3.fromRadians(
stored.longitude, stored.longitude,
stored.latitude, stored.latitude,
candidate.height, candidate.height,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
newest.position, newest.position,
); );
return true; return true;
@@ -66,9 +67,9 @@ export function courseBetweenCartesians(from, to, minChordM = 25) {
const dy = to.y - from.y; const dy = to.y - from.y;
const dz = to.z - from.z; const dz = to.z - from.z;
if (dx * dx + dy * dy + dz * dz < minChordM * minChordM) return null; if (dx * dx + dy * dy + dz * dz < minChordM * minChordM) return null;
const carto = Cesium.Cartographic.fromCartesian( const carto = geo.cartographic.fromCartesian(
from, from,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_mmCarto, _mmCarto,
); );
if (!carto) return null; if (!carto) return null;
@@ -436,8 +437,8 @@ export function arcOffsetEnu(speedMps, trackDeg, turnRateDps, dtSec, result) {
} }
const _forwardFixArc = { east: 0, north: 0, endCourseDeg: 0 }; const _forwardFixArc = { east: 0, north: 0, endCourseDeg: 0 };
const _forwardFixEnu = new Cesium.Matrix4(); const _forwardFixEnu = geo.matrix4.create();
const _forwardFixOffset = new Cesium.Cartesian3(); const _forwardFixOffset = geo.cartesian3.create();
/** /**
* Create a forward-only synthetic fix when a repeated position epoch carries * Create a forward-only synthetic fix when a repeated position epoch carries
@@ -474,21 +475,21 @@ export function synthesizeForwardKinematicsFix(newest, next = {}) {
dtSec, dtSec,
_forwardFixArc, _forwardFixArc,
); );
Cesium.Cartesian3.fromElements( geo.cartesian3.fromElements(
_forwardFixArc.east, _forwardFixArc.east,
_forwardFixArc.north, _forwardFixArc.north,
0, 0,
_forwardFixOffset, _forwardFixOffset,
); );
const enu = Cesium.Transforms.eastNorthUpToFixedFrame( const enu = geo.transforms.eastNorthUpToFixedFrame(
newest.position, newest.position,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
_forwardFixEnu, _forwardFixEnu,
); );
const position = Cesium.Matrix4.multiplyByPoint( const position = geo.matrix4.multiplyByPoint(
enu, enu,
_forwardFixOffset, _forwardFixOffset,
new Cesium.Cartesian3(), geo.cartesian3.create(),
); );
return { return {
time: Cesium.JulianDate.fromDate(new Date(next.epochMs)), time: Cesium.JulianDate.fromDate(new Date(next.epochMs)),
+20 -20
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium'; import * as geo from '../geo/index.js';
const MAX_FRAME_ATTEMPTS = 120; const MAX_FRAME_ATTEMPTS = 120;
const MIN_TRACKED_RANGE_M = 150; const MIN_TRACKED_RANGE_M = 150;
@@ -59,7 +59,7 @@ export function trackedModelScaleForPixelCap({
export function trackedDisplayPositionForCamera(entity, time, result) { export function trackedDisplayPositionForCamera(entity, time, result) {
const displayedPosition = entity?.gevDisplayPosition?.(); const displayedPosition = entity?.gevDisplayPosition?.();
return displayedPosition return displayedPosition
? Cesium.Cartesian3.clone(displayedPosition, result) ? geo.cartesian3.clone(displayedPosition, result)
: entity?.position?.getValue(time, result); : entity?.position?.getValue(time, result);
} }
@@ -106,15 +106,15 @@ export function clampTrackedCameraPosition(
minimumRangeM = MIN_TRACKED_RANGE_M, minimumRangeM = MIN_TRACKED_RANGE_M,
) { ) {
const crossedOrigin = const crossedOrigin =
Cesium.Cartesian3.dot(camera.position, previousPosition) <= 0; geo.cartesian3.dot(camera.position, previousPosition) <= 0;
const forwardDistance = -Cesium.Cartesian3.dot( const forwardDistance = -geo.cartesian3.dot(
camera.position, camera.position,
camera.direction, camera.direction,
); );
const rangeSquared = Cesium.Cartesian3.magnitudeSquared(camera.position); const rangeSquared = geo.cartesian3.magnitudeSquared(camera.position);
if (crossedOrigin) { if (crossedOrigin) {
Cesium.Cartesian3.normalize(previousPosition, camera.position); geo.cartesian3.normalize(previousPosition, camera.position);
Cesium.Cartesian3.multiplyByScalar( geo.cartesian3.multiplyByScalar(
camera.position, camera.position,
minimumRangeM, minimumRangeM,
camera.position, camera.position,
@@ -125,7 +125,7 @@ export function clampTrackedCameraPosition(
forwardDistance < minimumRangeM || forwardDistance < minimumRangeM ||
rangeSquared < minimumRangeM * minimumRangeM rangeSquared < minimumRangeM * minimumRangeM
) { ) {
Cesium.Cartesian3.multiplyByScalar( geo.cartesian3.multiplyByScalar(
camera.direction, camera.direction,
-minimumRangeM, -minimumRangeM,
camera.position, camera.position,
@@ -155,11 +155,11 @@ export function clampTrackedCameraPosition(
export function applyTrackedCameraFrame(viewer, entity, viewFrom) { export function applyTrackedCameraFrame(viewer, entity, viewFrom) {
if (!viewer || !entity || !viewFrom) return; if (!viewer || !entity || !viewFrom) return;
let attempts = 0; let attempts = 0;
const trackedTransform = new Cesium.Matrix4(); const trackedTransform = geo.matrix4.create();
const trackedPosition = new Cesium.Cartesian3(); const trackedPosition = geo.cartesian3.create();
const resolvedViewFrom = new Cesium.Cartesian3(); const resolvedViewFrom = geo.cartesian3.create();
const cameraOffset = new Cesium.Cartesian3(); const cameraOffset = geo.cartesian3.create();
const previousCameraPosition = new Cesium.Cartesian3(); const previousCameraPosition = geo.cartesian3.create();
let framed = false; let framed = false;
const restoreZoomInertia = acquireStableTrackedZoom( const restoreZoomInertia = acquireStableTrackedZoom(
viewer.scene.screenSpaceCameraController, viewer.scene.screenSpaceCameraController,
@@ -201,24 +201,24 @@ export function applyTrackedCameraFrame(viewer, entity, viewFrom) {
if (attempts >= MAX_FRAME_ATTEMPTS) stop(); if (attempts >= MAX_FRAME_ATTEMPTS) stop();
return; return;
} }
const transform = Cesium.Transforms.eastNorthUpToFixedFrame( const transform = geo.transforms.eastNorthUpToFixedFrame(
position, position,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
trackedTransform, trackedTransform,
); );
const offset = const offset =
typeof viewFrom.getValue === 'function' typeof viewFrom.getValue === 'function'
? viewFrom.getValue(viewer.clock.currentTime, resolvedViewFrom) ? viewFrom.getValue(viewer.clock.currentTime, resolvedViewFrom)
: Cesium.Cartesian3.clone(viewFrom, resolvedViewFrom); : geo.cartesian3.clone(viewFrom, resolvedViewFrom);
if ( if (
!offset || !offset ||
Cesium.Cartesian3.magnitudeSquared(offset) < Cesium.Math.EPSILON12 geo.cartesian3.magnitudeSquared(offset) < geo.math.EPSILON12
) { ) {
stop(); stop();
return; return;
} }
viewer.camera.lookAtTransform(transform, offset); viewer.camera.lookAtTransform(transform, offset);
Cesium.Cartesian3.clone(viewer.camera.position, previousCameraPosition); geo.cartesian3.clone(viewer.camera.position, previousCameraPosition);
framed = true; framed = true;
return; return;
} }
@@ -227,10 +227,10 @@ export function applyTrackedCameraFrame(viewer, entity, viewFrom) {
// continuous camera writer. Only a programmatic zoom that bypasses the // continuous camera writer. Only a programmatic zoom that bypasses the
// controller's minimumZoomDistance needs correction. // controller's minimumZoomDistance needs correction.
if (clampTrackedCameraPosition(viewer.camera, previousCameraPosition)) { if (clampTrackedCameraPosition(viewer.camera, previousCameraPosition)) {
Cesium.Cartesian3.clone(viewer.camera.position, cameraOffset); geo.cartesian3.clone(viewer.camera.position, cameraOffset);
viewer.camera.lookAtTransform(viewer.camera.transform, cameraOffset); viewer.camera.lookAtTransform(viewer.camera.transform, cameraOffset);
} }
Cesium.Cartesian3.clone(viewer.camera.position, previousCameraPosition); geo.cartesian3.clone(viewer.camera.position, previousCameraPosition);
}); });
return stop; return stop;
} }
+15 -15
View File
@@ -14,7 +14,7 @@ import { applicationServices } from './services/application.js';
* Color theming is driven by the active shader mode via CSS custom properties. * Color theming is driven by the active shader mode via CSS custom properties.
*/ */
import * as Cesium from 'cesium'; import * as geo from './geo/index.js';
import { forward as toMGRS } from 'mgrs'; import { forward as toMGRS } from 'mgrs';
import { CITY_POIS } from './locations.js'; import { CITY_POIS } from './locations.js';
import { composeLocalityTag } from './hudLocality.js'; import { composeLocalityTag } from './hudLocality.js';
@@ -341,8 +341,8 @@ export class IntelHUD {
const cartographic = camera.positionCartographic; const cartographic = camera.positionCartographic;
if (!cartographic) return; if (!cartographic) return;
const lonDeg = Cesium.Math.toDegrees(cartographic.longitude); const lonDeg = geo.math.toDegrees(cartographic.longitude);
const latDeg = Cesium.Math.toDegrees(cartographic.latitude); const latDeg = geo.math.toDegrees(cartographic.latitude);
const altM = cartographic.height; const altM = cartographic.height;
const latDMS = this._toDMS(latDeg, 'lat'); const latDMS = this._toDMS(latDeg, 'lat');
const lonDMS = this._toDMS(lonDeg, 'lon'); const lonDMS = this._toDMS(lonDeg, 'lon');
@@ -407,7 +407,7 @@ export class IntelHUD {
// Off-nadir angle (ONA): camera pitch of -90 deg is nadir (straight down), // Off-nadir angle (ONA): camera pitch of -90 deg is nadir (straight down),
// so ONA = 90 + pitch gives 0 at nadir and increases toward the horizon. // so ONA = 90 + pitch gives 0 at nadir and increases toward the horizon.
const pitchDeg = Cesium.Math.toDegrees(camera.pitch); const pitchDeg = geo.math.toDegrees(camera.pitch);
const ona = Math.max(0, 90 + pitchDeg); const ona = Math.max(0, 90 + pitchDeg);
const onaEl = document.getElementById('hud-ona'); const onaEl = document.getElementById('hud-ona');
if (onaEl) onaEl.textContent = `ONA: ${ona.toFixed(1)}°`; if (onaEl) onaEl.textContent = `ONA: ${ona.toFixed(1)}°`;
@@ -512,19 +512,19 @@ export class IntelHUD {
const declination = const declination =
23.45 * 23.45 *
Math.sin( Math.sin(
Cesium.Math.toRadians( geo.math.toRadians(
(360 / 365) * (now.getUTCDate() + 30 * now.getUTCMonth() - 81), (360 / 365) * (now.getUTCDate() + 30 * now.getUTCMonth() - 81),
), ),
); );
const latRad = Cesium.Math.toRadians(lat); const latRad = geo.math.toRadians(lat);
const decRad = Cesium.Math.toRadians(declination); const decRad = geo.math.toRadians(declination);
const haRad = Cesium.Math.toRadians(hourAngle); const haRad = geo.math.toRadians(hourAngle);
// Standard formula: sin(el) = sin(lat)*sin(dec) + cos(lat)*cos(dec)*cos(ha) // Standard formula: sin(el) = sin(lat)*sin(dec) + cos(lat)*cos(dec)*cos(ha)
const sinEl = const sinEl =
Math.sin(latRad) * Math.sin(decRad) + Math.sin(latRad) * Math.sin(decRad) +
Math.cos(latRad) * Math.cos(decRad) * Math.cos(haRad); Math.cos(latRad) * Math.cos(decRad) * Math.cos(haRad);
// Clamp to [-1,1] to guard against floating-point drift before asin // Clamp to [-1,1] to guard against floating-point drift before asin
return Cesium.Math.toDegrees(Math.asin(Math.max(-1, Math.min(1, sinEl)))); return geo.math.toDegrees(Math.asin(Math.max(-1, Math.min(1, sinEl))));
} }
/** /**
@@ -571,10 +571,10 @@ export class IntelHUD {
_viewWindowKm(latDeg) { _viewWindowKm(latDeg) {
const rect = this.viewer.camera.computeViewRectangle(); const rect = this.viewer.camera.computeViewRectangle();
if (!rect) return null; if (!rect) return null;
const north = Cesium.Math.toDegrees(rect.north); const north = geo.math.toDegrees(rect.north);
const south = Cesium.Math.toDegrees(rect.south); const south = geo.math.toDegrees(rect.south);
let east = Cesium.Math.toDegrees(rect.east); let east = geo.math.toDegrees(rect.east);
let west = Cesium.Math.toDegrees(rect.west); let west = geo.math.toDegrees(rect.west);
let lonSpan = Math.abs(east - west); let lonSpan = Math.abs(east - west);
// Handle antimeridian wrap: if span exceeds 180 deg, take the shorter arc // Handle antimeridian wrap: if span exceeds 180 deg, take the shorter arc
if (lonSpan > 180) lonSpan = 360 - lonSpan; if (lonSpan > 180) lonSpan = 360 - lonSpan;
@@ -583,7 +583,7 @@ export class IntelHUD {
// longitude distance is scaled by cos(lat) to account for meridian convergence. // longitude distance is scaled by cos(lat) to account for meridian convergence.
const widthKm = Math.max( const widthKm = Math.max(
0, 0,
lonSpan * 111 * Math.cos(Cesium.Math.toRadians(latDeg)), lonSpan * 111 * Math.cos(geo.math.toRadians(latDeg)),
); );
const heightKm = Math.max(0, latSpan * 111); const heightKm = Math.max(0, latSpan * 111);
return { widthKm, heightKm }; return { widthKm, heightKm };
@@ -599,7 +599,7 @@ export class IntelHUD {
* @returns {number} Distance in kilometers. * @returns {number} Distance in kilometers.
*/ */
_haversineKm(lat1, lon1, lat2, lon2) { _haversineKm(lat1, lon1, lat2, lon2) {
const toRad = (deg) => Cesium.Math.toRadians(deg); const toRad = (deg) => geo.math.toRadians(deg);
const dLat = toRad(lat2 - lat1); const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1); const dLon = toRad(lon2 - lon1);
const a = const a =
+4 -1
View File
@@ -147,7 +147,10 @@ export function createFlightSnapshotRenderer({
history.push(synthetic); history.push(synthetic);
if (history.length > POSITION_HISTORY_LIMIT) history.shift(); if (history.length > POSITION_HISTORY_LIMIT) history.shift();
meta.turnRateDps = turnRateFromFixHistory(history); meta.turnRateDps = turnRateFromFixHistory(history);
if (isTracked) tracking._appendTrailFix(synthetic.position.clone()); if (isTracked)
tracking._appendTrailFix(
Cesium.Cartesian3.clone(synthetic.position),
);
} }
} }
} }
+4 -1
View File
@@ -118,7 +118,10 @@ export function createMilitarySnapshotRenderer({
history.push(synthetic); history.push(synthetic);
if (history.length > POSITION_HISTORY_LIMIT) history.shift(); if (history.length > POSITION_HISTORY_LIMIT) history.shift();
meta.turnRateDps = turnRateFromFixHistory(history); meta.turnRateDps = turnRateFromFixHistory(history);
if (isTracked) tracking._appendTrailFix(synthetic.position.clone()); if (isTracked)
tracking._appendTrailFix(
Cesium.Cartesian3.clone(synthetic.position),
);
} }
} }
} }
+18 -17
View File
@@ -2,6 +2,7 @@ import { isUnavailableCapability } from './sources/capability.js';
import { createOverpassFeatureSource } from './sources/overpassFeatures.js'; import { createOverpassFeatureSource } from './sources/overpassFeatures.js';
import { applicationServices } from './services/application.js'; import { applicationServices } from './services/application.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from './geo/index.js';
import { import {
viewportBias, viewportBias,
placesNearViewRecovery, placesNearViewRecovery,
@@ -543,18 +544,18 @@ export const GLOBE_VIEW = Object.freeze({
*/ */
export function flyToGlobeView(viewer, options = {}) { export function flyToGlobeView(viewer, options = {}) {
const carto = viewer.camera.positionCartographic; const carto = viewer.camera.positionCartographic;
const longitude = Cesium.Math.toDegrees(carto.longitude); const longitude = geo.math.toDegrees(carto.longitude);
const latitude = Cesium.Math.toDegrees(carto.latitude); const latitude = geo.math.toDegrees(carto.latitude);
viewer.camera.cancelFlight(); viewer.camera.cancelFlight();
viewer.camera.flyTo({ viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees( destination: geo.cartesian3.fromDegrees(
longitude, longitude,
latitude, latitude,
GLOBE_VIEW.heightM, GLOBE_VIEW.heightM,
), ),
orientation: { orientation: {
heading: 0, heading: 0,
pitch: Cesium.Math.toRadians(GLOBE_VIEW.pitchDeg), pitch: geo.math.toRadians(GLOBE_VIEW.pitchDeg),
roll: 0, roll: 0,
}, },
duration: finitePositive(options.duration) || GLOBE_VIEW.durationS, duration: finitePositive(options.duration) || GLOBE_VIEW.durationS,
@@ -614,7 +615,7 @@ export function flyToLandmark(viewer, lat, lon, options = {}) {
} = options; } = options;
// Sample terrain height (sync — uses loaded tiles; 0 if globe/terrain not ready) // Sample terrain height (sync — uses loaded tiles; 0 if globe/terrain not ready)
const targetCartographic = Cesium.Cartographic.fromDegrees(lon, lat); const targetCartographic = geo.cartographic.fromDegrees(lon, lat);
const sampledHeight = viewer.scene.globe?.getHeight(targetCartographic); const sampledHeight = viewer.scene.globe?.getHeight(targetCartographic);
// Use sampled height if available, otherwise the cached ground-floor // Use sampled height if available, otherwise the cached ground-floor
@@ -641,7 +642,7 @@ export function flyToLandmark(viewer, lat, lon, options = {}) {
const targetHeight = bounds const targetHeight = bounds
? terrainHeight + bounds.height / 2 ? terrainHeight + bounds.height / 2
: terrainHeight + buildingHeight; : terrainHeight + buildingHeight;
const targetPosition = Cesium.Cartesian3.fromDegrees(lon, lat, targetHeight); const targetPosition = geo.cartesian3.fromDegrees(lon, lat, targetHeight);
const boundingRadius = bounds ? buildingBoundingRadius(bounds) : 0; const boundingRadius = bounds ? buildingBoundingRadius(bounds) : 0;
const framingRange = bounds const framingRange = bounds
? Math.max( ? Math.max(
@@ -651,8 +652,8 @@ export function flyToLandmark(viewer, lat, lon, options = {}) {
: range; : range;
const hpr = new Cesium.HeadingPitchRange( const hpr = new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(heading), geo.math.toRadians(heading),
Cesium.Math.toRadians(pitch), geo.math.toRadians(pitch),
framingRange, framingRange,
); );
@@ -1095,7 +1096,7 @@ export function viewportMetrics(viewport) {
const latSpanKm = Math.abs(latSpanDeg) * KM_PER_DEGREE; const latSpanKm = Math.abs(latSpanDeg) * KM_PER_DEGREE;
const lonSpanKm = const lonSpanKm =
lonSpanDeg * KM_PER_DEGREE * Math.cos(Cesium.Math.toRadians(centerLat)); lonSpanDeg * KM_PER_DEGREE * Math.cos(geo.math.toRadians(centerLat));
return { return {
latSpanDeg, latSpanDeg,
lonSpanDeg, lonSpanDeg,
@@ -1157,12 +1158,12 @@ const PLACE_FALLBACK_HALF_SPAN_KM = 20;
/** Great-circle distance in km (small enough here that the spherical model is fine). */ /** Great-circle distance in km (small enough here that the spherical model is fine). */
function greatCircleKm(lat1, lng1, lat2, lng2) { function greatCircleKm(lat1, lng1, lat2, lng2) {
const dLat = Cesium.Math.toRadians(lat2 - lat1); const dLat = geo.math.toRadians(lat2 - lat1);
const dLng = Cesium.Math.toRadians(lng2 - lng1); const dLng = geo.math.toRadians(lng2 - lng1);
const a = const a =
Math.sin(dLat / 2) ** 2 + Math.sin(dLat / 2) ** 2 +
Math.cos(Cesium.Math.toRadians(lat1)) * Math.cos(geo.math.toRadians(lat1)) *
Math.cos(Cesium.Math.toRadians(lat2)) * Math.cos(geo.math.toRadians(lat2)) *
Math.sin(dLng / 2) ** 2; Math.sin(dLng / 2) ** 2;
return 6371 * 2 * Math.asin(Math.min(1, Math.sqrt(a))); return 6371 * 2 * Math.asin(Math.min(1, Math.sqrt(a)));
} }
@@ -1210,7 +1211,7 @@ export function placeFramingViewport(
const latHalfDeg = PLACE_FALLBACK_HALF_SPAN_KM / KM_PER_DEGREE; const latHalfDeg = PLACE_FALLBACK_HALF_SPAN_KM / KM_PER_DEGREE;
// Guard the cosine so a near-polar anchor cannot blow the longitude half-extent up. // Guard the cosine so a near-polar anchor cannot blow the longitude half-extent up.
const cosLat = Math.max(0.05, Math.cos(Cesium.Math.toRadians(anchorLat))); const cosLat = Math.max(0.05, Math.cos(geo.math.toRadians(anchorLat)));
const lngHalfDeg = PLACE_FALLBACK_HALF_SPAN_KM / (KM_PER_DEGREE * cosLat); const lngHalfDeg = PLACE_FALLBACK_HALF_SPAN_KM / (KM_PER_DEGREE * cosLat);
const wrapLng = (lng) => ((((lng + 180) % 360) + 360) % 360) - 180; const wrapLng = (lng) => ((((lng + 180) % 360) + 360) % 360) - 180;
return { return {
@@ -1349,7 +1350,7 @@ function flyToViewportBounds(viewer, viewport, options = {}) {
// Same short-way-round rule for the reported centre: averaging raw longitudes // Same short-way-round rule for the reported centre: averaging raw longitudes
// puts a dateline-crossing box's centre on the opposite side of the planet. // puts a dateline-crossing box's centre on the opposite side of the planet.
return { return {
targetPosition: Cesium.Cartesian3.fromDegrees( targetPosition: geo.cartesian3.fromDegrees(
metrics.centerLng, metrics.centerLng,
metrics.centerLat, metrics.centerLat,
0, 0,
@@ -1379,7 +1380,7 @@ function buildingBoundingRadius(bounds) {
function rangeForBoundingSphere(viewer, radius) { function rangeForBoundingSphere(viewer, radius) {
const frustum = viewer.camera.frustum; const frustum = viewer.camera.frustum;
const verticalFov = Number(frustum?.fov) || Cesium.Math.toRadians(60); const verticalFov = Number(frustum?.fov) || geo.math.toRadians(60);
const aspectRatio = Math.max(0.5, Number(frustum?.aspectRatio) || 1); const aspectRatio = Math.max(0.5, Number(frustum?.aspectRatio) || 1);
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * aspectRatio); const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * aspectRatio);
const limitingFov = Math.min(verticalFov, horizontalFov); const limitingFov = Math.min(verticalFov, horizontalFov);
@@ -1537,7 +1538,7 @@ function pointInPolygon(lon, lat, coordinates) {
function approximateDistanceM(latA, lonA, latB, lonB) { function approximateDistanceM(latA, lonA, latB, lonB) {
const latitudeScale = 111320; const latitudeScale = 111320;
const longitudeScale = const longitudeScale =
latitudeScale * Math.cos(Cesium.Math.toRadians((latA + latB) / 2)); latitudeScale * Math.cos(geo.math.toRadians((latA + latB) / 2));
return Math.hypot( return Math.hypot(
(latB - latA) * latitudeScale, (latB - latA) * latitudeScale,
(lonB - lonA) * longitudeScale, (lonB - lonA) * longitudeScale,
+4 -3
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from './geo/index.js';
import { import {
holdContinuousRender, holdContinuousRender,
releaseContinuousRender, releaseContinuousRender,
@@ -16,7 +17,7 @@ export class OrbitController {
this.target = null; this.target = null;
this.radius = 500; this.radius = 500;
this.pitch = -30; this.pitch = -30;
this.speed = Cesium.Math.toRadians(6); // ~6°/sec → full rotation in ~60s this.speed = geo.math.toRadians(6); // ~6°/sec → full rotation in ~60s
this.angle = 0; this.angle = 0;
this._removeListener = null; this._removeListener = null;
} }
@@ -35,7 +36,7 @@ export class OrbitController {
this.target = targetCartesian; this.target = targetCartesian;
this.radius = options.radius || this.radius; this.radius = options.radius || this.radius;
this.pitch = options.pitch || this.pitch; this.pitch = options.pitch || this.pitch;
this.speed = Cesium.Math.toRadians(options.speed || 6); this.speed = geo.math.toRadians(options.speed || 6);
this.active = true; this.active = true;
// Orbit mutates the camera from preRender — without a hold the loop // Orbit mutates the camera from preRender — without a hold the loop
// starves after its first idle frame. (perf wave 2) // starves after its first idle frame. (perf wave 2)
@@ -56,7 +57,7 @@ export class OrbitController {
const hpr = new Cesium.HeadingPitchRange( const hpr = new Cesium.HeadingPitchRange(
this.angle, this.angle,
Cesium.Math.toRadians(this.pitch), geo.math.toRadians(this.pitch),
this.radius, this.radius,
); );
this.viewer.camera.lookAt(this.target, hpr); this.viewer.camera.lookAt(this.target, hpr);
+16 -16
View File
@@ -46,7 +46,7 @@
* are per-layer, keyed by that layer's icao space). * are per-layer, keyed by that layer's icao space).
*/ */
import * as Cesium from 'cesium'; import * as geo from '../geo/index.js';
/** Taxi threshold: a cached snap answers directly for moves up to this far from /** Taxi threshold: a cached snap answers directly for moves up to this far from
* the sampled spot (m). Past it the value is demoted to a held last-known and a * the sampled spot (m). Past it the value is demoted to a held last-known and a
@@ -178,13 +178,13 @@ export function createGroundSnap({ groundFloor }) {
const entries = new Map(); const entries = new Map();
let windowStartMs = 0; let windowStartMs = 0;
let windowCount = 0; let windowCount = 0;
const scratchCarto = new Cesium.Cartographic(); const scratchCarto = geo.cartographic.create();
const scratchSurfacePos = new Cesium.Cartesian3(); const scratchSurfacePos = geo.cartesian3.create();
/** Separate from `scratchCarto`: the held-evidence check runs on paths that /** Separate from `scratchCarto`: the held-evidence check runs on paths that
* return BEFORE the sample writes `scratchCarto`, and on the sampling path it * return BEFORE the sample writes `scratchCarto`, and on the sampling path it
* must not disturb the cartographic that `reportValidatedMeshFloorCell` reads * must not disturb the cartographic that `reportValidatedMeshFloorCell` reads
* back after the sample lands. */ * back after the sample lands. */
const scratchHeldCarto = new Cesium.Cartographic(); const scratchHeldCarto = geo.cartographic.create();
/** /**
* The last measurement for a contact whose fresh snap has been demoted — it * The last measurement for a contact whose fresh snap has been demoted — it
@@ -205,7 +205,7 @@ export function createGroundSnap({ groundFloor }) {
if (!entry || !entry.held || entry.h == null || !entry.samplePos) if (!entry || !entry.held || entry.h == null || !entry.samplePos)
return null; return null;
if ( if (
Cesium.Cartesian3.distanceSquared(surfacePos, entry.samplePos) > geo.cartesian3.distanceSquared(surfacePos, entry.samplePos) >
HELD_SNAP_MAX_DRIFT_M * HELD_SNAP_MAX_DRIFT_M HELD_SNAP_MAX_DRIFT_M * HELD_SNAP_MAX_DRIFT_M
) { ) {
return dropHold(entry); return dropHold(entry);
@@ -253,15 +253,15 @@ export function createGroundSnap({ groundFloor }) {
* @returns {number|null} * @returns {number|null}
*/ */
function freshMeasuredFloorAt(surfacePos) { function freshMeasuredFloorAt(surfacePos) {
const carto = Cesium.Cartographic.fromCartesian( const carto = geo.cartographic.fromCartesian(
surfacePos, surfacePos,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
scratchHeldCarto, scratchHeldCarto,
); );
if (!carto) return null; if (!carto) return null;
return cachedMeshFloor( return cachedMeshFloor(
Cesium.Math.toDegrees(carto.latitude), geo.math.toDegrees(carto.latitude),
Cesium.Math.toDegrees(carto.longitude), geo.math.toDegrees(carto.longitude),
); );
} }
@@ -284,7 +284,7 @@ export function createGroundSnap({ groundFloor }) {
* @returns {number|null} * @returns {number|null}
*/ */
function heightFor(viewer, icao, pos, getExclusions) { function heightFor(viewer, icao, pos, getExclusions) {
const surfacePos = Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface( const surfacePos = geo.ellipsoid.WGS84.scaleToGeodeticSurface(
pos, pos,
scratchSurfacePos, scratchSurfacePos,
); );
@@ -292,7 +292,7 @@ export function createGroundSnap({ groundFloor }) {
const cached = entries.get(icao); const cached = entries.get(icao);
if (cached && cached.h != null && cached.samplePos && !cached.held) { if (cached && cached.h != null && cached.samplePos && !cached.held) {
if ( if (
Cesium.Cartesian3.distanceSquared(surfacePos, cached.samplePos) <= geo.cartesian3.distanceSquared(surfacePos, cached.samplePos) <=
MOVE_INVALIDATE_M * MOVE_INVALIDATE_M MOVE_INVALIDATE_M * MOVE_INVALIDATE_M
) { ) {
return cached.h; return cached.h;
@@ -339,9 +339,9 @@ export function createGroundSnap({ groundFloor }) {
windowCount += 1; windowCount += 1;
let sampled; let sampled;
try { try {
const carto = Cesium.Cartographic.fromCartesian( const carto = geo.cartographic.fromCartesian(
pos, pos,
Cesium.Ellipsoid.WGS84, geo.ellipsoid.WGS84,
scratchCarto, scratchCarto,
); );
// sampleHeight throws when unsupported (no depth textures) — that's a miss. // sampleHeight throws when unsupported (no depth textures) — that's a miss.
@@ -357,8 +357,8 @@ export function createGroundSnap({ groundFloor }) {
if (!Number.isFinite(sampled) || sampled < -150) return miss(); if (!Number.isFinite(sampled) || sampled < -150) return miss();
if (meshFloorPreferred()) { if (meshFloorPreferred()) {
reportValidatedMeshFloorCell( reportValidatedMeshFloorCell(
Cesium.Math.toDegrees(scratchCarto.latitude), geo.math.toDegrees(scratchCarto.latitude),
Cesium.Math.toDegrees(scratchCarto.longitude), geo.math.toDegrees(scratchCarto.longitude),
sampled, sampled,
); );
} }
@@ -366,7 +366,7 @@ export function createGroundSnap({ groundFloor }) {
// contact is now, and it outranks a memory of where it was. // contact is now, and it outranks a memory of where it was.
entries.set(icao, { entries.set(icao, {
h: sampled, h: sampled,
samplePos: Cesium.Cartesian3.clone(surfacePos), samplePos: geo.cartesian3.clone(surfacePos),
held: false, held: false,
nextRetryMs: 0, nextRetryMs: 0,
misses: 0, misses: 0,
+10 -16
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from './geo/index.js';
import { BLOOM_INTENSITY_DEFAULT, BLOOM_SCALE_VERSION } from './bloom.js'; import { BLOOM_INTENSITY_DEFAULT, BLOOM_SCALE_VERSION } from './bloom.js';
import { import {
migrateDetectionState, migrateDetectionState,
@@ -263,15 +264,11 @@ export class ShareLinkManager {
if (this._destroyed || !state) if (this._destroyed || !state)
return { succeeded: false, reason: 'unavailable' }; return { succeeded: false, reason: 'unavailable' };
const view = { const view = {
destination: Cesium.Cartesian3.fromDegrees( destination: geo.cartesian3.fromDegrees(state.lon, state.lat, state.alt),
state.lon,
state.lat,
state.alt,
),
orientation: { orientation: {
heading: Cesium.Math.toRadians(state.heading), heading: geo.math.toRadians(state.heading),
pitch: Cesium.Math.toRadians(state.pitch), pitch: geo.math.toRadians(state.pitch),
roll: Cesium.Math.toRadians(state.roll), roll: geo.math.toRadians(state.roll),
}, },
}; };
let cameraPromise = Promise.resolve({ let cameraPromise = Promise.resolve({
@@ -548,21 +545,18 @@ export class ShareLinkManager {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('v', '2'); params.set('v', '2');
params.set('lat', Cesium.Math.toDegrees(carto.latitude).toFixed(4)); params.set('lat', geo.math.toDegrees(carto.latitude).toFixed(4));
params.set('lon', Cesium.Math.toDegrees(carto.longitude).toFixed(4)); params.set('lon', geo.math.toDegrees(carto.longitude).toFixed(4));
params.set('alt', Math.round(carto.height).toString()); params.set('alt', Math.round(carto.height).toString());
params.set( params.set(
'heading', 'heading',
Math.round(Cesium.Math.toDegrees(camera.heading)).toString(), Math.round(geo.math.toDegrees(camera.heading)).toString(),
); );
params.set( params.set(
'pitch', 'pitch',
Math.round(Cesium.Math.toDegrees(camera.pitch)).toString(), Math.round(geo.math.toDegrees(camera.pitch)).toString(),
);
params.set(
'roll',
Math.round(Cesium.Math.toDegrees(camera.roll)).toString(),
); );
params.set('roll', Math.round(geo.math.toDegrees(camera.roll)).toString());
params.set('style', STYLE_TO_URL[this._currentStyle] || 'normal'); params.set('style', STYLE_TO_URL[this._currentStyle] || 'normal');
params.set('bloom', this._bloomEnabled ? '1' : '0'); params.set('bloom', this._bloomEnabled ? '1' : '0');
params.set('sharpen', this._sharpenEnabled ? '1' : '0'); params.set('sharpen', this._sharpenEnabled ? '1' : '0');
+4 -3
View File
@@ -2,6 +2,7 @@
* One-click camera transfer for layer-owned world targets. * One-click camera transfer for layer-owned world targets.
*/ */
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import * as geo from './geo/index.js';
export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus'; export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus';
export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9; export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9;
@@ -26,10 +27,10 @@ export function isValidWorldFocusTarget(detail) {
// Vessel and fire targets are surface-anchored Earth positions. Merely // Vessel and fire targets are surface-anchored Earth positions. Merely
// finite coordinates near the ECEF origin cannot be flown to, and must be // finite coordinates near the ECEF origin cannot be flown to, and must be
// rejected before the camera policy releases a current follow owner. // rejected before the camera policy releases a current follow owner.
const magnitude = Cesium.Cartesian3.magnitude(position); const magnitude = geo.cartesian3.magnitude(position);
return ( return (
Number.isFinite(magnitude) && Number.isFinite(magnitude) &&
magnitude >= Cesium.Ellipsoid.WGS84.minimumRadius * 0.95 magnitude >= geo.ellipsoid.WGS84.minimumRadius * 0.95
); );
} }
@@ -85,7 +86,7 @@ export function flyToWorldTarget(viewer, target = {}) {
{ {
offset: new Cesium.HeadingPitchRange( offset: new Cesium.HeadingPitchRange(
heading, heading,
Cesium.Math.toRadians(framing.pitchDeg), geo.math.toRadians(framing.pitchDeg),
framing.rangeM, framing.rangeM,
), ),
duration, duration,