mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(satellites): render on Navara
Render satellite fleets and selected targets through mover banks and orbit lines. Enable the celestial HUD on Navara and keep native follow explicitly in ENU. Add deterministic interaction, share, orbit, motion and cleanup acceptance QA. Record complete targeted acceptance and remaining shared cross-shell tile failures.
This commit is contained in:
1 parent
000ef99464
commit
6bdb995fe3
25 files changed
+727
-53
No files matched your search
@@ -366,7 +366,7 @@
|
|||||||
"src/layers/vessels/render/contract.js",
|
"src/layers/vessels/render/contract.js",
|
||||||
"src/layers/vessels/renderer.test.mjs",
|
"src/layers/vessels/renderer.test.mjs",
|
||||||
"src/layers/vessels/render/navara.js",
|
"src/layers/vessels/render/navara.js",
|
||||||
"src/layers/shared/render/navaraView.js",
|
"src/layers/shared/render/engineView.js",
|
||||||
"scripts/qa-vessels-engines.mjs",
|
"scripts/qa-vessels-engines.mjs",
|
||||||
"src/layers/vessels/evidence.js",
|
"src/layers/vessels/evidence.js",
|
||||||
"src/layers/vessels/index.js",
|
"src/layers/vessels/index.js",
|
||||||
@@ -1143,5 +1143,10 @@
|
|||||||
"src/bench/satelliteFixture.test.mjs",
|
"src/bench/satelliteFixture.test.mjs",
|
||||||
"src/layers/satellites/render/cesium.js",
|
"src/layers/satellites/render/cesium.js",
|
||||||
"src/celestialRing/render/cesium.js",
|
"src/celestialRing/render/cesium.js",
|
||||||
"scripts/qa-satellites.mjs"
|
"scripts/qa-satellites.mjs",
|
||||||
|
"scripts/qa-satellites-compare.mjs",
|
||||||
|
"src/celestialRing/view.js",
|
||||||
|
"src/layers/satellites/render/navara.js",
|
||||||
|
"src/layers/satellites/render/navara.test.mjs",
|
||||||
|
"src/layers/satellites/render/cesium.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -2609,8 +2609,10 @@
|
|||||||
"src/layers/vessels/render/cesium.js",
|
"src/layers/vessels/render/cesium.js",
|
||||||
"src/layers/localAdsb/render/navara.js",
|
"src/layers/localAdsb/render/navara.js",
|
||||||
"src/layers/vessels/render/navara.js",
|
"src/layers/vessels/render/navara.js",
|
||||||
"src/layers/shared/render/navaraView.js",
|
"src/layers/satellites/render/navara.js",
|
||||||
|
"src/layers/shared/render/engineView.js",
|
||||||
"src/celestialRing/render/cesium.js",
|
"src/celestialRing/render/cesium.js",
|
||||||
|
"src/celestialRing/view.js",
|
||||||
"src/layers/satellites/render/cesium.js"
|
"src/layers/satellites/render/cesium.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
@@ -3193,6 +3195,8 @@
|
|||||||
"src/layers/localAdsb/render/cesium.js",
|
"src/layers/localAdsb/render/cesium.js",
|
||||||
"src/layers/vessels/render/cesium.js",
|
"src/layers/vessels/render/cesium.js",
|
||||||
"src/celestialRing/render/cesium.js",
|
"src/celestialRing/render/cesium.js",
|
||||||
|
"src/celestialRing/view.js",
|
||||||
|
"src/layers/shared/render/engineView.js",
|
||||||
"src/layers/satellites/render/cesium.js"
|
"src/layers/satellites/render/cesium.js"
|
||||||
],
|
],
|
||||||
"external": [
|
"external": [
|
||||||
@@ -3690,6 +3694,8 @@
|
|||||||
"src/cyberSonar.js",
|
"src/cyberSonar.js",
|
||||||
"src/app/sourceSlot.js",
|
"src/app/sourceSlot.js",
|
||||||
"src/celestialRing.js",
|
"src/celestialRing.js",
|
||||||
|
"src/celestialRing/view.js",
|
||||||
|
"src/layers/shared/render/engineView.js",
|
||||||
"src/data/cameraSensitivity.js",
|
"src/data/cameraSensitivity.js",
|
||||||
"src/data/contactPlayback.js",
|
"src/data/contactPlayback.js",
|
||||||
"src/data/contactTrailRenderer.js",
|
"src/data/contactTrailRenderer.js",
|
||||||
|
|||||||
@@ -17,13 +17,19 @@ test('the Navara mode registers the aircraft layers’ and vessels’ renderers,
|
|||||||
registerNavaraLayerRenderers(),
|
registerNavaraLayerRenderers(),
|
||||||
]);
|
]);
|
||||||
await registerNavaraLayerRenderers();
|
await registerNavaraLayerRenderers();
|
||||||
for (const id of ['flights', 'military', 'local-adsb', 'ais-live-vessels'])
|
for (const id of [
|
||||||
|
'flights',
|
||||||
|
'military',
|
||||||
|
'local-adsb',
|
||||||
|
'satellites',
|
||||||
|
'ais-live-vessels',
|
||||||
|
])
|
||||||
assert.equal(layerAvailableOn(id, 'navara'), true, id);
|
assert.equal(layerAvailableOn(id, 'navara'), true, id);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
createLayerAvailability('navara')
|
createLayerAvailability('navara')
|
||||||
.available()
|
.available()
|
||||||
.map(({ id }) => id),
|
.map(({ id }) => id),
|
||||||
['flights', 'military', 'local-adsb', 'ais-live-vessels'],
|
['flights', 'military', 'local-adsb', 'satellites', 'ais-live-vessels'],
|
||||||
);
|
);
|
||||||
assert.equal(rendererFor('satellites', { kind: 'navara' }), null);
|
assert.equal(layerAvailableOn('satellites', 'navara'), true);
|
||||||
});
|
});
|
||||||
@@ -55,6 +55,9 @@ const SHELL_CREDIT_KEYS = new Set([
|
|||||||
*/
|
*/
|
||||||
export async function registerNavaraLayerRenderers() {
|
export async function registerNavaraLayerRenderers() {
|
||||||
const loaders = {
|
const loaders = {
|
||||||
|
satellites: async () =>
|
||||||
|
(await import('../layers/satellites/render/navara.js'))
|
||||||
|
.createSatellitesNavaraRenderer,
|
||||||
flights: async () =>
|
flights: async () =>
|
||||||
(await import('../layers/flights/render/navara.js'))
|
(await import('../layers/flights/render/navara.js'))
|
||||||
.createFlightsNavaraRenderer,
|
.createFlightsNavaraRenderer,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as geo from './geo/index.js';
|
import * as geo from './geo/index.js';
|
||||||
|
import { createCelestialView } from './celestialRing/view.js';
|
||||||
import { engineFromViewer } from './globe/cesium/engineFromViewer.js';
|
import { engineFromViewer } from './globe/cesium/engineFromViewer.js';
|
||||||
import {
|
import {
|
||||||
createCelestialEphemeris,
|
createCelestialEphemeris,
|
||||||
@@ -394,7 +395,8 @@ export class CelestialRing {
|
|||||||
* @param {{enabled?:boolean,onAutoDisable?:Function}} [options]
|
* @param {{enabled?:boolean,onAutoDisable?:Function}} [options]
|
||||||
*/
|
*/
|
||||||
constructor(viewer, { enabled = true, onAutoDisable = null } = {}) {
|
constructor(viewer, { enabled = true, onAutoDisable = null } = {}) {
|
||||||
this.viewer = viewer;
|
this._engineView = viewer?.kind ? createCelestialView(viewer) : null;
|
||||||
|
this.viewer = this._engineView?.view ?? viewer;
|
||||||
this.enabled = !!enabled;
|
this.enabled = !!enabled;
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this._onAutoDisable =
|
this._onAutoDisable =
|
||||||
@@ -420,7 +422,7 @@ export class CelestialRing {
|
|||||||
this._ephemeris = createCelestialEphemeris();
|
this._ephemeris = createCelestialEphemeris();
|
||||||
|
|
||||||
this._buildDOM();
|
this._buildDOM();
|
||||||
this._removePostRender = viewer.scene.postRender.addEventListener(() =>
|
this._removePostRender = this.viewer.scene.postRender.addEventListener(() =>
|
||||||
this._draw(),
|
this._draw(),
|
||||||
);
|
);
|
||||||
// Pre-existing staleness fix (perf wave 2 review): the ephemeris was
|
// Pre-existing staleness fix (perf wave 2 review): the ephemeris was
|
||||||
@@ -825,5 +827,6 @@ export class CelestialRing {
|
|||||||
if (this._removePostRender) this._removePostRender();
|
if (this._removePostRender) this._removePostRender();
|
||||||
this._removePostRender = null;
|
this._removePostRender = null;
|
||||||
this._root?.remove();
|
this._root?.remove();
|
||||||
|
this._engineView?.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// The shared engine view has no renderer-library imports. Reuse its cached
|
||||||
|
// camera basis for the celestial canvas, with flight and host ownership here.
|
||||||
|
import { registerViewerEngine } from '../globe/cesium/engineFromViewer.js';
|
||||||
|
import { createEngineLayerView } from '../layers/shared/render/engineView.js';
|
||||||
|
export function createCelestialView(engine) {
|
||||||
|
const adapter = createEngineLayerView(engine, { layerId: 'celestial-ring' });
|
||||||
|
const { view } = adapter;
|
||||||
|
view.container = engine.host.container;
|
||||||
|
view.scene.canvas = engine.host.canvas;
|
||||||
|
view.scene.postRender = engine.frames.postRender;
|
||||||
|
view.scene.requestRender = () => engine.render.request('celestial-ring');
|
||||||
|
view.camera.flyTo = ({ easingFunction, ...options }) =>
|
||||||
|
engine.camera.flyTo({ ...options, easing: 'cubicInOut' });
|
||||||
|
const unregister = registerViewerEngine(view, engine);
|
||||||
|
return {
|
||||||
|
view,
|
||||||
|
destroy() {
|
||||||
|
unregister();
|
||||||
|
adapter.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -158,7 +158,7 @@ function pointPart(options, store, factories, requestRender, owners) {
|
|||||||
const { point, depthTest } = options;
|
const { point, depthTest } = options;
|
||||||
const style = {
|
const style = {
|
||||||
outlineWidth: point.outlinePx,
|
outlineWidth: point.outlinePx,
|
||||||
outlineColor: Cesium.Color.BLACK,
|
outlineColor: new Cesium.Color(...point.outlineColor),
|
||||||
scaleByDistance: nearFar(point.scaleByDistance),
|
scaleByDistance: nearFar(point.scaleByDistance),
|
||||||
disableDepthTestDistance: depthTestDistance(depthTest),
|
disableDepthTestDistance: depthTestDistance(depthTest),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -285,9 +285,6 @@
|
|||||||
* - launches: the replay timeline (replay.js, Date.now/performance.now),
|
* - launches: the replay timeline (replay.js, Date.now/performance.now),
|
||||||
* the mission orbit's GMST frame (orbitRendering.js new Date()), launch
|
* the mission orbit's GMST frame (orbitRendering.js new Date()), launch
|
||||||
* pad and panel throttles (phase 3 Task 15a);
|
* pad and panel throttles (phase 3 Task 15a);
|
||||||
* - satellites: the point cloud's SGP4 propagation and orbit bake
|
|
||||||
* (rendering.js new Date()/Date.now) (Task 14a); only the tracked
|
|
||||||
* satellite reads the app clock;
|
|
||||||
* - wall-clock throttles and feed receipt stamps everywhere (they measure
|
* - wall-clock throttles and feed receipt stamps everywhere (they measure
|
||||||
* real elapsed time, not scene time).
|
* real elapsed time, not scene time).
|
||||||
* @property {() => number} now The current app time.
|
* @property {() => number} now The current app time.
|
||||||
@@ -321,8 +318,9 @@
|
|||||||
* each sprite is a square of sizePx CSS px times its scale, times the
|
* each sprite is a square of sizePx CSS px times its scale, times the
|
||||||
* NearFarScalar [nearM, nearScale, farM, farScale] when given (Cesium's
|
* NearFarScalar [nearM, nearScale, farM, farScale] when given (Cesium's
|
||||||
* interpolation: over the squared camera distance, eased by ^0.2).
|
* interpolation: over the squared camera distance, eased by ^0.2).
|
||||||
* @property {{ sizePx: number, outlinePx?: number, scaleByDistance?: number[] }} [point]
|
* @property {{ sizePx: number, outlinePx?: number, outlineColor?: number[], scaleByDistance?: number[] }} [point]
|
||||||
* A filled disc of sizePx CSS px diameter with a dark outline.
|
* A filled disc of sizePx CSS px diameter plus outlinePx on each side;
|
||||||
|
* outlineColor is rgba in 0..1 (opaque black by default).
|
||||||
* @property {{ url: string, minimumPixelSize?: number, maximumScale?: number,
|
* @property {{ url: string, minimumPixelSize?: number, maximumScale?: number,
|
||||||
* colorBlend?: { mode: 'mix'|'replace', amount: number }, unlit?: boolean }} [model]
|
* colorBlend?: { mode: 'mix'|'replace', amount: number }, unlit?: boolean }} [model]
|
||||||
* A glTF drawn per instance in metres, at least minimumPixelSize px.
|
* A glTF drawn per instance in metres, at least minimumPixelSize px.
|
||||||
|
|||||||
@@ -77,9 +77,13 @@ function moverRadius(options, scale, distance, modelRadiusPx = null) {
|
|||||||
? options.point
|
? options.point
|
||||||
: null;
|
: null;
|
||||||
const size = style
|
const size = style
|
||||||
? style.sizePx * distanceScale(style.scaleByDistance, distance)
|
? (style.sizePx * scale +
|
||||||
|
(options.kind === 'point' ? 2 * style.outlinePx : 0)) *
|
||||||
|
distanceScale(style.scaleByDistance, distance)
|
||||||
: Math.max(options.model.minimumPixelSize, 24);
|
: Math.max(options.model.minimumPixelSize, 24);
|
||||||
return Math.max((size * scale) / 2, PICK_RADIUS_PX) + PICK_RADIUS_PX;
|
return (
|
||||||
|
Math.max((style ? size : size * scale) / 2, PICK_RADIUS_PX) + PICK_RADIUS_PX
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformed(matrix, flat, index, result) {
|
function transformed(matrix, flat, index, result) {
|
||||||
|
|||||||
@@ -104,7 +104,17 @@ export function normalizeMoverSetOptions(options) {
|
|||||||
} else if (kind === 'point') {
|
} else if (kind === 'point') {
|
||||||
const point = options.point;
|
const point = options.point;
|
||||||
if (!isObject(point)) fail('a point set needs point options');
|
if (!isObject(point)) fail('a point set needs point options');
|
||||||
|
const outlineColor = point.outlineColor ?? [0, 0, 0, 1];
|
||||||
|
if (
|
||||||
|
!Array.isArray(outlineColor) ||
|
||||||
|
outlineColor.length !== 4 ||
|
||||||
|
!outlineColor.every(
|
||||||
|
(value) => isFiniteNumber(value) && value >= 0 && value <= 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fail('point.outlineColor must be [r, g, b, a] in 0..1');
|
||||||
normalized.point = {
|
normalized.point = {
|
||||||
|
outlineColor: Object.freeze([...outlineColor]),
|
||||||
sizePx: positive(point.sizePx, 'point.sizePx'),
|
sizePx: positive(point.sizePx, 'point.sizePx'),
|
||||||
outlinePx: nonNegative(point.outlinePx, 'point.outlinePx', 0),
|
outlinePx: nonNegative(point.outlinePx, 'point.outlinePx', 0),
|
||||||
scaleByDistance: scaleByDistanceOf(point.scaleByDistance),
|
scaleByDistance: scaleByDistanceOf(point.scaleByDistance),
|
||||||
|
|||||||
@@ -236,3 +236,31 @@ test('a set is ready() once what it draws has loaded, never once destroyed', ()
|
|||||||
'an engine part without ready() draws at once',
|
'an engine part without ready() draws at once',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('point outline RGBA is validated and copied independently of the caller', () => {
|
||||||
|
const color = [1, 0.5, 0, 0.3];
|
||||||
|
const options = normalizeMoverSetOptions({
|
||||||
|
id: 'outlined',
|
||||||
|
kind: 'point',
|
||||||
|
capacity: 1,
|
||||||
|
point: { sizePx: 14, outlinePx: 2, outlineColor: color },
|
||||||
|
});
|
||||||
|
color[0] = 0;
|
||||||
|
assert.deepEqual(options.point.outlineColor, [1, 0.5, 0, 0.3]);
|
||||||
|
assert.ok(Object.isFrozen(options.point.outlineColor));
|
||||||
|
for (const outlineColor of [
|
||||||
|
[1, 0, 0],
|
||||||
|
[1, 0, 0, 2],
|
||||||
|
[NaN, 0, 0, 1],
|
||||||
|
])
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
normalizeMoverSetOptions({
|
||||||
|
id: 'bad',
|
||||||
|
kind: 'point',
|
||||||
|
capacity: 1,
|
||||||
|
point: { sizePx: 14, outlineColor },
|
||||||
|
}),
|
||||||
|
TypeError,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -76,10 +76,12 @@ const frameCamera = {
|
|||||||
cameraHeight: 0,
|
cameraHeight: 0,
|
||||||
fovDeg: 60,
|
fovDeg: 60,
|
||||||
viewportHeightPx: 0,
|
viewportHeightPx: 0,
|
||||||
|
pixelRatio: 1,
|
||||||
};
|
};
|
||||||
function updateMover(view, ctx, mover) {
|
function updateMover(view, ctx, mover) {
|
||||||
const raw = view.camera.raw;
|
const raw = view.camera.raw;
|
||||||
const height = ctx?.getRenderer().domElement.clientHeight;
|
const renderer = ctx?.getRenderer();
|
||||||
|
const height = renderer?.domElement.clientHeight;
|
||||||
if (!raw || !(height > 0)) return;
|
if (!raw || !(height > 0)) return;
|
||||||
raw.updateMatrixWorld();
|
raw.updateMatrixWorld();
|
||||||
const e = raw.matrixWorld.elements;
|
const e = raw.matrixWorld.elements;
|
||||||
@@ -97,6 +99,7 @@ function updateMover(view, ctx, mover) {
|
|||||||
Math.hypot(position.x, position.y, position.z) - EARTH_RADIUS_M;
|
Math.hypot(position.x, position.y, position.z) - EARTH_RADIUS_M;
|
||||||
frameCamera.fovDeg = raw.fov;
|
frameCamera.fovDeg = raw.fov;
|
||||||
frameCamera.viewportHeightPx = height;
|
frameCamera.viewportHeightPx = height;
|
||||||
|
frameCamera.pixelRatio = renderer.getPixelRatio?.() ?? 1;
|
||||||
mover.update(frameCamera);
|
mover.update(frameCamera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,15 +40,18 @@ uniform vec4 uGevScaleByDistance;
|
|||||||
uniform float uGevHasScaleByDistance;
|
uniform float uGevHasScaleByDistance;
|
||||||
uniform float uGevDisableWithinM;
|
uniform float uGevDisableWithinM;
|
||||||
uniform float uGevOutlinePx;
|
uniform float uGevOutlinePx;
|
||||||
|
uniform float uGevPixelRatio;
|
||||||
varying vec4 vGevColor;
|
varying vec4 vGevColor;
|
||||||
varying vec2 vGevLocal;
|
varying vec2 vGevLocal;
|
||||||
varying float vGevOutline;
|
varying float vGevOutline;
|
||||||
|
varying float vGevPixelDistance;
|
||||||
${OVERLAY_DEPTH_VERTEX}
|
${OVERLAY_DEPTH_VERTEX}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const VERTEX_PROJECT = /* glsl */ `
|
const VERTEX_PROJECT = /* glsl */ `
|
||||||
vec4 mvCenter = modelViewMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
vec4 mvCenter = modelViewMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
float gevScale = gevStyle.y;
|
float gevScale = gevStyle.y;
|
||||||
|
float gevDistanceScale = 1.0;
|
||||||
float gevDistance = length(mvCenter.xyz);
|
float gevDistance = length(mvCenter.xyz);
|
||||||
if (uGevHasScaleByDistance > 0.5) {
|
if (uGevHasScaleByDistance > 0.5) {
|
||||||
// Cesium's czm_nearFarScalar: over the squared distance, eased by ^0.2.
|
// Cesium's czm_nearFarScalar: over the squared distance, eased by ^0.2.
|
||||||
@@ -60,9 +63,21 @@ if (uGevHasScaleByDistance > 0.5) {
|
|||||||
1.0
|
1.0
|
||||||
);
|
);
|
||||||
t = pow(t, 0.2);
|
t = pow(t, 0.2);
|
||||||
gevScale *= mix(uGevScaleByDistance.y, uGevScaleByDistance.w, t);
|
gevDistanceScale = mix(uGevScaleByDistance.y, uGevScaleByDistance.w, t);
|
||||||
|
gevScale *= gevDistanceScale;
|
||||||
}
|
}
|
||||||
float gevSizePx = uGevSizePx * gevScale;
|
float gevSizePx = uGevSizePx * gevScale;
|
||||||
|
vGevOutline = 0.0;
|
||||||
|
vGevPixelDistance = 0.0;
|
||||||
|
#ifdef GEV_POINT
|
||||||
|
float gevBorderPx = 2.0 * uGevOutlinePx * gevDistanceScale;
|
||||||
|
float gevOutlinedSize = gevSizePx + gevBorderPx;
|
||||||
|
vGevOutline = gevBorderPx / max(gevOutlinedSize, 1e-3);
|
||||||
|
// Cesium reserves three physical pixels for the point's smooth edge.
|
||||||
|
gevSizePx = gevOutlinedSize + 3.0 / uGevPixelRatio;
|
||||||
|
vGevPixelDistance = 2.0 / max(gevSizePx * uGevPixelRatio, 1e-3);
|
||||||
|
if (gevStyle.y <= 0.0) gevSizePx = 0.0;
|
||||||
|
#endif
|
||||||
float gevCos = cos(gevStyle.x);
|
float gevCos = cos(gevStyle.x);
|
||||||
float gevSin = sin(gevStyle.x);
|
float gevSin = sin(gevStyle.x);
|
||||||
vec2 gevCorner = position.xy;
|
vec2 gevCorner = position.xy;
|
||||||
@@ -75,7 +90,6 @@ mvPosition.xy += gevTurned * gevSizePx * uGevFootprint * max(-mvCenter.z, 0.0);
|
|||||||
gl_Position = projectionMatrix * mvPosition;
|
gl_Position = projectionMatrix * mvPosition;
|
||||||
vGevColor = gevColor;
|
vGevColor = gevColor;
|
||||||
vGevLocal = gevCorner;
|
vGevLocal = gevCorner;
|
||||||
vGevOutline = uGevOutlinePx / max(gevSizePx, 1e-3);
|
|
||||||
vGevDepthFree = gevDistance < uGevDisableWithinM ? 1.0 : 0.0;
|
vGevDepthFree = gevDistance < uGevDisableWithinM ? 1.0 : 0.0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -87,9 +101,11 @@ vMapUv = mix(gevUv.xy, gevUv.zw, uv);
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const FRAGMENT_HEAD = /* glsl */ `
|
const FRAGMENT_HEAD = /* glsl */ `
|
||||||
|
uniform vec4 uGevOutlineColor;
|
||||||
varying vec4 vGevColor;
|
varying vec4 vGevColor;
|
||||||
varying vec2 vGevLocal;
|
varying vec2 vGevLocal;
|
||||||
varying float vGevOutline;
|
varying float vGevOutline;
|
||||||
|
varying float vGevPixelDistance;
|
||||||
${OVERLAY_DEPTH_FRAGMENT}
|
${OVERLAY_DEPTH_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -103,8 +119,14 @@ const fragmentColor = (point) =>
|
|||||||
if (gevOverlayHidden()) discard;
|
if (gevOverlayHidden()) discard;
|
||||||
float gevRadius = length(vGevLocal) * 2.0;
|
float gevRadius = length(vGevLocal) * 2.0;
|
||||||
if (gevRadius > 1.0) discard;
|
if (gevRadius > 1.0) discard;
|
||||||
diffuseColor *= sRGBTransferEOTF(vGevColor);
|
float gevMaxDistance = max(0.0, 0.5 - vGevPixelDistance);
|
||||||
if (gevRadius > 1.0 - vGevOutline) diffuseColor.rgb = vec3(0.0);
|
float gevDistanceToCenter = gevRadius * 0.5;
|
||||||
|
float gevWholeAlpha = 1.0 - smoothstep(gevMaxDistance, 0.5, gevDistanceToCenter);
|
||||||
|
float gevInner = 1.0 - vGevOutline;
|
||||||
|
float gevInnerAlpha = 1.0 - smoothstep(gevMaxDistance * gevInner, 0.5 * gevInner, gevDistanceToCenter);
|
||||||
|
diffuseColor *= sRGBTransferEOTF(mix(uGevOutlineColor, vGevColor, gevInnerAlpha));
|
||||||
|
diffuseColor.a *= gevWholeAlpha;
|
||||||
|
if (diffuseColor.a < 0.005) discard;
|
||||||
`
|
`
|
||||||
: /* glsl */ `
|
: /* glsl */ `
|
||||||
#include <color_fragment>
|
#include <color_fragment>
|
||||||
@@ -129,7 +151,7 @@ export function patchMoverShader(shader, uniforms, { point = false } = {}) {
|
|||||||
let vertex = replace(
|
let vertex = replace(
|
||||||
shader.vertexShader,
|
shader.vertexShader,
|
||||||
'void main() {',
|
'void main() {',
|
||||||
`${VERTEX_HEAD}\nvoid main() {`,
|
`${point ? '#define GEV_POINT\n' : ''}${VERTEX_HEAD}\nvoid main() {`,
|
||||||
);
|
);
|
||||||
vertex = replace(vertex, '#include <project_vertex>', VERTEX_PROJECT);
|
vertex = replace(vertex, '#include <project_vertex>', VERTEX_PROJECT);
|
||||||
vertex = replace(vertex, '#include <uv_vertex>', VERTEX_UV);
|
vertex = replace(vertex, '#include <uv_vertex>', VERTEX_UV);
|
||||||
@@ -212,6 +234,10 @@ export function createMoverMesh(
|
|||||||
value: typeof depthTest === 'object' ? depthTest.disableWithinM : -1,
|
value: typeof depthTest === 'object' ? depthTest.disableWithinM : -1,
|
||||||
},
|
},
|
||||||
uGevOutlinePx: { value: point ? options.point.outlinePx : 0 },
|
uGevOutlinePx: { value: point ? options.point.outlinePx : 0 },
|
||||||
|
uGevOutlineColor: {
|
||||||
|
value: point ? [...options.point.outlineColor] : [0, 0, 0, 1],
|
||||||
|
},
|
||||||
|
uGevPixelRatio: { value: 1 },
|
||||||
uGevDepthMode: {
|
uGevDepthMode: {
|
||||||
value:
|
value:
|
||||||
depthTest === 'always'
|
depthTest === 'always'
|
||||||
@@ -232,7 +258,7 @@ export function createMoverMesh(
|
|||||||
onTextureLoad();
|
onTextureLoad();
|
||||||
}),
|
}),
|
||||||
transparent: true,
|
transparent: true,
|
||||||
alphaTest: 0.02,
|
alphaTest: point ? 0.005 : 0.02,
|
||||||
// Drawn after tone mapping, with no depth buffer: the shader tests the
|
// Drawn after tone mapping, with no depth buffer: the shader tests the
|
||||||
// scene depth itself (overlayDepth.js).
|
// scene depth itself (overlayDepth.js).
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
@@ -299,7 +325,14 @@ export function createMoverMesh(
|
|||||||
* origin moved to the camera when it has gone too far (a full rewrite).
|
* origin moved to the camera when it has gone too far (a full rewrite).
|
||||||
* Returns true when it rewrote the instances.
|
* Returns true when it rewrote the instances.
|
||||||
*/
|
*/
|
||||||
update({ cameraPosition, cameraHeight, fovDeg, viewportHeightPx }) {
|
update({
|
||||||
|
cameraPosition,
|
||||||
|
cameraHeight,
|
||||||
|
fovDeg,
|
||||||
|
viewportHeightPx,
|
||||||
|
pixelRatio = 1,
|
||||||
|
}) {
|
||||||
|
uniforms.uGevPixelRatio.value = pixelRatio;
|
||||||
uniforms.uGevFootprint.value = footprintPerDepth(
|
uniforms.uGevFootprint.value = footprintPerDepth(
|
||||||
fovDeg,
|
fovDeg,
|
||||||
viewportHeightPx,
|
viewportHeightPx,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
aircraftIconCells,
|
aircraftIconCells,
|
||||||
drawAircraftIconAtlas,
|
drawAircraftIconAtlas,
|
||||||
} from '../../../data/aircraftIcons.js';
|
} from '../../../data/aircraftIcons.js';
|
||||||
import { createNavaraLayerView } from '../../shared/render/navaraView.js';
|
import { createEngineLayerView } from '../../shared/render/engineView.js';
|
||||||
|
|
||||||
const BASE_SIZE_PX = 20;
|
const BASE_SIZE_PX = 20;
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ export function createAircraftNavaraRenderer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- the view ------------------------------------------------------------
|
// --- the view ------------------------------------------------------------
|
||||||
const layerView = createNavaraLayerView(engine, {
|
const layerView = createEngineLayerView(engine, {
|
||||||
layerId,
|
layerId,
|
||||||
wrap: (run) => inFrameDo(run),
|
wrap: (run) => inFrameDo(run),
|
||||||
isDestroyed: () => destroyed,
|
isDestroyed: () => destroyed,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as geo from '../../geo/index.js';
|
||||||
import { createControls } from './controls.js';
|
import { createControls } from './controls.js';
|
||||||
import { createCatalog } from './catalog.js';
|
import { createCatalog } from './catalog.js';
|
||||||
import { createLabels } from './labels.js';
|
import { createLabels } from './labels.js';
|
||||||
@@ -43,6 +44,36 @@ export function createSatellitesLayer({ services, source }) {
|
|||||||
parts.lifecycle.methods,
|
parts.lifecycle.methods,
|
||||||
parts.ingestion.methods,
|
parts.ingestion.methods,
|
||||||
{
|
{
|
||||||
|
qaDrawn() {
|
||||||
|
if (!state._renderer) return null;
|
||||||
|
const plain = (p) => (p ? { x: p.x, y: p.y, z: p.z } : null);
|
||||||
|
return {
|
||||||
|
enabled: state._enabled,
|
||||||
|
points: [...state._points].map(([id, point]) => ({
|
||||||
|
id,
|
||||||
|
position: plain(point.position),
|
||||||
|
show: state._enabled && state._params.showPoints && point.show,
|
||||||
|
size: point.pixelSize,
|
||||||
|
alpha: point.color.alpha,
|
||||||
|
})),
|
||||||
|
tracked: plain(getRenderer().trackedPosition?.(state._trackedEntity)),
|
||||||
|
rings: [...state._orbitPaths].map(([id, path]) => ({
|
||||||
|
id,
|
||||||
|
show: path.primitive.show,
|
||||||
|
anchors: Array.from({ length: 8 }, (_, i) =>
|
||||||
|
plain(
|
||||||
|
geo.matrix4.multiplyByPoint(
|
||||||
|
path.primitive.modelMatrix,
|
||||||
|
path.basePositions[
|
||||||
|
Math.floor((i * (path.basePositions.length - 1)) / 8)
|
||||||
|
],
|
||||||
|
geo.cartesian3.create(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
satelliteVisualsVisible: parts.controls.satelliteVisualsVisible,
|
satelliteVisualsVisible: parts.controls.satelliteVisualsVisible,
|
||||||
satelliteCatalogModeChanged: parts.controls.satelliteCatalogModeChanged,
|
satelliteCatalogModeChanged: parts.controls.satelliteCatalogModeChanged,
|
||||||
createIssOverlayEntry: parts.labels.createIssOverlayEntry,
|
createIssOverlayEntry: parts.labels.createIssOverlayEntry,
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ export function createInteraction({
|
|||||||
if (
|
if (
|
||||||
layerState._trackedNorad &&
|
layerState._trackedNorad &&
|
||||||
layerState._viewer &&
|
layerState._viewer &&
|
||||||
getRenderer().follow.current() &&
|
((getRenderer().follow.current() &&
|
||||||
getRenderer().follow.current() !== layerState._trackedEntity
|
getRenderer().follow.current() !== layerState._trackedEntity) ||
|
||||||
|
(getRenderer().kind === 'navara' &&
|
||||||
|
!getRenderer().follow.current() &&
|
||||||
|
!getRenderer().follow.releasing()))
|
||||||
) {
|
) {
|
||||||
parts.tracking._clearTracking(true, {
|
parts.tracking._clearTracking(true, {
|
||||||
origin:
|
origin:
|
||||||
@@ -56,8 +59,7 @@ export function createInteraction({
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Check if it's a satellite point (id is NORAD catalog number)
|
// Check if it's a satellite point (id is NORAD catalog number)
|
||||||
const prim = picked.primitive;
|
if (Number.isFinite(getRenderer().pickedNorad(picked))) {
|
||||||
if (prim && prim.id != null) {
|
|
||||||
const noradId = getRenderer().pickedNorad(picked);
|
const noradId = getRenderer().pickedNorad(picked);
|
||||||
if (!isNaN(noradId) && layerState._catalog.has(noradId)) {
|
if (!isNaN(noradId) && layerState._catalog.has(noradId)) {
|
||||||
parts.tracking._cancelPendingTrackingRestore();
|
parts.tracking._cancelPendingTrackingRestore();
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export function createSatellitesCesiumRenderer(engine, { viewer } = {}) {
|
|||||||
createTracked({ position, viewFrom }) {
|
createTracked({ position, viewFrom }) {
|
||||||
return viewer.entities.add({
|
return viewer.entities.add({
|
||||||
position: new Cesium.CallbackProperty(position, false),
|
position: new Cesium.CallbackProperty(position, false),
|
||||||
viewFrom,
|
viewFrom: new Cesium.Cartesian3(viewFrom.x, viewFrom.y, viewFrom.z),
|
||||||
|
trackingReferenceFrame: Cesium.TrackingReferenceFrame.ENU,
|
||||||
point: {
|
point: {
|
||||||
pixelSize: 14,
|
pixelSize: 14,
|
||||||
color: Cesium.Color.YELLOW,
|
color: Cesium.Color.YELLOW,
|
||||||
@@ -64,13 +65,26 @@ export function createSatellitesCesiumRenderer(engine, { viewer } = {}) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
trackedPosition: (entity) =>
|
||||||
|
entity?.position?.getValue(viewer.clock.currentTime),
|
||||||
removeTracked: (entity) => viewer?.entities.remove(entity),
|
removeTracked: (entity) => viewer?.entities.remove(entity),
|
||||||
follow: {
|
follow: {
|
||||||
start: (entity) => {
|
start: (entity) => {
|
||||||
viewer.trackedEntity = entity;
|
if (engine) {
|
||||||
|
engine.camera.follow({
|
||||||
|
native: entity,
|
||||||
|
position: () => entity.position.getValue(viewer.clock.currentTime),
|
||||||
|
viewFrom: () => entity.viewFrom.getValue(viewer.clock.currentTime),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
viewer.camera?.cancelFlight();
|
||||||
|
viewer.trackedEntity = entity;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
release: () => {
|
release: () => {
|
||||||
if (viewer) viewer.trackedEntity = undefined;
|
const current = engine?.camera.following();
|
||||||
|
if (current && current.native === viewer?.trackedEntity) current.stop();
|
||||||
|
else if (viewer) viewer.trackedEntity = undefined;
|
||||||
},
|
},
|
||||||
current: () => viewer?.trackedEntity,
|
current: () => viewer?.trackedEntity,
|
||||||
onChanged: (listener) =>
|
onChanged: (listener) =>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import { createSatellitesCesiumRenderer } from './cesium.js';
|
||||||
|
|
||||||
|
test('native satellite viewFrom fills the scratch vector used by EntityView', () => {
|
||||||
|
const viewer = { entities: new Cesium.EntityCollection() };
|
||||||
|
const renderer = createSatellitesCesiumRenderer(null, { viewer });
|
||||||
|
const viewFrom = geo.cartesian3.create(-450000, -450000, 350000);
|
||||||
|
const entity = renderer.createTracked({
|
||||||
|
position: () => geo.cartesian3.fromDegrees(0, 0, 420000),
|
||||||
|
viewFrom,
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
entity.trackingReferenceFrame,
|
||||||
|
Cesium.TrackingReferenceFrame.ENU,
|
||||||
|
);
|
||||||
|
const scratch = new Cesium.Cartesian3();
|
||||||
|
const value = entity.viewFrom.getValue(Cesium.JulianDate.now(), scratch);
|
||||||
|
assert.equal(value, scratch);
|
||||||
|
assert.deepEqual(
|
||||||
|
[scratch.x, scratch.y, scratch.z],
|
||||||
|
[viewFrom.x, viewFrom.y, viewFrom.z],
|
||||||
|
);
|
||||||
|
renderer.removeTracked(entity);
|
||||||
|
assert.equal(viewer.entities.values.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('native satellite follow goes through the shared camera contract', () => {
|
||||||
|
let options;
|
||||||
|
const viewer = {
|
||||||
|
entities: new Cesium.EntityCollection(),
|
||||||
|
clock: { currentTime: Cesium.JulianDate.now() },
|
||||||
|
};
|
||||||
|
const engine = {
|
||||||
|
camera: {
|
||||||
|
follow: (spec) => {
|
||||||
|
options = spec;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const renderer = createSatellitesCesiumRenderer(engine, { viewer });
|
||||||
|
const position = geo.cartesian3.fromDegrees(0, 0, 20200000);
|
||||||
|
const offset = geo.cartesian3.create(-1800000, -1800000, 1400000);
|
||||||
|
const entity = renderer.createTracked({
|
||||||
|
position: () => position,
|
||||||
|
viewFrom: offset,
|
||||||
|
});
|
||||||
|
renderer.follow.start(entity);
|
||||||
|
assert.equal(options.native, entity);
|
||||||
|
assert.equal(options.position(), position);
|
||||||
|
assert.deepEqual({ ...options.viewFrom() }, { ...offset });
|
||||||
|
});
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
// Satellite points, rigid orbit rings and follow through the globe contract.
|
||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import { createMoverBank } from '../../aircraft/render/navara.js';
|
||||||
|
import { createEngineLayerView } from '../../shared/render/engineView.js';
|
||||||
|
import { selectionDispatcherFor } from '../../../data/selectionDispatcher.js';
|
||||||
|
import { WHITE, constantRgba } from '../../shared/color.js';
|
||||||
|
|
||||||
|
const YELLOW = constantRgba(1, 1, 0);
|
||||||
|
export function createSatellitesNavaraRenderer(engine) {
|
||||||
|
const banks = new Map();
|
||||||
|
const points = new Map();
|
||||||
|
const tracked = new Set();
|
||||||
|
const orbits = new Set();
|
||||||
|
const dirty = new Set();
|
||||||
|
let destroyed = false;
|
||||||
|
let inFrame = false;
|
||||||
|
let queued = false;
|
||||||
|
let visible = true;
|
||||||
|
let sequence = 0;
|
||||||
|
let releasing = false;
|
||||||
|
let starting = null;
|
||||||
|
const rgba = new Float64Array(4);
|
||||||
|
const fields = {
|
||||||
|
position: null,
|
||||||
|
color: rgba,
|
||||||
|
scale: 1,
|
||||||
|
show: true,
|
||||||
|
pickId: null,
|
||||||
|
};
|
||||||
|
const positionField = { position: null };
|
||||||
|
const colorField = { color: rgba };
|
||||||
|
const showField = { show: true };
|
||||||
|
const flush = () => {
|
||||||
|
queued = false;
|
||||||
|
for (const bank of dirty) bank.commit();
|
||||||
|
dirty.clear();
|
||||||
|
};
|
||||||
|
const onCommit = (bank) => {
|
||||||
|
dirty.add(bank);
|
||||||
|
if (!inFrame && !queued) {
|
||||||
|
queued = true;
|
||||||
|
queueMicrotask(flush);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const frame = (run) => {
|
||||||
|
inFrame = true;
|
||||||
|
try {
|
||||||
|
return run();
|
||||||
|
} finally {
|
||||||
|
inFrame = false;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const layerView = createEngineLayerView(engine, {
|
||||||
|
layerId: 'satellites',
|
||||||
|
wrap: frame,
|
||||||
|
isDestroyed: () => destroyed,
|
||||||
|
});
|
||||||
|
const copyColor = (color) => {
|
||||||
|
rgba[0] = color.red;
|
||||||
|
rgba[1] = color.green;
|
||||||
|
rgba[2] = color.blue;
|
||||||
|
rgba[3] = color.alpha;
|
||||||
|
};
|
||||||
|
const write = (point, set, index) => {
|
||||||
|
copyColor(point.values.color);
|
||||||
|
fields.position = point.values.position;
|
||||||
|
fields.show = point.values.show;
|
||||||
|
fields.pickId = point.id;
|
||||||
|
set.set(index, fields);
|
||||||
|
};
|
||||||
|
const bankFor = (point) => {
|
||||||
|
const v = point.values;
|
||||||
|
const scale = v.scaleByDistance;
|
||||||
|
const outline = v.outlineColor;
|
||||||
|
const depth =
|
||||||
|
v.disableDepthTestDistance === Infinity
|
||||||
|
? 'always'
|
||||||
|
: v.disableDepthTestDistance > 0
|
||||||
|
? { disableWithinM: v.disableDepthTestDistance }
|
||||||
|
: 'normal';
|
||||||
|
const key = [
|
||||||
|
point.family,
|
||||||
|
v.pixelSize,
|
||||||
|
v.outlineWidth,
|
||||||
|
outline.red,
|
||||||
|
outline.green,
|
||||||
|
outline.blue,
|
||||||
|
outline.alpha,
|
||||||
|
...Object.values(scale ?? {}),
|
||||||
|
v.disableDepthTestDistance,
|
||||||
|
].join(':');
|
||||||
|
let entry = banks.get(key);
|
||||||
|
if (!entry) {
|
||||||
|
const id = ++sequence;
|
||||||
|
const bank = createMoverBank(engine, {
|
||||||
|
options: (capacity, generation) => ({
|
||||||
|
id: `satellites-points-${id}-${generation}`,
|
||||||
|
kind: 'point',
|
||||||
|
capacity,
|
||||||
|
pickable: true,
|
||||||
|
depthTest: depth,
|
||||||
|
order: point.family === 'tracked' ? 100 : 0,
|
||||||
|
point: {
|
||||||
|
sizePx: v.pixelSize,
|
||||||
|
outlinePx: v.outlineWidth,
|
||||||
|
outlineColor: [
|
||||||
|
outline.red,
|
||||||
|
outline.green,
|
||||||
|
outline.blue,
|
||||||
|
outline.alpha,
|
||||||
|
],
|
||||||
|
scaleByDistance: scale
|
||||||
|
? [scale.near, scale.nearValue, scale.far, scale.farValue]
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
write,
|
||||||
|
onCommit,
|
||||||
|
});
|
||||||
|
entry = { bank, family: point.family };
|
||||||
|
banks.set(key, entry);
|
||||||
|
bank.setVisible(point.family === 'tracked' || visible);
|
||||||
|
}
|
||||||
|
return entry.bank;
|
||||||
|
};
|
||||||
|
const createPoint = (spec, family = 'fleet') => {
|
||||||
|
const point = {
|
||||||
|
id: spec.id,
|
||||||
|
family,
|
||||||
|
gevBank: null,
|
||||||
|
gevIndex: -1,
|
||||||
|
values: {
|
||||||
|
position: geo.cartesian3.clone(spec.position),
|
||||||
|
color: spec.color ?? WHITE,
|
||||||
|
show: spec.show !== false,
|
||||||
|
pixelSize: spec.pixelSize ?? 1,
|
||||||
|
outlineColor: spec.outlineColor ?? WHITE,
|
||||||
|
outlineWidth: spec.outlineWidth ?? 0,
|
||||||
|
scaleByDistance: spec.scaleByDistance ?? null,
|
||||||
|
disableDepthTestDistance: spec.disableDepthTestDistance ?? 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.defineProperties(point, {
|
||||||
|
position: {
|
||||||
|
get: () => point.values.position,
|
||||||
|
set(value) {
|
||||||
|
geo.cartesian3.clone(value, point.values.position);
|
||||||
|
positionField.position = point.values.position;
|
||||||
|
point.gevBank?.update(point, positionField);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
get: () => point.values.color,
|
||||||
|
set(value) {
|
||||||
|
point.values.color = value;
|
||||||
|
copyColor(value);
|
||||||
|
point.gevBank?.update(point, colorField);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
get: () => point.values.show,
|
||||||
|
set(value) {
|
||||||
|
if (point.values.show === Boolean(value)) return;
|
||||||
|
point.values.show = Boolean(value);
|
||||||
|
showField.show = point.values.show;
|
||||||
|
point.gevBank?.update(point, showField);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const key of [
|
||||||
|
'pixelSize',
|
||||||
|
'outlineWidth',
|
||||||
|
'outlineColor',
|
||||||
|
'scaleByDistance',
|
||||||
|
'disableDepthTestDistance',
|
||||||
|
])
|
||||||
|
Object.defineProperty(point, key, {
|
||||||
|
get: () => point.values[key],
|
||||||
|
set(value) {
|
||||||
|
if (point.values[key] === value) return;
|
||||||
|
point.values[key] = value;
|
||||||
|
bankFor(point).add(point);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
bankFor(point).add(point);
|
||||||
|
return point;
|
||||||
|
};
|
||||||
|
const collection = {
|
||||||
|
get show() {
|
||||||
|
return visible;
|
||||||
|
},
|
||||||
|
set show(value) {
|
||||||
|
visible = Boolean(value);
|
||||||
|
for (const entry of banks.values())
|
||||||
|
if (entry.family === 'fleet') entry.bank.setVisible(visible);
|
||||||
|
},
|
||||||
|
add(spec) {
|
||||||
|
const point = createPoint(spec);
|
||||||
|
points.set(point.id, point);
|
||||||
|
return point;
|
||||||
|
},
|
||||||
|
remove(point) {
|
||||||
|
if (!points.delete(point.id)) return false;
|
||||||
|
point.gevBank?.remove(point);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
removeAll() {
|
||||||
|
for (const point of points.values()) point.gevBank?.remove(point);
|
||||||
|
points.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const follow = {
|
||||||
|
start(entity) {
|
||||||
|
starting = entity;
|
||||||
|
try {
|
||||||
|
const followLabel = {
|
||||||
|
id: entity.gevTrackedId,
|
||||||
|
model: () => entity.gevLabelModel,
|
||||||
|
position: () => entity.gevDisplayPosition?.() ?? entity.sample(),
|
||||||
|
};
|
||||||
|
entity.follow = engine.camera.follow({
|
||||||
|
position: entity.sample,
|
||||||
|
viewFrom: () => entity.viewFrom,
|
||||||
|
label: followLabel,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
starting = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
release() {
|
||||||
|
releasing = true;
|
||||||
|
try {
|
||||||
|
engine.camera.following()?.stop();
|
||||||
|
} finally {
|
||||||
|
releasing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
releasing: () => releasing,
|
||||||
|
current() {
|
||||||
|
if (starting) return starting;
|
||||||
|
const current = engine.camera.following();
|
||||||
|
for (const entity of tracked)
|
||||||
|
if (entity.follow === current && current) return entity;
|
||||||
|
return current;
|
||||||
|
},
|
||||||
|
onChanged: (listener) =>
|
||||||
|
engine.camera.followChanged.addEventListener(listener),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
kind: 'navara',
|
||||||
|
engine,
|
||||||
|
view: layerView.view,
|
||||||
|
createPoints: () => collection,
|
||||||
|
removePoints: () => collection.removeAll(),
|
||||||
|
project: (position, result) =>
|
||||||
|
engine.projector.worldToWindow(position, result),
|
||||||
|
onFrame: (listener) =>
|
||||||
|
layerView.view.on('preRender', () => {
|
||||||
|
listener();
|
||||||
|
for (const entity of tracked) {
|
||||||
|
const position = entity.sample();
|
||||||
|
if (position) entity.point.position = position;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createOrbit({ positions, width, color }) {
|
||||||
|
const line = engine.lines.createLine({
|
||||||
|
id: `satellites-orbit-${++sequence}`,
|
||||||
|
widthPx: width,
|
||||||
|
color: [color.red, color.green, color.blue, 0.6],
|
||||||
|
depthFailAlpha: 0.35 / 0.6,
|
||||||
|
arc: 'geodesic',
|
||||||
|
});
|
||||||
|
line.setPositions(positions);
|
||||||
|
const primitive = {
|
||||||
|
line,
|
||||||
|
modelMatrix: geo.matrix4.clone(geo.matrix4.IDENTITY),
|
||||||
|
visible: true,
|
||||||
|
get show() {
|
||||||
|
return this.visible;
|
||||||
|
},
|
||||||
|
set show(value) {
|
||||||
|
this.visible = Boolean(value);
|
||||||
|
line.show(this.visible);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
orbits.add(primitive);
|
||||||
|
return primitive;
|
||||||
|
},
|
||||||
|
commitOrbit: (primitive) =>
|
||||||
|
primitive.line.setModelMatrix(primitive.modelMatrix),
|
||||||
|
removeOrbit(primitive) {
|
||||||
|
primitive.line.destroy();
|
||||||
|
orbits.delete(primitive);
|
||||||
|
},
|
||||||
|
createTracked({ id, position, viewFrom }) {
|
||||||
|
const entity = {
|
||||||
|
id: `satellites:${id}`,
|
||||||
|
sample: position,
|
||||||
|
viewFrom,
|
||||||
|
follow: null,
|
||||||
|
position: { getValue: position },
|
||||||
|
point: createPoint(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
position: position(),
|
||||||
|
pixelSize: 14,
|
||||||
|
color: YELLOW,
|
||||||
|
outlineColor: WHITE,
|
||||||
|
outlineWidth: 2,
|
||||||
|
disableDepthTestDistance: Infinity,
|
||||||
|
},
|
||||||
|
'tracked',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
tracked.add(entity);
|
||||||
|
return entity;
|
||||||
|
},
|
||||||
|
trackedPosition: (entity) => entity?.point.position,
|
||||||
|
removeTracked(entity) {
|
||||||
|
entity.point.gevBank?.remove(entity.point);
|
||||||
|
tracked.delete(entity);
|
||||||
|
if (entity.follow && engine.camera.following() === entity.follow)
|
||||||
|
follow.release();
|
||||||
|
},
|
||||||
|
follow,
|
||||||
|
listenClick(listener) {
|
||||||
|
const dispatcher = selectionDispatcherFor(engine);
|
||||||
|
if (!dispatcher)
|
||||||
|
throw new Error('Satellite selection dispatcher unavailable');
|
||||||
|
const remove = dispatcher.register('satellites', {
|
||||||
|
owns: (id) => points.has(Number(id)),
|
||||||
|
onClick: listener,
|
||||||
|
});
|
||||||
|
return { destroy: remove };
|
||||||
|
},
|
||||||
|
pickedNorad: (picked) => Number(picked?.id ?? picked?.primitive?.id),
|
||||||
|
isTrackedPick: (picked, entity) =>
|
||||||
|
Boolean(
|
||||||
|
entity &&
|
||||||
|
Number(picked?.id ?? picked?.primitive?.id) === entity.point.id,
|
||||||
|
),
|
||||||
|
destroy() {
|
||||||
|
destroyed = true;
|
||||||
|
for (const orbit of orbits) orbit.line.destroy();
|
||||||
|
orbits.clear();
|
||||||
|
for (const entry of banks.values()) entry.bank.destroy();
|
||||||
|
banks.clear();
|
||||||
|
dirty.clear();
|
||||||
|
points.clear();
|
||||||
|
tracked.clear();
|
||||||
|
layerView.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as geo from '../../../geo/index.js';
|
||||||
|
import { createFakeEngine } from '../../../globe/testing/fakeEngine.js';
|
||||||
|
import { WHITE, nearFarScale } from '../../shared/color.js';
|
||||||
|
import { createSatellitesNavaraRenderer } from './navara.js';
|
||||||
|
|
||||||
|
const position = geo.cartesian3.fromDegrees(0, 0, 420000);
|
||||||
|
const spec = (id) => ({
|
||||||
|
id,
|
||||||
|
position,
|
||||||
|
color: WHITE,
|
||||||
|
pixelSize: 12,
|
||||||
|
outlineColor: WHITE.withAlpha(0.3),
|
||||||
|
outlineWidth: 1,
|
||||||
|
scaleByDistance: nearFarScale(1e6, 1.5, 2e7, 0.6),
|
||||||
|
});
|
||||||
|
|
||||||
|
test('satellite banks retain styles, survive growth and update the real store', async () => {
|
||||||
|
const engine = createFakeEngine();
|
||||||
|
const renderer = createSatellitesNavaraRenderer(engine);
|
||||||
|
const points = renderer.createPoints();
|
||||||
|
const all = Array.from({ length: 1500 }, (_, i) => points.add(spec(i)));
|
||||||
|
await Promise.resolve();
|
||||||
|
const [listed] = engine.movers.list();
|
||||||
|
const set = engine.fake.moverSet(listed.id);
|
||||||
|
assert.equal(engine.movers.list().length, 1);
|
||||||
|
assert.equal(set.store.count, 1500);
|
||||||
|
assert.equal(set.store.pickIds[699], 699);
|
||||||
|
assert.deepEqual(set.options.point.outlineColor, [1, 1, 1, 0.3]);
|
||||||
|
const before = set.commits;
|
||||||
|
const remove = renderer.onFrame(() => {
|
||||||
|
all[0].position = { x: 1, y: 2, z: 3 };
|
||||||
|
all[0].show = false;
|
||||||
|
});
|
||||||
|
engine.fake.frame();
|
||||||
|
remove();
|
||||||
|
assert.equal(engine.fake.moverSet(listed.id).commits, before + 1);
|
||||||
|
assert.deepEqual([...set.store.positions.slice(0, 3)], [1, 2, 3]);
|
||||||
|
assert.equal(set.store.shows[0], 0);
|
||||||
|
all[1].pixelSize = 14;
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(set.store.count, 1499);
|
||||||
|
assert.equal(engine.movers.list().length, 2);
|
||||||
|
points.removeAll();
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.ok(engine.movers.list().every((s) => s.count === 0));
|
||||||
|
renderer.destroy();
|
||||||
|
assert.equal(engine.movers.list().length, 0);
|
||||||
|
engine.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tracked glyph uses the follow sample and stays visible independently of fleet', async () => {
|
||||||
|
const engine = createFakeEngine();
|
||||||
|
const renderer = createSatellitesNavaraRenderer(engine);
|
||||||
|
const points = renderer.createPoints();
|
||||||
|
points.add(spec(25544));
|
||||||
|
let sample = position;
|
||||||
|
const entity = renderer.createTracked({
|
||||||
|
id: 25544,
|
||||||
|
position: () => sample,
|
||||||
|
viewFrom: { x: -450000, y: -450000, z: 350000 },
|
||||||
|
});
|
||||||
|
entity.gevTrackedId = '25544';
|
||||||
|
renderer.follow.start(entity);
|
||||||
|
points.show = false;
|
||||||
|
const remove = renderer.onFrame(() => {});
|
||||||
|
sample = { x: position.x + 7600, y: 100, z: 0 };
|
||||||
|
engine.fake.frame();
|
||||||
|
remove();
|
||||||
|
assert.deepEqual({ ...renderer.trackedPosition(entity) }, sample);
|
||||||
|
assert.equal(renderer.follow.current(), entity);
|
||||||
|
const sets = engine.movers.list().map((s) => engine.fake.moverSet(s.id));
|
||||||
|
assert.equal(sets.find((s) => s.options.order === 100).store.shows[0], 1);
|
||||||
|
assert.equal(sets.find((s) => s.options.order === 0).store.count, 0);
|
||||||
|
renderer.removeTracked(entity);
|
||||||
|
assert.equal(renderer.follow.current(), null);
|
||||||
|
renderer.destroy();
|
||||||
|
engine.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('orbit starts at identity, rotates without rebuilding and releases lines', () => {
|
||||||
|
const engine = createFakeEngine();
|
||||||
|
const renderer = createSatellitesNavaraRenderer(engine);
|
||||||
|
const orbit = renderer.createOrbit({
|
||||||
|
positions: [position, { x: 0, y: 6778137, z: 0 }],
|
||||||
|
width: 2.5,
|
||||||
|
color: WHITE,
|
||||||
|
});
|
||||||
|
assert.deepEqual(orbit.modelMatrix, geo.matrix4.IDENTITY);
|
||||||
|
const before = engine.lines.list()[0];
|
||||||
|
orbit.modelMatrix[12] = 123;
|
||||||
|
renderer.commitOrbit(orbit);
|
||||||
|
assert.equal(engine.lines.list()[0].id, before.id);
|
||||||
|
orbit.show = false;
|
||||||
|
assert.equal(engine.lines.list()[0].shown, false);
|
||||||
|
renderer.removeOrbit(orbit);
|
||||||
|
assert.equal(engine.lines.list().length, 0);
|
||||||
|
renderer.destroy();
|
||||||
|
engine.destroy();
|
||||||
|
});
|
||||||
@@ -59,6 +59,7 @@ export function createRendering({
|
|||||||
|
|
||||||
layerState._orbitPaths.set(noradId, {
|
layerState._orbitPaths.set(noradId, {
|
||||||
primitive,
|
primitive,
|
||||||
|
basePositions,
|
||||||
// Same Date object as computeOrbitPath's internal fixedGmst → identical
|
// Same Date object as computeOrbitPath's internal fixedGmst → identical
|
||||||
// GMST value (gstime is pure), so delta starts at exactly 0 and the
|
// GMST value (gstime is pure), so delta starts at exactly 0 and the
|
||||||
// initial identity modelMatrix is correct until the first ring tick.
|
// initial identity modelMatrix is correct until the first ring tick.
|
||||||
|
|||||||
@@ -449,6 +449,7 @@ export function createTracking({
|
|||||||
);
|
);
|
||||||
|
|
||||||
layerState._trackedEntity = getRenderer().createTracked({
|
layerState._trackedEntity = getRenderer().createTracked({
|
||||||
|
id: noradId,
|
||||||
position,
|
position,
|
||||||
viewFrom,
|
viewFrom,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// The view a layer renderer on the Navara engine gives its logic (shared by
|
// An engine-contract view shared by layer renderers and the celestial HUD:
|
||||||
// the aircraft and vessel renderers): `camera` camera-shaped and `scene`
|
// `camera` camera-shaped and `scene`
|
||||||
// scene-shaped adapters over engine.camera.pose() (refreshed once per frame,
|
// scene-shaped adapters over engine.camera.pose() (refreshed once per frame,
|
||||||
// and whenever the camera moved), with the engine registered for the scene
|
// and whenever the camera moved), with the engine registered for the scene
|
||||||
// (engineFromViewer) so the shared helpers (iconOrientation, focus) project
|
// (engineFromViewer) so the shared helpers (iconOrientation, focus) project
|
||||||
@@ -16,7 +16,7 @@ import { registerViewerEngine } from '../../../globe/cesium/engineFromViewer.js'
|
|||||||
* (the renderer's frame commit scope).
|
* (the renderer's frame commit scope).
|
||||||
* @returns {{ view: object, freshCamera: () => object, destroy: () => void }}
|
* @returns {{ view: object, freshCamera: () => object, destroy: () => void }}
|
||||||
*/
|
*/
|
||||||
export function createNavaraLayerView(
|
export function createEngineLayerView(
|
||||||
engine,
|
engine,
|
||||||
{ layerId, wrap = (run) => run(), isDestroyed = () => false },
|
{ layerId, wrap = (run) => run(), isDestroyed = () => false },
|
||||||
) {
|
) {
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import * as geo from '../../../geo/index.js';
|
import * as geo from '../../../geo/index.js';
|
||||||
import { createMoverBank } from '../../aircraft/render/navara.js';
|
import { createMoverBank } from '../../aircraft/render/navara.js';
|
||||||
import { createNavaraLayerView } from '../../shared/render/navaraView.js';
|
import { createEngineLayerView } from '../../shared/render/engineView.js';
|
||||||
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
|
import { SPRITE_LAYER_ORDER } from '../../../data/spriteOrder.js';
|
||||||
import { allVesselChevronIcons, VESSEL_ICON_PX } from '../icons.js';
|
import { allVesselChevronIcons, VESSEL_ICON_PX } from '../icons.js';
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ export function createVesselsNavaraRenderer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- the view --------------------------------------------------------------
|
// --- the view --------------------------------------------------------------
|
||||||
const layerView = createNavaraLayerView(engine, {
|
const layerView = createEngineLayerView(engine, {
|
||||||
layerId: LAYER_ID,
|
layerId: LAYER_ID,
|
||||||
wrap: inFrameDo,
|
wrap: inFrameDo,
|
||||||
isDestroyed: () => destroyed,
|
isDestroyed: () => destroyed,
|
||||||
|
|||||||
@@ -414,21 +414,15 @@ export class StyleManager extends ShellFacade {
|
|||||||
exitPanels: () => this._panelChrome.exitCockpit(),
|
exitPanels: () => this._panelChrome.exitCockpit(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Full-globe sun/moon ring. It is a crisp screen-space overlay above the
|
// Full-globe sun/moon ring on either engine, below the HUD/readout.
|
||||||
// Cesium canvas but below the HUD/detection/readout z ladder.
|
this.celestialRing = new CelestialRing(viewer ?? engine, {
|
||||||
// Viewer-bound modules (the ring, cockpit) are not created without a
|
enabled: false,
|
||||||
// viewer (the experimental Navara engine); detection and the tracked
|
onAutoDisable: () =>
|
||||||
// readout are the engine's.
|
this.setCelestialRingEnabled(false, {
|
||||||
this.celestialRing = viewer
|
syncShare: !!this.shareLinkManager,
|
||||||
? new CelestialRing(viewer, {
|
focus: false,
|
||||||
enabled: false,
|
}),
|
||||||
onAutoDisable: () =>
|
});
|
||||||
this.setCelestialRingEnabled(false, {
|
|
||||||
syncShare: !!this.shareLinkManager,
|
|
||||||
focus: false,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Share Link Manager
|
// Share Link Manager
|
||||||
this.shareLinkManager = new ShareLinkManager(engine, {
|
this.shareLinkManager = new ShareLinkManager(engine, {
|
||||||
|
|||||||
Reference in new issue
Block a user