refactor(launches,cctv,alpr): take vector maths from src/geo

Launches selection/replay/panel/policy/policyHelpers/state/camera, CCTV geometry/model/projection/navigation/cards/geometryQueue/state and ALPR model/visuals now use src/geo for Math, Cartesian2/3, Cartographic, Matrix3/4, Quaternion and ENU frames. launches/paths.js keeps Cesium (EllipsoidGeodesic). In camera.js, EllipsoidGeodesic, BoundingSphere.fromPoints/union and the BoundingSphere constructor stay on Cesium; the geo Cartographics passed into EllipsoidGeodesic are only read field by field and cloned there (computeProperties). The CCTV plane orientation stays a Cesium Quaternion and plane dimensions a Cesium Cartesian2 (entity ConstantProperty values). launches-layer and alpr-cameras own the src/geo files.
This commit is contained in:
German Khokhlov committed 2026-10-15 00:18:35 +04:00
1 parent ee8b94d987
commit a915bdb3e5
17 files changed
+215 -214

No files matched your search

+30 -2
View File
@@ -1145,7 +1145,21 @@
"src/layers/launches/source.js",
"src/layers/launches/state.js",
"src/layers/launches/testing.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"]
},
@@ -1406,7 +1420,21 @@
"src/sources/httpBody.js",
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/layers/alpr/floors.js"
"src/layers/alpr/floors.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", "@mapbox/vector-tile", "pbf"]
},
+6 -6
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { EARTH_MEAN_RADIUS_M } from './policy.js';
export * from './records.js';
@@ -12,9 +12,9 @@ export * from './records.js';
*/
export function destinationPointDeg(latDeg, lonDeg, bearingDeg, distanceM) {
const angularDistance = distanceM / EARTH_MEAN_RADIUS_M;
const bearing = Cesium.Math.toRadians(bearingDeg);
const lat1 = Cesium.Math.toRadians(latDeg);
const lon1 = Cesium.Math.toRadians(lonDeg);
const bearing = geo.math.toRadians(bearingDeg);
const lat1 = geo.math.toRadians(latDeg);
const lon1 = geo.math.toRadians(lonDeg);
const lat2 = Math.asin(
Math.sin(lat1) * Math.cos(angularDistance) +
Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(bearing),
@@ -26,8 +26,8 @@ export function destinationPointDeg(latDeg, lonDeg, bearingDeg, distanceM) {
Math.cos(angularDistance) - Math.sin(lat1) * Math.sin(lat2),
);
return {
latitude: Cesium.Math.toDegrees(lat2),
longitude: Cesium.Math.toDegrees(lon2),
latitude: geo.math.toDegrees(lat2),
longitude: geo.math.toDegrees(lon2),
};
}
+4 -8
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { destinationPointDeg } from './model.js';
import {
DIRECTION_CONE_M,
@@ -95,13 +95,9 @@ export function directionWedgePositions(record, heightM = 0, heightAt = null) {
return Number.isFinite(height) ? height : heightM;
};
return [
Cesium.Cartesian3.fromDegrees(record.longitude, record.latitude, heightM),
Cesium.Cartesian3.fromDegrees(left.longitude, left.latitude, corner(left)),
Cesium.Cartesian3.fromDegrees(
right.longitude,
right.latitude,
corner(right),
),
geo.cartesian3.fromDegrees(record.longitude, record.latitude, heightM),
geo.cartesian3.fromDegrees(left.longitude, left.latitude, corner(left)),
geo.cartesian3.fromDegrees(right.longitude, right.latitude, corner(right)),
];
}
+3 -3
View File
@@ -8,7 +8,7 @@ import {
CCTV_FRAME_CANVAS_H,
applyFrameResult,
} from '../../data/cctvCards.js';
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { horizonOccluder } from '../../data/iconOrientation.js';
import { selectCctvLod, applyEvictionGrace } from '../../data/cctvLod.js';
import {
@@ -85,8 +85,8 @@ export function createCards({ state: layerState, services, parts, source }) {
}
const scene = layerState._viewer.scene;
const carto = layerState._viewer.camera.positionCartographic;
const viewerLat = carto ? Cesium.Math.toDegrees(carto.latitude) : 0;
const viewerLon = carto ? Cesium.Math.toDegrees(carto.longitude) : 0;
const viewerLat = carto ? geo.math.toDegrees(carto.latitude) : 0;
const viewerLon = carto ? geo.math.toDegrees(carto.longitude) : 0;
// The active camera is excluded from ambient selection ALWAYS (not just
// after its async activation settles). It is published separately in the
// protected lane below, and grace never applies to it.
+13 -16
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { createFrustumVolumePrimitive } from '../../data/cctvViewshed.js';
import {
PROJECTION_VERT_ASPECT,
@@ -272,7 +273,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
*/
function frustumCartesians(geometry) {
const at = (p) => Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.alt);
const at = (p) => geo.cartesian3.fromDegrees(p.lon, p.lat, p.alt);
return {
mount: at(geometry.mount),
capCenter: at(geometry.capCenter),
@@ -280,7 +281,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
tr: at(geometry.corners.tr),
br: at(geometry.corners.br),
bl: at(geometry.corners.bl),
label: Cesium.Cartesian3.fromDegrees(
label: geo.cartesian3.fromDegrees(
geometry.topCenter.lon,
geometry.topCenter.lat,
geometry.topCenter.alt + 1.2,
@@ -301,26 +302,22 @@ export function createGeometry({ state: layerState, services, parts, source }) {
function frustumFrameEcef(camera, atPos) {
const h = parts.model.toRad(camera.headingDeg);
const p = parts.model.toRad(camera.pitchDeg);
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(atPos);
const rot = Cesium.Matrix4.getMatrix3(enu, new Cesium.Matrix3());
const enu = geo.transforms.eastNorthUpToFixedFrame(atPos);
const rot = geo.matrix4.getMatrix3(enu, geo.matrix3.create());
// ENU components: view axis + the perpendicular "up" of the pitched cap.
const dirEnu = new Cesium.Cartesian3(
const dirEnu = geo.cartesian3.create(
Math.sin(h) * Math.cos(p),
Math.cos(h) * Math.cos(p),
Math.sin(p),
);
const upEnu = new Cesium.Cartesian3(
const upEnu = geo.cartesian3.create(
-Math.sin(p) * Math.sin(h),
-Math.sin(p) * Math.cos(h),
Math.cos(p),
);
return {
dir: Cesium.Matrix3.multiplyByVector(
rot,
dirEnu,
new Cesium.Cartesian3(),
),
up: Cesium.Matrix3.multiplyByVector(rot, upEnu, new Cesium.Cartesian3()),
dir: geo.matrix3.multiplyByVector(rot, dirEnu, geo.cartesian3.create()),
up: geo.matrix3.multiplyByVector(rot, upEnu, geo.cartesian3.create()),
};
}
@@ -462,10 +459,10 @@ export function createGeometry({ state: layerState, services, parts, source }) {
sampleMeshFloorCells(layerState._viewer?.scene, [point], {
excludeObjects: excludeObjects.filter(Boolean),
viewerLat: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.latitude)
? geo.math.toDegrees(viewerCarto.latitude)
: undefined,
viewerLon: viewerCarto
? Cesium.Math.toDegrees(viewerCarto.longitude)
? geo.math.toDegrees(viewerCarto.longitude)
: undefined,
});
}
@@ -623,7 +620,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
if (Math.abs(rangeScale - 1) > 0.0001) return; // slider overrides the clamp
try {
const mountAlt = parts.ground.groundAltFor(record) + camera.mountHeightM;
const mountPos = Cesium.Cartesian3.fromDegrees(
const mountPos = geo.cartesian3.fromDegrees(
camera.lon,
camera.lat,
mountAlt,
@@ -637,7 +634,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
}
const hit = scene.pickFromRay(new Cesium.Ray(mountPos, dir), exclude);
if (!hit?.position) return;
const dist = Cesium.Cartesian3.distance(mountPos, hit.position);
const dist = geo.cartesian3.distance(mountPos, hit.position);
record.probeClampRangeM = activationProbeClampRange(camera.rangeM, dist);
} catch {
// probe failure → keep the unclamped range
+3 -3
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
GEO_PROGRESS_NOTIFY_INTERVAL_MS,
GEO_PROGRESS_NOTIFY_BATCH_LIMIT,
@@ -298,10 +298,10 @@ export function createGeometryQueue({
const active = parts.selection.getActiveRecord();
const carto = layerState._viewer?.camera?.positionCartographic;
const refLat = carto
? Cesium.Math.toDegrees(carto.latitude)
? geo.math.toDegrees(carto.latitude)
: (active?.camera.lat ?? 0);
const refLon = carto
? Cesium.Math.toDegrees(carto.longitude)
? geo.math.toDegrees(carto.longitude)
: (active?.camera.lon ?? 0);
const pending = layerState._records
.filter((record) => record !== active)
+23 -20
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { staticFrameRefreshMs } from '../../data/cctvLod.js';
import { frameFetchDue, cardFetchPolicy } from '../../data/cctvCards.js';
import { DEFAULT_CAMERA_CALIBRATION } from './policy.js';
@@ -13,7 +14,7 @@ export function createModel({ state: layerState, services, parts, source }) {
*/
function toRad(deg) {
return Cesium.Math.toRadians(deg);
return geo.math.toRadians(deg);
}
/**
@@ -283,8 +284,8 @@ export function createModel({ state: layerState, services, parts, source }) {
const lon2 = lon1 + Math.atan2(y, x);
return {
lat: Cesium.Math.toDegrees(lat2),
lon: Cesium.Math.toDegrees(lon2),
lat: geo.math.toDegrees(lat2),
lon: geo.math.toDegrees(lon2),
};
}
@@ -328,8 +329,8 @@ export function createModel({ state: layerState, services, parts, source }) {
function currentViewContext() {
const carto = layerState._viewer?.camera?.positionCartographic;
if (!carto) return 'none';
const lat = Cesium.Math.toDegrees(carto.latitude);
const lon = Cesium.Math.toDegrees(carto.longitude);
const lat = geo.math.toDegrees(carto.latitude);
const lon = geo.math.toDegrees(carto.longitude);
const alt = carto.height || 0;
const zoomBucket =
alt < 1500
@@ -404,24 +405,26 @@ export function createModel({ state: layerState, services, parts, source }) {
function planeOrientationFor(camera, capCenterPos) {
const frame = parts.geometry.frustumFrameEcef(camera, capCenterPos);
const right = Cesium.Cartesian3.cross(
const right = geo.cartesian3.cross(
frame.dir,
frame.up,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
const normal = Cesium.Cartesian3.negate(frame.dir, new Cesium.Cartesian3());
const m = new Cesium.Matrix3(
right.x,
frame.up.x,
normal.x,
right.y,
frame.up.y,
normal.y,
right.z,
frame.up.z,
normal.z,
);
return Cesium.Quaternion.fromRotationMatrix(m);
const normal = geo.cartesian3.negate(frame.dir, geo.cartesian3.create());
// Column-major rotation whose columns are right, up and normal.
const m = geo.matrix3.create();
m[0] = right.x;
m[1] = right.y;
m[2] = right.z;
m[3] = frame.up.x;
m[4] = frame.up.y;
m[5] = frame.up.z;
m[6] = normal.x;
m[7] = normal.y;
m[8] = normal.z;
// The quaternion becomes an entity's ConstantProperty value, so it stays a
// Cesium Quaternion there (that property clones and compares its value).
return geo.quaternion.fromRotationMatrix(m, new Cesium.Quaternion());
}
/**
+3 -2
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { CCTV_FOCUS_RESULT } from './policy.js';
export function createNavigation({
@@ -15,8 +16,8 @@ export function createNavigation({
function nearestCameraIdToViewer() {
const carto = layerState._viewer?.camera?.positionCartographic;
if (!carto || !layerState._records.length) return null;
const lat = Cesium.Math.toDegrees(carto.latitude);
const lon = Cesium.Math.toDegrees(carto.longitude);
const lat = geo.math.toDegrees(carto.latitude);
const lon = geo.math.toDegrees(carto.longitude);
let best = null;
for (const record of layerState._records) {
+6 -5
View File
@@ -1,5 +1,6 @@
import { attachCctvVideo } from './videoPlayback.js';
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
CCTV_PROJECTION_OVERLAY_SOURCE_ID,
CCTV_PROJECTION_OVERLAY_SOURCE_OPTIONS,
@@ -77,7 +78,7 @@ export function createProjection({
);
}
if (runtime.labelPosition) {
Cesium.Cartesian3.clone(positions.label, runtime.labelPosition);
geo.cartesian3.clone(positions.label, runtime.labelPosition);
}
}
@@ -124,8 +125,8 @@ export function createProjection({
/** Create the native monitor plane plus its cached host-label presentation. */
function createProjectionPlane(record, runtime, geometry, positions) {
runtime.labelPosition ||= new Cesium.Cartesian3();
Cesium.Cartesian3.clone(positions.label, runtime.labelPosition);
runtime.labelPosition ||= geo.cartesian3.create();
geo.cartesian3.clone(positions.label, runtime.labelPosition);
runtime.cameraId = String(record.camera.id);
runtime.overlayEntry = createCctvProjectionOverlayEntry({
cameraId: runtime.cameraId,
@@ -142,7 +143,7 @@ export function createProjection({
positions.capCenter,
),
plane: {
plane: new Cesium.Plane(Cesium.Cartesian3.UNIT_Z, 0.0),
plane: new Cesium.Plane(geo.cartesian3.UNIT_Z, 0.0),
dimensions: new Cesium.Cartesian2(
geometry.halfW * 2,
geometry.halfH * 2,
@@ -186,7 +187,7 @@ export function createProjection({
video: null,
planeEntity: null,
cameraId: String(record.camera.id),
labelPosition: new Cesium.Cartesian3(),
labelPosition: geo.cartesian3.create(),
overlayEntry: null,
planeMaterial: null,
buffers: null,
+2 -2
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
export function createState({ services }) {
const {
@@ -74,7 +74,7 @@ export function createState({ services }) {
layerState._activeFocusStyleCount = 0;
layerState._scratchFocusScreen = new Cesium.Cartesian2();
layerState._scratchFocusScreen = geo.cartesian2.create();
// Staggered geometry-load queue state (see startGeometryLoadQueue).
+72 -84
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
REPLAY_ASCENT_CAMERA_OFFSET_RAD,
REPLAY_ORBIT_CAMERA_OFFSET_RAD,
@@ -18,12 +19,12 @@ export function createCamera({ state: layerState, services, parts, source }) {
if (!path?.length) return fallback;
const current = parts.paths.samplePath(path, progress);
const currentCartographic =
Cesium.Ellipsoid.WGS84.cartesianToCartographic(current);
geo.ellipsoid.WGS84.cartesianToCartographic(current);
if (!currentCartographic) return fallback;
for (const step of [0.01, 0.025, 0.05, 0.1, 0.2]) {
const next = parts.paths.samplePath(path, Math.min(1, progress + step));
const nextCartographic =
Cesium.Ellipsoid.WGS84.cartesianToCartographic(next);
geo.ellipsoid.WGS84.cartesianToCartographic(next);
if (!nextCartographic) continue;
const geodesic = new Cesium.EllipsoidGeodesic(
currentCartographic,
@@ -36,7 +37,7 @@ export function createCamera({ state: layerState, services, parts, source }) {
// HeadingPitchRange positions the camera opposite its heading vector.
// Passing the forward path heading therefore keeps the camera behind
// the vehicle, with the remaining ascent receding into the scene.
return Cesium.Math.zeroToTwoPi(geodesic.startHeading);
return geo.math.zeroToTwoPi(geodesic.startHeading);
}
}
return fallback;
@@ -49,8 +50,8 @@ export function createCamera({ state: layerState, services, parts, source }) {
*/
function replayInitialCameraHeading(path) {
return Cesium.Math.zeroToTwoPi(
cameraHeadingForPath(path, 0, Math.PI) + Cesium.Math.PI_OVER_TWO,
return geo.math.zeroToTwoPi(
cameraHeadingForPath(path, 0, Math.PI) + geo.math.PI_OVER_TWO,
);
}
@@ -63,10 +64,10 @@ export function createCamera({ state: layerState, services, parts, source }) {
*/
function replayChaseCameraHeading(pathHeading, orbitBlend = 0) {
const blend = Cesium.Math.clamp(Number(orbitBlend) || 0, 0, 1);
return Cesium.Math.zeroToTwoPi(
const blend = geo.math.clamp(Number(orbitBlend) || 0, 0, 1);
return geo.math.zeroToTwoPi(
pathHeading +
Cesium.Math.lerp(
geo.math.lerp(
REPLAY_ASCENT_CAMERA_OFFSET_RAD,
REPLAY_ORBIT_CAMERA_OFFSET_RAD,
blend,
@@ -86,17 +87,17 @@ export function createCamera({ state: layerState, services, parts, source }) {
function smoothReplayCameraHeading(
previous,
desired,
maxStepRad = Cesium.Math.toRadians(2),
maxStepRad = geo.math.toRadians(2),
) {
if (!Number.isFinite(previous)) return Cesium.Math.zeroToTwoPi(desired);
if (!Number.isFinite(desired)) return Cesium.Math.zeroToTwoPi(previous);
const delta = Cesium.Math.negativePiToPi(desired - previous);
const step = Cesium.Math.clamp(
if (!Number.isFinite(previous)) return geo.math.zeroToTwoPi(desired);
if (!Number.isFinite(desired)) return geo.math.zeroToTwoPi(previous);
const delta = geo.math.negativePiToPi(desired - previous);
const step = geo.math.clamp(
delta,
-Math.abs(maxStepRad),
Math.abs(maxStepRad),
);
return Cesium.Math.zeroToTwoPi(previous + step);
return geo.math.zeroToTwoPi(previous + step);
}
/**
@@ -108,7 +109,7 @@ export function createCamera({ state: layerState, services, parts, source }) {
function missionZoomPitch(rangeM) {
const range = Math.max(0, Number(rangeM) || 0);
const blend = Cesium.Math.clamp(
const blend = geo.math.clamp(
(Math.log(Math.max(range, MISSION_CLOSE_VIEW_RANGE_M)) -
Math.log(MISSION_CLOSE_VIEW_RANGE_M)) /
(Math.log(MISSION_GLOBE_VIEW_RANGE_M) -
@@ -116,11 +117,7 @@ export function createCamera({ state: layerState, services, parts, source }) {
0,
1,
);
return Cesium.Math.lerp(
Cesium.Math.toRadians(-42),
-Cesium.Math.PI_OVER_TWO,
blend,
);
return geo.math.lerp(geo.math.toRadians(-42), -geo.math.PI_OVER_TWO, blend);
}
/**
@@ -133,48 +130,44 @@ export function createCamera({ state: layerState, services, parts, source }) {
function replayCameraView(state, altitudeM) {
const altitude = Math.max(0, Number(altitudeM) || 0);
const localRange = Cesium.Math.clamp(
const localRange = geo.math.clamp(
REPLAY_INITIAL_RANGE_M + altitude * 0.7,
REPLAY_INITIAL_RANGE_M,
REPLAY_LOCAL_MAX_RANGE_M,
);
const rawContextBlend = Cesium.Math.clamp(
const rawContextBlend = geo.math.clamp(
(altitude - 20000) / (REPLAY_CONTEXT_ALTITUDE_END_M - 20000),
0,
1,
);
const contextBlend =
rawContextBlend * rawContextBlend * (3 - 2 * rawContextBlend);
const contextRange = Cesium.Math.clamp(
const contextRange = geo.math.clamp(
180000 + altitude * 3.8,
MISSION_CLOSE_VIEW_RANGE_M,
REPLAY_CONTEXT_MAX_RANGE_M,
);
const ascentRange = Cesium.Math.lerp(
localRange,
contextRange,
contextBlend,
);
const ascentPitch = Cesium.Math.lerp(
Cesium.Math.toRadians(-20),
Cesium.Math.toRadians(-34),
const ascentRange = geo.math.lerp(localRange, contextRange, contextBlend);
const ascentPitch = geo.math.lerp(
geo.math.toRadians(-20),
geo.math.toRadians(-34),
contextBlend,
);
if (state?.ascending) {
return { range: ascentRange, pitch: ascentPitch };
}
const rawBlend = Cesium.Math.clamp(
const rawBlend = geo.math.clamp(
(Number(state?.phaseProgress) || 0) / REPLAY_ORBIT_PULLBACK_FRACTION,
0,
1,
);
const blend = rawBlend * rawBlend * (3 - 2 * rawBlend);
return {
range: Cesium.Math.lerp(ascentRange, REPLAY_ORBIT_GLOBE_RANGE_M, blend),
range: geo.math.lerp(ascentRange, REPLAY_ORBIT_GLOBE_RANGE_M, blend),
// Keep an oblique tactical view of the complete orbit rather than ending
// in a nadir view. This also prevents the camera from appearing to
// return toward the launch site after insertion.
pitch: Cesium.Math.lerp(ascentPitch, Cesium.Math.toRadians(-45), blend),
pitch: geo.math.lerp(ascentPitch, geo.math.toRadians(-45), blend),
};
}
@@ -188,16 +181,15 @@ export function createCamera({ state: layerState, services, parts, source }) {
function replayOrbitGlobeAnchor(position, orbitBlend = 0) {
if (!position) return position;
const blend = Cesium.Math.clamp(Number(orbitBlend) || 0, 0, 1);
if (blend <= 0) return Cesium.Cartesian3.clone(position);
const cartographic =
Cesium.Ellipsoid.WGS84.cartesianToCartographic(position);
if (!cartographic) return Cesium.Cartesian3.clone(position);
const blend = geo.math.clamp(Number(orbitBlend) || 0, 0, 1);
if (blend <= 0) return geo.cartesian3.clone(position);
const cartographic = geo.ellipsoid.WGS84.cartesianToCartographic(position);
if (!cartographic) return geo.cartesian3.clone(position);
const altitude = Math.max(0, cartographic.height || 0);
return Cesium.Cartesian3.fromRadians(
return geo.cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
Cesium.Math.lerp(altitude, altitude * 0.1, blend),
geo.math.lerp(altitude, altitude * 0.1, blend),
);
}
@@ -216,15 +208,15 @@ export function createCamera({ state: layerState, services, parts, source }) {
function replayOrbitCameraTarget(vehicleAnchor, frameCenter, orbitBlend = 0) {
if (!vehicleAnchor) return vehicleAnchor;
if (!frameCenter) return Cesium.Cartesian3.clone(vehicleAnchor);
if (!frameCenter) return geo.cartesian3.clone(vehicleAnchor);
const blend =
Cesium.Math.clamp(Number(orbitBlend) || 0, 0, 1) *
geo.math.clamp(Number(orbitBlend) || 0, 0, 1) *
REPLAY_ORBIT_FRAME_CENTER_BLEND;
return Cesium.Cartesian3.lerp(
return geo.cartesian3.lerp(
vehicleAnchor,
frameCenter,
blend,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
}
@@ -249,63 +241,59 @@ export function createCamera({ state: layerState, services, parts, source }) {
pitch,
) {
if (!position || !tangentPosition || !target) return null;
const radial = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.clone(position),
new Cesium.Cartesian3(),
const radial = geo.cartesian3.normalize(
geo.cartesian3.clone(position),
geo.cartesian3.create(),
);
const tangent = Cesium.Cartesian3.subtract(
const tangent = geo.cartesian3.subtract(
tangentPosition,
position,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
if (Cesium.Cartesian3.magnitudeSquared(tangent) < 1) return null;
Cesium.Cartesian3.normalize(tangent, tangent);
const orbitNormal = Cesium.Cartesian3.cross(
if (geo.cartesian3.magnitudeSquared(tangent) < 1) return null;
geo.cartesian3.normalize(tangent, tangent);
const orbitNormal = geo.cartesian3.cross(
radial,
tangent,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
if (Cesium.Cartesian3.magnitudeSquared(orbitNormal) < 1e-12) return null;
Cesium.Cartesian3.normalize(orbitNormal, orbitNormal);
if (geo.cartesian3.magnitudeSquared(orbitNormal) < 1e-12) return null;
geo.cartesian3.normalize(orbitNormal, orbitNormal);
const distance = Math.max(1, Number(range) || 1);
const elevation = Cesium.Math.clamp(
const elevation = geo.math.clamp(
Math.abs(Number(pitch) || 0),
Cesium.Math.toRadians(5),
Cesium.Math.toRadians(80),
geo.math.toRadians(5),
geo.math.toRadians(80),
);
const planeOffset = Cesium.Cartesian3.multiplyByScalar(
const planeOffset = geo.cartesian3.multiplyByScalar(
orbitNormal,
Math.cos(elevation) * distance,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
const radialOffset = Cesium.Cartesian3.multiplyByScalar(
const radialOffset = geo.cartesian3.multiplyByScalar(
radial,
Math.sin(elevation) * distance,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
const destination = Cesium.Cartesian3.add(
const destination = geo.cartesian3.add(
target,
planeOffset,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
Cesium.Cartesian3.add(destination, radialOffset, destination);
const direction = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(target, destination, new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
geo.cartesian3.add(destination, radialOffset, destination);
const direction = geo.cartesian3.normalize(
geo.cartesian3.subtract(target, destination, geo.cartesian3.create()),
geo.cartesian3.create(),
);
const radialAlongView = Cesium.Cartesian3.multiplyByScalar(
const radialAlongView = geo.cartesian3.multiplyByScalar(
direction,
Cesium.Cartesian3.dot(radial, direction),
new Cesium.Cartesian3(),
geo.cartesian3.dot(radial, direction),
geo.cartesian3.create(),
);
const up = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(
radial,
radialAlongView,
new Cesium.Cartesian3(),
),
new Cesium.Cartesian3(),
const up = geo.cartesian3.normalize(
geo.cartesian3.subtract(radial, radialAlongView, geo.cartesian3.create()),
geo.cartesian3.create(),
);
return { destination, direction, up };
}
@@ -321,8 +309,8 @@ export function createCamera({ state: layerState, services, parts, source }) {
function replayOrbitFrameSphere(orbitPath = []) {
const earth = new Cesium.BoundingSphere(
Cesium.Cartesian3.ZERO,
Cesium.Ellipsoid.WGS84.maximumRadius,
geo.cartesian3.ZERO,
geo.ellipsoid.WGS84.maximumRadius,
);
if (!Array.isArray(orbitPath) || orbitPath.length < 2) return earth;
const orbit = Cesium.BoundingSphere.fromPoints(orbitPath);
@@ -351,13 +339,13 @@ export function createCamera({ state: layerState, services, parts, source }) {
const range = Math.max(0, Number(baseRange) || 0);
const altitude = Math.max(0, Number(altitudeM) || 0);
const frameRadius = Math.max(0, Number(frameRadiusM) || 0);
const blend = Cesium.Math.clamp(Number(orbitBlend) || 0, 0, 1);
const blend = geo.math.clamp(Number(orbitBlend) || 0, 0, 1);
const globeAndVehicleRange = Math.max(
range,
altitude + Cesium.Ellipsoid.WGS84.maximumRadius * 2.4,
altitude + geo.ellipsoid.WGS84.maximumRadius * 2.4,
frameRadius * 3,
);
return Cesium.Math.lerp(range, globeAndVehicleRange, blend);
return geo.math.lerp(range, globeAndVehicleRange, blend);
}
return {
cameraHeadingForPath,
+2 -1
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
MISSION_CLOSE_VIEW_RANGE_M,
MISSION_GLOBE_VIEW_RANGE_M,
@@ -360,7 +361,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
Cesium.JulianDate.now(layerState._declutterTime),
);
const altitudeM = position
? Cesium.Cartographic.fromCartesian(position)?.height
? geo.cartographic.fromCartesian(position)?.height
: null;
setMissionPanelField(
'[data-mission-distance]',
+4 -4
View File
@@ -1,4 +1,4 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
export const WINDOW_DAYS = 30;
@@ -48,9 +48,9 @@ export const REPLAY_ORBIT_GLOBE_RANGE_M = 18000000;
export const REPLAY_ORBIT_PULLBACK_FRACTION = 0.2;
export const REPLAY_ASCENT_CAMERA_OFFSET_RAD = Cesium.Math.toRadians(30);
export const REPLAY_ASCENT_CAMERA_OFFSET_RAD = geo.math.toRadians(30);
export const REPLAY_ORBIT_CAMERA_OFFSET_RAD = Cesium.Math.toRadians(45);
export const REPLAY_ORBIT_CAMERA_OFFSET_RAD = geo.math.toRadians(45);
export const REPLAY_ORBIT_FRAME_CENTER_BLEND = 0.45;
@@ -64,7 +64,7 @@ export const MAX_POST_TLE_RETRIES = 1;
export const POST_TLE_RETRY_DELAY_MS = 1500;
export const EARTH_ROTATION_RAD_PER_SEC = Cesium.Math.TWO_PI / 86164.0905;
export const EARTH_ROTATION_RAD_PER_SEC = geo.math.TWO_PI / 86164.0905;
export const PROJECTED_ASCENT_ROTATION_SEC = 600;
+3 -6
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
SATELLITE_STANDALONE_DEFAULTS,
LAUNCH_PAD_ZONE_MAX_CAMERA_HEIGHT_M,
@@ -226,15 +227,11 @@ export function createPolicyHelpers({
const range = parts.panel.missionHoverPreviewRange(
layerState._viewer.camera.positionCartographic?.height,
);
const position = Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat);
const position = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
layerState._viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(position, 0),
{
offset: new Cesium.HeadingPitchRange(
0,
-Cesium.Math.PI_OVER_TWO,
range,
),
offset: new Cesium.HeadingPitchRange(0, -geo.math.PI_OVER_TWO, range),
duration: 0.8,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
},
+12 -15
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import {
REPLAY_ASCENT_FALLBACK_SEC,
REPLAY_ASCENT_MIN_SEC,
@@ -52,10 +53,10 @@ export function createReplay({ state: layerState, services, parts, source }) {
if (!previous || !next) return next;
const distance = Math.hypot(next.x - previous.x, next.y - previous.y);
if (!Number.isFinite(distance) || distance > snapDistance) return next;
const amount = Cesium.Math.clamp(Number(alpha) || 0, 0, 1);
const amount = geo.math.clamp(Number(alpha) || 0, 0, 1);
return {
x: Cesium.Math.lerp(previous.x, next.x, amount),
y: Cesium.Math.lerp(previous.y, next.y, amount),
x: geo.math.lerp(previous.x, next.x, amount),
y: geo.math.lerp(previous.y, next.y, amount),
};
}
@@ -77,13 +78,13 @@ export function createReplay({ state: layerState, services, parts, source }) {
.slice(1)
.reduce(
(total, point, index) =>
total + Cesium.Cartesian3.distance(ascentPath[index], point),
total + geo.cartesian3.distance(ascentPath[index], point),
0,
);
realAscentSeconds = Math.max(180, pathLength / 9000);
}
if (!realAscentSeconds) realAscentSeconds = REPLAY_ASCENT_FALLBACK_SEC * 50;
return Cesium.Math.clamp(
return geo.math.clamp(
REPLAY_ASCENT_FALLBACK_SEC * (realAscentSeconds / 600),
REPLAY_ASCENT_MIN_SEC,
REPLAY_ASCENT_MAX_SEC,
@@ -99,11 +100,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
function normalizeReplaySpeed(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return 1;
const clamped = Cesium.Math.clamp(
numeric,
REPLAY_SPEED_MIN,
REPLAY_SPEED_MAX,
);
const clamped = geo.math.clamp(numeric, REPLAY_SPEED_MIN, REPLAY_SPEED_MAX);
return Math.round(clamped / REPLAY_SPEED_STEP) * REPLAY_SPEED_STEP;
}
@@ -414,7 +411,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
track.lastCameraHeading,
);
const orbitBlend = !state.ascending
? Cesium.Math.clamp(
? geo.math.clamp(
(Number(state.phaseProgress) || 0) /
REPLAY_ORBIT_PULLBACK_FRACTION,
0,
@@ -428,16 +425,16 @@ export function createReplay({ state: layerState, services, parts, source }) {
if (state.ascending) track.orbitCameraWorldFrame = false;
const nowMs = performance.now();
const frameDurationMs = Number.isFinite(lastCameraUpdateMs)
? Cesium.Math.clamp(nowMs - lastCameraUpdateMs, 4, 50)
? geo.math.clamp(nowMs - lastCameraUpdateMs, 4, 50)
: 1000 / 60;
lastCameraUpdateMs = nowMs;
track.lastCameraHeading = parts.camera.smoothReplayCameraHeading(
track.lastCameraHeading,
desiredHeading,
(Cesium.Math.toRadians(2) * frameDurationMs) / (1000 / 60),
(geo.math.toRadians(2) * frameDurationMs) / (1000 / 60),
);
const cartographic =
Cesium.Ellipsoid.WGS84.cartesianToCartographic(position);
geo.ellipsoid.WGS84.cartesianToCartographic(position);
const altitude = Math.max(0, cartographic?.height || 0);
const cameraView = parts.camera.replayCameraView(state, altitude);
const defaultOrbitTarget = !state.ascending
@@ -518,7 +515,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
{
offset: new Cesium.HeadingPitchRange(
initialHeading,
Cesium.Math.toRadians(-20),
geo.math.toRadians(-20),
REPLAY_INITIAL_RANGE_M,
),
duration: 1.4,
+27 -36
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { MISSION_GLOBE_VIEW_RANGE_M, MISSION_FOCUS_RANGE_M } from './policy.js';
export function createSelection({
@@ -24,7 +25,7 @@ export function createSelection({
fovy >= Math.PI
)
return;
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
const earthRadius = geo.ellipsoid.WGS84.maximumRadius;
const keyholeRadius = getKeyholeGeometry(width, height).radius;
const targetScreenRadius = keyholeRadius * 0.61;
const angularRadius = Math.atan(
@@ -33,14 +34,14 @@ export function createSelection({
const distance = earthRadius / Math.max(Math.sin(angularRadius), 1e-4);
const altitude = Math.max(earthRadius * 1.55, distance - earthRadius);
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromRadians(
destination: geo.cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
altitude,
),
orientation: {
heading: viewer.camera.heading,
pitch: -Cesium.Math.PI_OVER_TWO,
pitch: -geo.math.PI_OVER_TWO,
roll: 0,
},
duration,
@@ -99,7 +100,7 @@ export function createSelection({
if (!layerState._viewer || !launch || layerState._replayCameraLaunchId)
return;
stopMissionZoomAnchor();
const target = Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat);
const target = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
const range = Math.max(
2000,
Number(initialRange) || MISSION_GLOBE_VIEW_RANGE_M,
@@ -127,15 +128,12 @@ export function createSelection({
const orbitPositions = orbitEntity?.polyline?.positions?.getValue(
Cesium.JulianDate.now(),
);
const launchPosition = Cesium.Cartesian3.fromDegrees(
launch.lon,
launch.lat,
);
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
let range = 18000000;
if (orbitPositions?.length > 1) {
const canvas = layerState._viewer.scene.canvas;
const fovy =
layerState._viewer.camera.frustum?.fovy || Cesium.Math.toRadians(60);
layerState._viewer.camera.frustum?.fovy || geo.math.toRadians(60);
const aspect = Math.max(
0.1,
(canvas.clientWidth || canvas.width) /
@@ -145,7 +143,7 @@ export function createSelection({
const paddedHalfAngle = Math.min(fovy, fovx) * 0.5 * 0.62;
const orbitExtent = orbitPositions.reduce(
(largest, point) =>
Math.max(largest, Cesium.Cartesian3.distance(launchPosition, point)),
Math.max(largest, geo.cartesian3.distance(launchPosition, point)),
0,
);
range = Math.max(
@@ -153,39 +151,35 @@ export function createSelection({
18000000,
);
}
const targetNormal = Cesium.Cartesian3.normalize(
const targetNormal = geo.cartesian3.normalize(
launchPosition,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
const destination = Cesium.Cartesian3.add(
const destination = geo.cartesian3.add(
launchPosition,
Cesium.Cartesian3.multiplyByScalar(
geo.cartesian3.multiplyByScalar(
targetNormal,
range,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
),
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
const direction = Cesium.Cartesian3.negate(
const direction = geo.cartesian3.negate(
targetNormal,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
let right = Cesium.Cartesian3.cross(
let right = geo.cartesian3.cross(
direction,
Cesium.Cartesian3.UNIT_Z,
new Cesium.Cartesian3(),
geo.cartesian3.UNIT_Z,
geo.cartesian3.create(),
);
if (Cesium.Cartesian3.magnitudeSquared(right) < 1e-8) {
right = Cesium.Cartesian3.cross(
direction,
Cesium.Cartesian3.UNIT_Y,
right,
);
if (geo.cartesian3.magnitudeSquared(right) < 1e-8) {
right = geo.cartesian3.cross(direction, geo.cartesian3.UNIT_Y, right);
}
Cesium.Cartesian3.normalize(right, right);
const up = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
geo.cartesian3.normalize(right, right);
const up = geo.cartesian3.normalize(
geo.cartesian3.cross(right, direction, geo.cartesian3.create()),
geo.cartesian3.create(),
);
layerState._viewer.camera.flyTo({
destination,
@@ -202,10 +196,7 @@ export function createSelection({
if (!layerState._viewer || !launch) return;
parts.replay.stopMissionReplay();
stopMissionZoomAnchor();
const launchPosition = Cesium.Cartesian3.fromDegrees(
launch.lon,
launch.lat,
);
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
layerState._viewer.selectedEntity = layerState._dataSource.entities.getById(
`rocket-launch:${launch.id}`,
);
@@ -214,7 +205,7 @@ export function createSelection({
{
offset: new Cesium.HeadingPitchRange(
0,
Cesium.Math.toRadians(-42),
geo.math.toRadians(-42),
MISSION_FOCUS_RANGE_M,
),
duration: 1.4,
+2 -1
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
export function createState({ services }) {
const { setOverlayEntries, setOverlaySourceVisible, clearOverlaySource } =
@@ -125,7 +126,7 @@ export function createState({ services }) {
state._declutterOccluder = new Cesium.EllipsoidalOccluder(
Cesium.Ellipsoid.WGS84,
new Cesium.Cartesian3(),
geo.cartesian3.create(),
);
state._missionRingDate = new Date(0);