mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 04:14:02 +00:00
Read what the camera follows through one Core module
src/data/followed.js answers what the camera follows: the camera port's target, or the entity a layer still tracks itself, seen through the same fields. Ownership checks, render holds, guards, voice actions, the CCTV pacing and the navigation release paths read it instead of Cesium's trackedEntity, so they keep working when layers follow through the port. The release paths stop following, cancel the flight and release the camera through the port. The rotate refusal names the followed target by its name or label title.
This commit is contained in:
1 parent
e40a4c78d5
commit
2adec097de
29 files changed
+404
-97
No files matched your search
@@ -992,6 +992,7 @@
|
||||
"src/styles/surveillance.js",
|
||||
"src/styles/thermal.js",
|
||||
"src/ui/applicationShell.js",
|
||||
"src/data/followed.js",
|
||||
"src/render/cesium/camera.js",
|
||||
"src/render/cesium/follow.js",
|
||||
"src/render/rendererOf.js",
|
||||
@@ -1256,7 +1257,9 @@
|
||||
"src/data/bhoteKoshiFloodPath.js",
|
||||
"src/maps/catalog.js",
|
||||
"src/scenes/director.js",
|
||||
"src/data/followed.js",
|
||||
"src/render/cesium/camera.js",
|
||||
"src/data/entityRecords.js",
|
||||
"src/render/cesium/follow.js",
|
||||
"src/geo/index.js",
|
||||
"src/render/rendererOf.js",
|
||||
@@ -1572,6 +1575,7 @@
|
||||
"src/data/aircraftRecession.js",
|
||||
"src/data/motionModel.js",
|
||||
"src/layers/vessels/selection.js",
|
||||
"src/data/followed.js",
|
||||
"src/layers/vessels/state.js",
|
||||
"src/layers/vessels/testing.js",
|
||||
"src/layers/vessels/tracking.js",
|
||||
@@ -1600,6 +1604,7 @@
|
||||
"src/input/sensorPick.js",
|
||||
"src/input/dispatcher.js",
|
||||
"src/render/cesium/cameraInput.js",
|
||||
"src/render/rendererOf.js",
|
||||
"src/render/cesium/input.js",
|
||||
"src/render/cesium/ctrlWheelZoom.js",
|
||||
"src/render/input/router.js"
|
||||
@@ -1753,6 +1758,7 @@
|
||||
"src/layers/launches/controls.js",
|
||||
"src/layers/launches/index.js",
|
||||
"src/render/cesium/camera.js",
|
||||
"src/data/entityRecords.js",
|
||||
"src/render/cesium/follow.js",
|
||||
"src/layers/launches/ingestion.js",
|
||||
"src/layers/launches/launchPad.js",
|
||||
@@ -1776,6 +1782,7 @@
|
||||
"src/render/cesium/common.js",
|
||||
"src/layers/launches/rendering.js",
|
||||
"src/layers/launches/replay.js",
|
||||
"src/data/followed.js",
|
||||
"src/layers/launches/selection.js",
|
||||
"src/layers/launches/source.js",
|
||||
"src/layers/launches/state.js",
|
||||
@@ -1917,6 +1924,7 @@
|
||||
"src/layers/awareness/rendering.js",
|
||||
"src/layers/awareness/state.js",
|
||||
"src/layers/awareness/subject.js",
|
||||
"src/data/followed.js",
|
||||
"src/layers/awareness/testing.js",
|
||||
"src/ui/panelRailGeometry.js",
|
||||
"src/i18n/index.js",
|
||||
@@ -2059,6 +2067,7 @@
|
||||
"src/layers/cctv/geometry.js",
|
||||
"src/data/entityRecords.js",
|
||||
"src/layers/cctv/geometryQueue.js",
|
||||
"src/data/followed.js",
|
||||
"src/layers/cctv/ground.js",
|
||||
"src/layers/cctv/headingConfidence.js",
|
||||
"src/geo/index.js",
|
||||
@@ -2225,6 +2234,7 @@
|
||||
"modules": [
|
||||
"src/data/headingDegrees.js",
|
||||
"src/layers/alpr/index.js",
|
||||
"src/data/followed.js",
|
||||
"src/geo/index.js",
|
||||
"src/layers/alpr/model.js",
|
||||
"src/layers/alpr/policy.js",
|
||||
@@ -2300,7 +2310,9 @@
|
||||
"src/layers/radio/lifecycle.js",
|
||||
"src/layers/radio/model.js",
|
||||
"src/layers/radio/navigation.js",
|
||||
"src/data/followed.js",
|
||||
"src/render/cesium/camera.js",
|
||||
"src/data/entityRecords.js",
|
||||
"src/render/cesium/follow.js",
|
||||
"src/render/rendererOf.js",
|
||||
"src/render/camera/events.js",
|
||||
@@ -2647,6 +2659,7 @@
|
||||
"src/app/stateChannel.js",
|
||||
"src/app/surfaceServices.js",
|
||||
"src/app/tools.js",
|
||||
"src/data/followed.js",
|
||||
"src/app/viewer.js",
|
||||
"src/render/cesium/ctrlWheelZoom.js",
|
||||
"src/sky/starrySky.js",
|
||||
@@ -3726,6 +3739,7 @@
|
||||
"src/keylessGeocoder.js",
|
||||
"src/layers/aircraft/classification.js",
|
||||
"src/layers/alpr/index.js",
|
||||
"src/data/followed.js",
|
||||
"src/layers/alpr/model.js",
|
||||
"src/layers/alpr/policy.js",
|
||||
"src/layers/alpr/presentation.js",
|
||||
@@ -4255,6 +4269,7 @@
|
||||
"src/layers/firms/anchors.js",
|
||||
"src/locations.js",
|
||||
"src/render/cesium/camera.js",
|
||||
"src/data/entityRecords.js",
|
||||
"src/render/cesium/follow.js",
|
||||
"src/render/rendererOf.js",
|
||||
"src/render/camera/events.js",
|
||||
@@ -4267,6 +4282,7 @@
|
||||
"src/render/cesium/common.js",
|
||||
"src/cityPois.js",
|
||||
"src/cameraGroundGuard.js",
|
||||
"src/data/followed.js",
|
||||
"src/search/alpsGeocoder.js",
|
||||
"src/search/defaults.js",
|
||||
"src/search/geospatial.js",
|
||||
@@ -4350,7 +4366,10 @@
|
||||
"exports": ["./ui/navigation"],
|
||||
"modules": [
|
||||
"src/navigationPolicy.js",
|
||||
"src/geo/index.js",
|
||||
"src/ui/navigationController.js",
|
||||
"src/render/cesium/camera.js",
|
||||
"src/data/followed.js",
|
||||
"src/app/stateChannel.js",
|
||||
"src/data/cameraArrival.js",
|
||||
"src/i18n/locale.js",
|
||||
@@ -4365,7 +4384,18 @@
|
||||
"src/i18n/locales/uk.json",
|
||||
"src/i18n/locales/zh-CN.json",
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/i18n/index.js"
|
||||
"src/i18n/index.js",
|
||||
"src/render/rendererOf.js",
|
||||
"src/render/camera/events.js",
|
||||
"src/geo/celestial.js",
|
||||
"src/geo/quaternion.js",
|
||||
"src/geo/frames.js",
|
||||
"src/geo/ellipsoid.js",
|
||||
"src/geo/vector.js",
|
||||
"src/render/cesium/follow.js",
|
||||
"src/render/cesium/internals.js",
|
||||
"src/render/cesium/common.js",
|
||||
"src/data/entityRecords.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -5169,6 +5199,7 @@
|
||||
"src/i18n/locales/zh-Hant.json",
|
||||
"src/data/geoImportImages.js",
|
||||
"src/data/geoImportPins.js",
|
||||
"src/data/followed.js",
|
||||
"src/layers/streetLevel/tileMath.js",
|
||||
"src/layers/streetLevel/view.js",
|
||||
"src/data/inputOwnership.js",
|
||||
@@ -5177,6 +5208,8 @@
|
||||
"src/data/pickRegistry.js",
|
||||
"src/input/dispatcher.js",
|
||||
"src/render/cesium/cameraInput.js",
|
||||
"src/render/rendererOf.js",
|
||||
"src/data/entityRecords.js",
|
||||
"src/render/cesium/input.js",
|
||||
"src/render/cesium/ctrlWheelZoom.js",
|
||||
"src/render/input/router.js",
|
||||
|
||||
+5
-6
@@ -1,3 +1,4 @@
|
||||
import { onFollowChanged } from '../data/followed.js';
|
||||
import { SceneDirector } from '../scenes/director.js';
|
||||
import { initAnnotations } from '../annotations/index.js';
|
||||
import { initDrawTool } from '../annotations/drawTool.js';
|
||||
@@ -146,12 +147,10 @@ export function createApplicationTools({
|
||||
|
||||
// The follow camera recomputes the tracked target's dead-reckon position
|
||||
// every frame — tracking anything is a per-frame animation. (perf wave 2)
|
||||
const removeTrackingListener = viewer.trackedEntityChanged.addEventListener(
|
||||
() => {
|
||||
if (viewer.trackedEntity) holdContinuousRender('tracked-entity');
|
||||
else releaseContinuousRender('tracked-entity');
|
||||
},
|
||||
);
|
||||
const removeTrackingListener = onFollowChanged(viewer, (followed) => {
|
||||
if (followed) holdContinuousRender('tracked-entity');
|
||||
else releaseContinuousRender('tracked-entity');
|
||||
});
|
||||
|
||||
// Hidden-state suspension (perf wave 2): when the window/tab is hidden,
|
||||
// stop the default render loop outright — a hidden canvas repaints for
|
||||
|
||||
@@ -289,9 +289,12 @@ test('replaced viewer releases the old tracking listener without duplicate subsc
|
||||
true,
|
||||
);
|
||||
const id = activeCameraMotionId();
|
||||
first.viewer.trackedEntityChanged.raiseEvent({ id: 'stale-aircraft' });
|
||||
// As Cesium's setter does: the value changes, then the event fires.
|
||||
first.viewer.trackedEntity = { id: 'stale-aircraft' };
|
||||
first.viewer.trackedEntityChanged.raiseEvent(first.viewer.trackedEntity);
|
||||
assert.equal(activeCameraMotionId(), id);
|
||||
next.viewer.trackedEntityChanged.raiseEvent({ id: 'new-aircraft' });
|
||||
next.viewer.trackedEntity = { id: 'new-aircraft' };
|
||||
next.viewer.trackedEntityChanged.raiseEvent(next.viewer.trackedEntity);
|
||||
assert.equal(getActiveCameraMotion(), null);
|
||||
assert.equal(hasRenderHold(), false);
|
||||
});
|
||||
@@ -598,7 +601,7 @@ for (const mode of ['once', 'continuous']) {
|
||||
|
||||
test('direct globe rotation refuses a tracked camera without disturbing its reference frame', (t) => {
|
||||
const { viewer, camera, target } = harness(t);
|
||||
viewer.trackedEntity = { id: 'tracked-aircraft' };
|
||||
viewer.trackedEntity = { id: 'tracked-aircraft', name: 'tracked-aircraft' };
|
||||
camera.lookAt(target, new Cesium.HeadingPitchRange(0, radians(-40), 1_000));
|
||||
const before = pose(camera);
|
||||
const transform = Cesium.Matrix4.clone(camera.transform);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* lifts the eye if it sits below a usable clearance. Heading and pitch are
|
||||
* held, so the correction reads as the shot settling.
|
||||
*/
|
||||
import { onFollowChanged } from './data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { ownCameraArrival } from './data/cameraArrival.js';
|
||||
import { heightsFor } from './queries/heights.js';
|
||||
@@ -164,8 +165,7 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
|
||||
release();
|
||||
};
|
||||
release = ownCameraArrival(viewer, stop);
|
||||
const removeTracking = viewer.trackedEntityChanged?.addEventListener(stop);
|
||||
if (removeTracking) removers.push(removeTracking);
|
||||
removers.push(onFollowChanged(viewer, stop));
|
||||
documentRef?.addEventListener?.('keydown', stop, true);
|
||||
// A manual gesture ends the guard, as it interrupts camera motion elsewhere.
|
||||
for (const type of ['pointerdown', 'wheel'])
|
||||
|
||||
@@ -205,7 +205,9 @@ for (const handoff of [
|
||||
},
|
||||
});
|
||||
} else if (handoff === 'tracking') {
|
||||
viewer.trackedEntityChanged.raiseEvent({ id: 'contact' });
|
||||
// As Cesium's setter does: the value changes, then the event fires.
|
||||
viewer.trackedEntity = { id: 'contact' };
|
||||
viewer.trackedEntityChanged.raiseEvent(viewer.trackedEntity);
|
||||
} else if (handoff === 'follow' || handoff === 'UI') {
|
||||
navigation._stampNavigation({ cancelPendingSelection: false });
|
||||
} else if (handoff === 'keyboard') {
|
||||
|
||||
@@ -97,7 +97,7 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () =>
|
||||
// The transaction has exactly two camera-owner mutations, and each one
|
||||
// stamps:
|
||||
// 1. selectedLayer.trackById(id) -> viewer.trackedEntity
|
||||
// -> viewer.trackedEntityChanged -> _stampNavigation()
|
||||
// -> onFollowChanged(viewer) -> _stampNavigation()
|
||||
// 2. cockpitView.enter() -> onCameraTakeover() -> _stampNavigation()
|
||||
const transaction = body(
|
||||
cockpitTracking,
|
||||
@@ -117,7 +117,7 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () =>
|
||||
// Seam 1: any tracker handoff stamps, so the adoption step is covered.
|
||||
assert.match(
|
||||
ui,
|
||||
/viewer\.trackedEntityChanged\.addEventListener\(\s*\(entity\) => \{\s*if \(entity && !this\._disposed\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/,
|
||||
/onFollowChanged\(\s*this\.viewer,\s*\(followed\) => \{\s*if \(followed && !this\._disposed\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/,
|
||||
'tracker handoff must stamp',
|
||||
);
|
||||
// Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above.
|
||||
@@ -190,10 +190,10 @@ test('accepted navigation releases through PR15-aware ownership before flight',
|
||||
'origin: trackingOrigin',
|
||||
'satellitesLayer.stopTracking?.({ origin: trackingOrigin })',
|
||||
'rocketLaunchesLayer.releaseCameraOwnership?.()',
|
||||
'this.viewer.trackedEntity = undefined;',
|
||||
'stopFollowing(this.viewer);',
|
||||
"interruptCameraMotion('explicit-navigation')",
|
||||
'this.viewer.camera.cancelFlight();',
|
||||
'this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);',
|
||||
'camera?.cancelFlight();',
|
||||
'camera?.release();',
|
||||
], 'follow release');
|
||||
assert.match(release, /flightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/);
|
||||
assert.match(release, /militaryFlightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/);
|
||||
|
||||
+9
-9
@@ -18,6 +18,7 @@
|
||||
* globe motion, opposite to the camera's travel around Earth.
|
||||
*/
|
||||
|
||||
import { followedOf, isFollowing, onFollowChanged } from './data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { GLOBE_VIEW } from './locations.js';
|
||||
import {
|
||||
@@ -1041,7 +1042,7 @@ export function interruptCameraMotion(reason = 'interrupt') {
|
||||
// While an entity is TRACKED, the follow camera owns the reference frame —
|
||||
// resetting the lookAt transform here flings the camera into space (field
|
||||
// finding). Leave the transform to the tracker; it re-asserts every frame.
|
||||
const tracking = Boolean(_viewer?.trackedEntity);
|
||||
const tracking = isFollowing(_viewer);
|
||||
const wasRoute = wasActive && _active.kind === 'route';
|
||||
if (
|
||||
wasActive &&
|
||||
@@ -1190,7 +1191,7 @@ function onTick() {
|
||||
// only describes Cesium's rotation while the frame is identity.
|
||||
if (
|
||||
_viewer.scene.mode !== Cesium.SceneMode.SCENE3D ||
|
||||
_viewer.trackedEntity ||
|
||||
isFollowing(_viewer) ||
|
||||
_isCockpitActive?.() ||
|
||||
!Cesium.Matrix4.equals(cam.transform, Cesium.Matrix4.IDENTITY)
|
||||
) {
|
||||
@@ -1413,8 +1414,8 @@ export function initCameraVerbs(
|
||||
if (_trackingRemover) _trackingRemover();
|
||||
// Cockpit can acquire then clear tracking before the next clock tick.
|
||||
// Yield on acquisition itself without changing legacy camera motions.
|
||||
_trackingRemover = viewer.trackedEntityChanged?.addEventListener((entity) => {
|
||||
if (entity && _active?.kind === 'rotate_globe')
|
||||
_trackingRemover = onFollowChanged(viewer, (followed) => {
|
||||
if (followed && _active?.kind === 'rotate_globe')
|
||||
interruptCameraMotion('globe-camera-owner-changed');
|
||||
});
|
||||
for (const rm of _inputRemovers) rm();
|
||||
@@ -1522,15 +1523,14 @@ export function moveCamera(args = {}, runNavigation = null) {
|
||||
if (
|
||||
(motion === 'rotate_globe' ||
|
||||
(motion === 'orbit' && typeof runNavigation !== 'function')) &&
|
||||
_viewer?.trackedEntity
|
||||
isFollowing(_viewer)
|
||||
) {
|
||||
// The follow camera owns a tracked view; a lookAt orbit fights it frame
|
||||
// by frame and stop rips the camera out (field finding). Honest refusal
|
||||
// until tracked-orbit rides the follow camera natively (roadmap).
|
||||
const followed = followedOf(_viewer);
|
||||
const label =
|
||||
_viewer.trackedEntity?.name ||
|
||||
_viewer.trackedEntity?.id ||
|
||||
'the tracked target';
|
||||
followed?.name || followed?.labelModel?.title || 'the tracked target';
|
||||
return {
|
||||
ok: false,
|
||||
action: 'move_camera',
|
||||
@@ -1701,7 +1701,7 @@ export function moveCamera(args = {}, runNavigation = null) {
|
||||
const preserveCameraFlight =
|
||||
motion === 'orbit' &&
|
||||
_viewer.scene.tweens.length > 0 &&
|
||||
!_viewer.trackedEntity;
|
||||
!isFollowing(_viewer);
|
||||
return typeof runNavigation === 'function'
|
||||
? runNavigation(start, { preserveCameraFlight })
|
||||
: start();
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
/**
|
||||
* Activates the nearest CCTV record on layer enable and flies only when no
|
||||
* aircraft tracker or cockpit camera owns the view.
|
||||
* followed target or cockpit camera owns the view.
|
||||
* @param {Object} options Layer-enable focus inputs.
|
||||
* @param {*} options.trackedEntity Current Cesium tracked entity, if any.
|
||||
* @param {*} options.followed What the camera follows, if anything
|
||||
* (`followedOf`).
|
||||
* @param {boolean} options.cockpitActive Whether cockpit mode owns the camera.
|
||||
* @param {() => (string|null)} options.activate Nearest-camera activation.
|
||||
* @param {(cameraId: string) => *} options.fly Optional camera flight.
|
||||
* @returns {*} Activated camera ID for a held view, or the flight result.
|
||||
*/
|
||||
export function runCctvLayerEnableFocus({
|
||||
trackedEntity = null,
|
||||
followed = null,
|
||||
cockpitActive = false,
|
||||
activate,
|
||||
fly,
|
||||
} = {}) {
|
||||
const cameraId = activate?.();
|
||||
if (!cameraId) return false;
|
||||
if (trackedEntity || cockpitActive) return cameraId;
|
||||
if (followed || cockpitActive) return cameraId;
|
||||
return fly?.(cameraId);
|
||||
}
|
||||
|
||||
@@ -25,11 +26,11 @@ export function runCctvLayerEnableFocus({
|
||||
* chain. Either observation suppresses the optional CCTV focus flight.
|
||||
* @param {Object} [before={}] Ownership immediately before the await.
|
||||
* @param {Object} [after={}] Ownership immediately after the await.
|
||||
* @returns {{ trackedEntity: *, cockpitActive: boolean }} Conservative ownership.
|
||||
* @returns {{ followed: *, cockpitActive: boolean }} Conservative ownership.
|
||||
*/
|
||||
function mergeCctvEnableOwnership(before = {}, after = {}) {
|
||||
return {
|
||||
trackedEntity: before.trackedEntity || after.trackedEntity || null,
|
||||
followed: before.followed || after.followed || null,
|
||||
cockpitActive: !!(before.cockpitActive || after.cockpitActive),
|
||||
};
|
||||
}
|
||||
@@ -60,7 +61,7 @@ export async function runCctvLayerEnableTransition({
|
||||
const ownershipBefore = readOwnership?.() || {};
|
||||
if (target === true) {
|
||||
debug?.('[UI:CCTV] ownership before setEnabled await', {
|
||||
trackedId: ownershipBefore.trackedEntity?.id ?? null,
|
||||
trackedId: ownershipBefore.followed?.key ?? null,
|
||||
cockpitActive: !!ownershipBefore.cockpitActive,
|
||||
});
|
||||
}
|
||||
@@ -68,7 +69,7 @@ export async function runCctvLayerEnableTransition({
|
||||
const ownershipAfter = readOwnership?.() || {};
|
||||
if (target === true) {
|
||||
debug?.('[UI:CCTV] ownership after setEnabled await', {
|
||||
trackedId: ownershipAfter.trackedEntity?.id ?? null,
|
||||
trackedId: ownershipAfter.followed?.key ?? null,
|
||||
cockpitActive: !!ownershipAfter.cockpitActive,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
|
||||
test('CCTV layer enable activates nearest without stealing a tracked or cockpit view', () => {
|
||||
for (const ownership of [
|
||||
{ trackedEntity: { id: 'tracked-plane' }, cockpitActive: false },
|
||||
{ trackedEntity: null, cockpitActive: true },
|
||||
{ followed: { key: 'tracked-plane' }, cockpitActive: false },
|
||||
{ followed: null, cockpitActive: true },
|
||||
]) {
|
||||
const calls = [];
|
||||
const result = runCctvLayerEnableFocus({
|
||||
@@ -48,15 +48,15 @@ test('CCTV layer enable activates nearest without stealing a tracked or cockpit
|
||||
});
|
||||
|
||||
test('CCTV enable retains a pre-await tracking snapshot when tracking clears during enable', async () => {
|
||||
let trackedEntity = { id: 'tracked-plane' };
|
||||
let followed = { key: 'tracked-plane' };
|
||||
let flyCalls = 0;
|
||||
const diagnostics = [];
|
||||
const result = await runCctvLayerEnableTransition({
|
||||
target: true,
|
||||
readOwnership: () => ({ trackedEntity, cockpitActive: false }),
|
||||
readOwnership: () => ({ followed, cockpitActive: false }),
|
||||
setEnabled: async () => {
|
||||
await Promise.resolve();
|
||||
trackedEntity = null;
|
||||
followed = null;
|
||||
},
|
||||
shouldFocus: () => true,
|
||||
activate: () => 'cam-near',
|
||||
@@ -83,7 +83,7 @@ test('CCTV disable transition does not emit enable-ownership diagnostics', async
|
||||
|
||||
const result = await runCctvLayerEnableTransition({
|
||||
target: false,
|
||||
readOwnership: () => ({ trackedEntity: { id: 'tracked-plane' }, cockpitActive: false }),
|
||||
readOwnership: () => ({ followed: { key: 'tracked-plane' }, cockpitActive: false }),
|
||||
setEnabled: async (target) => transitions.push(target),
|
||||
shouldFocus: () => true,
|
||||
activate: () => 'cam-near',
|
||||
|
||||
@@ -378,7 +378,7 @@ test('camera icons are violet for video feeds, cyan for stills, amber when activ
|
||||
test('geometry drain pacing yields to tracked and cockpit camera ownership', () => {
|
||||
assert.deepEqual(cctvGeometryDrainPacing(), { batchSize: 4, delayMs: 120 });
|
||||
assert.deepEqual(
|
||||
cctvGeometryDrainPacing({ trackedEntity: { id: 'flight-1' } }),
|
||||
cctvGeometryDrainPacing({ followed: { key: 'flights:1' } }),
|
||||
{ batchSize: 2, delayMs: 250 },
|
||||
);
|
||||
assert.deepEqual(cctvGeometryDrainPacing({ cockpitActive: true }), {
|
||||
@@ -393,13 +393,13 @@ test('geometry drain pacing yields to tracked and cockpit camera ownership', ()
|
||||
});
|
||||
|
||||
test('geometry drain rechecks pacing when tracking releases between batches', () => {
|
||||
let trackedEntity = { id: 'flight-1' };
|
||||
let followed = { key: 'flights:1' };
|
||||
const queue = Array.from({ length: 10 }, (_, index) => index + 1);
|
||||
const visited = [];
|
||||
const runBatch = () =>
|
||||
processCctvGeometryDrainBatch({
|
||||
queue,
|
||||
readOwnership: () => ({ trackedEntity, cockpitActive: false }),
|
||||
readOwnership: () => ({ followed, cockpitActive: false }),
|
||||
visit: (record) => visited.push(record),
|
||||
progress: () => {},
|
||||
complete: () => {},
|
||||
@@ -409,7 +409,7 @@ test('geometry drain rechecks pacing when tracking releases between batches', ()
|
||||
assert.deepEqual(trackedBatch, { hasMore: true, batchSize: 2, delayMs: 250 });
|
||||
assert.deepEqual(visited, [1, 2]);
|
||||
|
||||
trackedEntity = null;
|
||||
followed = null;
|
||||
const untrackedBatch = runBatch();
|
||||
assert.deepEqual(untrackedBatch, {
|
||||
hasMore: true,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @module data/followed
|
||||
* @description What the camera follows, for the Core code that reads it.
|
||||
*
|
||||
* A layer follows through the camera port with a FollowTarget that also
|
||||
* carries what Core shows about it (key, display position, sprite, label).
|
||||
* Until every layer does, a layer may still track an entity of its own
|
||||
* through Cesium's `trackedEntity`; that entity is seen here through the
|
||||
* same fields, read from its entity record. Readers ask this module, never
|
||||
* `trackedEntity`, so they work either way.
|
||||
*/
|
||||
import { findCamera } from '../render/rendererOf.js';
|
||||
import { readEntityRecord } from './entityRecords.js';
|
||||
|
||||
/**
|
||||
* @typedef {object} Followed
|
||||
* @property {string|null} key Stable id, e.g. `'flights:a1b2c3'`.
|
||||
* @property {string} [name] Display name, when the target has one.
|
||||
* @property {() => object|null} [displayPosition] Where the follow camera
|
||||
* frames the target this frame (the layer's display cache).
|
||||
* @property {() => object|null} [visualPosition] Where the target is drawn.
|
||||
* @property {object} [sprite] Size of the drawn billboard.
|
||||
* @property {object} [labelModel]
|
||||
* @property {string} [selectionOrigin]
|
||||
* @property {object} [entity] The tracked entity, for a layer that still
|
||||
* tracks one itself.
|
||||
*/
|
||||
|
||||
/** @type {WeakMap<object, Followed>} */
|
||||
const entityViews = new WeakMap();
|
||||
|
||||
function entityView(entity) {
|
||||
let view = entityViews.get(entity);
|
||||
if (!view) {
|
||||
const id = String(entity.id ?? '').trim();
|
||||
view = {
|
||||
entity,
|
||||
get key() {
|
||||
const explicit = String(readEntityRecord(entity)?.trackedId ?? '');
|
||||
return explicit.trim() || (id ? `entity:${id}` : null);
|
||||
},
|
||||
get name() {
|
||||
return entity.name;
|
||||
},
|
||||
get displayPosition() {
|
||||
return readEntityRecord(entity)?.displayPosition;
|
||||
},
|
||||
get visualPosition() {
|
||||
return readEntityRecord(entity)?.visualPosition;
|
||||
},
|
||||
get sprite() {
|
||||
return readEntityRecord(entity)?.sprite;
|
||||
},
|
||||
get labelModel() {
|
||||
return readEntityRecord(entity)?.labelModel;
|
||||
},
|
||||
get selectionOrigin() {
|
||||
return readEntityRecord(entity)?.selectionOrigin;
|
||||
},
|
||||
};
|
||||
entityViews.set(entity, view);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* What `viewer`'s camera follows, or null. The same object comes back for
|
||||
* as long as the same thing is followed.
|
||||
* @param {object|null|undefined} viewer
|
||||
* @returns {Followed|null}
|
||||
*/
|
||||
export function followedOf(viewer) {
|
||||
if (!viewer) return null;
|
||||
const target = findCamera(viewer)?.followed;
|
||||
if (target) return target;
|
||||
const entity = viewer.trackedEntity;
|
||||
return entity ? entityView(entity) : null;
|
||||
}
|
||||
|
||||
/** Whether `viewer`'s camera follows anything. */
|
||||
export function isFollowing(viewer) {
|
||||
return followedOf(viewer) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `cb` whenever what `viewer`'s camera follows changes.
|
||||
* @param {object} viewer
|
||||
* @param {(followed: Followed|null) => void} cb
|
||||
* @returns {() => void} Off.
|
||||
*/
|
||||
export function onFollowChanged(viewer, cb) {
|
||||
let last = followedOf(viewer);
|
||||
let port = null;
|
||||
let offPort = null;
|
||||
// The port is looked up again on every change: a renderer bound after
|
||||
// this subscribed replaces the one it listened to. A Cesium port's follow
|
||||
// also sets `trackedEntity`, so the change still arrives through it.
|
||||
const listenToPort = () => {
|
||||
const current = findCamera(viewer);
|
||||
if (current === port) return;
|
||||
offPort?.();
|
||||
port = current;
|
||||
offPort = port?.on('followChanged', check) ?? null;
|
||||
};
|
||||
function check() {
|
||||
listenToPort();
|
||||
const now = followedOf(viewer);
|
||||
if (now === last) return;
|
||||
last = now;
|
||||
cb(now);
|
||||
}
|
||||
listenToPort();
|
||||
const offTracking = viewer.trackedEntityChanged?.addEventListener(check);
|
||||
return () => {
|
||||
offPort?.();
|
||||
offPort = null;
|
||||
offTracking?.();
|
||||
};
|
||||
}
|
||||
|
||||
/** Stop the camera following anything, a layer's own tracking included. */
|
||||
export function stopFollowing(viewer) {
|
||||
if (!viewer) return;
|
||||
const camera = findCamera(viewer);
|
||||
if (camera?.followed) camera.follow(null);
|
||||
else if (viewer.trackedEntity) viewer.trackedEntity = undefined;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import * as Cesium from 'cesium';
|
||||
import { entityRecord } from './entityRecords.js';
|
||||
import {
|
||||
followedOf,
|
||||
isFollowing,
|
||||
onFollowChanged,
|
||||
stopFollowing,
|
||||
} from './followed.js';
|
||||
import { bindRenderer } from '../render/rendererOf.js';
|
||||
import { createCameraEvents } from '../render/camera/events.js';
|
||||
|
||||
/** A viewer whose `trackedEntity` behaves as Cesium's setter does. */
|
||||
function trackingViewer() {
|
||||
const trackedEntityChanged = new Cesium.Event();
|
||||
let tracked;
|
||||
return {
|
||||
trackedEntityChanged,
|
||||
get trackedEntity() {
|
||||
return tracked;
|
||||
},
|
||||
set trackedEntity(value) {
|
||||
if (tracked === value) return;
|
||||
tracked = value;
|
||||
trackedEntityChanged.raiseEvent(value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** A camera port that follows, for a viewer bound to a renderer. */
|
||||
function followingPort() {
|
||||
const events = createCameraEvents({ pose: () => ({}), report: () => {} });
|
||||
let followed = null;
|
||||
return {
|
||||
on: (event, cb) => events.on(event, cb),
|
||||
get followed() {
|
||||
return followed;
|
||||
},
|
||||
follow(target) {
|
||||
if (followed === target) return;
|
||||
followed = target;
|
||||
events.emit('followChanged');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('a layer’s own tracked entity is seen through its entity record', () => {
|
||||
const viewer = trackingViewer();
|
||||
assert.equal(followedOf(viewer), null);
|
||||
assert.equal(isFollowing(viewer), false);
|
||||
const entity = new Cesium.Entity({ id: 'e1', name: 'N1' });
|
||||
const record = entityRecord(entity);
|
||||
record.trackedId = 'flights:abc';
|
||||
record.displayPosition = () => ({ x: 1, y: 2, z: 3 });
|
||||
record.sprite = { width: 28 };
|
||||
viewer.trackedEntity = entity;
|
||||
const followed = followedOf(viewer);
|
||||
assert.equal(followed, followedOf(viewer), 'the same view each time');
|
||||
assert.equal(followed.key, 'flights:abc');
|
||||
assert.equal(followed.name, 'N1');
|
||||
assert.deepEqual(followed.displayPosition(), { x: 1, y: 2, z: 3 });
|
||||
assert.equal(followed.sprite.width, 28);
|
||||
assert.equal(followed.entity, entity);
|
||||
viewer.trackedEntity = new Cesium.Entity({ id: 'plain' });
|
||||
assert.equal(followedOf(viewer).key, 'entity:plain', 'no tracked id');
|
||||
});
|
||||
|
||||
test('a port follow comes before any tracked entity, and is the target itself', (t) => {
|
||||
const viewer = trackingViewer();
|
||||
const camera = followingPort();
|
||||
t.after(bindRenderer(viewer, { camera }));
|
||||
viewer.trackedEntity = new Cesium.Entity({ id: 'helper' });
|
||||
const target = { key: 'flights:abc', position: () => null };
|
||||
camera.follow(target);
|
||||
assert.equal(followedOf(viewer), target);
|
||||
});
|
||||
|
||||
test('onFollowChanged fires once per change, from either source', (t) => {
|
||||
const viewer = trackingViewer();
|
||||
const camera = followingPort();
|
||||
t.after(bindRenderer(viewer, { camera }));
|
||||
const seen = [];
|
||||
const off = onFollowChanged(viewer, (f) => seen.push(f?.key ?? null));
|
||||
const target = { key: 'flights:abc' };
|
||||
// A port follow on Cesium sets trackedEntity too: still one change.
|
||||
camera.follow(target);
|
||||
// Ending it, Cesium clears its tracking before the port announces.
|
||||
viewer.trackedEntity = undefined;
|
||||
camera.follow(null);
|
||||
const entity = new Cesium.Entity({ id: 'legacy' });
|
||||
viewer.trackedEntity = entity;
|
||||
viewer.trackedEntity = entity;
|
||||
off();
|
||||
viewer.trackedEntity = undefined;
|
||||
assert.deepEqual(seen, ['flights:abc', null, 'entity:legacy']);
|
||||
});
|
||||
|
||||
test('stopFollowing ends a port follow, or a layer’s own tracking', (t) => {
|
||||
const viewer = trackingViewer();
|
||||
viewer.trackedEntity = new Cesium.Entity({ id: 'legacy' });
|
||||
stopFollowing(viewer);
|
||||
assert.equal(viewer.trackedEntity, undefined);
|
||||
const camera = followingPort();
|
||||
t.after(bindRenderer(viewer, { camera }));
|
||||
camera.follow({ key: 'k' });
|
||||
stopFollowing(viewer);
|
||||
assert.equal(camera.followed, null);
|
||||
stopFollowing(null);
|
||||
});
|
||||
|
||||
test('a renderer bound after subscribing is still heard', (t) => {
|
||||
const viewer = { trackedEntityChanged: new Cesium.Event() };
|
||||
const seen = [];
|
||||
onFollowChanged(viewer, (f) => seen.push(f?.key ?? null));
|
||||
const camera = followingPort();
|
||||
t.after(bindRenderer(viewer, { camera }));
|
||||
// Any change re-resolves the port; a Cesium follow raises one.
|
||||
viewer.trackedEntityChanged.raiseEvent();
|
||||
camera.follow({ key: 'k' });
|
||||
camera.follow(null);
|
||||
assert.deepEqual(seen, ['k', null]);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followedOf, stopFollowing } from './followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
|
||||
const STYLE = [
|
||||
@@ -113,8 +114,9 @@ export function createImportedPins(dataSource, viewer) {
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
if (viewer.trackedEntity && pins.includes(viewer.trackedEntity))
|
||||
viewer.trackedEntity = undefined;
|
||||
const followedEntity = followedOf(viewer)?.entity;
|
||||
if (followedEntity && pins.includes(followedEntity))
|
||||
stopFollowing(viewer);
|
||||
if (viewer.selectedEntity && pins.includes(viewer.selectedEntity))
|
||||
viewer.selectedEntity = undefined;
|
||||
if (mounted && !viewer.scene.primitives?.isDestroyed?.())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFollowing } from '../../data/followed.js';
|
||||
import { isUnavailableCapability } from '../../sources/capability.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
@@ -369,13 +370,12 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
||||
{
|
||||
id: 'find-camera',
|
||||
label: t('SHOW NEAREST'),
|
||||
title: state.viewer?.trackedEntity
|
||||
title: isFollowing(state.viewer)
|
||||
? t(
|
||||
'Stop following the current object before navigating to a camera',
|
||||
)
|
||||
: t('Move to the nearest loaded camera and show its details'),
|
||||
disabled:
|
||||
!state.enabled || !count || Boolean(state.viewer?.trackedEntity),
|
||||
disabled: !state.enabled || !count || isFollowing(state.viewer),
|
||||
onClick: focusNearest,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFollowing } from '../../data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { inputFor } from '../../input/dispatcher.js';
|
||||
import {
|
||||
@@ -499,7 +500,7 @@ export function createAlprPresentation({ state, services, source }) {
|
||||
|
||||
function focusNearest() {
|
||||
const camera = state.viewer?.camera;
|
||||
if (!state.enabled || !camera?.positionWC || state.viewer.trackedEntity)
|
||||
if (!state.enabled || !camera?.positionWC || isFollowing(state.viewer))
|
||||
return false;
|
||||
let nearest = null,
|
||||
distance = Infinity;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followedOf } from '../../data/followed.js';
|
||||
import {
|
||||
AWARENESS_RADIUS_M,
|
||||
AWARENESS_RELATIONSHIP,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
SUBJECT_PRESENCE,
|
||||
AWARENESS_PAGE_SIZE,
|
||||
} from './policy.js';
|
||||
import { readEntityRecord } from '../../data/entityRecords.js';
|
||||
|
||||
// coverageLabel is machine-composed English from installations/controls.js
|
||||
// (pinned by src/data/militaryInstallations.test.mjs); translate the known
|
||||
@@ -192,9 +192,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
*/
|
||||
|
||||
function currentTrackedFlightSubject() {
|
||||
const trackedKey = normalizeContextId(
|
||||
readEntityRecord(layerState.viewer?.trackedEntity)?.trackedId,
|
||||
);
|
||||
const trackedKey = normalizeContextId(followedOf(layerState.viewer)?.key);
|
||||
if (!trackedKey) return null;
|
||||
const subjects = [
|
||||
flightsLayer.getTrackedSubject?.(),
|
||||
@@ -261,19 +259,15 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
||||
) {
|
||||
if (!subject?.position) return null;
|
||||
if (subject.layerId === 'flights' || subject.layerId === 'military') {
|
||||
const trackedPosition = readEntityRecord(
|
||||
layerState.viewer?.trackedEntity,
|
||||
)?.displayPosition?.();
|
||||
const followed = followedOf(layerState.viewer);
|
||||
const trackedPosition = followed?.displayPosition?.();
|
||||
if (trackedPosition) {
|
||||
return {
|
||||
position: Cesium.Cartesian3.clone(trackedPosition),
|
||||
// Only the follow camera's own contact proves presence this way; a
|
||||
// different tracked entity says nothing about this subject.
|
||||
presence:
|
||||
String(
|
||||
readEntityRecord(layerState.viewer?.trackedEntity)?.trackedId ||
|
||||
'',
|
||||
) === subjectKey(subject)
|
||||
String(followed?.key || '') === subjectKey(subject)
|
||||
? SUBJECT_PRESENCE.LIVE
|
||||
: SUBJECT_PRESENCE.UNCHECKED,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followedOf } from '../../data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
GEO_PROGRESS_NOTIFY_INTERVAL_MS,
|
||||
@@ -125,16 +126,16 @@ export function createGeometryQueue({
|
||||
* throughput without restarting the queue.
|
||||
*
|
||||
* @param {Object} [ownership={}] Current camera-ownership state.
|
||||
* @param {*} [ownership.trackedEntity] Cesium tracked entity, if any.
|
||||
* @param {*} [ownership.followed] What the camera follows, if anything.
|
||||
* @param {boolean} [ownership.cockpitActive] Whether cockpit owns the camera.
|
||||
* @returns {{ batchSize: number, delayMs: number }} Drain pacing.
|
||||
*/
|
||||
|
||||
function cctvGeometryDrainPacing({
|
||||
trackedEntity = null,
|
||||
followed = null,
|
||||
cockpitActive = false,
|
||||
} = {}) {
|
||||
if (trackedEntity || cockpitActive) {
|
||||
if (followed || cockpitActive) {
|
||||
return {
|
||||
batchSize: GEO_TRACKING_BATCH_SIZE,
|
||||
delayMs: GEO_TRACKING_BATCH_DELAY_MS,
|
||||
@@ -213,7 +214,7 @@ export function createGeometryQueue({
|
||||
const batchResult = processCctvGeometryDrainBatch({
|
||||
queue: layerState._geoQueue,
|
||||
readOwnership: () => ({
|
||||
trackedEntity: layerState._viewer.trackedEntity,
|
||||
followed: followedOf(layerState._viewer),
|
||||
cockpitActive:
|
||||
typeof document !== 'undefined' &&
|
||||
document.body?.classList.contains('cockpit-mode'),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFollowing } from '../../data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { CCTV_FOCUS_RESULT } from './policy.js';
|
||||
import { cameraOf } from '../../render/cesium/camera.js';
|
||||
@@ -52,7 +53,7 @@ export function createNavigation({
|
||||
console.debug('[Data:CCTV] focus ignored while cockpit owns the camera');
|
||||
return CCTV_FOCUS_RESULT.COCKPIT_ACTIVE;
|
||||
}
|
||||
if (viewer.trackedEntity) {
|
||||
if (isFollowing(viewer)) {
|
||||
console.debug(
|
||||
'[Data:CCTV] focus ignored while a tracked entity owns the camera',
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { stopFollowing } from '../../data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { cameraOf } from '../../render/cesium/camera.js';
|
||||
@@ -354,7 +355,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
||||
syncReplayButton();
|
||||
parts.overlays.syncMissionOverlayEntries();
|
||||
parts.policyHelpers.releaseAircraftTracking(layerState._dataManager);
|
||||
layerState._viewer.trackedEntity = undefined;
|
||||
stopFollowing(layerState._viewer);
|
||||
const camera = cameraOf(layerState._viewer);
|
||||
camera.cancelFlight();
|
||||
layerState._animationStarts.set(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFollowing } from '../../data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
HORIZON_CAMERA_MOVE_EPSILON_M,
|
||||
@@ -149,7 +150,7 @@ export function createNavigation({
|
||||
/** Radio may move the globe only while no tracked entity owns the follow camera. */
|
||||
|
||||
function radioCameraNavigationAllowed(viewer = layerState._viewer) {
|
||||
return Boolean(viewer?.camera) && !viewer.trackedEntity;
|
||||
return Boolean(viewer?.camera) && !isFollowing(viewer);
|
||||
}
|
||||
|
||||
function radioGlobeRecenterPlan(viewer = layerState._viewer) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { onFollowChanged } from '../../data/followed.js';
|
||||
import { inputFor } from '../../input/dispatcher.js';
|
||||
import { VESSEL_OVERLAY_SOURCE_ID } from '../../data/vesselLabels.js';
|
||||
|
||||
@@ -97,14 +98,11 @@ export function createSelection({
|
||||
state.keyTarget = keyTarget;
|
||||
state.keydownHandler = onVesselKeyDown;
|
||||
keyTarget.addEventListener('keydown', state.keydownHandler);
|
||||
// Vessels never set viewer.trackedEntity, so any new tracked entity belongs
|
||||
// to another layer and takes interaction ownership of the scene.
|
||||
state.trackedEntityRemover = viewer.trackedEntityChanged.addEventListener(
|
||||
() => {
|
||||
if (viewer.trackedEntity && state.selectedRecord)
|
||||
clearVesselInspection();
|
||||
},
|
||||
);
|
||||
// Vessels never follow, so anything newly followed belongs to another
|
||||
// layer and takes interaction ownership of the scene.
|
||||
state.trackedEntityRemover = onFollowChanged(viewer, (followed) => {
|
||||
if (followed && state.selectedRecord) clearVesselInspection();
|
||||
});
|
||||
}
|
||||
|
||||
/** Select one live vessel and request one UI-owned camera transfer. */
|
||||
|
||||
@@ -9,13 +9,14 @@ import { createCameraEvents } from '../camera/events.js';
|
||||
import { listenerList, toCartesian, uncaught } from './common.js';
|
||||
import { cameraFlightActive } from './internals.js';
|
||||
import { createCesiumFollow } from './follow.js';
|
||||
import { cameraOf, provideUnboundCamera } from '../rendererOf.js';
|
||||
import { cameraOf, findCamera, provideUnboundCamera } from '../rendererOf.js';
|
||||
|
||||
/**
|
||||
* `cameraOf`, for code that already reaches Cesium: importing it from here
|
||||
* loads the factory a viewer with no renderer bound gets its camera from.
|
||||
* `cameraOf` and `findCamera`, for code that already reaches Cesium:
|
||||
* importing them from here loads the factory a viewer with no renderer
|
||||
* bound gets its camera from.
|
||||
*/
|
||||
export { cameraOf };
|
||||
export { cameraOf, findCamera };
|
||||
|
||||
/** The contract's easings as Cesium's. */
|
||||
export const EASINGS = Object.freeze({
|
||||
|
||||
@@ -33,6 +33,13 @@ const INJECTED = {
|
||||
'./layers/recent-imagery': 'src/layers/recentImagery/rendering.js',
|
||||
};
|
||||
|
||||
/**
|
||||
* Modules whose use of the bridge holds up without the factory: with no
|
||||
* port they read Cesium's `trackedEntity` instead, which is all an unbound
|
||||
* viewer can follow by.
|
||||
*/
|
||||
const FALLS_BACK = new Set(['src/data/followed.js']);
|
||||
|
||||
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
|
||||
/** The browser module an export resolves to, or null for Node-only ones. */
|
||||
@@ -112,6 +119,7 @@ function reachable(entry, { eager = false } = {}) {
|
||||
for (const { specifier, lazy } of specifiersOf(file)) {
|
||||
if (eager && lazy) continue;
|
||||
const next = resolve(file, specifier);
|
||||
if (next === BRIDGE && FALLS_BACK.has(file)) continue;
|
||||
if (next && !seen.has(next)) {
|
||||
seen.add(next);
|
||||
queue.push(next);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* State is persisted to localStorage and can be exported/imported as JSON.
|
||||
*/
|
||||
|
||||
import { isFollowing } from '../data/followed.js';
|
||||
import { cancelCameraArrival } from '../data/cameraArrival.js';
|
||||
import { resolveCameraPose, resolveCameraMove } from '../director/camera.js';
|
||||
import { createCameraMotion } from './cameraMotion.js';
|
||||
@@ -114,7 +115,7 @@ export class SceneDirector {
|
||||
this._interactionTransitions = 0;
|
||||
this._interactions = createSceneInteractions(viewer, {
|
||||
available: () =>
|
||||
!this._destroyed && !this._running && !this.viewer.trackedEntity,
|
||||
!this._destroyed && !this._running && !isFollowing(this.viewer),
|
||||
execute: (action, signal) =>
|
||||
this._trackWork(this._executeInteraction(action, signal)),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { followedOf } from '../data/followed.js';
|
||||
import { nextTick } from 'vue';
|
||||
import { resolveImageryHost } from '../layers/weather/imageryHost.js';
|
||||
import { ShellFacade } from './shellFacade.js';
|
||||
@@ -1174,7 +1175,7 @@ export class StyleManager extends ShellFacade {
|
||||
setEnabled: (next) =>
|
||||
this._dataManager.setEnabled('cctv', next, { origin: 'user' }),
|
||||
readOwnership: () => ({
|
||||
trackedEntity: this.viewer?.trackedEntity,
|
||||
followed: followedOf(this.viewer),
|
||||
cockpitActive: !!this.cockpitView?.active,
|
||||
}),
|
||||
shouldFocus: () =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { onFollowChanged } from '../data/followed.js';
|
||||
import { isExplicitLayerStateOrigin } from '../data/layerState.js';
|
||||
import {
|
||||
registerCctvFocusRequestListener,
|
||||
@@ -80,11 +81,13 @@ export class LayerBindings {
|
||||
window,
|
||||
this._worldRequestFocusHandler,
|
||||
);
|
||||
this._navigationOwnerChangedRemover =
|
||||
this.viewer.trackedEntityChanged.addEventListener((entity) => {
|
||||
if (entity && !this._disposed)
|
||||
this._navigationOwnerChangedRemover = onFollowChanged(
|
||||
this.viewer,
|
||||
(followed) => {
|
||||
if (followed && !this._disposed)
|
||||
this._stampNavigation({ cancelPendingSelection: false });
|
||||
});
|
||||
},
|
||||
);
|
||||
// Vessel/installation focus flies without ever assigning a tracked entity,
|
||||
// so it cannot reach the listener above. It announces instead.
|
||||
this._removeNavigationAuthorityListener =
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { findCamera } from '../render/cesium/camera.js';
|
||||
import { stopFollowing } from '../data/followed.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { createStateChannel } from '../app/stateChannel.js';
|
||||
@@ -367,9 +369,10 @@ export class LocationNavigation {
|
||||
} catch {
|
||||
/* best-effort release */
|
||||
}
|
||||
this.viewer.trackedEntity = undefined;
|
||||
this.viewer.camera.cancelFlight();
|
||||
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
stopFollowing(this.viewer);
|
||||
const camera = findCamera(this.viewer);
|
||||
camera?.cancelFlight();
|
||||
camera?.release();
|
||||
this._beginWorldJumpTransition();
|
||||
|
||||
let resolveReset;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { findCamera } from '../render/cesium/camera.js';
|
||||
import { stopFollowing } from '../data/followed.js';
|
||||
import { cancelCameraArrival } from '../data/cameraArrival.js';
|
||||
import { createStateChannel } from '../app/stateChannel.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import {
|
||||
beginDeferredNavigation,
|
||||
reassertNavigationHandoff,
|
||||
@@ -176,12 +177,13 @@ export class NavigationController {
|
||||
} catch {
|
||||
/* best-effort release */
|
||||
}
|
||||
this.viewer.trackedEntity = undefined;
|
||||
stopFollowing(this.viewer);
|
||||
this.interruptCameraMotion('explicit-navigation');
|
||||
this.stopOrbit();
|
||||
if (!preserveCameraFlight) this.viewer.camera.cancelFlight();
|
||||
const camera = findCamera(this.viewer);
|
||||
if (!preserveCameraFlight) camera?.cancelFlight();
|
||||
try {
|
||||
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||
camera?.release();
|
||||
} catch {
|
||||
/* teardown race */
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFollowing, stopFollowing } from '../data/followed.js';
|
||||
import { interruptGlobeRotation } from '../cameraVerbs.js';
|
||||
import {
|
||||
layerSnapshot,
|
||||
@@ -396,7 +397,7 @@ export function createGevActionRunner({
|
||||
// Explicit navigation while TRACKING supersedes the follow camera —
|
||||
// otherwise the tracker drags the view back and "I flew there but can't
|
||||
// do anything" (field finding). track_entity manages its own handoff.
|
||||
if (name === 'zoom_to_globe' && viewer.trackedEntity) {
|
||||
if (name === 'zoom_to_globe' && isFollowing(viewer)) {
|
||||
stopAllTracking(viewer, dataManager);
|
||||
}
|
||||
|
||||
@@ -1130,7 +1131,7 @@ export function createGevActionRunner({
|
||||
return;
|
||||
}
|
||||
interruptCameraMotion('nav:fly_to_location');
|
||||
if (viewer.trackedEntity) stopAllTracking(viewer, dataManager);
|
||||
if (isFollowing(viewer)) stopAllTracking(viewer, dataManager);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -2570,7 +2571,7 @@ function stopAllTracking(viewer, dataManager) {
|
||||
failed.add(layerId);
|
||||
}
|
||||
}
|
||||
if (viewer) viewer.trackedEntity = undefined;
|
||||
stopFollowing(viewer);
|
||||
if (failed.size) {
|
||||
const failedLayerIds = [...failed];
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user