mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(launches): Cesium rendering behind a renderer interface
This commit is contained in:
1 parent
1839d62160
commit
4baefeb8f9
31 files changed
+1202
-420
No files matched your search
@@ -39,6 +39,7 @@ const RENDERED_LAYER_DIRS = [
|
||||
'src/layers/localAdsb/',
|
||||
'src/layers/vessels/',
|
||||
'src/layers/satellites/',
|
||||
'src/layers/launches/',
|
||||
'src/layers/aircraft/',
|
||||
'src/layers/shared/',
|
||||
];
|
||||
|
||||
@@ -1149,5 +1149,11 @@
|
||||
"src/layers/satellites/render/navara.js",
|
||||
"src/layers/satellites/render/navara.test.mjs",
|
||||
"src/layers/satellites/render/cesium.test.mjs",
|
||||
"src/geo/ellipsoidGeodesic.test.mjs"
|
||||
"src/geo/ellipsoidGeodesic.test.mjs",
|
||||
"src/bench/launchFixture.js",
|
||||
"src/layers/launches/replayClock.test.mjs",
|
||||
"scripts/qa-launches.mjs",
|
||||
"src/geo/boundingSphere.test.mjs",
|
||||
"src/layers/launches/render/cesium.js",
|
||||
"src/layers/launches/launchPadFrame.test.mjs"
|
||||
]
|
||||
@@ -529,6 +529,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -695,6 +696,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -819,6 +821,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -888,6 +891,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -948,6 +952,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -994,6 +999,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1039,6 +1045,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1102,6 +1109,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1145,6 +1153,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1167,6 +1176,8 @@
|
||||
"src/layers/launches/camera.js",
|
||||
"src/layers/launches/controls.js",
|
||||
"src/layers/launches/index.js",
|
||||
"src/layers/shared/color.js",
|
||||
"src/layers/launches/render/cesium.js",
|
||||
"src/layers/launches/ingestion.js",
|
||||
"src/layers/launches/launchPad.js",
|
||||
"src/layers/launches/lifecycle.js",
|
||||
@@ -1189,6 +1200,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1242,6 +1254,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1286,6 +1299,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1344,6 +1358,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1384,6 +1399,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1442,6 +1458,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1503,6 +1520,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1540,6 +1558,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1590,6 +1609,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -1624,6 +1644,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -2086,6 +2107,7 @@
|
||||
"src/layers/launches/camera.js",
|
||||
"src/layers/launches/controls.js",
|
||||
"src/layers/launches/index.js",
|
||||
"src/layers/launches/render/cesium.js",
|
||||
"src/layers/launches/ingestion.js",
|
||||
"src/layers/launches/launchPad.js",
|
||||
"src/layers/launches/lifecycle.js",
|
||||
@@ -2580,6 +2602,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -2901,6 +2924,7 @@
|
||||
"src/layers/launches/camera.js",
|
||||
"src/layers/launches/controls.js",
|
||||
"src/layers/launches/index.js",
|
||||
"src/layers/launches/render/cesium.js",
|
||||
"src/layers/launches/ingestion.js",
|
||||
"src/layers/launches/launchPad.js",
|
||||
"src/layers/launches/lifecycle.js",
|
||||
@@ -3192,6 +3216,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -3292,6 +3317,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -3776,6 +3802,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
@@ -3831,6 +3858,7 @@
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/cartographic.js",
|
||||
"src/geo/ellipsoidGeodesic.js",
|
||||
"src/geo/boundingSphere.js",
|
||||
"src/geo/cartesian2.js",
|
||||
"src/geo/cartesian3.js",
|
||||
"src/geo/matrix3.js",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// military:synthetic[:count] readsb rows, `count` aircraft (1 000); the
|
||||
// first 25 are also synthetic civil flights
|
||||
// vessels:fixture 12 000 AIS rows, or those within an area
|
||||
// launches:fixture three LL2 missions; also supplies the satellite dependency
|
||||
// satellites:fixture 839 core TLEs + 6000 dense points
|
||||
// local-adsb:fixture the local receivers' answer (server format,
|
||||
// /api/local-receivers/aircraft): 40 aircraft
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
syntheticMilitary,
|
||||
syntheticVessels,
|
||||
} from '../../bench/w1Feeds.js';
|
||||
import { launchFixture } from '../../bench/launchFixture.js';
|
||||
import { createLaunchSource } from '../../layers/launches/source.js';
|
||||
import { satelliteFixture } from '../../bench/satelliteFixture.js';
|
||||
import { createSatelliteSource } from '../../layers/satellites/source.js';
|
||||
import { createLocalReceiverFeeds } from '../../layers/localAdsb/feeds.js';
|
||||
@@ -33,6 +36,7 @@ const KINDS = Object.freeze({
|
||||
military: new Set(['synthetic']),
|
||||
vessels: new Set(['fixture']),
|
||||
satellites: new Set(['fixture']),
|
||||
launches: new Set(['fixture']),
|
||||
'local-adsb': new Set(['fixture']),
|
||||
});
|
||||
|
||||
@@ -48,7 +52,7 @@ export function parseQaFeed(search = '') {
|
||||
(count !== null && !(Number.isInteger(count) && count > 0))
|
||||
)
|
||||
throw new TypeError(
|
||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], military:synthetic[:count], vessels:fixture, satellites:fixture, local-adsb:fixture)`,
|
||||
`qaFeed: unknown entry "${item}" (flights:synthetic[:count], military:synthetic[:count], vessels:fixture, satellites:fixture, launches:fixture, local-adsb:fixture)`,
|
||||
);
|
||||
return { layer, kind, count };
|
||||
});
|
||||
@@ -64,6 +68,18 @@ const json = (payload) =>
|
||||
export function createQaSources(entries, { now = () => Date.now() } = {}) {
|
||||
const sources = {};
|
||||
for (const { layer, count } of entries) {
|
||||
if (layer === 'launches') {
|
||||
sources.launches = createLaunchSource({
|
||||
fetchImpl: async (url) =>
|
||||
String(url).includes('/celestrak/')
|
||||
? new Response(satelliteFixture('stations'))
|
||||
: json(launchFixture()),
|
||||
});
|
||||
sources.satellites ??= createSatelliteSource({
|
||||
fetchImpl: async (url) =>
|
||||
new Response(satelliteFixture(String(url).split('/').at(-1))),
|
||||
});
|
||||
}
|
||||
if (layer === 'satellites')
|
||||
sources.satellites = createSatelliteSource({
|
||||
fetchImpl: async (url) =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
|
||||
// Which engine draws which layer: the layer renderer registry (plan, "layer
|
||||
// renderer interface") and the layer availability API built on it.
|
||||
//
|
||||
@@ -88,6 +89,7 @@ export const layerAvailableOn = (layerId, engineKind) =>
|
||||
|
||||
/** Cesium renderers extracted from their layers (render/cesium.js). */
|
||||
export const CESIUM_LAYER_RENDERERS = Object.freeze({
|
||||
'rocket-launches': createLaunchesCesiumRenderer,
|
||||
satellites: createSatellitesCesiumRenderer,
|
||||
flights: createFlightsCesiumRenderer,
|
||||
military: createMilitaryCesiumRenderer,
|
||||
|
||||
@@ -56,8 +56,9 @@ test('the default registry: every layer on Cesium, none yet on Navara', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('the aircraft, vessels and satellites have extracted Cesium renderers; the others still draw themselves', () => {
|
||||
test('the aircraft, vessels, satellites and launches have extracted Cesium renderers; the others still draw themselves', () => {
|
||||
assert.deepEqual(Object.keys(CESIUM_LAYER_RENDERERS), [
|
||||
'rocket-launches',
|
||||
'satellites',
|
||||
'flights',
|
||||
'military',
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { rendererFor } from './renderers.js';
|
||||
import { createLaunchesCesiumRenderer } from '../../layers/launches/render/cesium.js';
|
||||
import { createRocketLaunchesLayer } from '../../layers/launches/index.js';
|
||||
import * as geometry from '../../celestialRing.js';
|
||||
import * as overlays from '../../overlays/worldOverlay.js';
|
||||
import * as render from '../../renderGovernor.js';
|
||||
|
||||
/** Construct one layer using the application scene owners and a supplied source. */
|
||||
export function createApplicationLaunches({ source, satellites }) {
|
||||
export function createApplicationLaunches({
|
||||
source,
|
||||
satellites,
|
||||
renderer = {
|
||||
create: ({ engine = null, ...options }) =>
|
||||
engine
|
||||
? rendererFor('rocket-launches', engine, options)
|
||||
: createLaunchesCesiumRenderer(null, options),
|
||||
},
|
||||
}) {
|
||||
return createRocketLaunchesLayer({
|
||||
source,
|
||||
services: { satellites, geometry, overlays, render },
|
||||
services: { satellites, geometry, overlays, render, renderer },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Deterministic Launch Library 2 records for phase-3 mission/replay acceptance.
|
||||
import { SATELLITE_FIXTURE_EPOCH } from './satelliteFixture.js';
|
||||
export const LAUNCH_FIXTURE_EPOCH = SATELLITE_FIXTURE_EPOCH;
|
||||
export function launchFixture() {
|
||||
const definitions = [
|
||||
[
|
||||
'qa-iss',
|
||||
'QA ISS supply mission',
|
||||
'ISS (ZARYA)',
|
||||
'NASA',
|
||||
-80.604333,
|
||||
28.608389,
|
||||
'Launch Complex 39A',
|
||||
'Low Earth Orbit',
|
||||
],
|
||||
[
|
||||
'qa-meo',
|
||||
'QA navigation mission',
|
||||
'QA MEO payload',
|
||||
'Rocket Lab',
|
||||
177.864,
|
||||
-39.262,
|
||||
'Launch Complex 1',
|
||||
'Medium Earth Orbit',
|
||||
],
|
||||
[
|
||||
'qa-gto',
|
||||
'QA transfer mission',
|
||||
'QA GTO payload',
|
||||
'SpaceX',
|
||||
-52.768,
|
||||
5.236,
|
||||
'QA Equatorial Pad',
|
||||
'Geostationary Transfer Orbit',
|
||||
],
|
||||
];
|
||||
return {
|
||||
results: definitions.map(
|
||||
([id, name, payload, provider, lon, lat, pad, orbit], i) => ({
|
||||
id,
|
||||
name,
|
||||
net: new Date(LAUNCH_FIXTURE_EPOCH - (i + 1) * 86400000).toISOString(),
|
||||
status: { name: 'Launch Successful' },
|
||||
launch_service_provider: { name: provider },
|
||||
pad: {
|
||||
name: pad,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
location: { name: pad },
|
||||
},
|
||||
mission: {
|
||||
name: payload,
|
||||
description: 'Deterministic QA fixture',
|
||||
orbit: { name: orbit },
|
||||
},
|
||||
timeline: [
|
||||
{ name: 'MECO', relative_time: '00:02:30' },
|
||||
{ name: 'SECO', relative_time: '00:09:00' },
|
||||
{ name: 'Payload deployment', relative_time: '00:10:00' },
|
||||
],
|
||||
rocket: {
|
||||
payloads: [
|
||||
{
|
||||
id: `${id}-payload`,
|
||||
payload: {
|
||||
name: payload,
|
||||
type: { name: 'QA spacecraft' },
|
||||
mass: 500,
|
||||
},
|
||||
},
|
||||
],
|
||||
launcher_stage:
|
||||
i === 0
|
||||
? [
|
||||
{
|
||||
id: 'qa-booster',
|
||||
type: 'First stage',
|
||||
launcher: { serial_number: 'QA-01' },
|
||||
landing: {
|
||||
attempt: true,
|
||||
success: true,
|
||||
type: { name: 'Drone Ship' },
|
||||
landing_location: {
|
||||
name: 'QA Recovery Platform',
|
||||
latitude: 28.32,
|
||||
longitude: -75.12,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
trajectory: [],
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -124,7 +124,7 @@ test('every scene click handler consults ownership before it picks', () => {
|
||||
'src/layers/installations/selection.js',
|
||||
'viewer.scene.pick(click.position)',
|
||||
],
|
||||
['src/layers/launches/lifecycle.js', 'drillPick(movement.position'],
|
||||
['src/layers/launches/render/cesium.js', 'drillPick(movement.position'],
|
||||
['src/layers/radio/interaction.js', 'pickedRadioStationAt(click.position)'],
|
||||
[
|
||||
'src/layers/satellites/render/cesium.js',
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// Ported from CesiumJS 1.138 (Apache-2.0) — BoundingSphere.js subset.
|
||||
// Exact mission framing: Ritter/box fit and the union of two spheres.
|
||||
import * as Cartesian3 from './cartesian3.js';
|
||||
import { Check } from './check.js';
|
||||
import { DEBUG } from './debug.js';
|
||||
const defined = (value) => value !== null && value !== undefined;
|
||||
export function create(center = Cartesian3.ZERO, radius = 0) {
|
||||
return { center: Cartesian3.clone(center), radius };
|
||||
}
|
||||
export function clone(sphere, result = create()) {
|
||||
Cartesian3.clone(sphere.center, result.center);
|
||||
result.radius = sphere.radius;
|
||||
return result;
|
||||
}
|
||||
const fromPointsXMin = Cartesian3.create();
|
||||
const fromPointsYMin = Cartesian3.create();
|
||||
const fromPointsZMin = Cartesian3.create();
|
||||
const fromPointsXMax = Cartesian3.create();
|
||||
const fromPointsYMax = Cartesian3.create();
|
||||
const fromPointsZMax = Cartesian3.create();
|
||||
const fromPointsCurrentPos = Cartesian3.create();
|
||||
const fromPointsScratch = Cartesian3.create();
|
||||
const fromPointsRitterCenter = Cartesian3.create();
|
||||
const fromPointsMinBoxPt = Cartesian3.create();
|
||||
const fromPointsMaxBoxPt = Cartesian3.create();
|
||||
const fromPointsNaiveCenterScratch = Cartesian3.create();
|
||||
const unionScratch = Cartesian3.create();
|
||||
const unionScratchCenter = Cartesian3.create();
|
||||
export function fromPoints(positions, result) {
|
||||
if (!defined(result)) {
|
||||
result = create();
|
||||
}
|
||||
|
||||
if (!defined(positions) || positions.length === 0) {
|
||||
result.center = Cartesian3.clone(Cartesian3.ZERO, result.center);
|
||||
result.radius = 0.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
const currentPos = Cartesian3.clone(positions[0], fromPointsCurrentPos);
|
||||
|
||||
const xMin = Cartesian3.clone(currentPos, fromPointsXMin);
|
||||
const yMin = Cartesian3.clone(currentPos, fromPointsYMin);
|
||||
const zMin = Cartesian3.clone(currentPos, fromPointsZMin);
|
||||
|
||||
const xMax = Cartesian3.clone(currentPos, fromPointsXMax);
|
||||
const yMax = Cartesian3.clone(currentPos, fromPointsYMax);
|
||||
const zMax = Cartesian3.clone(currentPos, fromPointsZMax);
|
||||
|
||||
const numPositions = positions.length;
|
||||
let i;
|
||||
for (i = 1; i < numPositions; i++) {
|
||||
Cartesian3.clone(positions[i], currentPos);
|
||||
|
||||
const x = currentPos.x;
|
||||
const y = currentPos.y;
|
||||
const z = currentPos.z;
|
||||
|
||||
// Store points containing the the smallest and largest components
|
||||
if (x < xMin.x) {
|
||||
Cartesian3.clone(currentPos, xMin);
|
||||
}
|
||||
|
||||
if (x > xMax.x) {
|
||||
Cartesian3.clone(currentPos, xMax);
|
||||
}
|
||||
|
||||
if (y < yMin.y) {
|
||||
Cartesian3.clone(currentPos, yMin);
|
||||
}
|
||||
|
||||
if (y > yMax.y) {
|
||||
Cartesian3.clone(currentPos, yMax);
|
||||
}
|
||||
|
||||
if (z < zMin.z) {
|
||||
Cartesian3.clone(currentPos, zMin);
|
||||
}
|
||||
|
||||
if (z > zMax.z) {
|
||||
Cartesian3.clone(currentPos, zMax);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute x-, y-, and z-spans (Squared distances b/n each component's min. and max.).
|
||||
const xSpan = Cartesian3.magnitudeSquared(
|
||||
Cartesian3.subtract(xMax, xMin, fromPointsScratch),
|
||||
);
|
||||
const ySpan = Cartesian3.magnitudeSquared(
|
||||
Cartesian3.subtract(yMax, yMin, fromPointsScratch),
|
||||
);
|
||||
const zSpan = Cartesian3.magnitudeSquared(
|
||||
Cartesian3.subtract(zMax, zMin, fromPointsScratch),
|
||||
);
|
||||
|
||||
// Set the diameter endpoints to the largest span.
|
||||
let diameter1 = xMin;
|
||||
let diameter2 = xMax;
|
||||
let maxSpan = xSpan;
|
||||
if (ySpan > maxSpan) {
|
||||
maxSpan = ySpan;
|
||||
diameter1 = yMin;
|
||||
diameter2 = yMax;
|
||||
}
|
||||
if (zSpan > maxSpan) {
|
||||
maxSpan = zSpan;
|
||||
diameter1 = zMin;
|
||||
diameter2 = zMax;
|
||||
}
|
||||
|
||||
// Calculate the center of the initial sphere found by Ritter's algorithm
|
||||
const ritterCenter = fromPointsRitterCenter;
|
||||
ritterCenter.x = (diameter1.x + diameter2.x) * 0.5;
|
||||
ritterCenter.y = (diameter1.y + diameter2.y) * 0.5;
|
||||
ritterCenter.z = (diameter1.z + diameter2.z) * 0.5;
|
||||
|
||||
// Calculate the radius of the initial sphere found by Ritter's algorithm
|
||||
let radiusSquared = Cartesian3.magnitudeSquared(
|
||||
Cartesian3.subtract(diameter2, ritterCenter, fromPointsScratch),
|
||||
);
|
||||
let ritterRadius = Math.sqrt(radiusSquared);
|
||||
|
||||
// Find the center of the sphere found using the Naive method.
|
||||
const minBoxPt = fromPointsMinBoxPt;
|
||||
minBoxPt.x = xMin.x;
|
||||
minBoxPt.y = yMin.y;
|
||||
minBoxPt.z = zMin.z;
|
||||
|
||||
const maxBoxPt = fromPointsMaxBoxPt;
|
||||
maxBoxPt.x = xMax.x;
|
||||
maxBoxPt.y = yMax.y;
|
||||
maxBoxPt.z = zMax.z;
|
||||
|
||||
const naiveCenter = Cartesian3.midpoint(
|
||||
minBoxPt,
|
||||
maxBoxPt,
|
||||
fromPointsNaiveCenterScratch,
|
||||
);
|
||||
|
||||
// Begin 2nd pass to find naive radius and modify the ritter sphere.
|
||||
let naiveRadius = 0;
|
||||
for (i = 0; i < numPositions; i++) {
|
||||
Cartesian3.clone(positions[i], currentPos);
|
||||
|
||||
// Find the furthest point from the naive center to calculate the naive radius.
|
||||
const r = Cartesian3.magnitude(
|
||||
Cartesian3.subtract(currentPos, naiveCenter, fromPointsScratch),
|
||||
);
|
||||
if (r > naiveRadius) {
|
||||
naiveRadius = r;
|
||||
}
|
||||
|
||||
// Make adjustments to the Ritter Sphere to include all points.
|
||||
const oldCenterToPointSquared = Cartesian3.magnitudeSquared(
|
||||
Cartesian3.subtract(currentPos, ritterCenter, fromPointsScratch),
|
||||
);
|
||||
if (oldCenterToPointSquared > radiusSquared) {
|
||||
const oldCenterToPoint = Math.sqrt(oldCenterToPointSquared);
|
||||
// Calculate new radius to include the point that lies outside
|
||||
ritterRadius = (ritterRadius + oldCenterToPoint) * 0.5;
|
||||
radiusSquared = ritterRadius * ritterRadius;
|
||||
// Calculate center of new Ritter sphere
|
||||
const oldToNew = oldCenterToPoint - ritterRadius;
|
||||
ritterCenter.x =
|
||||
(ritterRadius * ritterCenter.x + oldToNew * currentPos.x) /
|
||||
oldCenterToPoint;
|
||||
ritterCenter.y =
|
||||
(ritterRadius * ritterCenter.y + oldToNew * currentPos.y) /
|
||||
oldCenterToPoint;
|
||||
ritterCenter.z =
|
||||
(ritterRadius * ritterCenter.z + oldToNew * currentPos.z) /
|
||||
oldCenterToPoint;
|
||||
}
|
||||
}
|
||||
|
||||
if (ritterRadius < naiveRadius) {
|
||||
Cartesian3.clone(ritterCenter, result.center);
|
||||
result.radius = ritterRadius;
|
||||
} else {
|
||||
Cartesian3.clone(naiveCenter, result.center);
|
||||
result.radius = naiveRadius;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
export function union(left, right, result) {
|
||||
if (DEBUG) {
|
||||
Check.typeOf.object('left', left);
|
||||
Check.typeOf.object('right', right);
|
||||
}
|
||||
|
||||
if (!defined(result)) {
|
||||
result = create();
|
||||
}
|
||||
|
||||
const leftCenter = left.center;
|
||||
const leftRadius = left.radius;
|
||||
const rightCenter = right.center;
|
||||
const rightRadius = right.radius;
|
||||
|
||||
const toRightCenter = Cartesian3.subtract(
|
||||
rightCenter,
|
||||
leftCenter,
|
||||
unionScratch,
|
||||
);
|
||||
const centerSeparation = Cartesian3.magnitude(toRightCenter);
|
||||
|
||||
if (leftRadius >= centerSeparation + rightRadius) {
|
||||
// Left sphere wins.
|
||||
clone(left, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (rightRadius >= centerSeparation + leftRadius) {
|
||||
// Right sphere wins.
|
||||
clone(right, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// There are two tangent points, one on far side of each sphere.
|
||||
const halfDistanceBetweenTangentPoints =
|
||||
(leftRadius + centerSeparation + rightRadius) * 0.5;
|
||||
|
||||
// Compute the center point halfway between the two tangent points.
|
||||
const center = Cartesian3.multiplyByScalar(
|
||||
toRightCenter,
|
||||
(-leftRadius + halfDistanceBetweenTangentPoints) / centerSeparation,
|
||||
unionScratchCenter,
|
||||
);
|
||||
Cartesian3.add(center, leftCenter, center);
|
||||
Cartesian3.clone(center, result.center);
|
||||
result.radius = halfDistanceBetweenTangentPoints;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from './index.js';
|
||||
const tuple = (s) => [s.center.x, s.center.y, s.center.z, s.radius];
|
||||
test('mission sphere fit and Earth/orbit union match native Cesium exactly', () => {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const positions = Array.from({ length: i * 3 }, (_, j) =>
|
||||
geo.cartesian3.fromDegrees(
|
||||
((j * 73 + i * 11) % 360) - 180,
|
||||
((j * 29) % 170) - 85,
|
||||
5e5 + i * 1e6,
|
||||
),
|
||||
);
|
||||
const fit = geo.boundingSphere.fromPoints(positions),
|
||||
native = Cesium.BoundingSphere.fromPoints(positions);
|
||||
assert.deepEqual(tuple(fit), tuple(native));
|
||||
const earth = geo.boundingSphere.create(geo.cartesian3.ZERO, 6378137);
|
||||
assert.deepEqual(
|
||||
tuple(geo.boundingSphere.union(earth, fit)),
|
||||
tuple(
|
||||
Cesium.BoundingSphere.union(
|
||||
new Cesium.BoundingSphere(Cesium.Cartesian3.ZERO, 6378137),
|
||||
native,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
test('union handles containment, in-place results and empty fits', () => {
|
||||
const outer = geo.boundingSphere.create(geo.cartesian3.create(1, 2, 3), 20),
|
||||
inner = geo.boundingSphere.create(geo.cartesian3.create(2, 2, 3), 1);
|
||||
assert.equal(geo.boundingSphere.union(outer, inner, inner), inner);
|
||||
assert.deepEqual(tuple(inner), tuple(outer));
|
||||
assert.deepEqual(tuple(geo.boundingSphere.fromPoints([])), [0, 0, 0, 0]);
|
||||
});
|
||||
@@ -15,3 +15,4 @@ export * as quaternion from './quaternion.js';
|
||||
export * as transforms from './transforms.js';
|
||||
|
||||
export * as ellipsoidGeodesic from './ellipsoidGeodesic.js';
|
||||
export * as boundingSphere from './boundingSphere.js';
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
REPLAY_ASCENT_CAMERA_OFFSET_RAD,
|
||||
@@ -14,7 +13,13 @@ import {
|
||||
REPLAY_ORBIT_FRAME_CENTER_BLEND,
|
||||
} from './policy.js';
|
||||
|
||||
export function createCamera({ state: layerState, services, parts, source }) {
|
||||
export function createCamera({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
function cameraHeadingForPath(path, progress, fallback = Math.PI) {
|
||||
if (!path?.length) return fallback;
|
||||
const current = parts.paths.samplePath(path, progress);
|
||||
@@ -26,7 +31,7 @@ export function createCamera({ state: layerState, services, parts, source }) {
|
||||
const nextCartographic =
|
||||
geo.ellipsoid.WGS84.cartesianToCartographic(next);
|
||||
if (!nextCartographic) continue;
|
||||
const geodesic = new Cesium.EllipsoidGeodesic(
|
||||
const geodesic = geo.ellipsoidGeodesic.create(
|
||||
currentCartographic,
|
||||
nextCartographic,
|
||||
);
|
||||
@@ -308,17 +313,13 @@ export function createCamera({ state: layerState, services, parts, source }) {
|
||||
*/
|
||||
|
||||
function replayOrbitFrameSphere(orbitPath = []) {
|
||||
const earth = new Cesium.BoundingSphere(
|
||||
const earth = geo.boundingSphere.create(
|
||||
geo.cartesian3.ZERO,
|
||||
geo.ellipsoid.WGS84.maximumRadius,
|
||||
);
|
||||
if (!Array.isArray(orbitPath) || orbitPath.length < 2) return earth;
|
||||
const orbit = Cesium.BoundingSphere.fromPoints(orbitPath);
|
||||
return Cesium.BoundingSphere.union(
|
||||
earth,
|
||||
orbit,
|
||||
new Cesium.BoundingSphere(),
|
||||
);
|
||||
const orbit = geo.boundingSphere.fromPoints(orbitPath);
|
||||
return geo.boundingSphere.union(earth, orbit, geo.boundingSphere.create());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { appClock } from '../../time/clock.js';
|
||||
export function createControls({ state: layerState, services, parts, source }) {
|
||||
const methods = {
|
||||
id: 'rocket-launches',
|
||||
@@ -26,7 +27,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
*/
|
||||
getAnalystRecords(maxCount = Infinity) {
|
||||
if (!layerState._enabled) return [];
|
||||
const now = Date.now();
|
||||
const now = appClock.now();
|
||||
const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
||||
const result = [];
|
||||
for (const launch of layerState._launches || []) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { createPolicyHelpers } from './policyHelpers.js';
|
||||
import { createIngestion } from './ingestion.js';
|
||||
import { createOrbitRendering } from './orbitRendering.js';
|
||||
@@ -21,7 +22,15 @@ export function createRocketLaunchesLayer({ services, source }) {
|
||||
throw new TypeError('A launches source is required');
|
||||
const state = createState({ services });
|
||||
const parts = {};
|
||||
const context = { state, services, parts, source };
|
||||
const getRenderer = () => {
|
||||
if (!state._renderer || state._renderer.view !== state._viewer)
|
||||
state._renderer = services.renderer.create({
|
||||
viewer: state._viewer,
|
||||
engine: state._engine,
|
||||
});
|
||||
return state._renderer;
|
||||
};
|
||||
const context = { state, services, parts, source, getRenderer };
|
||||
parts.policyHelpers = createPolicyHelpers(context);
|
||||
parts.ingestion = createIngestion(context);
|
||||
parts.orbitRendering = createOrbitRendering(context);
|
||||
@@ -43,6 +52,88 @@ export function createRocketLaunchesLayer({ services, source }) {
|
||||
parts.lifecycle.methods,
|
||||
parts.ingestion.methods,
|
||||
{
|
||||
qaDrawn() {
|
||||
if (!state._viewer || !state._dataSource) return null;
|
||||
const value = (v) =>
|
||||
typeof v?.getValue === 'function'
|
||||
? v.getValue(state._declutterTime)
|
||||
: v;
|
||||
const plain = (p) => (p ? { x: p.x, y: p.y, z: p.z } : null);
|
||||
const anchors = (positions) =>
|
||||
!positions?.length
|
||||
? []
|
||||
: Array.from({ length: 8 }, (_, i) =>
|
||||
plain(positions[Math.floor((i * (positions.length - 1)) / 8)]),
|
||||
);
|
||||
const track = state._replayTracks.get(state._selectedLaunchId);
|
||||
const frame = track?.getReplayState();
|
||||
return {
|
||||
enabled: state._enabled,
|
||||
selected: state._selectedLaunchId,
|
||||
speed: state._replaySpeed,
|
||||
paused: state._replayPaused,
|
||||
records: state._launches.map((l) => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
longitude: l.lon,
|
||||
latitude: l.lat,
|
||||
orbit: l.orbit?.name,
|
||||
})),
|
||||
points: state._dataSource.entities.values
|
||||
.filter((e) => e.point && e.position)
|
||||
.map((e) => {
|
||||
const color = value(e.point.color);
|
||||
return {
|
||||
id: e.id,
|
||||
position: plain(value(e.position)),
|
||||
show:
|
||||
state._dataSource.show &&
|
||||
e.show &&
|
||||
value(e.point.show) !== false,
|
||||
size: value(e.point.pixelSize),
|
||||
color: color
|
||||
? [color.red, color.green, color.blue, color.alpha]
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
paths: state._dataSource.entities.values
|
||||
.filter((e) => e.polyline)
|
||||
.map((e) => ({
|
||||
id: e.id,
|
||||
show: state._dataSource.show && e.show,
|
||||
anchors: anchors(value(e.polyline.positions)),
|
||||
})),
|
||||
rings: [...state._missionOrbitPrimitives].map(([id, path]) => ({
|
||||
id,
|
||||
show: path.primitive.show,
|
||||
anchors: anchors(path.basePositions).map((p) =>
|
||||
plain(
|
||||
geo.matrix4.multiplyByPoint(
|
||||
path.primitive.modelMatrix,
|
||||
p,
|
||||
geo.cartesian3.create(),
|
||||
),
|
||||
),
|
||||
),
|
||||
})),
|
||||
replay: frame
|
||||
? {
|
||||
...frame,
|
||||
active: state._replayCameraLaunchId === state._selectedLaunchId,
|
||||
ascentDurationSec: track.ascentDurationSec,
|
||||
position: plain(
|
||||
parts.paths.samplePath(
|
||||
frame.ascending
|
||||
? track.ascentPath
|
||||
: track.animatedOrbitPath,
|
||||
frame.phaseProgress,
|
||||
),
|
||||
),
|
||||
}
|
||||
: null,
|
||||
padVisible: state._launchPadZonePrimitive?.show === true,
|
||||
};
|
||||
},
|
||||
satelliteParamsForSpaceMissions:
|
||||
parts.policyHelpers.satelliteParamsForSpaceMissions,
|
||||
satelliteParamsAfterSpaceMissions:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { LAUNCH_PAD_ZONE_RADIUS_M } from './policy.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
|
||||
export function createLaunchPad({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
function hideLaunchPadZone() {
|
||||
if (layerState._launchPadZonePrimitive)
|
||||
@@ -13,134 +14,60 @@ export function createLaunchPad({
|
||||
}
|
||||
|
||||
function removeLaunchPadZonePrimitive() {
|
||||
if (
|
||||
layerState._launchPadZonePrimitive &&
|
||||
layerState._viewer?.scene?.primitives
|
||||
) {
|
||||
layerState._viewer.scene.primitives.remove(
|
||||
layerState._launchPadZonePrimitive,
|
||||
);
|
||||
}
|
||||
if (layerState._launchPadZonePrimitive)
|
||||
getRenderer().removePrimitive(layerState._launchPadZonePrimitive);
|
||||
layerState._launchPadZonePrimitive = null;
|
||||
layerState._launchPadZoneLaunchId = null;
|
||||
}
|
||||
|
||||
function createLaunchPadZonePrimitive(launch) {
|
||||
removeLaunchPadZonePrimitive();
|
||||
const material = new Cesium.Material({
|
||||
fabric: {
|
||||
type: 'GevLaunchPadZone',
|
||||
uniforms: {
|
||||
color: Cesium.Color.fromCssColorString('#22e6e6'),
|
||||
fillAlpha: 0.105,
|
||||
rimAlpha: 0.72,
|
||||
},
|
||||
source: `
|
||||
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||
vec2 centered = (materialInput.st - vec2(0.5)) * 2.0;
|
||||
float radius = length(centered);
|
||||
float inside = 1.0 - smoothstep(0.985, 1.0, radius);
|
||||
float rim = smoothstep(0.952, 0.985, radius) * inside;
|
||||
material.diffuse = color.rgb;
|
||||
material.emission = color.rgb * rim * 0.35;
|
||||
material.alpha = color.a * inside * mix(fillAlpha, rimAlpha, rim);
|
||||
return material;
|
||||
}`,
|
||||
},
|
||||
});
|
||||
const geometry = new Cesium.EllipseGeometry({
|
||||
center: Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat),
|
||||
semiMajorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
||||
semiMinorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
||||
granularity: Cesium.Math.toRadians(0.08),
|
||||
vertexFormat:
|
||||
Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat,
|
||||
});
|
||||
layerState._launchPadZonePrimitive =
|
||||
layerState._viewer.scene.primitives.add(
|
||||
new Cesium.GroundPrimitive({
|
||||
geometryInstances: new Cesium.GeometryInstance({
|
||||
geometry,
|
||||
id: `rocket-pad-zone:${launch.id}`,
|
||||
}),
|
||||
appearance: new Cesium.MaterialAppearance({
|
||||
material,
|
||||
translucent: true,
|
||||
closed: false,
|
||||
faceForward: true,
|
||||
flat: true,
|
||||
// Keep the zone classified onto the photoreal surface, but bias only its
|
||||
// rasterized depth toward the camera. This avoids coplanar fragments
|
||||
// being intermittently buried by the launch-pad mesh at oblique angles
|
||||
// without adding a world-space height that would make the ring float.
|
||||
renderState: {
|
||||
depthTest: {
|
||||
enabled: true,
|
||||
},
|
||||
depthMask: false,
|
||||
polygonOffset: {
|
||||
enabled: true,
|
||||
factor: -1,
|
||||
units: -4,
|
||||
},
|
||||
blending: Cesium.BlendingState.ALPHA_BLEND,
|
||||
},
|
||||
}),
|
||||
classificationType: Cesium.ClassificationType.BOTH,
|
||||
asynchronous: true,
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
layerState._launchPadZonePrimitive = getRenderer().createPad(launch);
|
||||
layerState._launchPadZoneLaunchId = launch.id;
|
||||
}
|
||||
|
||||
function initLaunchPadZonePrimitive() {
|
||||
if (!layerState._viewer || layerState._launchPadZoneRemover) return;
|
||||
layerState._launchPadZoneRemover =
|
||||
layerState._viewer.scene.preRender.addEventListener(() => {
|
||||
if (!layerState._enabled || !layerState._dataSource?.show) {
|
||||
hideLaunchPadZone();
|
||||
return;
|
||||
}
|
||||
const nowMs = Date.now();
|
||||
if (nowMs - layerState._lastMissionRingRotationMs >= 1000) {
|
||||
layerState._missionRingDate.setTime(nowMs);
|
||||
parts.orbitRendering.updateMissionOrbitPrimitiveFrames(
|
||||
layerState._missionRingDate,
|
||||
);
|
||||
layerState._lastMissionRingRotationMs = nowMs;
|
||||
}
|
||||
const launch = layerState._launches.find(
|
||||
(item) => item.id === layerState._selectedLaunchId,
|
||||
layerState._launchPadZoneRemover = getRenderer().on('preRender', () => {
|
||||
if (!layerState._enabled || !layerState._dataSource?.show) {
|
||||
hideLaunchPadZone();
|
||||
return;
|
||||
}
|
||||
const nowMs = Date.now();
|
||||
if (nowMs - layerState._lastMissionRingRotationMs >= 1000) {
|
||||
layerState._missionRingDate.setTime(appClock.now());
|
||||
parts.orbitRendering.updateMissionOrbitPrimitiveFrames(
|
||||
layerState._missionRingDate,
|
||||
);
|
||||
const camera = layerState._viewer?.camera;
|
||||
if (!launch || !camera) {
|
||||
hideLaunchPadZone();
|
||||
return;
|
||||
}
|
||||
const launchPosition = Cesium.Cartesian3.fromDegrees(
|
||||
launch.lon,
|
||||
launch.lat,
|
||||
);
|
||||
const visible = parts.policyHelpers.launchPadZoneVisible({
|
||||
layerActive: Boolean(layerState._dataSource?.show),
|
||||
selectedLaunchId: layerState._selectedLaunchId,
|
||||
launchId: launch.id,
|
||||
cameraHeightM: camera.positionCartographic?.height,
|
||||
cameraDistanceM: Cesium.Cartesian3.distance(
|
||||
camera.positionWC,
|
||||
launchPosition,
|
||||
),
|
||||
});
|
||||
if (!visible) {
|
||||
hideLaunchPadZone();
|
||||
return;
|
||||
}
|
||||
if (layerState._launchPadZoneLaunchId !== launch.id)
|
||||
createLaunchPadZonePrimitive(launch);
|
||||
layerState._launchPadZonePrimitive.show = true;
|
||||
layerState._lastMissionRingRotationMs = nowMs;
|
||||
}
|
||||
const launch = layerState._launches.find(
|
||||
(item) => item.id === layerState._selectedLaunchId,
|
||||
);
|
||||
const camera = layerState._viewer?.camera;
|
||||
if (!launch || !camera) {
|
||||
hideLaunchPadZone();
|
||||
return;
|
||||
}
|
||||
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||
const visible = parts.policyHelpers.launchPadZoneVisible({
|
||||
layerActive: Boolean(layerState._dataSource?.show),
|
||||
selectedLaunchId: layerState._selectedLaunchId,
|
||||
launchId: launch.id,
|
||||
cameraHeightM: camera.positionCartographic?.height,
|
||||
cameraDistanceM: geo.cartesian3.distance(
|
||||
camera.positionWC,
|
||||
launchPosition,
|
||||
),
|
||||
});
|
||||
if (!visible) {
|
||||
hideLaunchPadZone();
|
||||
return;
|
||||
}
|
||||
if (layerState._launchPadZoneLaunchId !== launch.id)
|
||||
createLaunchPadZonePrimitive(launch);
|
||||
layerState._launchPadZonePrimitive.show = true;
|
||||
});
|
||||
}
|
||||
|
||||
function destroyLaunchPadZonePrimitive() {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { createLaunchPad } from './launchPad.js';
|
||||
test('selected pad frame uses neutral geometry and the physical orbit epoch', () => {
|
||||
const launch = { id: 'qa', lon: -80.6, lat: 28.6 };
|
||||
let tick,
|
||||
created = 0,
|
||||
epoch;
|
||||
const state = {
|
||||
_viewer: {
|
||||
camera: {
|
||||
positionWC: geo.cartesian3.fromDegrees(launch.lon, launch.lat, 500),
|
||||
positionCartographic: { height: 500 },
|
||||
},
|
||||
},
|
||||
_enabled: true,
|
||||
_dataSource: { show: true },
|
||||
_selectedLaunchId: 'qa',
|
||||
_launches: [launch],
|
||||
_launchPadZoneRemover: null,
|
||||
_lastMissionRingRotationMs: 0,
|
||||
_missionRingDate: new Date(0),
|
||||
_launchPadZonePrimitive: null,
|
||||
_launchPadZoneLaunchId: null,
|
||||
};
|
||||
const renderer = {
|
||||
on: (event, fn) => {
|
||||
assert.equal(event, 'preRender');
|
||||
tick = fn;
|
||||
return () => {};
|
||||
},
|
||||
createPad: () => {
|
||||
created++;
|
||||
return { show: true };
|
||||
},
|
||||
removePrimitive: () => {},
|
||||
};
|
||||
const parts = {
|
||||
orbitRendering: {
|
||||
updateMissionOrbitPrimitiveFrames: (date) => {
|
||||
epoch = date.getTime();
|
||||
},
|
||||
},
|
||||
policyHelpers: {
|
||||
launchPadZoneVisible: ({ cameraDistanceM }) => cameraDistanceM < 1000,
|
||||
},
|
||||
};
|
||||
try {
|
||||
appClock.freeze(123000);
|
||||
createLaunchPad({
|
||||
state,
|
||||
services: {},
|
||||
parts,
|
||||
source: {},
|
||||
getRenderer: () => renderer,
|
||||
}).initLaunchPadZonePrimitive();
|
||||
tick();
|
||||
assert.equal(created, 1);
|
||||
assert.equal(state._launchPadZonePrimitive.show, true);
|
||||
assert.equal(epoch, 123000);
|
||||
} finally {
|
||||
appClock.reset();
|
||||
}
|
||||
});
|
||||
@@ -1,61 +1,54 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPointerFree } from '../../data/inputOwnership.js';
|
||||
|
||||
export function createLifecycle({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||
|
||||
const methods = {
|
||||
init(viewer) {
|
||||
layerState._viewer = viewer;
|
||||
init(viewer, { engine = null } = {}) {
|
||||
layerState._engine = engine;
|
||||
layerState._renderer = services.renderer.create({ viewer, engine });
|
||||
layerState._viewer = layerState._renderer.view;
|
||||
layerState._declutterOccluder = getRenderer().occluder();
|
||||
layerState._enabled = false;
|
||||
parts.panel.createMissionPanel();
|
||||
parts.overlays.createReplayVehicleOverlay();
|
||||
layerState._dataSource = new Cesium.CustomDataSource('rocket-launches');
|
||||
layerState._dataSource.show = false;
|
||||
viewer.dataSources.add(layerState._dataSource);
|
||||
layerState._dataSource = getRenderer().createDataSource();
|
||||
layerState._count = 0;
|
||||
layerState._lastUpdate = null;
|
||||
layerState._lastError = null;
|
||||
layerState._orbitMatches = 0;
|
||||
parts.overlays.clearMissionOverlaySources();
|
||||
layerState._clickHandler = new Cesium.ScreenSpaceEventHandler(
|
||||
viewer.scene.canvas,
|
||||
const enabled = () => layerState._enabled && layerState._dataSource?.show;
|
||||
layerState._clickHandler = getRenderer().listenClick(
|
||||
layerState._dataSource,
|
||||
(entity) => {
|
||||
const launchId = parts.selection.entityLaunchId(entity);
|
||||
if (!launchId) return;
|
||||
parts.selection.setSelectedMission(launchId);
|
||||
parts.selection.focusMission(
|
||||
layerState._launches.find((l) => l.id === launchId),
|
||||
);
|
||||
},
|
||||
{ enabled },
|
||||
);
|
||||
layerState._clickHandler.setInputAction((movement) => {
|
||||
// A tool owns the pointer (src/data/inputOwnership.js): yield the click.
|
||||
if (!isPointerFree()) return;
|
||||
if (!layerState._enabled || !layerState._dataSource?.show) return;
|
||||
const entity = viewer.scene
|
||||
.drillPick(movement.position, 12)
|
||||
.map((picked) => picked?.id)
|
||||
.find((candidate) => parts.selection.entityLaunchId(candidate));
|
||||
const launchId = parts.selection.entityLaunchId(entity);
|
||||
if (!launchId) return;
|
||||
parts.selection.setSelectedMission(launchId);
|
||||
parts.selection.focusMission(
|
||||
layerState._launches.find((launch) => launch.id === launchId),
|
||||
);
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
layerState._moveHandler = new Cesium.ScreenSpaceEventHandler(
|
||||
viewer.scene.canvas,
|
||||
layerState._moveHandler = getRenderer().listenMove(
|
||||
layerState._dataSource,
|
||||
(entity) => {
|
||||
layerState._viewer.scene.canvas.style.cursor = entity
|
||||
? 'pointer'
|
||||
: '';
|
||||
},
|
||||
{ enabled },
|
||||
);
|
||||
layerState._moveHandler.setInputAction((movement) => {
|
||||
if (!layerState._enabled || !layerState._dataSource?.show) return;
|
||||
const missionEntity = viewer.scene
|
||||
.drillPick(movement.endPosition, 12)
|
||||
.map((picked) => picked?.id)
|
||||
.find((candidate) => parts.selection.entityLaunchId(candidate));
|
||||
viewer.scene.canvas.style.cursor = missionEntity ? 'pointer' : '';
|
||||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||
// Apply horizon visibility before Cesium traverses and picks the scene.
|
||||
// A postRender write is one frame late and can remain visibly stale when
|
||||
// request-on-demand rendering stops after a globe camera move.
|
||||
layerState._declutterHandler = viewer.scene.preRender.addEventListener(
|
||||
layerState._declutterHandler = getRenderer().on(
|
||||
'preRender',
|
||||
parts.rendering.updateMissionFrame,
|
||||
);
|
||||
parts.launchPad.initLaunchPadZonePrimitive();
|
||||
@@ -153,9 +146,8 @@ export function createLifecycle({
|
||||
}
|
||||
layerState._missionRosterPreviewOwnership = null;
|
||||
layerState._missionRoster = null;
|
||||
layerState._viewer = null;
|
||||
if (layerState._dataSource)
|
||||
viewer.dataSources.remove(layerState._dataSource, true);
|
||||
getRenderer().removeDataSource(layerState._dataSource);
|
||||
layerState._dataSource = null;
|
||||
layerState._count = 0;
|
||||
layerState._orbitMatches = 0;
|
||||
@@ -168,6 +160,10 @@ export function createLifecycle({
|
||||
layerState._satelliteStateBeforeMission = null;
|
||||
layerState._satelliteActivationPromise = null;
|
||||
layerState._dataManager = null;
|
||||
layerState._renderer?.destroy();
|
||||
layerState._renderer = null;
|
||||
layerState._engine = null;
|
||||
layerState._viewer = null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { rgbaFromCss } from '../shared/color.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { WINDOW_DAYS } from './policy.js';
|
||||
|
||||
export function createModel({ state: layerState, services, parts, source }) {
|
||||
export function createModel({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
function finiteCoordinate(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
@@ -137,23 +145,20 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
const identity =
|
||||
`${launch.provider || ''} ${launch.name || ''} ${launch.missionName || ''}`.toLowerCase();
|
||||
if (/nasa|national aeronautics/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#ff9f43');
|
||||
return rgbaFromCss('#ff9f43');
|
||||
if (/starlink|spacex|space exploration/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#4cc9f0');
|
||||
if (/rocket lab/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#7bed9f');
|
||||
if (/isro|indian space/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#ff66c4');
|
||||
return rgbaFromCss('#4cc9f0');
|
||||
if (/rocket lab/.test(identity)) return rgbaFromCss('#7bed9f');
|
||||
if (/isro|indian space/.test(identity)) return rgbaFromCss('#ff66c4');
|
||||
if (/cnsa|china national|long march/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#ffd166');
|
||||
if (/blue origin/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#a78bfa');
|
||||
return rgbaFromCss('#ffd166');
|
||||
if (/blue origin/.test(identity)) return rgbaFromCss('#a78bfa');
|
||||
if (/ula|united launch alliance/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#f97316');
|
||||
return rgbaFromCss('#f97316');
|
||||
if (/arianespace|esa|european space/.test(identity))
|
||||
return Cesium.Color.fromCssColorString('#60a5fa');
|
||||
if (launch.provider) return Cesium.Color.fromCssColorString('#c084fc');
|
||||
return Cesium.Color.fromCssColorString('#22e6e6');
|
||||
return rgbaFromCss('#60a5fa');
|
||||
if (launch.provider) return rgbaFromCss('#c084fc');
|
||||
return rgbaFromCss('#22e6e6');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +170,7 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
|
||||
function normalizeRocketLaunches(payload, now = new Date()) {
|
||||
function normalizeRocketLaunches(payload, now = new Date(appClock.now())) {
|
||||
const launches = Array.isArray(payload) ? payload : payload?.results;
|
||||
if (!Array.isArray(launches)) return [];
|
||||
const cutoff = now.getTime() - WINDOW_DAYS * 86400000;
|
||||
|
||||
@@ -1,68 +1,12 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
MISSION_ORBIT_PATTERN_GROUPS,
|
||||
MISSION_ORBIT_DASHES_PER_GROUP,
|
||||
} from './policy.js';
|
||||
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { rgbaFromCss } from '../shared/color.js';
|
||||
export function createOrbitRendering({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const { orbitFrameModelMatrix } = services.satellites;
|
||||
|
||||
/**
|
||||
* Register the selected-orbit tactical material once. Each group begins with
|
||||
* one compact round dot followed by one hundred short dashes.
|
||||
*/
|
||||
|
||||
function ensureMissionOrbitPatternRegistered() {
|
||||
if (layerState._missionOrbitPatternRegistered) return;
|
||||
new Cesium.Material({
|
||||
fabric: {
|
||||
type: 'GevMissionOrbitTactical',
|
||||
uniforms: {
|
||||
color: Cesium.Color.CYAN,
|
||||
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
||||
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
||||
},
|
||||
source: `
|
||||
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||
float groupPosition = fract(materialInput.st.s * groupCount);
|
||||
float markPosition = groupPosition * (dashCount + 1.0);
|
||||
float markIndex = floor(markPosition);
|
||||
float localPosition = fract(markPosition);
|
||||
float centerDistance = abs(localPosition - 0.5);
|
||||
float edge = max(fwidth(localPosition) * 1.35, 0.012);
|
||||
float dashAlong = 1.0 - smoothstep(0.27 - edge, 0.27 + edge, centerDistance);
|
||||
float dashAcross = 1.0 - smoothstep(0.12, 0.24, abs(materialInput.st.t - 0.5));
|
||||
float dash = dashAlong * dashAcross;
|
||||
float dotAlong = (localPosition - 0.5) / 0.32;
|
||||
float dotAcross = (materialInput.st.t - 0.5) / 0.5;
|
||||
float dot = 1.0 - smoothstep(0.78, 1.0, length(vec2(dotAlong, dotAcross)));
|
||||
float isDot = 1.0 - step(0.5, markIndex);
|
||||
float visible = mix(dash, dot, isDot);
|
||||
material.diffuse = color.rgb;
|
||||
material.emission = color.rgb * mix(0.07, 0.65, isDot);
|
||||
material.alpha = color.a * visible * mix(0.58, 1.0, isDot);
|
||||
return material;
|
||||
}`,
|
||||
},
|
||||
});
|
||||
layerState._missionOrbitPatternRegistered = true;
|
||||
}
|
||||
|
||||
function createMissionOrbitPatternMaterial(color) {
|
||||
ensureMissionOrbitPatternRegistered();
|
||||
return Cesium.Material.fromType('GevMissionOrbitTactical', {
|
||||
color,
|
||||
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
||||
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
||||
});
|
||||
}
|
||||
|
||||
function missionOrbitPrimitiveVisible(launchId) {
|
||||
return Boolean(
|
||||
layerState._enabled &&
|
||||
@@ -81,8 +25,8 @@ export function createOrbitRendering({
|
||||
|
||||
function removeMissionOrbitPrimitives() {
|
||||
for (const path of layerState._missionOrbitPrimitives.values()) {
|
||||
if (path.primitive && layerState._viewer?.scene?.primitives) {
|
||||
layerState._viewer.scene.primitives.remove(path.primitive);
|
||||
if (path.primitive && layerState._viewer) {
|
||||
getRenderer().removePrimitive(path.primitive);
|
||||
}
|
||||
}
|
||||
layerState._missionOrbitPrimitives.clear();
|
||||
@@ -96,8 +40,9 @@ export function createOrbitRendering({
|
||||
nowDate,
|
||||
path.primitive.modelMatrix,
|
||||
);
|
||||
getRenderer().commitOrbit(path.primitive);
|
||||
if (path.labelBakePosition && path.labelPosition) {
|
||||
Cesium.Matrix4.multiplyByPoint(
|
||||
geo.matrix4.multiplyByPoint(
|
||||
path.primitive.modelMatrix,
|
||||
path.labelBakePosition,
|
||||
path.labelPosition,
|
||||
@@ -113,71 +58,30 @@ export function createOrbitRendering({
|
||||
!Number.isFinite(satelliteTrack.gmstAtBake)
|
||||
)
|
||||
return false;
|
||||
const collection = new Cesium.PolylineCollection();
|
||||
collection.add({
|
||||
const collection = getRenderer().createOrbit({
|
||||
positions: orbitPath,
|
||||
width: 3,
|
||||
material: createMissionOrbitPatternMaterial(
|
||||
Cesium.Color.fromCssColorString('#22e6e6').withAlpha(0.95),
|
||||
),
|
||||
color: rgbaFromCss('#22e6e6').withAlpha(0.95),
|
||||
});
|
||||
collection.show = missionOrbitPrimitiveVisible(launch.id);
|
||||
orbitFrameModelMatrix(
|
||||
satelliteTrack.gmstAtBake,
|
||||
new Date(),
|
||||
new Date(appClock.now()),
|
||||
collection.modelMatrix,
|
||||
);
|
||||
layerState._viewer.scene.primitives.add(collection);
|
||||
layerState._missionOrbitPrimitives.set(launch.id, {
|
||||
primitive: collection,
|
||||
basePositions: orbitPath,
|
||||
gmstAtBake: satelliteTrack.gmstAtBake,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function MissionOrbitPatternMaterialProperty(color) {
|
||||
ensureMissionOrbitPatternRegistered();
|
||||
this._color = color;
|
||||
this._definitionChanged = new Cesium.Event();
|
||||
}
|
||||
Object.defineProperties(MissionOrbitPatternMaterialProperty.prototype, {
|
||||
isConstant: {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
definitionChanged: {
|
||||
get() {
|
||||
return this._definitionChanged;
|
||||
},
|
||||
},
|
||||
});
|
||||
MissionOrbitPatternMaterialProperty.prototype.getType = function getType() {
|
||||
return 'GevMissionOrbitTactical';
|
||||
};
|
||||
MissionOrbitPatternMaterialProperty.prototype.getValue = function getValue(
|
||||
time,
|
||||
result,
|
||||
) {
|
||||
if (!Cesium.defined(result)) result = {};
|
||||
result.color = this._color;
|
||||
result.groupCount = MISSION_ORBIT_PATTERN_GROUPS;
|
||||
result.dashCount = MISSION_ORBIT_DASHES_PER_GROUP;
|
||||
return result;
|
||||
};
|
||||
MissionOrbitPatternMaterialProperty.prototype.equals = function equals(
|
||||
other,
|
||||
) {
|
||||
return this === other;
|
||||
};
|
||||
return {
|
||||
ensureMissionOrbitPatternRegistered,
|
||||
createMissionOrbitPatternMaterial,
|
||||
missionOrbitPrimitiveVisible,
|
||||
syncMissionOrbitPrimitiveVisibility,
|
||||
removeMissionOrbitPrimitives,
|
||||
updateMissionOrbitPrimitiveFrames,
|
||||
addMissionOrbitPrimitive,
|
||||
MissionOrbitPatternMaterialProperty,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { engineFromViewer } from '../../globe/cesium/engineFromViewer.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
createCyberSonarSampler,
|
||||
isCyberContactSonarActive as isCyberSonarActive,
|
||||
@@ -12,7 +11,13 @@ import {
|
||||
ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_OPTIONS,
|
||||
} from './policy.js';
|
||||
|
||||
export function createOverlays({ state: layerState, services, parts, source }) {
|
||||
export function createOverlays({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
/**
|
||||
* Resolve whether a surface mission anchor is safely on the camera-facing
|
||||
* side of Earth. The small positive limb margin prevents labels anchored just
|
||||
@@ -29,25 +34,24 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
||||
limbMargin = 0.012,
|
||||
) {
|
||||
if (!cameraPosition || !markerPosition) return false;
|
||||
const cameraMagnitude = Cesium.Cartesian3.magnitude(cameraPosition);
|
||||
const cameraMagnitude = geo.cartesian3.magnitude(cameraPosition);
|
||||
if (
|
||||
!Number.isFinite(cameraMagnitude) ||
|
||||
cameraMagnitude <= Cesium.Ellipsoid.WGS84.maximumRadius
|
||||
cameraMagnitude <= geo.ellipsoid.WGS84.maximumRadius
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const cameraDirection = Cesium.Cartesian3.normalize(
|
||||
const cameraDirection = geo.cartesian3.normalize(
|
||||
cameraPosition,
|
||||
new Cesium.Cartesian3(),
|
||||
geo.cartesian3.create(),
|
||||
);
|
||||
const markerDirection = Cesium.Cartesian3.normalize(
|
||||
const markerDirection = geo.cartesian3.normalize(
|
||||
markerPosition,
|
||||
new Cesium.Cartesian3(),
|
||||
geo.cartesian3.create(),
|
||||
);
|
||||
const limbThreshold =
|
||||
Cesium.Ellipsoid.WGS84.maximumRadius / cameraMagnitude;
|
||||
const limbThreshold = geo.ellipsoid.WGS84.maximumRadius / cameraMagnitude;
|
||||
return (
|
||||
Cesium.Cartesian3.dot(cameraDirection, markerDirection) >
|
||||
geo.cartesian3.dot(cameraDirection, markerDirection) >
|
||||
limbThreshold + limbMargin
|
||||
);
|
||||
}
|
||||
@@ -484,15 +488,7 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
||||
hideReplayVehicleOverlay();
|
||||
return null;
|
||||
}
|
||||
// The engine's projector (engineFromViewer), as the world overlay.
|
||||
const projector = engineFromViewer(layerState._viewer)?.projector;
|
||||
const project = (point) =>
|
||||
projector
|
||||
? projector.worldToWindow(point)
|
||||
: Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
layerState._viewer.scene,
|
||||
point,
|
||||
);
|
||||
const project = (point) => getRenderer().project(point);
|
||||
const windowPosition = project(position);
|
||||
if (!windowPosition) {
|
||||
hideReplayVehicleOverlay();
|
||||
@@ -584,7 +580,7 @@ export function createOverlays({ state: layerState, services, parts, source }) {
|
||||
}
|
||||
layerState._replayVehicleOverlay.style.setProperty(
|
||||
'--mission-replay-rotation',
|
||||
`${Cesium.Math.toDegrees(vehicleRotation)}deg`,
|
||||
`${geo.math.toDegrees(vehicleRotation)}deg`,
|
||||
);
|
||||
|
||||
const mission = shortMissionLabel(launch.name, 22).toUpperCase();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
@@ -6,7 +5,13 @@ import {
|
||||
MISSION_GLOBE_VIEW_RANGE_M,
|
||||
} from './policy.js';
|
||||
|
||||
export function createPanel({ state: layerState, services, parts, source }) {
|
||||
export function createPanel({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
/**
|
||||
* Preserve the user's globe scale for roster previews while avoiding an
|
||||
* accidental surface-level fly-to when the list is opened from a close view.
|
||||
@@ -359,10 +364,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
|
||||
`rocket-satellite:${layerState._selectedLaunchId}`,
|
||||
);
|
||||
const position = satellite?.position?.getValue(
|
||||
Cesium.JulianDate.fromDate(
|
||||
new Date(appClock.now()),
|
||||
layerState._declutterTime,
|
||||
),
|
||||
getRenderer().time(appClock.now()),
|
||||
);
|
||||
const altitudeM = position
|
||||
? geo.cartographic.fromCartesian(position)?.height
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
SATELLITE_STANDALONE_DEFAULTS,
|
||||
@@ -11,6 +10,7 @@ export function createPolicyHelpers({
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
/**
|
||||
* Derive the temporary Satellite display mode required by Space Missions.
|
||||
@@ -229,11 +229,11 @@ export function createPolicyHelpers({
|
||||
);
|
||||
const position = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||
layerState._viewer.camera.flyToBoundingSphere(
|
||||
new Cesium.BoundingSphere(position, 0),
|
||||
geo.boundingSphere.create(position, 0),
|
||||
{
|
||||
offset: new Cesium.HeadingPitchRange(0, -geo.math.PI_OVER_TWO, range),
|
||||
offset: getRenderer().range(0, -geo.math.PI_OVER_TWO, range),
|
||||
duration: 0.8,
|
||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
easingFunction: getRenderer().easing,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// Native mission graphics stay here; shared mission logic uses plain geometry.
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPointerFree } from '../../../data/inputOwnership.js';
|
||||
import {
|
||||
MISSION_ORBIT_PATTERN_GROUPS,
|
||||
MISSION_ORBIT_DASHES_PER_GROUP,
|
||||
LAUNCH_PAD_ZONE_RADIUS_M,
|
||||
} from '../policy.js';
|
||||
export function createLaunchesCesiumRenderer(engine, { viewer } = {}) {
|
||||
let patternRegistered = false;
|
||||
const scratchTime = new Cesium.JulianDate();
|
||||
const color = (value) =>
|
||||
value?.red !== undefined
|
||||
? new Cesium.Color(value.red, value.green, value.blue, value.alpha)
|
||||
: value;
|
||||
function ensureMissionOrbitPatternRegistered() {
|
||||
if (patternRegistered) return;
|
||||
new Cesium.Material({
|
||||
fabric: {
|
||||
type: 'GevMissionOrbitTactical',
|
||||
uniforms: {
|
||||
color: Cesium.Color.CYAN,
|
||||
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
||||
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
||||
},
|
||||
source: `
|
||||
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||
float groupPosition = fract(materialInput.st.s * groupCount);
|
||||
float markPosition = groupPosition * (dashCount + 1.0);
|
||||
float markIndex = floor(markPosition);
|
||||
float localPosition = fract(markPosition);
|
||||
float centerDistance = abs(localPosition - 0.5);
|
||||
float edge = max(fwidth(localPosition) * 1.35, 0.012);
|
||||
float dashAlong = 1.0 - smoothstep(0.27 - edge, 0.27 + edge, centerDistance);
|
||||
float dashAcross = 1.0 - smoothstep(0.12, 0.24, abs(materialInput.st.t - 0.5));
|
||||
float dash = dashAlong * dashAcross;
|
||||
float dotAlong = (localPosition - 0.5) / 0.32;
|
||||
float dotAcross = (materialInput.st.t - 0.5) / 0.5;
|
||||
float dot = 1.0 - smoothstep(0.78, 1.0, length(vec2(dotAlong, dotAcross)));
|
||||
float isDot = 1.0 - step(0.5, markIndex);
|
||||
float visible = mix(dash, dot, isDot);
|
||||
material.diffuse = color.rgb;
|
||||
material.emission = color.rgb * mix(0.07, 0.65, isDot);
|
||||
material.alpha = color.a * visible * mix(0.58, 1.0, isDot);
|
||||
return material;
|
||||
}`,
|
||||
},
|
||||
});
|
||||
patternRegistered = true;
|
||||
}
|
||||
|
||||
function createMissionOrbitPatternMaterial(color) {
|
||||
ensureMissionOrbitPatternRegistered();
|
||||
return Cesium.Material.fromType('GevMissionOrbitTactical', {
|
||||
color,
|
||||
groupCount: MISSION_ORBIT_PATTERN_GROUPS,
|
||||
dashCount: MISSION_ORBIT_DASHES_PER_GROUP,
|
||||
});
|
||||
}
|
||||
|
||||
function MissionOrbitPatternMaterialProperty(color) {
|
||||
ensureMissionOrbitPatternRegistered();
|
||||
this._color = color;
|
||||
this._definitionChanged = new Cesium.Event();
|
||||
}
|
||||
Object.defineProperties(MissionOrbitPatternMaterialProperty.prototype, {
|
||||
isConstant: {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
definitionChanged: {
|
||||
get() {
|
||||
return this._definitionChanged;
|
||||
},
|
||||
},
|
||||
});
|
||||
MissionOrbitPatternMaterialProperty.prototype.getType = function getType() {
|
||||
return 'GevMissionOrbitTactical';
|
||||
};
|
||||
MissionOrbitPatternMaterialProperty.prototype.getValue = function getValue(
|
||||
time,
|
||||
result,
|
||||
) {
|
||||
if (!Cesium.defined(result)) result = {};
|
||||
result.color = this._color;
|
||||
result.groupCount = MISSION_ORBIT_PATTERN_GROUPS;
|
||||
result.dashCount = MISSION_ORBIT_DASHES_PER_GROUP;
|
||||
return result;
|
||||
};
|
||||
MissionOrbitPatternMaterialProperty.prototype.equals = function equals(
|
||||
other,
|
||||
) {
|
||||
return this === other;
|
||||
};
|
||||
function createPad(launch) {
|
||||
const material = new Cesium.Material({
|
||||
fabric: {
|
||||
type: 'GevLaunchPadZone',
|
||||
uniforms: {
|
||||
color: Cesium.Color.fromCssColorString('#22e6e6'),
|
||||
fillAlpha: 0.105,
|
||||
rimAlpha: 0.72,
|
||||
},
|
||||
source: `
|
||||
czm_material czm_getMaterial(czm_materialInput materialInput) {
|
||||
czm_material material = czm_getDefaultMaterial(materialInput);
|
||||
vec2 centered = (materialInput.st - vec2(0.5)) * 2.0;
|
||||
float radius = length(centered);
|
||||
float inside = 1.0 - smoothstep(0.985, 1.0, radius);
|
||||
float rim = smoothstep(0.952, 0.985, radius) * inside;
|
||||
material.diffuse = color.rgb;
|
||||
material.emission = color.rgb * rim * 0.35;
|
||||
material.alpha = color.a * inside * mix(fillAlpha, rimAlpha, rim);
|
||||
return material;
|
||||
}`,
|
||||
},
|
||||
});
|
||||
const geometry = new Cesium.EllipseGeometry({
|
||||
center: Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat),
|
||||
semiMajorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
||||
semiMinorAxis: LAUNCH_PAD_ZONE_RADIUS_M,
|
||||
granularity: Cesium.Math.toRadians(0.08),
|
||||
vertexFormat:
|
||||
Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat,
|
||||
});
|
||||
const primitive = viewer.scene.primitives.add(
|
||||
new Cesium.GroundPrimitive({
|
||||
geometryInstances: new Cesium.GeometryInstance({
|
||||
geometry,
|
||||
id: `rocket-pad-zone:${launch.id}`,
|
||||
}),
|
||||
appearance: new Cesium.MaterialAppearance({
|
||||
material,
|
||||
translucent: true,
|
||||
closed: false,
|
||||
faceForward: true,
|
||||
flat: true,
|
||||
// Keep the zone classified onto the photoreal surface, but bias only its
|
||||
// rasterized depth toward the camera. This avoids coplanar fragments
|
||||
// being intermittently buried by the launch-pad mesh at oblique angles
|
||||
// without adding a world-space height that would make the ring float.
|
||||
renderState: {
|
||||
depthTest: {
|
||||
enabled: true,
|
||||
},
|
||||
depthMask: false,
|
||||
polygonOffset: {
|
||||
enabled: true,
|
||||
factor: -1,
|
||||
units: -4,
|
||||
},
|
||||
blending: Cesium.BlendingState.ALPHA_BLEND,
|
||||
},
|
||||
}),
|
||||
classificationType: Cesium.ClassificationType.BOTH,
|
||||
asynchronous: true,
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
return primitive;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'cesium',
|
||||
engine,
|
||||
view: viewer,
|
||||
time(epochMs) {
|
||||
return Cesium.JulianDate.fromDate(new Date(epochMs), scratchTime);
|
||||
},
|
||||
occluder: () =>
|
||||
new Cesium.EllipsoidalOccluder(
|
||||
Cesium.Ellipsoid.WGS84,
|
||||
Cesium.Cartesian3.ZERO,
|
||||
),
|
||||
callback: (fn) => new Cesium.CallbackProperty(fn, false),
|
||||
range: (heading, pitch, range) =>
|
||||
new Cesium.HeadingPitchRange(heading, pitch, range),
|
||||
easing: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
createDataSource() {
|
||||
const source = new Cesium.CustomDataSource('rocket-launches');
|
||||
source.show = false;
|
||||
viewer.dataSources.add(source);
|
||||
return source;
|
||||
},
|
||||
removeDataSource: (source) => viewer?.dataSources.remove(source, true),
|
||||
addEntity(source, spec) {
|
||||
const options = { ...spec };
|
||||
for (const key of ['point', 'billboard'])
|
||||
if (spec[key])
|
||||
options[key] = {
|
||||
...spec[key],
|
||||
...(spec[key].color ? { color: color(spec[key].color) } : {}),
|
||||
...(spec[key].outlineColor
|
||||
? { outlineColor: color(spec[key].outlineColor) }
|
||||
: {}),
|
||||
};
|
||||
if (spec.polyline) {
|
||||
const { arc, ...line } = spec.polyline;
|
||||
options.polyline = {
|
||||
...line,
|
||||
material: color(line.material),
|
||||
...(arc
|
||||
? {
|
||||
arcType:
|
||||
arc === 'none'
|
||||
? Cesium.ArcType.NONE
|
||||
: Cesium.ArcType.GEODESIC,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return source.entities.add(options);
|
||||
},
|
||||
dash: (options) =>
|
||||
new Cesium.PolylineDashMaterialProperty({
|
||||
...options,
|
||||
color: color(options.color),
|
||||
}),
|
||||
patternMaterial: (value) =>
|
||||
new MissionOrbitPatternMaterialProperty(color(value)),
|
||||
createOrbit({ positions, width, color: rgba }) {
|
||||
const collection = new Cesium.PolylineCollection();
|
||||
collection.add({
|
||||
positions,
|
||||
width,
|
||||
material: createMissionOrbitPatternMaterial(color(rgba)),
|
||||
});
|
||||
viewer.scene.primitives.add(collection);
|
||||
return collection;
|
||||
},
|
||||
commitOrbit() {},
|
||||
removePrimitive: (primitive) => viewer?.scene.primitives.remove(primitive),
|
||||
createPad,
|
||||
project: (position, result) =>
|
||||
Cesium.SceneTransforms.worldToWindowCoordinates(
|
||||
viewer.scene,
|
||||
position,
|
||||
result,
|
||||
),
|
||||
on: (event, listener) => viewer.scene[event].addEventListener(listener),
|
||||
listenClick(source, listener, { enabled = () => true } = {}) {
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
handler.setInputAction((movement) => {
|
||||
if (!isPointerFree()) return;
|
||||
if (!enabled()) return;
|
||||
const entity = viewer.scene
|
||||
.drillPick(movement.position, 12)
|
||||
.map((p) => p?.id)
|
||||
.find((e) => e?.id?.startsWith('rocket-'));
|
||||
listener(entity);
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
||||
return { destroy: () => handler.destroy() };
|
||||
},
|
||||
listenMove(source, listener, { enabled = () => true } = {}) {
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
||||
handler.setInputAction((movement) => {
|
||||
if (!enabled()) return;
|
||||
const entity = viewer.scene
|
||||
.drillPick(movement.endPosition, 12)
|
||||
.map((p) => p?.id)
|
||||
.find((e) => e?.id?.startsWith('rocket-'));
|
||||
listener(entity);
|
||||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||
return { destroy: () => handler.destroy() };
|
||||
},
|
||||
destroy() {},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { rgbaFromCss } from '../shared/color.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import {
|
||||
TRAJECTORY_STAGE_COLORS,
|
||||
@@ -12,6 +13,7 @@ export function createRendering({
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const { getSatelliteOrbitTrack, findSatelliteOrbitTrackInTle } =
|
||||
services.satellites;
|
||||
@@ -34,10 +36,8 @@ export function createRendering({
|
||||
}
|
||||
parts.panel.updateMissionTelemetry();
|
||||
// The app clock's now, as the JulianDate Cesium's entity properties take.
|
||||
const time = Cesium.JulianDate.fromDate(
|
||||
new Date(appClock.now()),
|
||||
layerState._declutterTime,
|
||||
);
|
||||
const time = getRenderer().time(appClock.now());
|
||||
layerState._declutterTime = time;
|
||||
layerState._declutterOccluder.cameraPosition =
|
||||
layerState._viewer.scene.camera.positionWC;
|
||||
parts.overlays.updateReplayVehicleOverlay(layerState._declutterOccluder);
|
||||
@@ -77,7 +77,7 @@ export function createRendering({
|
||||
}
|
||||
|
||||
function addLaunchEntity(launch, activeTleText = layerState._activeTleText) {
|
||||
const position = Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||
const position = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||
const overlayRecord = {
|
||||
launch,
|
||||
anchorPosition: position,
|
||||
@@ -114,7 +114,7 @@ export function createRendering({
|
||||
);
|
||||
});
|
||||
if (satelliteTrack) layerState._orbitMatches++;
|
||||
const entity = layerState._dataSource.entities.add({
|
||||
const entity = getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-launch:${launch.id}`,
|
||||
position,
|
||||
point: {
|
||||
@@ -129,8 +129,8 @@ export function createRendering({
|
||||
width: 24,
|
||||
height: 24,
|
||||
show: false,
|
||||
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
||||
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
||||
horizontalOrigin: 0,
|
||||
verticalOrigin: 0,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
properties: {
|
||||
@@ -147,6 +147,10 @@ export function createRendering({
|
||||
noradId: satelliteTrack?.noradId || '',
|
||||
},
|
||||
});
|
||||
// Publish the same anchor representation held by the active renderer.
|
||||
overlayRecord.anchorPosition = entity.position.getValue(
|
||||
getRenderer().time(appClock.now()),
|
||||
);
|
||||
const points = launch.trajectory
|
||||
.filter(
|
||||
(point) =>
|
||||
@@ -161,7 +165,7 @@ export function createRendering({
|
||||
point.stageName ||
|
||||
'trajectory',
|
||||
),
|
||||
position: Cesium.Cartesian3.fromDegrees(
|
||||
position: geo.cartesian3.fromDegrees(
|
||||
Number(point.longitude),
|
||||
Number(point.latitude),
|
||||
Number(point.altitude || 0),
|
||||
@@ -177,16 +181,16 @@ export function createRendering({
|
||||
});
|
||||
segments.forEach((segment, index) => {
|
||||
if (segment.positions.length < 2) return;
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-trajectory:${launch.id}:${index}`,
|
||||
polyline: {
|
||||
positions: parts.paths.surfaceSafePath(segment.positions),
|
||||
width: 2,
|
||||
material: Cesium.Color.fromCssColorString(
|
||||
material: rgbaFromCss(
|
||||
TRAJECTORY_STAGE_COLORS[index % TRAJECTORY_STAGE_COLORS.length],
|
||||
).withAlpha(0.8),
|
||||
clampToGround: false,
|
||||
arcType: Cesium.ArcType.NONE,
|
||||
arc: 'none',
|
||||
},
|
||||
properties: {
|
||||
launchId: launch.id,
|
||||
@@ -224,7 +228,7 @@ export function createRendering({
|
||||
new Date(insertionEpochMs),
|
||||
);
|
||||
if (propagatedInsertion) {
|
||||
insertionReference = Cesium.Cartesian3.fromDegrees(
|
||||
insertionReference = geo.cartesian3.fromDegrees(
|
||||
propagatedInsertion.longitude,
|
||||
propagatedInsertion.latitude,
|
||||
propagatedInsertion.altitude,
|
||||
@@ -237,7 +241,7 @@ export function createRendering({
|
||||
// ascent duration, and join the forward orbit tangent. Using the UTC
|
||||
// epoch as phase sent some ascents toward the far side of Earth and
|
||||
// forced a visible 180-degree corrective hook before insertion.
|
||||
const insertionProgress = Cesium.Math.clamp(
|
||||
const insertionProgress = geo.math.clamp(
|
||||
insertionDurationSec / orbitPeriodSec,
|
||||
1 / 96,
|
||||
0.16,
|
||||
@@ -259,14 +263,14 @@ export function createRendering({
|
||||
);
|
||||
// Cesium can evaluate CallbackProperty values multiple times while it
|
||||
// traverses one frame. Camera tracking and the HTML replay overlay must
|
||||
// use the exact same replay epoch as those callbacks; sampling Date.now()
|
||||
// use the exact same replay epoch as those callbacks; sampling appClock.now()
|
||||
// independently made the vehicle advance relative to the camera and then
|
||||
// snap back on the next frame.
|
||||
let replayStateForFrame = null;
|
||||
const sampleReplayState = () =>
|
||||
parts.replay.replayState(
|
||||
launch,
|
||||
layerState._animationStarts.get(launch.id) || Date.now(),
|
||||
layerState._animationStarts.get(launch.id) || appClock.now(),
|
||||
ascentDurationSec,
|
||||
REPLAY_ORBIT_DURATION_SEC,
|
||||
orbitPeriodSec,
|
||||
@@ -304,22 +308,22 @@ export function createRendering({
|
||||
if (path.length < 2) return;
|
||||
const reentryIndex = parts.paths.atmosphericReentryIndex(path);
|
||||
const reentryPath = path.slice(reentryIndex);
|
||||
const color = Cesium.Color.fromCssColorString(
|
||||
const color = rgbaFromCss(
|
||||
TRAJECTORY_STAGE_COLORS[
|
||||
(stageIndex + 1) % TRAJECTORY_STAGE_COLORS.length
|
||||
],
|
||||
);
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-reentry-recovery:${launch.id}:${stageIndex}`,
|
||||
polyline: {
|
||||
positions: path,
|
||||
width: 2,
|
||||
material: new Cesium.PolylineDashMaterialProperty({
|
||||
material: getRenderer().dash({
|
||||
color: color.withAlpha(0.82),
|
||||
dashLength: 12,
|
||||
dashPattern: 0xaaaa,
|
||||
}),
|
||||
arcType: Cesium.ArcType.NONE,
|
||||
arc: 'none',
|
||||
},
|
||||
properties: {
|
||||
launchId: launch.id,
|
||||
@@ -330,18 +334,17 @@ export function createRendering({
|
||||
},
|
||||
});
|
||||
if (reentryPath.length > 1) {
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-reentry:${launch.id}:${stageIndex}`,
|
||||
polyline: {
|
||||
positions: reentryPath,
|
||||
width: 2.5,
|
||||
material: new Cesium.PolylineDashMaterialProperty({
|
||||
color:
|
||||
Cesium.Color.fromCssColorString('#ffd166').withAlpha(0.9),
|
||||
material: getRenderer().dash({
|
||||
color: rgbaFromCss('#ffd166').withAlpha(0.9),
|
||||
dashLength: 8,
|
||||
dashPattern: 0xf0f0,
|
||||
}),
|
||||
arcType: Cesium.ArcType.NONE,
|
||||
arc: 'none',
|
||||
},
|
||||
properties: {
|
||||
launchId: launch.id,
|
||||
@@ -363,7 +366,7 @@ export function createRendering({
|
||||
}),
|
||||
);
|
||||
}
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-recovery-end:${launch.id}:${stageIndex}`,
|
||||
position: path.at(-1),
|
||||
point: {
|
||||
@@ -394,12 +397,12 @@ export function createRendering({
|
||||
lastOverlayWindowPosition: null,
|
||||
lastOverlayMode: null,
|
||||
});
|
||||
let livePosition = Cesium.Cartesian3.fromDegrees(
|
||||
let livePosition = geo.cartesian3.fromDegrees(
|
||||
orbitCurrent.longitude,
|
||||
orbitCurrent.latitude,
|
||||
orbitCurrent.altitude,
|
||||
);
|
||||
const liveTime = new Date();
|
||||
const liveTime = new Date(appClock.now());
|
||||
const liveTelemetry = { speedMps: null };
|
||||
layerState._satelliteTelemetry.set(launch.id, liveTelemetry);
|
||||
let liveFrameNumber = -1;
|
||||
@@ -410,13 +413,13 @@ export function createRendering({
|
||||
layerState._viewer?.scene?.frameState?.frameNumber ?? -1;
|
||||
if (frameNumber !== -1 && frameNumber === liveFrameNumber) return;
|
||||
liveFrameNumber = frameNumber;
|
||||
const nowMs = Date.now();
|
||||
const nowMs = appClock.now();
|
||||
liveTime.setTime(nowMs);
|
||||
if (satelliteTrack) {
|
||||
const propagated =
|
||||
satelliteTrack.positionAt?.(liveTime) || satelliteTrack.current;
|
||||
if (propagated) {
|
||||
Cesium.Cartesian3.fromDegrees(
|
||||
geo.cartesian3.fromDegrees(
|
||||
propagated.longitude,
|
||||
propagated.latitude,
|
||||
propagated.altitude,
|
||||
@@ -438,17 +441,15 @@ export function createRendering({
|
||||
) || livePosition;
|
||||
}
|
||||
};
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-satellite:${launch.id}`,
|
||||
position: new Cesium.CallbackProperty(() => {
|
||||
position: getRenderer().callback(() => {
|
||||
updateLiveState();
|
||||
return livePosition;
|
||||
}, false),
|
||||
point: {
|
||||
pixelSize: 6,
|
||||
color: Cesium.Color.fromCssColorString(
|
||||
satelliteTrack ? '#7bed9f' : '#ffd166',
|
||||
),
|
||||
color: rgbaFromCss(satelliteTrack ? '#7bed9f' : '#ffd166'),
|
||||
outlineWidth: 0,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
},
|
||||
@@ -488,18 +489,17 @@ export function createRendering({
|
||||
satelliteTrack,
|
||||
);
|
||||
if (!primitiveOrbitAdded) {
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-orbit:${launch.id}`,
|
||||
polyline: {
|
||||
positions: orbitPath,
|
||||
// The 3 px canvas gives the periodic dot room to read as round; the
|
||||
// shader masks the ten intervening dashes down to a thin center stroke.
|
||||
width: 3,
|
||||
material:
|
||||
new parts.orbitRendering.MissionOrbitPatternMaterialProperty(
|
||||
Cesium.Color.fromCssColorString('#c084fc').withAlpha(0.95),
|
||||
),
|
||||
arcType: Cesium.ArcType.NONE,
|
||||
material: getRenderer().patternMaterial(
|
||||
rgbaFromCss('#c084fc').withAlpha(0.95),
|
||||
),
|
||||
arc: 'none',
|
||||
},
|
||||
properties: {
|
||||
launchId: launch.id,
|
||||
@@ -514,10 +514,10 @@ export function createRendering({
|
||||
const primitivePath = layerState._missionOrbitPrimitives.get(launch.id);
|
||||
if (primitivePath) {
|
||||
primitivePath.labelBakePosition = orbitLabelBakePosition;
|
||||
primitivePath.labelPosition = Cesium.Matrix4.multiplyByPoint(
|
||||
primitivePath.labelPosition = geo.matrix4.multiplyByPoint(
|
||||
primitivePath.primitive.modelMatrix,
|
||||
orbitLabelBakePosition,
|
||||
new Cesium.Cartesian3(),
|
||||
geo.cartesian3.create(),
|
||||
);
|
||||
orbitLabelPosition = () => primitivePath.labelPosition;
|
||||
}
|
||||
@@ -531,13 +531,13 @@ export function createRendering({
|
||||
gapPx: 8,
|
||||
}),
|
||||
);
|
||||
const replayPosition = new Cesium.CallbackProperty(() => {
|
||||
const replayPosition = getRenderer().callback(() => {
|
||||
const state = getReplayState();
|
||||
return state.ascending
|
||||
? parts.paths.samplePath(ascentPath, state.phaseProgress)
|
||||
: parts.paths.samplePath(animatedOrbitPath, state.phaseProgress);
|
||||
}, false);
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-vehicle:${launch.id}`,
|
||||
position: replayPosition,
|
||||
properties: {
|
||||
@@ -549,7 +549,7 @@ export function createRendering({
|
||||
: 'Approximate trajectory animation',
|
||||
},
|
||||
});
|
||||
layerState._dataSource.entities.add({
|
||||
getRenderer().addEntity(layerState._dataSource, {
|
||||
id: `rocket-transfer:${launch.id}`,
|
||||
polyline: {
|
||||
positions: ascentPath,
|
||||
@@ -557,9 +557,9 @@ export function createRendering({
|
||||
// The animated vehicle uses Cartesian interpolation between these
|
||||
// exact samples. Prevent Cesium from replacing each segment with a
|
||||
// geodesic arc, which would visually separate the dot from the line.
|
||||
arcType: Cesium.ArcType.NONE,
|
||||
material: new Cesium.PolylineDashMaterialProperty({
|
||||
color: Cesium.Color.fromCssColorString('#7bed9f').withAlpha(0.8),
|
||||
arc: 'none',
|
||||
material: getRenderer().dash({
|
||||
color: rgbaFromCss('#7bed9f').withAlpha(0.8),
|
||||
dashLength: 24,
|
||||
dashPattern: 0xf0f0,
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import {
|
||||
REPLAY_ASCENT_FALLBACK_SEC,
|
||||
@@ -14,7 +14,13 @@ import {
|
||||
REPLAY_INITIAL_RANGE_M,
|
||||
} from './policy.js';
|
||||
|
||||
export function createReplay({ state: layerState, services, parts, source }) {
|
||||
export function createReplay({
|
||||
state: layerState,
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
/**
|
||||
* Rotate an upright screen-space rocket so its nose follows a projected path.
|
||||
* @param {{x: number, y: number}} from Current screen point.
|
||||
@@ -125,7 +131,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
orbitDurationSec,
|
||||
orbitPeriodSec,
|
||||
speed = 1,
|
||||
nowMs = Date.now(),
|
||||
nowMs = appClock.now(),
|
||||
preCountdownDurationSec = 0,
|
||||
loop = true,
|
||||
) {
|
||||
@@ -136,7 +142,8 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
Number(preCountdownDurationSec) || 0,
|
||||
);
|
||||
const preCountdownActive =
|
||||
preCountdownDuration > 0 && realSecondsSinceStart < -preCountdownDuration;
|
||||
preCountdownDuration > 0 &&
|
||||
realSecondsSinceStart < -REPLAY_COUNTDOWN_DURATION_SEC;
|
||||
const countdownActive = realSecondsSinceStart < 0 && !preCountdownActive;
|
||||
const countdownSeconds = countdownActive
|
||||
? Math.ceil(-realSecondsSinceStart)
|
||||
@@ -273,7 +280,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
const now =
|
||||
layerState._replayPaused && Number.isFinite(layerState._replayPausedAtMs)
|
||||
? layerState._replayPausedAtMs
|
||||
: Date.now();
|
||||
: appClock.now();
|
||||
for (const [launchId, startedAt] of layerState._animationStarts) {
|
||||
if (!Number.isFinite(startedAt)) continue;
|
||||
const elapsedMissionMs = (now - startedAt) * previousSpeed;
|
||||
@@ -294,13 +301,13 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
) {
|
||||
return layerState._replayPausedAtMs;
|
||||
}
|
||||
return Date.now();
|
||||
return appClock.now();
|
||||
}
|
||||
|
||||
function pauseMissionReplay() {
|
||||
if (!layerState._replayCameraLaunchId || layerState._replayPaused)
|
||||
return false;
|
||||
layerState._replayPausedAtMs = Date.now();
|
||||
layerState._replayPausedAtMs = appClock.now();
|
||||
layerState._replayPaused = true;
|
||||
syncReplayButton();
|
||||
return true;
|
||||
@@ -314,7 +321,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const resumedAt = Date.now();
|
||||
const resumedAt = appClock.now();
|
||||
const startedAt = layerState._animationStarts.get(
|
||||
layerState._replayCameraLaunchId,
|
||||
);
|
||||
@@ -341,12 +348,12 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
layerState._replayCameraRemover = null;
|
||||
layerState._viewer?.camera?.cancelFlight();
|
||||
if (layerState._viewer?.camera)
|
||||
layerState._viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
layerState._viewer.camera.lookAtTransform(geo.matrix4.IDENTITY);
|
||||
layerState._replayCameraLaunchId = null;
|
||||
layerState._replayPaused = false;
|
||||
layerState._replayPausedAtMs = null;
|
||||
if (stoppedLaunchId)
|
||||
layerState._animationStarts.set(stoppedLaunchId, Date.now());
|
||||
layerState._animationStarts.set(stoppedLaunchId, appClock.now());
|
||||
syncReplayButton();
|
||||
parts.overlays.syncMissionOverlayEntries();
|
||||
}
|
||||
@@ -381,7 +388,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
layerState._viewer.camera.cancelFlight();
|
||||
layerState._animationStarts.set(
|
||||
launchId,
|
||||
Date.now() +
|
||||
appClock.now() +
|
||||
(REPLAY_TILE_SETTLE_DELAY_SEC + REPLAY_COUNTDOWN_DURATION_SEC) * 1000,
|
||||
);
|
||||
let cameraReady = false;
|
||||
@@ -423,7 +430,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
orbitBlend,
|
||||
);
|
||||
if (state.ascending) track.orbitCameraWorldFrame = false;
|
||||
const nowMs = performance.now();
|
||||
const nowMs = appClock.now();
|
||||
const frameDurationMs = Number.isFinite(lastCameraUpdateMs)
|
||||
? geo.math.clamp(nowMs - lastCameraUpdateMs, 4, 50)
|
||||
: 1000 / 60;
|
||||
@@ -458,7 +465,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
if (state.ascending) {
|
||||
layerState._viewer.camera.lookAt(
|
||||
cameraTarget,
|
||||
new Cesium.HeadingPitchRange(
|
||||
getRenderer().range(
|
||||
track.lastCameraHeading,
|
||||
cameraView.pitch,
|
||||
cameraRange,
|
||||
@@ -488,9 +495,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
);
|
||||
if (orbitPose) {
|
||||
if (!track.orbitCameraWorldFrame) {
|
||||
layerState._viewer.camera.lookAtTransform(
|
||||
Cesium.Matrix4.IDENTITY,
|
||||
);
|
||||
layerState._viewer.camera.lookAtTransform(geo.matrix4.IDENTITY);
|
||||
track.orbitCameraWorldFrame = true;
|
||||
}
|
||||
layerState._viewer.camera.setView({
|
||||
@@ -511,15 +516,15 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
});
|
||||
|
||||
layerState._viewer.camera.flyToBoundingSphere(
|
||||
new Cesium.BoundingSphere(track.ascentPath[0], 0),
|
||||
geo.boundingSphere.create(track.ascentPath[0], 0),
|
||||
{
|
||||
offset: new Cesium.HeadingPitchRange(
|
||||
offset: getRenderer().range(
|
||||
initialHeading,
|
||||
geo.math.toRadians(-20),
|
||||
REPLAY_INITIAL_RANGE_M,
|
||||
),
|
||||
duration: 1.4,
|
||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
easingFunction: getRenderer().easing,
|
||||
complete: () => {
|
||||
cameraReady = true;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import { createReplay } from './replay.js';
|
||||
function setup() {
|
||||
const state = {
|
||||
_missionPanel: null,
|
||||
_replayCameraLaunchId: 'qa',
|
||||
_replayPaused: false,
|
||||
_replayPausedAtMs: null,
|
||||
_replaySpeed: 1,
|
||||
_animationStarts: new Map([['qa', 1000]]),
|
||||
};
|
||||
const parts = { paths: { orbitInsertionOffsetSeconds: () => 600 } };
|
||||
return {
|
||||
state,
|
||||
replay: createReplay({ state, services: {}, parts, source: {} }),
|
||||
};
|
||||
}
|
||||
test('five seconds of tile preparation are followed by the full ten-second countdown', () => {
|
||||
const { replay } = setup(),
|
||||
launch = { launchTime: '2026-10-15T12:00:00Z' };
|
||||
try {
|
||||
appClock.freeze(100000);
|
||||
assert.equal(
|
||||
replay.replayState(launch, 115000, 12, 28, 5400, 4, undefined, 5)
|
||||
.preCountdownActive,
|
||||
true,
|
||||
);
|
||||
appClock.freeze(105000);
|
||||
const countdown = replay.replayState(
|
||||
launch,
|
||||
115000,
|
||||
12,
|
||||
28,
|
||||
5400,
|
||||
4,
|
||||
undefined,
|
||||
5,
|
||||
);
|
||||
assert.equal(countdown.preCountdownActive, false);
|
||||
assert.equal(countdown.countdownSeconds, 10);
|
||||
appClock.freeze(114000);
|
||||
assert.equal(
|
||||
replay.replayState(launch, 115000, 12, 28, 5400, 4, undefined, 5)
|
||||
.countdownSeconds,
|
||||
1,
|
||||
);
|
||||
appClock.freeze(116000);
|
||||
assert.equal(
|
||||
replay.replayState(launch, 115000, 12, 28, 5400, 4, undefined, 5)
|
||||
.elapsedSinceStart,
|
||||
4,
|
||||
);
|
||||
} finally {
|
||||
appClock.reset();
|
||||
}
|
||||
});
|
||||
test('pause, resume and speed changes use app time without a wall-clock jump', () => {
|
||||
const { state, replay } = setup();
|
||||
try {
|
||||
appClock.freeze(4000);
|
||||
assert.equal(replay.pauseMissionReplay(), true);
|
||||
appClock.freeze(9500);
|
||||
assert.equal(replay.replayClockNow('qa'), 4000);
|
||||
assert.equal(replay.resumeMissionReplay(), true);
|
||||
assert.equal(state._animationStarts.get('qa'), 6500);
|
||||
replay.setReplaySpeed(4);
|
||||
assert.equal(state._animationStarts.get('qa'), 8750);
|
||||
} finally {
|
||||
appClock.reset();
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { appClock } from '../../time/clock.js';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { MISSION_GLOBE_VIEW_RANGE_M, MISSION_FOCUS_RANGE_M } from './policy.js';
|
||||
@@ -8,6 +7,7 @@ export function createSelection({
|
||||
services,
|
||||
parts,
|
||||
source,
|
||||
getRenderer,
|
||||
}) {
|
||||
const { getKeyholeGeometry } = services.geometry;
|
||||
|
||||
@@ -46,7 +46,7 @@ export function createSelection({
|
||||
roll: 0,
|
||||
},
|
||||
duration,
|
||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
easingFunction: getRenderer().easing,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function createSelection({
|
||||
stopMissionZoomAnchor();
|
||||
layerState._selectedLaunchId = launchId;
|
||||
layerState._explicitSelection = Boolean(launchId && isolate);
|
||||
if (launchId) layerState._animationStarts.set(launchId, Date.now());
|
||||
if (launchId) layerState._animationStarts.set(launchId, appClock.now());
|
||||
if (!launchId && layerState._viewer) {
|
||||
stopMissionZoomAnchor();
|
||||
layerState._viewer.selectedEntity = undefined;
|
||||
@@ -93,7 +93,7 @@ export function createSelection({
|
||||
layerState._missionZoomAnchorRemover = null;
|
||||
layerState._missionZoomAnchorId = null;
|
||||
if (layerState._viewer?.camera && !layerState._replayCameraLaunchId) {
|
||||
layerState._viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
layerState._viewer.camera.lookAtTransform(geo.matrix4.IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +109,7 @@ export function createSelection({
|
||||
layerState._missionZoomAnchorId = launch.id;
|
||||
layerState._viewer.camera.lookAt(
|
||||
target,
|
||||
new Cesium.HeadingPitchRange(
|
||||
0,
|
||||
parts.camera.missionZoomPitch(range),
|
||||
range,
|
||||
),
|
||||
getRenderer().range(0, parts.camera.missionZoomPitch(range), range),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,7 +123,7 @@ export function createSelection({
|
||||
`rocket-orbit:${launch.id}`,
|
||||
);
|
||||
const orbitPositions = orbitEntity?.polyline?.positions?.getValue(
|
||||
Cesium.JulianDate.fromDate(new Date(appClock.now())),
|
||||
getRenderer().time(appClock.now()),
|
||||
);
|
||||
const launchPosition = geo.cartesian3.fromDegrees(launch.lon, launch.lat);
|
||||
let range = 18000000;
|
||||
@@ -202,15 +198,15 @@ export function createSelection({
|
||||
`rocket-launch:${launch.id}`,
|
||||
);
|
||||
layerState._viewer.camera.flyToBoundingSphere(
|
||||
new Cesium.BoundingSphere(launchPosition, 0),
|
||||
geo.boundingSphere.create(launchPosition, 0),
|
||||
{
|
||||
offset: new Cesium.HeadingPitchRange(
|
||||
offset: getRenderer().range(
|
||||
0,
|
||||
geo.math.toRadians(-42),
|
||||
MISSION_FOCUS_RANGE_M,
|
||||
),
|
||||
duration: 1.4,
|
||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||
easingFunction: getRenderer().easing,
|
||||
complete: () => {
|
||||
if (layerState._selectedLaunchId === launch.id) {
|
||||
startMissionZoomAnchor(launch, MISSION_FOCUS_RANGE_M);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import * as geo from '../../geo/index.js';
|
||||
|
||||
export function createState({ services }) {
|
||||
@@ -122,17 +121,13 @@ export function createState({ services }) {
|
||||
|
||||
state._missionOrbitPrimitives = new Map();
|
||||
|
||||
state._declutterTime = new Cesium.JulianDate();
|
||||
|
||||
state._declutterOccluder = new Cesium.EllipsoidalOccluder(
|
||||
Cesium.Ellipsoid.WGS84,
|
||||
geo.cartesian3.create(),
|
||||
);
|
||||
state._renderer = null;
|
||||
state._engine = null;
|
||||
state._declutterTime = null;
|
||||
state._declutterOccluder = null;
|
||||
|
||||
state._missionRingDate = new Date(0);
|
||||
|
||||
state._missionOrbitPatternRegistered = false;
|
||||
|
||||
state._pathDistanceCache = new WeakMap();
|
||||
state._sourceController = new AbortController();
|
||||
return state;
|
||||
|
||||
@@ -44,6 +44,13 @@ export class Rgba {
|
||||
);
|
||||
}
|
||||
|
||||
/** Cesium's CSS form, including its float-to-byte conversion. */
|
||||
toCssColorString() {
|
||||
const byte = (v) => (v === 1 ? 255 : (v * 256) | 0);
|
||||
const rgb = `${byte(this.red)},${byte(this.green)},${byte(this.blue)}`;
|
||||
return this.alpha === 1 ? `rgb(${rgb})` : `rgba(${rgb},${this.alpha})`;
|
||||
}
|
||||
|
||||
/** `[red, green, blue, alpha]`, 0..1. */
|
||||
rgba() {
|
||||
return [this.red, this.green, this.blue, this.alpha];
|
||||
|
||||
@@ -75,3 +75,16 @@ test('Cesium billboards and models take an Rgba as their colour', () => {
|
||||
assert.notEqual(billboard.color, CYAN, 'the billboard keeps its own copy');
|
||||
collection.destroy();
|
||||
});
|
||||
|
||||
test('CSS serialization keeps native byte rounding and alpha formatting', async () => {
|
||||
const { constantRgba } = await import('./color.js');
|
||||
for (const values of [
|
||||
[0.1, 0.5, 1, 1],
|
||||
[1, 0, 0.3, 0.42],
|
||||
[0, 0, 0, 0],
|
||||
])
|
||||
assert.equal(
|
||||
constantRgba(...values).toCssColorString(),
|
||||
new Cesium.Color(...values).toCssColorString(),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user