Fly layer focus flights through the camera port

flyToBounds takes an easing, as flyTo does: in the contract, the Cesium
port and the reference recorder, pinned by a new conformance trace.

The flights of radio, awareness, CCTV, submarine cables, cyclones, the
launch preview, installations, ALPR, street-level framing, recent
imagery, weather and world focus now go through the renderer's camera
port. Street-level framing cancels its own flight handle rather than
whatever flight is current. worldFocus.js no longer reaches Cesium and
leaves the allow-list.

Layer pose reads stay for each layer's own slice. The local GeoJSON
flight stays on Cesium: gods-eye-view/infrastructure is used with plain
Cesium viewers that have no renderer bound.
This commit is contained in:
Sameh Khamis committed 2026-11-05 20:46:58 -08:00
1 parent f95324acb1
commit e3baeec223
31 files changed
+2349 -871

No files matched your search

+21
View File
@@ -1064,6 +1064,12 @@
"src/ui/visualPresets.js",
"src/ui/visualSettings.js",
"src/worldFocus.js",
"src/geo/index.js",
"src/geo/vector.js",
"src/geo/ellipsoid.js",
"src/geo/frames.js",
"src/geo/celestial.js",
"src/geo/quaternion.js",
"src/data/feedState.js",
"src/data/layerSnapshot.js",
"src/ui/aircraftDisplay.js",
@@ -1574,6 +1580,7 @@
"modules": [
"src/i18n/marker.js",
"src/layers/recentImagery/index.js",
"src/render/rendererOf.js",
"src/layers/recentImagery/source.js",
"src/layers/recentImagery/model.js",
"src/layers/recentImagery/catalog.js",
@@ -1714,6 +1721,7 @@
"src/layers/launches/paths.js",
"src/layers/launches/policy.js",
"src/layers/launches/policyHelpers.js",
"src/render/rendererOf.js",
"src/layers/launches/rendering.js",
"src/layers/launches/replay.js",
"src/layers/launches/selection.js",
@@ -1753,6 +1761,7 @@
"src/data/militaryInstallationData.js",
"src/data/installationNames.js",
"src/layers/installations/controls.js",
"src/render/rendererOf.js",
"src/layers/installations/index.js",
"src/layers/installations/ingestion.js",
"src/layers/installations/lifecycle.js",
@@ -1820,6 +1829,7 @@
"src/layers/awareness/controls.js",
"src/layers/awareness/dependencies.js",
"src/layers/awareness/focus.js",
"src/render/rendererOf.js",
"src/layers/awareness/history.js",
"src/layers/awareness/index.js",
"src/layers/awareness/ingestion.js",
@@ -1986,6 +1996,7 @@
"src/geoDistance.js",
"src/layers/cctv/modelSpecs.js",
"src/layers/cctv/navigation.js",
"src/render/rendererOf.js",
"src/layers/cctv/policy.js",
"src/layers/cctv/presentation.js",
"src/layers/cctv/projection.js",
@@ -2045,6 +2056,7 @@
"src/layers/streetLevel/providers/mapillary/index.js",
"src/layers/streetLevel/viewerHost.js",
"src/layers/streetLevel/cameraFraming.js",
"src/render/rendererOf.js",
"src/layers/streetLevel/marker.js",
"src/layers/streetLevel/view.js",
"src/layers/streetLevel/uiState.js",
@@ -2120,6 +2132,7 @@
"src/layers/alpr/model.js",
"src/layers/alpr/policy.js",
"src/layers/alpr/presentation.js",
"src/render/rendererOf.js",
"src/layers/alpr/visuals.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/markerState.js",
@@ -2179,6 +2192,7 @@
"src/layers/radio/lifecycle.js",
"src/layers/radio/model.js",
"src/layers/radio/navigation.js",
"src/render/rendererOf.js",
"src/layers/radio/playback.js",
"src/layers/radio/policy.js",
"src/layers/radio/presentation.js",
@@ -2224,6 +2238,7 @@
"src/layers/submarineCables/index.js",
"src/layers/submarineCables/ingestion.js",
"src/layers/submarineCables/interaction.js",
"src/render/rendererOf.js",
"src/layers/submarineCables/pickState.js",
"src/data/entityRecords.js",
"src/layers/submarineCables/lifecycle.js",
@@ -3821,6 +3836,12 @@
"src/ui/panelRailGeometry.js",
"src/ui/radioTunerModel.js",
"src/worldFocus.js",
"src/geo/index.js",
"src/geo/vector.js",
"src/geo/ellipsoid.js",
"src/geo/frames.js",
"src/geo/celestial.js",
"src/geo/quaternion.js",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js",
+9 -1
View File
@@ -3,6 +3,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
import alprCamerasLayer, {
createAlprCamerasLayer,
configureAlprSource,
@@ -342,6 +343,9 @@ function cameraHarness(layer = alprCamerasLayer) {
remove() {},
},
};
bindFakeCesiumCamera(viewer);
// The renderer's camera port relays moveEnd; the layer's own come on top.
const portMoveEndListeners = viewer.camera.moveEnd.numberOfListeners;
const setInput = Cesium.ScreenSpaceEventHandler.prototype.setInputAction;
Cesium.ScreenSpaceEventHandler.prototype.setInputAction = function (
fn,
@@ -364,6 +368,7 @@ function cameraHarness(layer = alprCamerasLayer) {
layer.enable();
return {
viewer,
portMoveEndListeners,
requests,
credits,
get source() {
@@ -932,7 +937,10 @@ test('two factories keep their requests, records and destruction independent', a
b.source.entities.values[0],
)?.labelModel.details.includes('Source: Test directory'),
);
assert.equal(a.viewer.camera.moveEnd.numberOfListeners, 0);
assert.equal(
a.viewer.camera.moveEnd.numberOfListeners,
a.portMoveEndListeners,
);
assert.equal(a.viewer.scene.postRender.numberOfListeners, 0);
} finally {
b.restore();
+14 -13
View File
@@ -13,6 +13,7 @@
//
// computeFrustumGeometry is PURE (no viewer, no scene queries) so it runs under
// plain node:test.
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
import { test } from 'node:test';
// Preserve structural pins across component qualification and formatter wrapping.
function componentFunctionSource(fn) {
@@ -868,6 +869,7 @@ test('CCTV focus distinguishes tracking ownership from a missing active camera',
},
},
};
bindFakeCesiumCamera(viewer);
const record = {
camera: { rangeM: 210, headingDeg: 41 },
position: { x: 1, y: 2, z: 3 },
@@ -899,6 +901,7 @@ test('CCTV focus reports when the camera flight starts', () => {
},
},
};
bindFakeCesiumCamera(viewer);
const record = {
camera: { rangeM: 210, headingDeg: 41 },
position: { x: 1, y: 2, z: 3 },
@@ -919,6 +922,7 @@ test('CCTV focus refuses camera flights while cockpit owns the view', () => {
},
},
};
bindFakeCesiumCamera(viewer);
const record = {
camera: { rangeM: 210, headingDeg: 41 },
position: { x: 1, y: 2, z: 3 },
@@ -1397,19 +1401,16 @@ test('CCTV drag-then-release over a camera is inert, while a clean tap activates
target.addEventListener(CCTV_FOCUS_REQUEST_EVENT, (event) =>
requests.push(event.detail),
);
bindCctvWorldClickGesture(
input,
() => {
activateCctvCameraFromWorldClick(
'atx-cam-3',
() => {
activationCalls += 1;
return CCTV_ACTIVATION_RESULT.ACTIVATED;
},
target,
);
},
);
bindCctvWorldClickGesture(input, () => {
activateCctvCameraFromWorldClick(
'atx-cam-3',
() => {
activationCalls += 1;
return CCTV_ACTIVATION_RESULT.ACTIVATED;
},
target,
);
});
handler.fire(GESTURE_TYPES.LEFT_DOWN, { position: { x: 10, y: 10 } });
timeMs = 20;
+35 -9
View File
@@ -48,6 +48,7 @@ import {
import { NAVIGATION_AUTHORITY_EVENT } from '../navigationPolicy.js';
import { withEntityRecord } from './entityRecords.js';
import { framePortFor } from '../testSupport/fakeFramePort.mjs';
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
const militaryAwarenessSource = readLayerSource(
new URL('./militaryAwareness.js', import.meta.url),
@@ -183,6 +184,7 @@ function installAwarenessRuntime({
},
container: new FakeElement(),
};
bindFakeCesiumCamera(viewer);
const layers = new Map(
AWARENESS_DEPENDENCIES.map((layerId) => [
layerId,
@@ -825,7 +827,10 @@ for (const fixture of [
let retrackCalls = 0;
try {
const trackedEntity = withEntityRecord({}, { trackedId: `${subject.layerId}:${subject.id}` });
const trackedEntity = withEntityRecord(
{},
{ trackedId: `${subject.layerId}:${subject.id}` },
);
runtime.viewer.trackedEntity = trackedEntity;
replaceMethod(
fixture.layer,
@@ -927,7 +932,10 @@ test('Contacts activation adopts the production military tracked-subject descrip
),
'mutating one accessor result must not alter the tracked source position',
);
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `military:${id}` });
runtime.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: `military:${id}` },
);
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
replaceMethod(
flightsLayer,
@@ -1032,7 +1040,10 @@ test('production military tracked-subject label falls back to registration befor
'getAllPositions id stays the identity hex',
);
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `military:${id}` });
runtime.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: `military:${id}` },
);
replaceMethod(flightsLayer, 'getTrackedSubject', () => null, restores);
replaceMethod(flightsLayer, 'trackById', () => true, restores);
replaceMethod(militaryFlightsLayer, 'trackById', () => true, restores);
@@ -1104,7 +1115,10 @@ test('a cached Context subject re-reads its label when enrichment lands after se
meta,
});
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `flights:${id}` });
runtime.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: `flights:${id}` },
);
replaceMethod(
militaryFlightsLayer,
'getTrackedSubject',
@@ -1247,7 +1261,10 @@ test('Contacts activation adopts the production civilian tracked-subject descrip
),
'mutating one accessor result must not alter the tracked source position',
);
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: `flights:${id}` });
runtime.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: `flights:${id}` },
);
replaceMethod(
militaryFlightsLayer,
'getTrackedSubject',
@@ -1334,7 +1351,10 @@ test('Contacts activation reconciles to a newer cross-layer tracked flight after
const restores = [];
try {
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'flights:first' });
runtime.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: 'flights:first' },
);
replaceMethod(
flightsLayer,
'getTrackedSubject',
@@ -1357,7 +1377,10 @@ test('Contacts activation reconciles to a newer cross-layer tracked flight after
civilianTracked = null;
militaryTracked = awarenessSubject('military', 'latest', positions.latest);
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'military:latest' });
runtime.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: 'military:latest' },
);
releaseDependencies();
await nextTurn();
await nextTurn();
@@ -1401,7 +1424,10 @@ test('tracked flight cleared during Contacts activation suppresses fallback auto
const focused = [];
try {
runtime.viewer.trackedEntity = withEntityRecord({}, { trackedId: 'flights:selected' });
runtime.viewer.trackedEntity = withEntityRecord(
{},
{ trackedId: 'flights:selected' },
);
replaceMethod(flightsLayer, 'getTrackedSubject', () => tracked, restores);
replaceMethod(
militaryFlightsLayer,
@@ -1999,7 +2025,7 @@ test('vessel entry and selection framing both use the 3 km focus radius', () =>
);
const focusRadiusUses =
militaryAwarenessSource.match(
/new\s*Cesium\.BoundingSphere\(\s*vessel\.position,\s*VESSEL_FOCUS_RADIUS_M,?\s*\)/g,
/\{\s*center:\s*vessel\.position,\s*radius:\s*VESSEL_FOCUS_RADIUS_M\s*\}/g,
) || [];
assert.equal(focusRadiusUses.length, 2);
});
File diff suppressed because it is too large. Load diff
+1179 -476
View File
File diff suppressed because it is too large. Load diff
+13 -8
View File
@@ -26,6 +26,7 @@ import {
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
import { alprMarkers } from './markerState.js';
import { heightsFor } from '../../queries/heights.js';
import { cameraOf } from '../../render/rendererOf.js';
export function createAlprPresentation({ state, services, source }) {
const { governorRequestRender } = services.render;
@@ -534,14 +535,18 @@ export function createAlprPresentation({ state, services, source }) {
validAlprGroundHeight(height) ? height : 0,
);
if (!selectRecord(nearest.id)) return false;
camera.flyToBoundingSphere(new Cesium.BoundingSphere(center, 30), {
duration: 1.2,
offset: new Cesium.HeadingPitchRange(
camera.heading || 0,
-Math.PI / 4,
800,
),
});
const port = cameraOf(state.viewer);
port.flyToBounds(
{ center, radius: 30 },
{
durationS: 1.2,
offset: {
heading: port.pose().heading || 0,
pitch: -Math.PI / 4,
range: 800,
},
},
);
governorRequestRender('alpr-focus');
return true;
}
+7 -6
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { VESSEL_FOCUS_RADIUS_M } from './policy.js';
import { cameraOf } from '../../render/rendererOf.js';
export function createFocus({ state: layerState, services, parts, source }) {
const flightsLayer = services.flights;
@@ -45,9 +46,9 @@ export function createFocus({ state: layerState, services, parts, source }) {
)
return true;
announceNavigationAuthority('context-vessel-focus');
layerState.viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(vessel.position, VESSEL_FOCUS_RADIUS_M),
{ duration: 1.4 },
cameraOf(layerState.viewer).flyToBounds(
{ center: vessel.position, radius: VESSEL_FOCUS_RADIUS_M },
{ durationS: 1.4 },
);
return true;
}
@@ -180,9 +181,9 @@ export function createFocus({ state: layerState, services, parts, source }) {
announceNavigationAuthority('context-vessel-autofocus', {
cancelPendingSelection: false,
});
layerState.viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(vessel.position, VESSEL_FOCUS_RADIUS_M),
{ duration: 1.6 },
cameraOf(layerState.viewer).flyToBounds(
{ center: vessel.position, radius: VESSEL_FOCUS_RADIUS_M },
{ durationS: 1.6 },
);
return true;
}
+9 -11
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { CCTV_FOCUS_RESULT } from './policy.js';
import { cameraOf } from '../../render/rendererOf.js';
export function createNavigation({
state: layerState,
@@ -59,19 +60,16 @@ export function createNavigation({
}
const { camera } = record;
const range = Math.max(280, camera.rangeM * 1.18);
viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(
record.position,
Math.max(40, camera.rangeM * 0.36),
),
cameraOf(viewer).flyToBounds(
{ center: record.position, radius: Math.max(40, camera.rangeM * 0.36) },
{
offset: new Cesium.HeadingPitchRange(
parts.model.toRad(camera.headingDeg),
parts.model.toRad(-22),
offset: {
heading: parts.model.toRad(camera.headingDeg),
pitch: parts.model.toRad(-22),
range,
),
duration: Math.max(0.2, duration || 0),
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
},
durationS: Math.max(0.2, duration || 0),
easing: 'cubicInOut',
},
);
return CCTV_FOCUS_RESULT.FOCUSED;
+10 -6
View File
@@ -23,6 +23,7 @@ import {
translateKnown,
} from '../../i18n/index.js';
import { formatQuantity } from '../../ui/unitLabels.js';
import { cameraOf } from '../../render/rendererOf.js';
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
// joiner after the hyphen (JetBrains Mono has no U+2011 non-breaking hyphen).
@@ -319,12 +320,15 @@ export function createCyclonesLayer({
selectedId !== storm.id
)
return;
return viewer.camera.flyToBoundingSphere(sphere, {
duration: matchMedia?.('(prefers-reduced-motion: reduce)')
?.matches
? 0
: 1.4,
});
cameraOf(viewer).flyToBounds(
{ center: sphere.center, radius: sphere.radius },
{
durationS: matchMedia?.('(prefers-reduced-motion: reduce)')
?.matches
? 0
: 1.4,
},
);
});
}
}
+7 -1
View File
@@ -8,6 +8,7 @@ import {
} from '../../data/inputOwnership.js';
import { isOwnedByOtherLayer } from '../../data/pickRegistry.js';
import { fakeInput } from '../../input/testDoubles.mjs';
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
/** Record each click subscription the layer makes, as the old handler fakes did. */
function recordHandlers(viewer, handlers) {
@@ -120,6 +121,7 @@ test('map selection yields to pointer owners and owns only its enabled handler',
},
},
};
bindFakeCesiumCamera(viewer);
recordHandlers(viewer, handlers);
layer.init(viewer);
layer.setRowControlsListener(() => notices++);
@@ -360,7 +362,10 @@ function harness(
setSelection(id) {
selection = id;
},
getFocusSphere: () => ({ radius: 500000 }),
getFocusSphere: () => ({
center: { x: 6378137, y: 0, z: 0 },
radius: 500000,
}),
clear() {
cleared++;
},
@@ -384,6 +389,7 @@ function harness(
navigation.push({ sphere, options }),
},
};
bindFakeCesiumCamera(viewer);
layer.init(viewer);
layer.attachShellServices({
runNavigation: (fn) => {
+11 -10
View File
@@ -6,6 +6,7 @@ import {
ANCHOR_REFRESH_M,
} from './policy.js';
import { formatNumber, t, tn } from '../../i18n/index.js';
import { cameraOf } from '../../render/rendererOf.js';
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
// hands back msg()-marked English plus values; getStats translates them here.
@@ -175,16 +176,16 @@ export function createControls({ state: layerState, services, parts, source }) {
// No camera flight without a real selection: a flight plus a stale subject
// reads as success to Context navigation and strands NEXT on this item.
if (!parts.selection.selectRecord(record.id)) return false;
layerState.viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(
Cesium.Cartesian3.fromDegrees(
record.longitude,
record.latitude,
parts.rendering.installationSurfaceHeightM(record),
),
18000,
),
{ duration: 1.4 },
cameraOf(layerState.viewer).flyToBounds(
{
center: {
lon: record.longitude,
lat: record.latitude,
height: parts.rendering.installationSurfaceHeightM(record),
},
radius: 18000,
},
{ durationS: 1.4 },
);
return true;
},
+6 -10
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { t } from '../../i18n/index.js';
import { cameraOf } from '../../render/rendererOf.js';
import {
SATELLITE_STANDALONE_DEFAULTS,
LAUNCH_PAD_ZONE_MAX_CAMERA_HEIGHT_M,
@@ -228,17 +229,12 @@ export function createPolicyHelpers({
const range = parts.panel.missionHoverPreviewRange(
layerState._viewer.camera.positionCartographic?.height,
);
const position = Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat);
layerState._viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(position, 0),
cameraOf(layerState._viewer).flyToBounds(
{ center: { lon: launch.lon, lat: launch.lat }, radius: 0 },
{
offset: new Cesium.HeadingPitchRange(
0,
-Cesium.Math.PI_OVER_TWO,
range,
),
duration: 0.8,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
offset: { heading: 0, pitch: -Cesium.Math.PI_OVER_TWO, range },
durationS: 0.8,
easing: 'cubicInOut',
},
);
}
+27 -30
View File
@@ -4,6 +4,7 @@ import {
GLOBAL_RADIO_ALTITUDE_M,
RADIO_GLOBE_RECENTER_MAX_HEIGHT_M,
} from './policy.js';
import { cameraOf } from '../../render/rendererOf.js';
export function createNavigation({
state: layerState,
@@ -229,7 +230,7 @@ export function createNavigation({
)
return;
layerState._activeRadioCameraFlight = null;
layerState._viewer.camera.cancelFlight();
cameraOf(layerState._viewer).cancelFlight();
}
function invalidateRadioCameraNavigation() {
@@ -247,7 +248,7 @@ export function createNavigation({
);
}
function startRadioCameraFlight(navigation, options, onComplete) {
function startRadioCameraFlight(navigation, flight, onComplete) {
if (!radioCameraNavigationIsCurrent(navigation)) return false;
const token = ++layerState._radioCameraFlightSequence;
layerState._activeRadioCameraFlight = {
@@ -260,10 +261,10 @@ export function createNavigation({
if (completed && radioCameraNavigationIsCurrent(navigation))
onComplete?.();
};
layerState._viewer.camera.flyTo({
...options,
complete: () => finish(true),
cancel: () => finish(false),
cameraOf(layerState._viewer).flyTo(flight.destination, {
...flight.options,
onComplete: () => finish(true),
onCancel: () => finish(false),
});
return true;
}
@@ -280,17 +281,15 @@ export function createNavigation({
return startRadioCameraFlight(
navigation,
{
destination: Cesium.Cartesian3.fromDegrees(
plan.lon,
plan.lat,
plan.height,
),
orientation: {
heading: plan.heading,
pitch: plan.pitch,
roll: plan.roll,
destination: { lon: plan.lon, lat: plan.lat, height: plan.height },
options: {
orientation: {
heading: plan.heading,
pitch: plan.pitch,
roll: plan.roll,
},
durationS: navigation.duration,
},
duration: navigation.duration,
},
() => {
navigation.phase = 'settled';
@@ -340,13 +339,15 @@ export function createNavigation({
activeNavigation,
{
destination: activeNavigation.recenterPlan.destination,
orientation: {
heading: activeNavigation.cameraState.heading,
pitch: activeNavigation.cameraState.pitch,
roll: activeNavigation.cameraState.roll,
options: {
orientation: {
heading: activeNavigation.cameraState.heading,
pitch: activeNavigation.cameraState.pitch,
roll: activeNavigation.cameraState.roll,
},
durationS: recenterDuration,
easing: 'cubicInOut',
},
duration: recenterDuration,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
},
() => {
activeNavigation.recentered = true;
@@ -363,14 +364,10 @@ export function createNavigation({
if (!station || !radioCameraNavigationAllowed(layerState._viewer))
return false;
cancelActiveRadioCameraFlight();
layerState._viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
station.lon,
station.lat,
85_000,
),
duration: 1.1,
});
cameraOf(layerState._viewer).flyTo(
{ lon: station.lon, lat: station.lat, height: 85_000 },
{ durationS: 1.1 },
);
return true;
}
return {
+8 -5
View File
@@ -34,6 +34,7 @@ import {
} from './model.js';
import { msg, t } from '../../i18n/index.js';
import { READOUT_WORDS, displayDay, displayMetres } from './display.js';
import { cameraOf } from '../../render/rendererOf.js';
/**
* Quiet time after the last arrow key before the focused day previews, so a
@@ -987,15 +988,17 @@ export function createRecentImageryLayer({
height: canvas?.clientHeight,
fovy: camera.frustum?.fovy,
});
camera.flyTo({
destination: ellipsoid.cartographicToCartesian({
cameraOf(_viewer).flyTo(
ellipsoid.cartographicToCartesian({
longitude: toRadians(target.lon),
latitude: toRadians(target.lat),
height,
}),
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
duration: ZOOM_FIT_SECONDS,
});
{
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
durationS: ZOOM_FIT_SECONDS,
},
);
return height;
},
+14 -8
View File
@@ -1,4 +1,5 @@
import test from 'node:test';
import * as Cesium from 'cesium';
import assert from 'node:assert/strict';
import { NO_IMAGERY_HOST } from '../../maps/imageryHost.js';
import { acquireImageryComparison } from '../../maps/imageryComparison.js';
@@ -24,6 +25,7 @@ import {
manualTimers,
settle,
} from './testDoubles.mjs';
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
const NOW = new Date('2026-09-21T15:00:00Z');
const LINK_BOX = {
@@ -133,6 +135,7 @@ function fixture({
});
if (viirs) layer.setSources({ viirs: true });
const viewer = { camera: {} };
bindFakeCesiumCamera(viewer);
const reasons = [];
layer.subscribe((snapshot, reason) => reasons.push(reason));
layer.init(viewer);
@@ -966,6 +969,8 @@ test('USE VIEW takes the camera rectangle and refuses a view over the cap instea
test('ZOOM IN: an oversized box or view flies top-down to its centre at the height whose view is 400 km wide', () => {
const f = fixture();
const flights = [];
const landing = (i) =>
Cesium.Ellipsoid.WGS84.cartesianToCartographic(flights[i].destination);
const deg = (rad) => (rad * 180) / Math.PI;
const rad = (value) => (value * Math.PI) / 180;
// A 1000 × 500 canvas with a 60° horizontal FOV.
@@ -976,7 +981,8 @@ test('ZOOM IN: an oversized box or view flies top-down to its centre at the heig
scene: {
canvas: { clientWidth: 1000, clientHeight: 500 },
ellipsoid: {
cartographicToCartesian: (c) => ({ ...c, cartesian: true }),
cartographicToCartesian: (c) =>
Cesium.Ellipsoid.WGS84.cartographicToCartesian(c),
cartesianToCartographic: (c) => c && { ...c },
},
},
@@ -999,9 +1005,9 @@ test('ZOOM IN: an oversized box or view flies top-down to its centre at the heig
assert.ok(Math.abs(height - expected) < 1e-6, String(height));
assert.equal(flights.length, 1);
const [flight] = flights;
assert.ok(Math.abs(deg(flight.destination.longitude) - 10) < 1e-9);
assert.ok(Math.abs(deg(flight.destination.latitude) - 10) < 1e-9);
assert.equal(flight.destination.height, height);
assert.ok(Math.abs(deg(landing(0).longitude) - 10) < 1e-9);
assert.ok(Math.abs(deg(landing(0).latitude) - 10) < 1e-9);
assert.ok(Math.abs(landing(0).height - height) < 1e-6);
assert.deepEqual(flight.orientation, {
heading: 0,
pitch: -Math.PI / 2,
@@ -1019,14 +1025,14 @@ test('ZOOM IN: an oversized box or view flies top-down to its centre at the heig
assert.equal(f.layer.useCurrentView(), false);
assert.equal(f.snap().zoomToFit, true);
f.layer.zoomToFit();
assert.ok(Math.abs(deg(flights[1].destination.longitude) + 97) < 1e-9);
assert.ok(Math.abs(deg(flights[1].destination.latitude) - 31) < 1e-9);
assert.ok(Math.abs(deg(landing(1).longitude) + 97) < 1e-9);
assert.ok(Math.abs(deg(landing(1).latitude) - 31) < 1e-9);
// Sky at the centre: the view rectangle's centre instead.
centreHit = null;
f.layer.useCurrentView();
f.layer.zoomToFit();
assert.ok(Math.abs(deg(flights[2].destination.longitude) + 98) < 1e-9);
assert.ok(Math.abs(deg(flights[2].destination.latitude) - 30) < 1e-9);
assert.ok(Math.abs(deg(landing(2).longitude) + 98) < 1e-9);
assert.ok(Math.abs(deg(landing(2).latitude) - 30) < 1e-9);
// The box tool's own oversized drag carries its box.
f.layer.reportBoxRefusal('Box is 2,226 km wide · limit 1,000 km', {
west: 0,
+18 -16
View File
@@ -1,6 +1,7 @@
import * as Cesium from 'cesium';
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
import { heightsFor } from '../../queries/heights.js';
import { cameraOf } from '../../render/rendererOf.js';
/** Fly the globe camera once to frame a photo after it opens. */
export function createCameraFraming({ state }) {
@@ -49,36 +50,37 @@ export function createCameraFraming({ state }) {
return;
const ground = groundHeightAt(position.lon, position.lat, altitude);
// Owned before the call: starting it cancels the previous flight (ours
// included) synchronously, and a zero-length one completes at once.
// included), and a zero-length one completes inside the call.
const current = {};
flight = current;
const release = () => {
if (flight === current) flight = null;
};
state.viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(
Cesium.Cartesian3.fromDegrees(position.lon, position.lat, ground + 2),
4,
),
const started = cameraOf(state.viewer).flyToBounds(
{
offset: new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(bearing || 0),
Cesium.Math.toRadians(-32),
140,
),
duration: 1.6,
complete: release,
cancel: release,
center: { lon: position.lon, lat: position.lat, height: ground + 2 },
radius: 4,
},
{
offset: {
heading: Cesium.Math.toRadians(bearing || 0),
pitch: Cesium.Math.toRadians(-32),
range: 140,
},
durationS: 1.6,
onComplete: release,
onCancel: release,
},
);
if (flight === current) current.handle = started;
}
/** Stop our framing flight if it is still in the air; another feature's flight is left alone. */
function cancel() {
if (!flight) return;
// Give up ownership first: cancelFlight delivers `cancel` synchronously.
const { handle } = flight;
flight = null;
state.viewer?.camera?.cancelFlight?.();
handle?.cancel();
}
return { attachNavigation, begin, frame, cancel };
+3 -2
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
import { createStreetLevelLayer } from './index.js';
import { MAPILLARY_CREDIT_HTML } from './providers/mapillary/policy.js';
import { createMapillaryProvider } from './providers/mapillary/index.js';
@@ -326,7 +327,7 @@ function fakeViewer() {
disableRootEvents: true,
onwheel: null,
});
return {
return bindFakeCesiumCamera({
credits,
flights,
scene: {
@@ -358,7 +359,7 @@ function fakeViewer() {
if (at !== -1) credits.splice(at, 1);
},
},
};
});
}
/** A layer over `providers`, initialised and enabled on a stand-in viewer. */
+14 -15
View File
@@ -1,5 +1,6 @@
import * as Cesium from 'cesium';
import { cablePicks } from './pickState.js';
import { cameraOf } from '../../render/rendererOf.js';
export function createInteraction({ state, inputFor }) {
function registerPickEntity(entity, info) {
@@ -45,22 +46,20 @@ export function createInteraction({ state, inputFor }) {
function flyToReference(viewer, reference) {
if (!viewer || !reference) return;
const destination = Cesium.Cartesian3.fromDegrees(
reference.lon,
reference.lat,
6500,
);
viewer.camera.cancelFlight();
viewer.camera.flyTo({
destination,
orientation: {
heading: viewer.camera.heading || 0,
pitch: Cesium.Math.toRadians(-52),
roll: 0,
const camera = cameraOf(viewer);
camera.cancelFlight();
camera.flyTo(
{ lon: reference.lon, lat: reference.lat, height: 6500 },
{
orientation: {
heading: camera.pose().heading || 0,
pitch: Cesium.Math.toRadians(-52),
roll: 0,
},
durationS: 1.35,
easing: 'cubicInOut',
},
duration: 1.35,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
});
);
}
return {
registerPickEntity,
+18 -12
View File
@@ -7,6 +7,7 @@ import { createWeatherRendering } from './rendering.js';
import { weatherDisplay, insideExtent, escapeCreditText } from './display.js';
import { createShellCesium, createShellScene } from './shellFixture.mjs';
import { formatScaleLabel } from '../../ui/unitLabels.js';
import { bindFakeCesiumCamera } from '../../testSupport/fakeCameraPort.mjs';
const times = ['2026-09-15T20:00:00.000Z', '2026-09-15T20:05:00.000Z'];
const base = {
@@ -58,18 +59,22 @@ async function shownLayer(id, manifest, params, view = {}) {
getDiagnostics: () => ({ time, loading: false, error: null }),
}),
});
layer.init({
camera: {
moveEnd: { addEventListener: () => () => {} },
flyTo: ({ destination }) => flights.push(destination),
pickEllipsoid: () =>
view.center ? Cesium.Cartesian3.fromDegrees(...view.center) : undefined,
},
scene: {
canvas: { clientWidth: 800, clientHeight: 600 },
globe: { ellipsoid: Cesium.Ellipsoid.WGS84 },
},
});
layer.init(
bindFakeCesiumCamera({
camera: {
moveEnd: { addEventListener: () => () => {} },
flyTo: ({ destination }) => flights.push(destination),
pickEllipsoid: () =>
view.center
? Cesium.Cartesian3.fromDegrees(...view.center)
: undefined,
},
scene: {
canvas: { clientWidth: 800, clientHeight: 600 },
globe: { ellipsoid: Cesium.Ellipsoid.WGS84 },
},
}),
);
if (params) layer.setParams(params);
layer.enable();
await layer.update();
@@ -638,6 +643,7 @@ function globeViewer() {
requestRender() {},
},
};
bindFakeCesiumCamera(viewer);
class Provider {
constructor(options) {
this.options = options;
+14 -11
View File
@@ -10,6 +10,7 @@ import {
} from './clock.js';
import { formatUtcDayMonth, t, msg } from '../../i18n/index.js';
import { formatScaleLabel } from '../../ui/unitLabels.js';
import { cameraOf } from '../../render/rendererOf.js';
/**
* A stored (English) failure in the language. HTTP failures from source.js
@@ -475,17 +476,19 @@ export function createWeatherLayer({
(lightning
? { west: 110, south: -25, east: 0, north: 80 }
: manifest.bounds);
runNavigation(() =>
viewer.camera.flyTo({
destination: cesium.Rectangle.fromDegrees(
b.west,
b.south,
b.east,
b.north,
),
duration: motion?.matches ? 0 : 1.4,
}),
);
runNavigation(() => {
cameraOf(viewer).flyTo(
{
rect: {
west: b.west,
south: b.south,
east: b.east,
north: b.north,
},
},
{ durationS: motion?.matches ? 0 : 1.4 },
);
});
}
if (clock && enabled) {
if (params.play === true) void clock.togglePlay();
+1
View File
@@ -182,6 +182,7 @@ export function createCesiumCamera({ scene, camera, report }) {
o.range ?? 0,
),
duration: options.durationS,
easingFunction: options.easing && EASINGS[options.easing],
...callbacks,
},
),
+1
View File
@@ -198,6 +198,7 @@ function cesiumDriver(clock, threshold) {
)
: undefined,
duration: options.durationS,
easingFunction: EASINGS[options.easing],
complete,
cancel,
},
+1 -2
View File
@@ -230,6 +230,5 @@
"src/ui/shareFollow.js",
"src/ui/visualEffects.js",
"src/voice/gevActions.js",
"src/voice/pointerPick.js",
"src/worldFocus.js"
"src/voice/pointerPick.js"
]
+21
View File
@@ -371,6 +371,27 @@ export const CAMERA_SCRIPTS = Object.freeze([
{ record: 'pose' },
],
},
{
name: 'flyToBounds easing',
steps: [
START,
{
do: 'flyToBounds',
args: [
{ center: { ...AUSTIN, height: 0 }, radius: 500 },
{
offset: { heading: 0.5, pitch: -0.4, range: 2500 },
durationS: 1,
easing: 'cubicInOut',
},
],
},
{ do: 'frames', count: 20 },
{ record: 'pose' },
{ do: 'frames', count: 70 },
{ record: 'pose' },
],
},
{
name: 'a cancelled flight stops where it was',
steps: [
@@ -2071,6 +2071,79 @@
}
]
},
"flyToBounds easing": {
"records": [
{
"step": 3,
"kind": "pose",
"value": {
"position": {
"x": -2267093.6902229865,
"y": -16670141.730590489,
"z": 9795550.536889307
},
"direction": {
"x": 0.13436019637409768,
"y": 0.918216318410333,
"z": -0.37259378716678554
},
"up": {
"x": 0.11222044188344235,
"y": 0.3594849211517312,
"z": 0.9263785208477249
},
"right": {
"x": 0.9845577230684848,
"y": -0.1662810394167284,
"z": -0.05474217639712155
},
"heading": 0.06349647744048603,
"pitch": -1.4221294499846318,
"roll": 6.283185231543937,
"height": 13094845.933521682,
"frameIsIdentity": true
}
},
{
"step": 5,
"kind": "pose",
"value": {
"position": {
"x": -744171.8196286416,
"y": -5464810.027402491,
"z": 3194736.500278188
},
"direction": {
"x": 0.5377602708229207,
"y": 0.6774719395626166,
"z": 0.5018422682773198
},
"up": {
"x": 0.1010227917440539,
"y": -0.6427253771880552,
"z": 0.7594066664618568
},
"right": {
"x": 0.8370234684121447,
"y": -0.35768122766471616,
"z": -0.41407227956428677
},
"heading": 0.4998993704309962,
"pitch": -0.4003616646350623,
"roll": 6.283184711710125,
"height": 973.9627016434621,
"frameIsIdentity": true
}
}
],
"flights": [
{
"step": 1,
"outcome": "complete",
"frames": 62
}
]
},
"a cancelled flight stops where it was": {
"records": [
{
+2 -2
View File
@@ -210,9 +210,9 @@ export interface CameraPort {
*/
flyToBounds(
sphere: Sphere,
options?: { offset?: HeadingPitchRange; durationS?: number } & Pick<
options?: { offset?: HeadingPitchRange } & Pick<
FlightOptions,
'onComplete' | 'onCancel'
'durationS' | 'easing' | 'onComplete' | 'onCancel'
>,
): Flight;
cancelFlight(): void;
+3 -2
View File
@@ -1,5 +1,6 @@
/** Shared Street Level test stand-ins: provider snapshot and provider, Mapillary source, photo viewer, Cesium viewer, ray-casting camera. */
import * as Cesium from 'cesium';
import { bindFakeCesiumCamera } from './fakeCameraPort.mjs';
import { MAPILLARY_CREDIT_HTML } from '../layers/streetLevel/providers/mapillary/policy.js';
import {
latToTileY,
@@ -190,7 +191,7 @@ export function fakeCesiumViewer({ view = false } = {}) {
},
},
);
return {
return bindFakeCesiumCamera({
credits,
flights,
scene: {
@@ -211,7 +212,7 @@ export function fakeCesiumViewer({ view = false } = {}) {
if (at !== -1) credits.splice(at, 1);
},
},
};
});
}
/**
+7 -4
View File
@@ -1,3 +1,4 @@
import * as Cesium from 'cesium';
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -15,6 +16,7 @@ import {
response,
settle,
} from '../layers/recentImagery/testDoubles.mjs';
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
const S18 = 'S30:2026-09-18';
const L16 = 'L30:2026-09-16';
@@ -128,6 +130,7 @@ function fixture({ fetchImpl, controller = fakeController() } = {}) {
camera: {},
scene: { canvas: { clientWidth: 1000 }, requestRender() {} },
};
bindFakeCesiumCamera(viewer);
layer.init(viewer);
layer.attachMapStackController(controller);
let active = false;
@@ -801,9 +804,7 @@ test('an oversized box offers ZOOM IN in a fixed slot at the end of the notice l
const f = fixture();
const flights = [];
f.viewer.scene.canvas.clientHeight = 500;
f.viewer.scene.ellipsoid = {
cartographicToCartesian: (c) => ({ ...c }),
};
f.viewer.scene.ellipsoid = Cesium.Ellipsoid.WGS84;
Object.assign(f.viewer.camera, {
frustum: { fovy: 2 * Math.atan(Math.tan(Math.PI / 6) / 2) },
flyTo: (options) => flights.push(options),
@@ -835,7 +836,9 @@ test('an oversized box offers ZOOM IN in a fixed slot at the end of the notice l
assert.equal(flights.length, 1);
assert.ok(
Math.abs(
flights[0].destination.height - 400_000 / (2 * Math.tan(Math.PI / 6)),
Cesium.Ellipsoid.WGS84.cartesianToCartographic(flights[0].destination)
.height -
400_000 / (2 * Math.tan(Math.PI / 6)),
) < 1e-6,
);
// Another refusal has nothing to fit; a box that succeeds hides it again.
+16 -17
View File
@@ -1,7 +1,8 @@
/**
* One-click camera transfer for layer-owned world targets.
*/
import * as Cesium from 'cesium';
import { WGS84, toRadians } from './geo/index.js';
import { rendererOf } from './render/rendererOf.js';
export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus';
export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9;
@@ -26,11 +27,8 @@ export function isValidWorldFocusTarget(detail) {
// Vessel and fire targets are surface-anchored Earth positions. Merely
// finite coordinates near the ECEF origin cannot be flown to, and must be
// rejected before the camera policy releases a current follow owner.
const magnitude = Cesium.Cartesian3.magnitude(position);
return (
Number.isFinite(magnitude) &&
magnitude >= Cesium.Ellipsoid.WGS84.minimumRadius * 0.95
);
const magnitude = Math.hypot(position.x, position.y, position.z);
return Number.isFinite(magnitude) && magnitude >= WGS84.radii.z * 0.95;
}
/** Announce a valid user-click focus request. */
@@ -71,25 +69,26 @@ export function routeWorldFocusRequest(event, runExplicitFocus, fly) {
/** Fly to a world target after ownership has been released. */
export function flyToWorldTarget(viewer, target = {}) {
const camera = viewer?.camera;
const camera = rendererOf(viewer)?.camera;
const framing = WORLD_FOCUS_FRAMING[target.kind];
if (!camera || !framing || !isValidWorldFocusTarget(target)) return false;
const heading = Number.isFinite(camera.heading) ? camera.heading : 0;
const poseHeading = camera.pose().heading;
const heading = Number.isFinite(poseHeading) ? poseHeading : 0;
const duration =
target.durationSec > 0
? target.durationSec
: WORLD_CLICK_FOCUS_DURATION_SEC;
camera.cancelFlight?.();
camera.flyToBoundingSphere(
new Cesium.BoundingSphere(target.position, framing.radiusM),
camera.cancelFlight();
camera.flyToBounds(
{ center: target.position, radius: framing.radiusM },
{
offset: new Cesium.HeadingPitchRange(
offset: {
heading,
Cesium.Math.toRadians(framing.pitchDeg),
framing.rangeM,
),
duration,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
pitch: toRadians(framing.pitchDeg),
range: framing.rangeM,
},
durationS: duration,
easing: 'cubicInOut',
},
);
return true;
+124 -30
View File
@@ -3,6 +3,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { bindFakeCesiumCamera } from './testSupport/fakeCameraPort.mjs';
import {
WORLD_CLICK_FOCUS_DURATION_SEC,
WORLD_FOCUS_FRAMING,
@@ -21,16 +22,28 @@ function stubCamera() {
return {
calls,
heading: Cesium.Math.toRadians(45),
cancelFlight() { calls.cancelFlight += 1; },
flyToBoundingSphere(sphere, options) { calls.flights.push({ sphere, options }); },
cancelFlight() {
calls.cancelFlight += 1;
},
flyToBoundingSphere(sphere, options) {
calls.flights.push({ sphere, options });
},
};
}
test('a focus request carries the clicked target to any registered listener', () => {
const target = new EventTarget();
const seen = [];
const dispose = registerWorldFocusRequestListener(target, (event) => seen.push(event.detail));
assert.equal(requestWorldFocus({ kind: 'vessel', position: POSITION, id: '123' }, target), true);
const dispose = registerWorldFocusRequestListener(target, (event) =>
seen.push(event.detail),
);
assert.equal(
requestWorldFocus(
{ kind: 'vessel', position: POSITION, id: '123' },
target,
),
true,
);
assert.equal(seen.length, 1);
assert.equal(seen[0].kind, 'vessel');
assert.equal(seen[0].id, '123');
@@ -44,7 +57,9 @@ test('a focus request carries the clicked target to any registered listener', ()
test('requests without a kind or a position are dropped, never dispatched', () => {
const target = new EventTarget();
let count = 0;
target.addEventListener(WORLD_FOCUS_REQUEST_EVENT, () => { count += 1; });
target.addEventListener(WORLD_FOCUS_REQUEST_EVENT, () => {
count += 1;
});
assert.equal(requestWorldFocus({ position: POSITION }, target), false);
assert.equal(requestWorldFocus({ kind: 'vessel' }, target), false);
assert.equal(requestWorldFocus(null, target), false);
@@ -74,51 +89,95 @@ test('a refusing policy (cockpit owns the camera) never reaches the flight', ()
const result = routeWorldFocusRequest(
{ detail: { kind: 'vessel', id: '123', position: POSITION } },
() => false,
() => { flights += 1; },
() => {
flights += 1;
},
);
assert.equal(result, false);
assert.equal(flights, 0);
});
test('malformed requests are inert', () => {
assert.equal(routeWorldFocusRequest(null, () => 'x', () => 'y'), false);
assert.equal(routeWorldFocusRequest({ detail: { kind: 'vessel' } }, () => 'x', () => 'y'), false);
assert.equal(
routeWorldFocusRequest(
null,
() => 'x',
() => 'y',
),
false,
);
assert.equal(
routeWorldFocusRequest(
{ detail: { kind: 'vessel' } },
() => 'x',
() => 'y',
),
false,
);
});
// A request that cannot fly must never cost the user their tracking: the UI
// policy releases the follow camera, so validation happens BEFORE it runs.
test('an unflyable request never reaches the release policy', () => {
const unflyable = [
{ kind: 'plane', position: POSITION }, // no framing for this kind
{ kind: 'vessel', position: { x: NaN, y: 0, z: 0 } }, // malformed position
{ kind: 'vessel', position: { x: 0, y: 0 } }, // truthy but incomplete
{ kind: 'plane', position: POSITION }, // no framing for this kind
{ kind: 'vessel', position: { x: NaN, y: 0, z: 0 } }, // malformed position
{ kind: 'vessel', position: { x: 0, y: 0 } }, // truthy but incomplete
{ kind: 'vessel', id: '123', position: { x: 0, y: 0, z: 0 } }, // ECEF origin
{ kind: 'fire', id: 'fire-1', position: { x: 1, y: 1, z: 1 } }, // finite but inside Earth
{ kind: 'vessel', position: 'somewhere' },
{ kind: 'vessel', position: POSITION }, // missing owner id
{ kind: 'vessel', position: POSITION }, // missing owner id
{ position: POSITION },
];
for (const detail of unflyable) {
assert.equal(isValidWorldFocusTarget(detail), false, JSON.stringify(detail));
assert.equal(
isValidWorldFocusTarget(detail),
false,
JSON.stringify(detail),
);
let policyRuns = 0;
const result = routeWorldFocusRequest(
{ detail },
() => { policyRuns += 1; return 'released'; },
() => {
policyRuns += 1;
return 'released';
},
() => 'flew',
);
assert.equal(result, false, JSON.stringify(detail));
assert.equal(policyRuns, 0, `the policy must not run for ${JSON.stringify(detail)}`);
assert.equal(
policyRuns,
0,
`the policy must not run for ${JSON.stringify(detail)}`,
);
// Nor may such a request be dispatched in the first place.
assert.equal(requestWorldFocus(detail, new EventTarget()), false);
}
assert.equal(isValidWorldFocusTarget({ kind: 'vessel', id: '123', position: POSITION }), true);
assert.equal(isValidWorldFocusTarget({ kind: 'fire', id: 'fire-1', position: POSITION }), true);
assert.equal(
isValidWorldFocusTarget({ kind: 'vessel', id: '123', position: POSITION }),
true,
);
assert.equal(
isValidWorldFocusTarget({ kind: 'fire', id: 'fire-1', position: POSITION }),
true,
);
});
test('the transfer flight supersedes any flight in progress and keeps the operator heading', () => {
const camera = stubCamera();
assert.equal(flyToWorldTarget({ camera }, { kind: 'vessel', id: '123', position: POSITION }), true);
assert.equal(camera.calls.cancelFlight, 1, 'a prior flight must be cancelled, not queued');
assert.equal(
flyToWorldTarget(bindFakeCesiumCamera({ camera }), {
kind: 'vessel',
id: '123',
position: POSITION,
}),
true,
);
assert.equal(
camera.calls.cancelFlight,
1,
'a prior flight must be cancelled, not queued',
);
assert.equal(camera.calls.flights.length, 1);
const { sphere, options } = camera.calls.flights[0];
assert.equal(sphere.radius, WORLD_FOCUS_FRAMING.vessel.radiusM);
@@ -131,12 +190,20 @@ test('the transfer flight supersedes any flight in progress and keeps the operat
test('fires frame wider than vessels — a fire is read by its surroundings', () => {
const camera = stubCamera();
flyToWorldTarget({ camera }, { kind: 'fire', id: 'fire-1', position: POSITION });
flyToWorldTarget(bindFakeCesiumCamera({ camera }), {
kind: 'fire',
id: 'fire-1',
position: POSITION,
});
const { sphere, options } = camera.calls.flights[0];
assert.equal(sphere.radius, WORLD_FOCUS_FRAMING.fire.radiusM);
assert.equal(options.offset.range, WORLD_FOCUS_FRAMING.fire.rangeM);
assert.ok(WORLD_FOCUS_FRAMING.fire.rangeM > WORLD_FOCUS_FRAMING.vessel.rangeM);
assert.ok(WORLD_FOCUS_FRAMING.fire.radiusM > WORLD_FOCUS_FRAMING.vessel.radiusM);
assert.ok(
WORLD_FOCUS_FRAMING.fire.rangeM > WORLD_FOCUS_FRAMING.vessel.rangeM,
);
assert.ok(
WORLD_FOCUS_FRAMING.fire.radiusM > WORLD_FOCUS_FRAMING.vessel.radiusM,
);
});
test('every framing is a real oblique standoff, not a nadir or an inside-out sphere', () => {
@@ -145,25 +212,52 @@ test('every framing is a real oblique standoff, not a nadir or an inside-out sph
for (const kind of kinds) {
const framing = WORLD_FOCUS_FRAMING[kind];
// Looking DOWN at the target, but obliquely — a nadir drop reads as a map.
assert.ok(framing.pitchDeg < 0, `${kind}: pitch must look down, got ${framing.pitchDeg}`);
assert.ok(framing.pitchDeg > -80, `${kind}: pitch must stay oblique, got ${framing.pitchDeg}`);
assert.ok(
framing.pitchDeg < 0,
`${kind}: pitch must look down, got ${framing.pitchDeg}`,
);
assert.ok(
framing.pitchDeg > -80,
`${kind}: pitch must stay oblique, got ${framing.pitchDeg}`,
);
// Standing off outside the framed sphere, at a human-readable distance.
assert.ok(framing.radiusM > 0, `${kind}: radius must be positive`);
assert.ok(
framing.rangeM > framing.radiusM,
`${kind}: range ${framing.rangeM} must stand off outside radius ${framing.radiusM}`,
);
assert.ok(framing.rangeM <= 10000, `${kind}: range must stay close-in, got ${framing.rangeM}`);
assert.ok(
framing.rangeM <= 10000,
`${kind}: range must stay close-in, got ${framing.rangeM}`,
);
}
// Exact shipped values — a silent retune must show up as a failing test.
assert.deepEqual({ ...WORLD_FOCUS_FRAMING.vessel }, { radiusM: 150, rangeM: 1200, pitchDeg: -30 });
assert.deepEqual({ ...WORLD_FOCUS_FRAMING.fire }, { radiusM: 400, rangeM: 3000, pitchDeg: -35 });
assert.deepEqual(
{ ...WORLD_FOCUS_FRAMING.vessel },
{ radiusM: 150, rangeM: 1200, pitchDeg: -30 },
);
assert.deepEqual(
{ ...WORLD_FOCUS_FRAMING.fire },
{ radiusM: 400, rangeM: 3000, pitchDeg: -35 },
);
});
test('unknown kinds and missing viewers issue no flight', () => {
const camera = stubCamera();
assert.equal(flyToWorldTarget({ camera }, { kind: 'plane', position: POSITION }), false);
assert.equal(flyToWorldTarget({ camera }, { kind: 'vessel' }), false);
assert.equal(flyToWorldTarget(null, { kind: 'vessel', position: POSITION }), false);
assert.equal(
flyToWorldTarget(bindFakeCesiumCamera({ camera }), {
kind: 'plane',
position: POSITION,
}),
false,
);
assert.equal(
flyToWorldTarget(bindFakeCesiumCamera({ camera }), { kind: 'vessel' }),
false,
);
assert.equal(
flyToWorldTarget(null, { kind: 'vessel', position: POSITION }),
false,
);
assert.equal(camera.calls.flights.length, 0);
});