orbit-studio / src /modules /satelliteGraphics.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
6.57 kB
import { HeadingPitchRoll, Math as CesiumMath, SceneMode, Transforms } from "@cesium/engine";
import type { Cartesian3, Quaternion } from "@cesium/engine";
import type { OrbitClass } from "../config/orbitClass";
// satelliteGraphics — the geometry decisions behind the satellite's visual
// components, as pure functions of plain inputs so they are testable without
// a Cesium scene. SatelliteComponentCollection adapts these descriptions into
// Cesium entities and primitives.
/**
* Ground track and sensor cone are only rendered for LEO satellites.
*
* The same "LEO" the info panel and the browser badge name, rather than a
* period threshold of its own: the two used to disagree by 8 minutes, so a
* satellite could be labelled LEO and still be refused a ground track. It also
* picks up the eccentricity test for free — a highly elliptical orbit that dips
* to a short period is not something to draw a swath corridor under.
*/
export function isLeo(orbitClass: OrbitClass): boolean {
return orbitClass === "LEO";
}
export interface OrbitPathTimes {
leadTime: number;
trailTime: number;
}
/** Lead/trail half a period (+5 s overlap) so the path closes into a full orbit. */
export function orbitPathTimes(orbitalPeriodMin: number): OrbitPathTimes {
const halfPeriod = (orbitalPeriodMin * 60) / 2 + 5;
return { leadTime: halfPeriod, trailTime: halfPeriod };
}
/** One full period ahead, nothing behind. */
export function orbitTrackTimes(orbitalPeriodMin: number): OrbitPathTimes {
return { leadTime: orbitalPeriodMin * 60, trailTime: 0 };
}
export interface GroundTrackDescription {
widthMeters: number;
}
export function groundTrackDescription(orbitClass: OrbitClass, swathKm: number): GroundTrackDescription | undefined {
if (!isLeo(orbitClass)) {
return undefined;
}
return { widthMeters: swathKm * 1000 };
}
export interface ConeDescription {
radiusMeters: number;
innerHalfAngleRad: number;
outerHalfAngleRad: number;
}
export function coneDescription(orbitClass: OrbitClass, fovDeg: number): ConeDescription | undefined {
if (!isLeo(orbitClass)) {
return undefined;
}
return {
radiusMeters: 1000000,
innerHalfAngleRad: CesiumMath.toRadians(0),
outerHalfAngleRad: CesiumMath.toRadians(fovDeg),
};
}
/**
* Straight down. Hoisted out of the callback below, which runs per satellite per
* frame; `headingPitchRollQuaternion` only reads it.
*/
const CONE_HEADING_PITCH_ROLL = new HeadingPitchRoll(0, CesiumMath.toRadians(180), 0);
/**
* The sensor cone's orientation, pointing at the ground beneath the satellite.
*
* Undefined when the position is. A sampled position has no value while its window
* is empty — during a rebuild, or before the first fill lands — and
* `Transforms.headingPitchRollQuaternion` throws `DeveloperError: origin is
* required.` from inside `DataSourceDisplay.update`, which is Cesium's render
* loop: it stops for the rest of the session.
*
* Declining costs nothing. cesium-sensor-volumes reads the position and the
* orientation through `Property.getValueOrUndefined` and hides the cone unless it
* has both, so it would reach the same conclusion from the absent position.
*/
export function coneOrientation(position: Cartesian3 | undefined): Quaternion | undefined {
if (!position) {
return undefined;
}
return Transforms.headingPitchRollQuaternion(position, CONE_HEADING_PITCH_ROLL);
}
/** Explicit model URL from catalog metadata wins; otherwise the name-convention path. */
export function modelUri(name: string, modelUrl?: string): string {
return modelUrl ?? `./data/models/${name.split(" ").join("-")}.glb`;
}
/**
* Whether the orbit renders as a path graphic (entity) instead of a polyline
* primitive: required for the tracked satellite and for scene modes without
* primitive model-matrix updates; all other satellites use the significantly
* faster primitive.
*/
export function orbitUsesPathGraphic(isTracked: boolean, sceneModeSupportsPrimitive: boolean): boolean {
return isTracked || !sceneModeSupportsPrimitive;
}
/**
* The Cesium projection a view-mode name asks for, or undefined for one that is
* not a projection at all.
*
* Here rather than in config/viewModes so that file stays Cesium-free, and here
* rather than inline in CesiumController so the mapping is testable without a
* viewer. "Sky" is deliberately absent: it renders in 3D but is a camera
* placement, and morphing on its behalf is what SkyView does for itself.
*/
export function cesiumSceneMode(viewMode: string): SceneMode | undefined {
switch (viewMode) {
case "3D":
return SceneMode.SCENE3D;
case "2D":
return SceneMode.SCENE2D;
case "Columbus":
return SceneMode.COLUMBUS_VIEW;
default:
return undefined;
}
}
/** Bounds on how often time-dependent geometry is re-cut, in simulation seconds. */
export const GEOMETRY_REFRESH_MIN_SECONDS = 1;
export const GEOMETRY_REFRESH_MAX_SECONDS = 10;
/**
* How often to re-cut the geometry that goes stale as the clock runs, given how
* many satellites are drawing it. Shared by the batched orbit tracks and the
* ground-track corridors, which have the same shape of problem.
*
* Neither is a rigid transform of itself as time passes — a fixed-frame track
* and an Earth-relative swath both have to be rebuilt rather than re-oriented —
* so the satellite runs on past the head of its own geometry until the next
* rebuild, at roughly 7.5 km a second in LEO. The interval is therefore an error
* budget, and the reason it is not simply "every frame" is that rebuilding is
* what used to cost 342 ms and 414 ms of main thread respectively at five
* thousand satellites.
*
* It scales with the count because both the error and the cost do, in opposite
* directions. A handful is a scene someone is looking closely at, and a second
* of lag there is under 10 km — sub-pixel on a globe. Thousands is a scene where
* any one of them is a few pixels in a thicket, and ten seconds of lag buys back
* the frame: measured at five thousand orbit tracks, going from ten seconds to
* three took the worst frame from 43 ms to 250 ms to close an error nobody was
* in a position to see. The satellite the camera is actually tracking sidesteps
* the question entirely — it gets an exact per-frame PathGraphic (see
* `orbitUsesPathGraphic`).
*/
export function geometryRefreshSeconds(count: number): number {
return Math.min(GEOMETRY_REFRESH_MAX_SECONDS, Math.max(GEOMETRY_REFRESH_MIN_SECONDS, count / 100));
}