mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +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/surveillance.js",
|
||||||
"src/styles/thermal.js",
|
"src/styles/thermal.js",
|
||||||
"src/ui/applicationShell.js",
|
"src/ui/applicationShell.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/render/cesium/camera.js",
|
"src/render/cesium/camera.js",
|
||||||
"src/render/cesium/follow.js",
|
"src/render/cesium/follow.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
@@ -1256,7 +1257,9 @@
|
|||||||
"src/data/bhoteKoshiFloodPath.js",
|
"src/data/bhoteKoshiFloodPath.js",
|
||||||
"src/maps/catalog.js",
|
"src/maps/catalog.js",
|
||||||
"src/scenes/director.js",
|
"src/scenes/director.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/render/cesium/camera.js",
|
"src/render/cesium/camera.js",
|
||||||
|
"src/data/entityRecords.js",
|
||||||
"src/render/cesium/follow.js",
|
"src/render/cesium/follow.js",
|
||||||
"src/geo/index.js",
|
"src/geo/index.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
@@ -1572,6 +1575,7 @@
|
|||||||
"src/data/aircraftRecession.js",
|
"src/data/aircraftRecession.js",
|
||||||
"src/data/motionModel.js",
|
"src/data/motionModel.js",
|
||||||
"src/layers/vessels/selection.js",
|
"src/layers/vessels/selection.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/layers/vessels/state.js",
|
"src/layers/vessels/state.js",
|
||||||
"src/layers/vessels/testing.js",
|
"src/layers/vessels/testing.js",
|
||||||
"src/layers/vessels/tracking.js",
|
"src/layers/vessels/tracking.js",
|
||||||
@@ -1600,6 +1604,7 @@
|
|||||||
"src/input/sensorPick.js",
|
"src/input/sensorPick.js",
|
||||||
"src/input/dispatcher.js",
|
"src/input/dispatcher.js",
|
||||||
"src/render/cesium/cameraInput.js",
|
"src/render/cesium/cameraInput.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
"src/render/cesium/input.js",
|
"src/render/cesium/input.js",
|
||||||
"src/render/cesium/ctrlWheelZoom.js",
|
"src/render/cesium/ctrlWheelZoom.js",
|
||||||
"src/render/input/router.js"
|
"src/render/input/router.js"
|
||||||
@@ -1753,6 +1758,7 @@
|
|||||||
"src/layers/launches/controls.js",
|
"src/layers/launches/controls.js",
|
||||||
"src/layers/launches/index.js",
|
"src/layers/launches/index.js",
|
||||||
"src/render/cesium/camera.js",
|
"src/render/cesium/camera.js",
|
||||||
|
"src/data/entityRecords.js",
|
||||||
"src/render/cesium/follow.js",
|
"src/render/cesium/follow.js",
|
||||||
"src/layers/launches/ingestion.js",
|
"src/layers/launches/ingestion.js",
|
||||||
"src/layers/launches/launchPad.js",
|
"src/layers/launches/launchPad.js",
|
||||||
@@ -1776,6 +1782,7 @@
|
|||||||
"src/render/cesium/common.js",
|
"src/render/cesium/common.js",
|
||||||
"src/layers/launches/rendering.js",
|
"src/layers/launches/rendering.js",
|
||||||
"src/layers/launches/replay.js",
|
"src/layers/launches/replay.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/layers/launches/selection.js",
|
"src/layers/launches/selection.js",
|
||||||
"src/layers/launches/source.js",
|
"src/layers/launches/source.js",
|
||||||
"src/layers/launches/state.js",
|
"src/layers/launches/state.js",
|
||||||
@@ -1917,6 +1924,7 @@
|
|||||||
"src/layers/awareness/rendering.js",
|
"src/layers/awareness/rendering.js",
|
||||||
"src/layers/awareness/state.js",
|
"src/layers/awareness/state.js",
|
||||||
"src/layers/awareness/subject.js",
|
"src/layers/awareness/subject.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/layers/awareness/testing.js",
|
"src/layers/awareness/testing.js",
|
||||||
"src/ui/panelRailGeometry.js",
|
"src/ui/panelRailGeometry.js",
|
||||||
"src/i18n/index.js",
|
"src/i18n/index.js",
|
||||||
@@ -2059,6 +2067,7 @@
|
|||||||
"src/layers/cctv/geometry.js",
|
"src/layers/cctv/geometry.js",
|
||||||
"src/data/entityRecords.js",
|
"src/data/entityRecords.js",
|
||||||
"src/layers/cctv/geometryQueue.js",
|
"src/layers/cctv/geometryQueue.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/layers/cctv/ground.js",
|
"src/layers/cctv/ground.js",
|
||||||
"src/layers/cctv/headingConfidence.js",
|
"src/layers/cctv/headingConfidence.js",
|
||||||
"src/geo/index.js",
|
"src/geo/index.js",
|
||||||
@@ -2225,6 +2234,7 @@
|
|||||||
"modules": [
|
"modules": [
|
||||||
"src/data/headingDegrees.js",
|
"src/data/headingDegrees.js",
|
||||||
"src/layers/alpr/index.js",
|
"src/layers/alpr/index.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/geo/index.js",
|
"src/geo/index.js",
|
||||||
"src/layers/alpr/model.js",
|
"src/layers/alpr/model.js",
|
||||||
"src/layers/alpr/policy.js",
|
"src/layers/alpr/policy.js",
|
||||||
@@ -2300,7 +2310,9 @@
|
|||||||
"src/layers/radio/lifecycle.js",
|
"src/layers/radio/lifecycle.js",
|
||||||
"src/layers/radio/model.js",
|
"src/layers/radio/model.js",
|
||||||
"src/layers/radio/navigation.js",
|
"src/layers/radio/navigation.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/render/cesium/camera.js",
|
"src/render/cesium/camera.js",
|
||||||
|
"src/data/entityRecords.js",
|
||||||
"src/render/cesium/follow.js",
|
"src/render/cesium/follow.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
"src/render/camera/events.js",
|
"src/render/camera/events.js",
|
||||||
@@ -2647,6 +2659,7 @@
|
|||||||
"src/app/stateChannel.js",
|
"src/app/stateChannel.js",
|
||||||
"src/app/surfaceServices.js",
|
"src/app/surfaceServices.js",
|
||||||
"src/app/tools.js",
|
"src/app/tools.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/app/viewer.js",
|
"src/app/viewer.js",
|
||||||
"src/render/cesium/ctrlWheelZoom.js",
|
"src/render/cesium/ctrlWheelZoom.js",
|
||||||
"src/sky/starrySky.js",
|
"src/sky/starrySky.js",
|
||||||
@@ -3726,6 +3739,7 @@
|
|||||||
"src/keylessGeocoder.js",
|
"src/keylessGeocoder.js",
|
||||||
"src/layers/aircraft/classification.js",
|
"src/layers/aircraft/classification.js",
|
||||||
"src/layers/alpr/index.js",
|
"src/layers/alpr/index.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/layers/alpr/model.js",
|
"src/layers/alpr/model.js",
|
||||||
"src/layers/alpr/policy.js",
|
"src/layers/alpr/policy.js",
|
||||||
"src/layers/alpr/presentation.js",
|
"src/layers/alpr/presentation.js",
|
||||||
@@ -4255,6 +4269,7 @@
|
|||||||
"src/layers/firms/anchors.js",
|
"src/layers/firms/anchors.js",
|
||||||
"src/locations.js",
|
"src/locations.js",
|
||||||
"src/render/cesium/camera.js",
|
"src/render/cesium/camera.js",
|
||||||
|
"src/data/entityRecords.js",
|
||||||
"src/render/cesium/follow.js",
|
"src/render/cesium/follow.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
"src/render/camera/events.js",
|
"src/render/camera/events.js",
|
||||||
@@ -4267,6 +4282,7 @@
|
|||||||
"src/render/cesium/common.js",
|
"src/render/cesium/common.js",
|
||||||
"src/cityPois.js",
|
"src/cityPois.js",
|
||||||
"src/cameraGroundGuard.js",
|
"src/cameraGroundGuard.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/search/alpsGeocoder.js",
|
"src/search/alpsGeocoder.js",
|
||||||
"src/search/defaults.js",
|
"src/search/defaults.js",
|
||||||
"src/search/geospatial.js",
|
"src/search/geospatial.js",
|
||||||
@@ -4350,7 +4366,10 @@
|
|||||||
"exports": ["./ui/navigation"],
|
"exports": ["./ui/navigation"],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/navigationPolicy.js",
|
"src/navigationPolicy.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/ui/navigationController.js",
|
"src/ui/navigationController.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/app/stateChannel.js",
|
"src/app/stateChannel.js",
|
||||||
"src/data/cameraArrival.js",
|
"src/data/cameraArrival.js",
|
||||||
"src/i18n/locale.js",
|
"src/i18n/locale.js",
|
||||||
@@ -4365,7 +4384,18 @@
|
|||||||
"src/i18n/locales/uk.json",
|
"src/i18n/locales/uk.json",
|
||||||
"src/i18n/locales/zh-CN.json",
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/i18n/locales/zh-Hant.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"]
|
"external": ["cesium"]
|
||||||
},
|
},
|
||||||
@@ -5169,6 +5199,7 @@
|
|||||||
"src/i18n/locales/zh-Hant.json",
|
"src/i18n/locales/zh-Hant.json",
|
||||||
"src/data/geoImportImages.js",
|
"src/data/geoImportImages.js",
|
||||||
"src/data/geoImportPins.js",
|
"src/data/geoImportPins.js",
|
||||||
|
"src/data/followed.js",
|
||||||
"src/layers/streetLevel/tileMath.js",
|
"src/layers/streetLevel/tileMath.js",
|
||||||
"src/layers/streetLevel/view.js",
|
"src/layers/streetLevel/view.js",
|
||||||
"src/data/inputOwnership.js",
|
"src/data/inputOwnership.js",
|
||||||
@@ -5177,6 +5208,8 @@
|
|||||||
"src/data/pickRegistry.js",
|
"src/data/pickRegistry.js",
|
||||||
"src/input/dispatcher.js",
|
"src/input/dispatcher.js",
|
||||||
"src/render/cesium/cameraInput.js",
|
"src/render/cesium/cameraInput.js",
|
||||||
|
"src/render/rendererOf.js",
|
||||||
|
"src/data/entityRecords.js",
|
||||||
"src/render/cesium/input.js",
|
"src/render/cesium/input.js",
|
||||||
"src/render/cesium/ctrlWheelZoom.js",
|
"src/render/cesium/ctrlWheelZoom.js",
|
||||||
"src/render/input/router.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 { SceneDirector } from '../scenes/director.js';
|
||||||
import { initAnnotations } from '../annotations/index.js';
|
import { initAnnotations } from '../annotations/index.js';
|
||||||
import { initDrawTool } from '../annotations/drawTool.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
|
// The follow camera recomputes the tracked target's dead-reckon position
|
||||||
// every frame — tracking anything is a per-frame animation. (perf wave 2)
|
// every frame — tracking anything is a per-frame animation. (perf wave 2)
|
||||||
const removeTrackingListener = viewer.trackedEntityChanged.addEventListener(
|
const removeTrackingListener = onFollowChanged(viewer, (followed) => {
|
||||||
() => {
|
if (followed) holdContinuousRender('tracked-entity');
|
||||||
if (viewer.trackedEntity) holdContinuousRender('tracked-entity');
|
else releaseContinuousRender('tracked-entity');
|
||||||
else releaseContinuousRender('tracked-entity');
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hidden-state suspension (perf wave 2): when the window/tab is hidden,
|
// Hidden-state suspension (perf wave 2): when the window/tab is hidden,
|
||||||
// stop the default render loop outright — a hidden canvas repaints for
|
// 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,
|
true,
|
||||||
);
|
);
|
||||||
const id = activeCameraMotionId();
|
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);
|
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(getActiveCameraMotion(), null);
|
||||||
assert.equal(hasRenderHold(), false);
|
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) => {
|
test('direct globe rotation refuses a tracked camera without disturbing its reference frame', (t) => {
|
||||||
const { viewer, camera, target } = harness(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));
|
camera.lookAt(target, new Cesium.HeadingPitchRange(0, radians(-40), 1_000));
|
||||||
const before = pose(camera);
|
const before = pose(camera);
|
||||||
const transform = Cesium.Matrix4.clone(camera.transform);
|
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
|
* lifts the eye if it sits below a usable clearance. Heading and pitch are
|
||||||
* held, so the correction reads as the shot settling.
|
* held, so the correction reads as the shot settling.
|
||||||
*/
|
*/
|
||||||
|
import { onFollowChanged } from './data/followed.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { ownCameraArrival } from './data/cameraArrival.js';
|
import { ownCameraArrival } from './data/cameraArrival.js';
|
||||||
import { heightsFor } from './queries/heights.js';
|
import { heightsFor } from './queries/heights.js';
|
||||||
@@ -164,8 +165,7 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
|
|||||||
release();
|
release();
|
||||||
};
|
};
|
||||||
release = ownCameraArrival(viewer, stop);
|
release = ownCameraArrival(viewer, stop);
|
||||||
const removeTracking = viewer.trackedEntityChanged?.addEventListener(stop);
|
removers.push(onFollowChanged(viewer, stop));
|
||||||
if (removeTracking) removers.push(removeTracking);
|
|
||||||
documentRef?.addEventListener?.('keydown', stop, true);
|
documentRef?.addEventListener?.('keydown', stop, true);
|
||||||
// A manual gesture ends the guard, as it interrupts camera motion elsewhere.
|
// A manual gesture ends the guard, as it interrupts camera motion elsewhere.
|
||||||
for (const type of ['pointerdown', 'wheel'])
|
for (const type of ['pointerdown', 'wheel'])
|
||||||
|
|||||||
@@ -205,7 +205,9 @@ for (const handoff of [
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (handoff === 'tracking') {
|
} 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') {
|
} else if (handoff === 'follow' || handoff === 'UI') {
|
||||||
navigation._stampNavigation({ cancelPendingSelection: false });
|
navigation._stampNavigation({ cancelPendingSelection: false });
|
||||||
} else if (handoff === 'keyboard') {
|
} 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
|
// The transaction has exactly two camera-owner mutations, and each one
|
||||||
// stamps:
|
// stamps:
|
||||||
// 1. selectedLayer.trackById(id) -> viewer.trackedEntity
|
// 1. selectedLayer.trackById(id) -> viewer.trackedEntity
|
||||||
// -> viewer.trackedEntityChanged -> _stampNavigation()
|
// -> onFollowChanged(viewer) -> _stampNavigation()
|
||||||
// 2. cockpitView.enter() -> onCameraTakeover() -> _stampNavigation()
|
// 2. cockpitView.enter() -> onCameraTakeover() -> _stampNavigation()
|
||||||
const transaction = body(
|
const transaction = body(
|
||||||
cockpitTracking,
|
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.
|
// Seam 1: any tracker handoff stamps, so the adoption step is covered.
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
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',
|
'tracker handoff must stamp',
|
||||||
);
|
);
|
||||||
// Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above.
|
// 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',
|
'origin: trackingOrigin',
|
||||||
'satellitesLayer.stopTracking?.({ origin: trackingOrigin })',
|
'satellitesLayer.stopTracking?.({ origin: trackingOrigin })',
|
||||||
'rocketLaunchesLayer.releaseCameraOwnership?.()',
|
'rocketLaunchesLayer.releaseCameraOwnership?.()',
|
||||||
'this.viewer.trackedEntity = undefined;',
|
'stopFollowing(this.viewer);',
|
||||||
"interruptCameraMotion('explicit-navigation')",
|
"interruptCameraMotion('explicit-navigation')",
|
||||||
'this.viewer.camera.cancelFlight();',
|
'camera?.cancelFlight();',
|
||||||
'this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);',
|
'camera?.release();',
|
||||||
], 'follow release');
|
], 'follow release');
|
||||||
assert.match(release, /flightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/);
|
assert.match(release, /flightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/);
|
||||||
assert.match(release, /militaryFlightsLayer\.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.
|
* globe motion, opposite to the camera's travel around Earth.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { followedOf, isFollowing, onFollowChanged } from './data/followed.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { GLOBE_VIEW } from './locations.js';
|
import { GLOBE_VIEW } from './locations.js';
|
||||||
import {
|
import {
|
||||||
@@ -1041,7 +1042,7 @@ export function interruptCameraMotion(reason = 'interrupt') {
|
|||||||
// While an entity is TRACKED, the follow camera owns the reference frame —
|
// While an entity is TRACKED, the follow camera owns the reference frame —
|
||||||
// resetting the lookAt transform here flings the camera into space (field
|
// resetting the lookAt transform here flings the camera into space (field
|
||||||
// finding). Leave the transform to the tracker; it re-asserts every frame.
|
// 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';
|
const wasRoute = wasActive && _active.kind === 'route';
|
||||||
if (
|
if (
|
||||||
wasActive &&
|
wasActive &&
|
||||||
@@ -1190,7 +1191,7 @@ function onTick() {
|
|||||||
// only describes Cesium's rotation while the frame is identity.
|
// only describes Cesium's rotation while the frame is identity.
|
||||||
if (
|
if (
|
||||||
_viewer.scene.mode !== Cesium.SceneMode.SCENE3D ||
|
_viewer.scene.mode !== Cesium.SceneMode.SCENE3D ||
|
||||||
_viewer.trackedEntity ||
|
isFollowing(_viewer) ||
|
||||||
_isCockpitActive?.() ||
|
_isCockpitActive?.() ||
|
||||||
!Cesium.Matrix4.equals(cam.transform, Cesium.Matrix4.IDENTITY)
|
!Cesium.Matrix4.equals(cam.transform, Cesium.Matrix4.IDENTITY)
|
||||||
) {
|
) {
|
||||||
@@ -1413,8 +1414,8 @@ export function initCameraVerbs(
|
|||||||
if (_trackingRemover) _trackingRemover();
|
if (_trackingRemover) _trackingRemover();
|
||||||
// Cockpit can acquire then clear tracking before the next clock tick.
|
// Cockpit can acquire then clear tracking before the next clock tick.
|
||||||
// Yield on acquisition itself without changing legacy camera motions.
|
// Yield on acquisition itself without changing legacy camera motions.
|
||||||
_trackingRemover = viewer.trackedEntityChanged?.addEventListener((entity) => {
|
_trackingRemover = onFollowChanged(viewer, (followed) => {
|
||||||
if (entity && _active?.kind === 'rotate_globe')
|
if (followed && _active?.kind === 'rotate_globe')
|
||||||
interruptCameraMotion('globe-camera-owner-changed');
|
interruptCameraMotion('globe-camera-owner-changed');
|
||||||
});
|
});
|
||||||
for (const rm of _inputRemovers) rm();
|
for (const rm of _inputRemovers) rm();
|
||||||
@@ -1522,15 +1523,14 @@ export function moveCamera(args = {}, runNavigation = null) {
|
|||||||
if (
|
if (
|
||||||
(motion === 'rotate_globe' ||
|
(motion === 'rotate_globe' ||
|
||||||
(motion === 'orbit' && typeof runNavigation !== 'function')) &&
|
(motion === 'orbit' && typeof runNavigation !== 'function')) &&
|
||||||
_viewer?.trackedEntity
|
isFollowing(_viewer)
|
||||||
) {
|
) {
|
||||||
// The follow camera owns a tracked view; a lookAt orbit fights it frame
|
// 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
|
// by frame and stop rips the camera out (field finding). Honest refusal
|
||||||
// until tracked-orbit rides the follow camera natively (roadmap).
|
// until tracked-orbit rides the follow camera natively (roadmap).
|
||||||
|
const followed = followedOf(_viewer);
|
||||||
const label =
|
const label =
|
||||||
_viewer.trackedEntity?.name ||
|
followed?.name || followed?.labelModel?.title || 'the tracked target';
|
||||||
_viewer.trackedEntity?.id ||
|
|
||||||
'the tracked target';
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
action: 'move_camera',
|
action: 'move_camera',
|
||||||
@@ -1701,7 +1701,7 @@ export function moveCamera(args = {}, runNavigation = null) {
|
|||||||
const preserveCameraFlight =
|
const preserveCameraFlight =
|
||||||
motion === 'orbit' &&
|
motion === 'orbit' &&
|
||||||
_viewer.scene.tweens.length > 0 &&
|
_viewer.scene.tweens.length > 0 &&
|
||||||
!_viewer.trackedEntity;
|
!isFollowing(_viewer);
|
||||||
return typeof runNavigation === 'function'
|
return typeof runNavigation === 'function'
|
||||||
? runNavigation(start, { preserveCameraFlight })
|
? runNavigation(start, { preserveCameraFlight })
|
||||||
: start();
|
: start();
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* Activates the nearest CCTV record on layer enable and flies only when no
|
* 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 {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 {boolean} options.cockpitActive Whether cockpit mode owns the camera.
|
||||||
* @param {() => (string|null)} options.activate Nearest-camera activation.
|
* @param {() => (string|null)} options.activate Nearest-camera activation.
|
||||||
* @param {(cameraId: string) => *} options.fly Optional camera flight.
|
* @param {(cameraId: string) => *} options.fly Optional camera flight.
|
||||||
* @returns {*} Activated camera ID for a held view, or the flight result.
|
* @returns {*} Activated camera ID for a held view, or the flight result.
|
||||||
*/
|
*/
|
||||||
export function runCctvLayerEnableFocus({
|
export function runCctvLayerEnableFocus({
|
||||||
trackedEntity = null,
|
followed = null,
|
||||||
cockpitActive = false,
|
cockpitActive = false,
|
||||||
activate,
|
activate,
|
||||||
fly,
|
fly,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const cameraId = activate?.();
|
const cameraId = activate?.();
|
||||||
if (!cameraId) return false;
|
if (!cameraId) return false;
|
||||||
if (trackedEntity || cockpitActive) return cameraId;
|
if (followed || cockpitActive) return cameraId;
|
||||||
return fly?.(cameraId);
|
return fly?.(cameraId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,11 +26,11 @@ export function runCctvLayerEnableFocus({
|
|||||||
* chain. Either observation suppresses the optional CCTV focus flight.
|
* chain. Either observation suppresses the optional CCTV focus flight.
|
||||||
* @param {Object} [before={}] Ownership immediately before the await.
|
* @param {Object} [before={}] Ownership immediately before the await.
|
||||||
* @param {Object} [after={}] Ownership immediately after 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 = {}) {
|
function mergeCctvEnableOwnership(before = {}, after = {}) {
|
||||||
return {
|
return {
|
||||||
trackedEntity: before.trackedEntity || after.trackedEntity || null,
|
followed: before.followed || after.followed || null,
|
||||||
cockpitActive: !!(before.cockpitActive || after.cockpitActive),
|
cockpitActive: !!(before.cockpitActive || after.cockpitActive),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -60,7 +61,7 @@ export async function runCctvLayerEnableTransition({
|
|||||||
const ownershipBefore = readOwnership?.() || {};
|
const ownershipBefore = readOwnership?.() || {};
|
||||||
if (target === true) {
|
if (target === true) {
|
||||||
debug?.('[UI:CCTV] ownership before setEnabled await', {
|
debug?.('[UI:CCTV] ownership before setEnabled await', {
|
||||||
trackedId: ownershipBefore.trackedEntity?.id ?? null,
|
trackedId: ownershipBefore.followed?.key ?? null,
|
||||||
cockpitActive: !!ownershipBefore.cockpitActive,
|
cockpitActive: !!ownershipBefore.cockpitActive,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ export async function runCctvLayerEnableTransition({
|
|||||||
const ownershipAfter = readOwnership?.() || {};
|
const ownershipAfter = readOwnership?.() || {};
|
||||||
if (target === true) {
|
if (target === true) {
|
||||||
debug?.('[UI:CCTV] ownership after setEnabled await', {
|
debug?.('[UI:CCTV] ownership after setEnabled await', {
|
||||||
trackedId: ownershipAfter.trackedEntity?.id ?? null,
|
trackedId: ownershipAfter.followed?.key ?? null,
|
||||||
cockpitActive: !!ownershipAfter.cockpitActive,
|
cockpitActive: !!ownershipAfter.cockpitActive,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
|
|
||||||
test('CCTV layer enable activates nearest without stealing a tracked or cockpit view', () => {
|
test('CCTV layer enable activates nearest without stealing a tracked or cockpit view', () => {
|
||||||
for (const ownership of [
|
for (const ownership of [
|
||||||
{ trackedEntity: { id: 'tracked-plane' }, cockpitActive: false },
|
{ followed: { key: 'tracked-plane' }, cockpitActive: false },
|
||||||
{ trackedEntity: null, cockpitActive: true },
|
{ followed: null, cockpitActive: true },
|
||||||
]) {
|
]) {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const result = runCctvLayerEnableFocus({
|
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 () => {
|
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;
|
let flyCalls = 0;
|
||||||
const diagnostics = [];
|
const diagnostics = [];
|
||||||
const result = await runCctvLayerEnableTransition({
|
const result = await runCctvLayerEnableTransition({
|
||||||
target: true,
|
target: true,
|
||||||
readOwnership: () => ({ trackedEntity, cockpitActive: false }),
|
readOwnership: () => ({ followed, cockpitActive: false }),
|
||||||
setEnabled: async () => {
|
setEnabled: async () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
trackedEntity = null;
|
followed = null;
|
||||||
},
|
},
|
||||||
shouldFocus: () => true,
|
shouldFocus: () => true,
|
||||||
activate: () => 'cam-near',
|
activate: () => 'cam-near',
|
||||||
@@ -83,7 +83,7 @@ test('CCTV disable transition does not emit enable-ownership diagnostics', async
|
|||||||
|
|
||||||
const result = await runCctvLayerEnableTransition({
|
const result = await runCctvLayerEnableTransition({
|
||||||
target: false,
|
target: false,
|
||||||
readOwnership: () => ({ trackedEntity: { id: 'tracked-plane' }, cockpitActive: false }),
|
readOwnership: () => ({ followed: { key: 'tracked-plane' }, cockpitActive: false }),
|
||||||
setEnabled: async (target) => transitions.push(target),
|
setEnabled: async (target) => transitions.push(target),
|
||||||
shouldFocus: () => true,
|
shouldFocus: () => true,
|
||||||
activate: () => 'cam-near',
|
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', () => {
|
test('geometry drain pacing yields to tracked and cockpit camera ownership', () => {
|
||||||
assert.deepEqual(cctvGeometryDrainPacing(), { batchSize: 4, delayMs: 120 });
|
assert.deepEqual(cctvGeometryDrainPacing(), { batchSize: 4, delayMs: 120 });
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
cctvGeometryDrainPacing({ trackedEntity: { id: 'flight-1' } }),
|
cctvGeometryDrainPacing({ followed: { key: 'flights:1' } }),
|
||||||
{ batchSize: 2, delayMs: 250 },
|
{ batchSize: 2, delayMs: 250 },
|
||||||
);
|
);
|
||||||
assert.deepEqual(cctvGeometryDrainPacing({ cockpitActive: true }), {
|
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', () => {
|
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 queue = Array.from({ length: 10 }, (_, index) => index + 1);
|
||||||
const visited = [];
|
const visited = [];
|
||||||
const runBatch = () =>
|
const runBatch = () =>
|
||||||
processCctvGeometryDrainBatch({
|
processCctvGeometryDrainBatch({
|
||||||
queue,
|
queue,
|
||||||
readOwnership: () => ({ trackedEntity, cockpitActive: false }),
|
readOwnership: () => ({ followed, cockpitActive: false }),
|
||||||
visit: (record) => visited.push(record),
|
visit: (record) => visited.push(record),
|
||||||
progress: () => {},
|
progress: () => {},
|
||||||
complete: () => {},
|
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(trackedBatch, { hasMore: true, batchSize: 2, delayMs: 250 });
|
||||||
assert.deepEqual(visited, [1, 2]);
|
assert.deepEqual(visited, [1, 2]);
|
||||||
|
|
||||||
trackedEntity = null;
|
followed = null;
|
||||||
const untrackedBatch = runBatch();
|
const untrackedBatch = runBatch();
|
||||||
assert.deepEqual(untrackedBatch, {
|
assert.deepEqual(untrackedBatch, {
|
||||||
hasMore: true,
|
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';
|
import * as Cesium from 'cesium';
|
||||||
|
|
||||||
const STYLE = [
|
const STYLE = [
|
||||||
@@ -113,8 +114,9 @@ export function createImportedPins(dataSource, viewer) {
|
|||||||
destroy() {
|
destroy() {
|
||||||
if (destroyed) return;
|
if (destroyed) return;
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
if (viewer.trackedEntity && pins.includes(viewer.trackedEntity))
|
const followedEntity = followedOf(viewer)?.entity;
|
||||||
viewer.trackedEntity = undefined;
|
if (followedEntity && pins.includes(followedEntity))
|
||||||
|
stopFollowing(viewer);
|
||||||
if (viewer.selectedEntity && pins.includes(viewer.selectedEntity))
|
if (viewer.selectedEntity && pins.includes(viewer.selectedEntity))
|
||||||
viewer.selectedEntity = undefined;
|
viewer.selectedEntity = undefined;
|
||||||
if (mounted && !viewer.scene.primitives?.isDestroyed?.())
|
if (mounted && !viewer.scene.primitives?.isDestroyed?.())
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFollowing } from '../../data/followed.js';
|
||||||
import { isUnavailableCapability } from '../../sources/capability.js';
|
import { isUnavailableCapability } from '../../sources/capability.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import {
|
import {
|
||||||
@@ -369,13 +370,12 @@ export function createAlprCamerasLayer({ source, services } = {}) {
|
|||||||
{
|
{
|
||||||
id: 'find-camera',
|
id: 'find-camera',
|
||||||
label: t('SHOW NEAREST'),
|
label: t('SHOW NEAREST'),
|
||||||
title: state.viewer?.trackedEntity
|
title: isFollowing(state.viewer)
|
||||||
? t(
|
? t(
|
||||||
'Stop following the current object before navigating to a camera',
|
'Stop following the current object before navigating to a camera',
|
||||||
)
|
)
|
||||||
: t('Move to the nearest loaded camera and show its details'),
|
: t('Move to the nearest loaded camera and show its details'),
|
||||||
disabled:
|
disabled: !state.enabled || !count || isFollowing(state.viewer),
|
||||||
!state.enabled || !count || Boolean(state.viewer?.trackedEntity),
|
|
||||||
onClick: focusNearest,
|
onClick: focusNearest,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFollowing } from '../../data/followed.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { inputFor } from '../../input/dispatcher.js';
|
import { inputFor } from '../../input/dispatcher.js';
|
||||||
import {
|
import {
|
||||||
@@ -499,7 +500,7 @@ export function createAlprPresentation({ state, services, source }) {
|
|||||||
|
|
||||||
function focusNearest() {
|
function focusNearest() {
|
||||||
const camera = state.viewer?.camera;
|
const camera = state.viewer?.camera;
|
||||||
if (!state.enabled || !camera?.positionWC || state.viewer.trackedEntity)
|
if (!state.enabled || !camera?.positionWC || isFollowing(state.viewer))
|
||||||
return false;
|
return false;
|
||||||
let nearest = null,
|
let nearest = null,
|
||||||
distance = Infinity;
|
distance = Infinity;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { followedOf } from '../../data/followed.js';
|
||||||
import {
|
import {
|
||||||
AWARENESS_RADIUS_M,
|
AWARENESS_RADIUS_M,
|
||||||
AWARENESS_RELATIONSHIP,
|
AWARENESS_RELATIONSHIP,
|
||||||
@@ -10,7 +11,6 @@ import {
|
|||||||
SUBJECT_PRESENCE,
|
SUBJECT_PRESENCE,
|
||||||
AWARENESS_PAGE_SIZE,
|
AWARENESS_PAGE_SIZE,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
import { readEntityRecord } from '../../data/entityRecords.js';
|
|
||||||
|
|
||||||
// coverageLabel is machine-composed English from installations/controls.js
|
// coverageLabel is machine-composed English from installations/controls.js
|
||||||
// (pinned by src/data/militaryInstallations.test.mjs); translate the known
|
// (pinned by src/data/militaryInstallations.test.mjs); translate the known
|
||||||
@@ -192,9 +192,7 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function currentTrackedFlightSubject() {
|
function currentTrackedFlightSubject() {
|
||||||
const trackedKey = normalizeContextId(
|
const trackedKey = normalizeContextId(followedOf(layerState.viewer)?.key);
|
||||||
readEntityRecord(layerState.viewer?.trackedEntity)?.trackedId,
|
|
||||||
);
|
|
||||||
if (!trackedKey) return null;
|
if (!trackedKey) return null;
|
||||||
const subjects = [
|
const subjects = [
|
||||||
flightsLayer.getTrackedSubject?.(),
|
flightsLayer.getTrackedSubject?.(),
|
||||||
@@ -261,19 +259,15 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
|||||||
) {
|
) {
|
||||||
if (!subject?.position) return null;
|
if (!subject?.position) return null;
|
||||||
if (subject.layerId === 'flights' || subject.layerId === 'military') {
|
if (subject.layerId === 'flights' || subject.layerId === 'military') {
|
||||||
const trackedPosition = readEntityRecord(
|
const followed = followedOf(layerState.viewer);
|
||||||
layerState.viewer?.trackedEntity,
|
const trackedPosition = followed?.displayPosition?.();
|
||||||
)?.displayPosition?.();
|
|
||||||
if (trackedPosition) {
|
if (trackedPosition) {
|
||||||
return {
|
return {
|
||||||
position: Cesium.Cartesian3.clone(trackedPosition),
|
position: Cesium.Cartesian3.clone(trackedPosition),
|
||||||
// Only the follow camera's own contact proves presence this way; a
|
// Only the follow camera's own contact proves presence this way; a
|
||||||
// different tracked entity says nothing about this subject.
|
// different tracked entity says nothing about this subject.
|
||||||
presence:
|
presence:
|
||||||
String(
|
String(followed?.key || '') === subjectKey(subject)
|
||||||
readEntityRecord(layerState.viewer?.trackedEntity)?.trackedId ||
|
|
||||||
'',
|
|
||||||
) === subjectKey(subject)
|
|
||||||
? SUBJECT_PRESENCE.LIVE
|
? SUBJECT_PRESENCE.LIVE
|
||||||
: SUBJECT_PRESENCE.UNCHECKED,
|
: SUBJECT_PRESENCE.UNCHECKED,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { followedOf } from '../../data/followed.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import {
|
import {
|
||||||
GEO_PROGRESS_NOTIFY_INTERVAL_MS,
|
GEO_PROGRESS_NOTIFY_INTERVAL_MS,
|
||||||
@@ -125,16 +126,16 @@ export function createGeometryQueue({
|
|||||||
* throughput without restarting the queue.
|
* throughput without restarting the queue.
|
||||||
*
|
*
|
||||||
* @param {Object} [ownership={}] Current camera-ownership state.
|
* @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.
|
* @param {boolean} [ownership.cockpitActive] Whether cockpit owns the camera.
|
||||||
* @returns {{ batchSize: number, delayMs: number }} Drain pacing.
|
* @returns {{ batchSize: number, delayMs: number }} Drain pacing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function cctvGeometryDrainPacing({
|
function cctvGeometryDrainPacing({
|
||||||
trackedEntity = null,
|
followed = null,
|
||||||
cockpitActive = false,
|
cockpitActive = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (trackedEntity || cockpitActive) {
|
if (followed || cockpitActive) {
|
||||||
return {
|
return {
|
||||||
batchSize: GEO_TRACKING_BATCH_SIZE,
|
batchSize: GEO_TRACKING_BATCH_SIZE,
|
||||||
delayMs: GEO_TRACKING_BATCH_DELAY_MS,
|
delayMs: GEO_TRACKING_BATCH_DELAY_MS,
|
||||||
@@ -213,7 +214,7 @@ export function createGeometryQueue({
|
|||||||
const batchResult = processCctvGeometryDrainBatch({
|
const batchResult = processCctvGeometryDrainBatch({
|
||||||
queue: layerState._geoQueue,
|
queue: layerState._geoQueue,
|
||||||
readOwnership: () => ({
|
readOwnership: () => ({
|
||||||
trackedEntity: layerState._viewer.trackedEntity,
|
followed: followedOf(layerState._viewer),
|
||||||
cockpitActive:
|
cockpitActive:
|
||||||
typeof document !== 'undefined' &&
|
typeof document !== 'undefined' &&
|
||||||
document.body?.classList.contains('cockpit-mode'),
|
document.body?.classList.contains('cockpit-mode'),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFollowing } from '../../data/followed.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { CCTV_FOCUS_RESULT } from './policy.js';
|
import { CCTV_FOCUS_RESULT } from './policy.js';
|
||||||
import { cameraOf } from '../../render/cesium/camera.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');
|
console.debug('[Data:CCTV] focus ignored while cockpit owns the camera');
|
||||||
return CCTV_FOCUS_RESULT.COCKPIT_ACTIVE;
|
return CCTV_FOCUS_RESULT.COCKPIT_ACTIVE;
|
||||||
}
|
}
|
||||||
if (viewer.trackedEntity) {
|
if (isFollowing(viewer)) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'[Data:CCTV] focus ignored while a tracked entity owns the camera',
|
'[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 * as Cesium from 'cesium';
|
||||||
import { t } from '../../i18n/index.js';
|
import { t } from '../../i18n/index.js';
|
||||||
import { cameraOf } from '../../render/cesium/camera.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
@@ -354,7 +355,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
syncReplayButton();
|
syncReplayButton();
|
||||||
parts.overlays.syncMissionOverlayEntries();
|
parts.overlays.syncMissionOverlayEntries();
|
||||||
parts.policyHelpers.releaseAircraftTracking(layerState._dataManager);
|
parts.policyHelpers.releaseAircraftTracking(layerState._dataManager);
|
||||||
layerState._viewer.trackedEntity = undefined;
|
stopFollowing(layerState._viewer);
|
||||||
const camera = cameraOf(layerState._viewer);
|
const camera = cameraOf(layerState._viewer);
|
||||||
camera.cancelFlight();
|
camera.cancelFlight();
|
||||||
layerState._animationStarts.set(
|
layerState._animationStarts.set(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFollowing } from '../../data/followed.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import {
|
import {
|
||||||
HORIZON_CAMERA_MOVE_EPSILON_M,
|
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. */
|
/** Radio may move the globe only while no tracked entity owns the follow camera. */
|
||||||
|
|
||||||
function radioCameraNavigationAllowed(viewer = layerState._viewer) {
|
function radioCameraNavigationAllowed(viewer = layerState._viewer) {
|
||||||
return Boolean(viewer?.camera) && !viewer.trackedEntity;
|
return Boolean(viewer?.camera) && !isFollowing(viewer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function radioGlobeRecenterPlan(viewer = layerState._viewer) {
|
function radioGlobeRecenterPlan(viewer = layerState._viewer) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { onFollowChanged } from '../../data/followed.js';
|
||||||
import { inputFor } from '../../input/dispatcher.js';
|
import { inputFor } from '../../input/dispatcher.js';
|
||||||
import { VESSEL_OVERLAY_SOURCE_ID } from '../../data/vesselLabels.js';
|
import { VESSEL_OVERLAY_SOURCE_ID } from '../../data/vesselLabels.js';
|
||||||
|
|
||||||
@@ -97,14 +98,11 @@ export function createSelection({
|
|||||||
state.keyTarget = keyTarget;
|
state.keyTarget = keyTarget;
|
||||||
state.keydownHandler = onVesselKeyDown;
|
state.keydownHandler = onVesselKeyDown;
|
||||||
keyTarget.addEventListener('keydown', state.keydownHandler);
|
keyTarget.addEventListener('keydown', state.keydownHandler);
|
||||||
// Vessels never set viewer.trackedEntity, so any new tracked entity belongs
|
// Vessels never follow, so anything newly followed belongs to another
|
||||||
// to another layer and takes interaction ownership of the scene.
|
// layer and takes interaction ownership of the scene.
|
||||||
state.trackedEntityRemover = viewer.trackedEntityChanged.addEventListener(
|
state.trackedEntityRemover = onFollowChanged(viewer, (followed) => {
|
||||||
() => {
|
if (followed && state.selectedRecord) clearVesselInspection();
|
||||||
if (viewer.trackedEntity && state.selectedRecord)
|
});
|
||||||
clearVesselInspection();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Select one live vessel and request one UI-owned camera transfer. */
|
/** 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 { listenerList, toCartesian, uncaught } from './common.js';
|
||||||
import { cameraFlightActive } from './internals.js';
|
import { cameraFlightActive } from './internals.js';
|
||||||
import { createCesiumFollow } from './follow.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
|
* `cameraOf` and `findCamera`, for code that already reaches Cesium:
|
||||||
* loads the factory a viewer with no renderer bound gets its camera from.
|
* 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. */
|
/** The contract's easings as Cesium's. */
|
||||||
export const EASINGS = Object.freeze({
|
export const EASINGS = Object.freeze({
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ const INJECTED = {
|
|||||||
'./layers/recent-imagery': 'src/layers/recentImagery/rendering.js',
|
'./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'));
|
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||||
|
|
||||||
/** The browser module an export resolves to, or null for Node-only ones. */
|
/** 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)) {
|
for (const { specifier, lazy } of specifiersOf(file)) {
|
||||||
if (eager && lazy) continue;
|
if (eager && lazy) continue;
|
||||||
const next = resolve(file, specifier);
|
const next = resolve(file, specifier);
|
||||||
|
if (next === BRIDGE && FALLS_BACK.has(file)) continue;
|
||||||
if (next && !seen.has(next)) {
|
if (next && !seen.has(next)) {
|
||||||
seen.add(next);
|
seen.add(next);
|
||||||
queue.push(next);
|
queue.push(next);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
* State is persisted to localStorage and can be exported/imported as JSON.
|
* 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 { cancelCameraArrival } from '../data/cameraArrival.js';
|
||||||
import { resolveCameraPose, resolveCameraMove } from '../director/camera.js';
|
import { resolveCameraPose, resolveCameraMove } from '../director/camera.js';
|
||||||
import { createCameraMotion } from './cameraMotion.js';
|
import { createCameraMotion } from './cameraMotion.js';
|
||||||
@@ -114,7 +115,7 @@ export class SceneDirector {
|
|||||||
this._interactionTransitions = 0;
|
this._interactionTransitions = 0;
|
||||||
this._interactions = createSceneInteractions(viewer, {
|
this._interactions = createSceneInteractions(viewer, {
|
||||||
available: () =>
|
available: () =>
|
||||||
!this._destroyed && !this._running && !this.viewer.trackedEntity,
|
!this._destroyed && !this._running && !isFollowing(this.viewer),
|
||||||
execute: (action, signal) =>
|
execute: (action, signal) =>
|
||||||
this._trackWork(this._executeInteraction(action, signal)),
|
this._trackWork(this._executeInteraction(action, signal)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { followedOf } from '../data/followed.js';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { resolveImageryHost } from '../layers/weather/imageryHost.js';
|
import { resolveImageryHost } from '../layers/weather/imageryHost.js';
|
||||||
import { ShellFacade } from './shellFacade.js';
|
import { ShellFacade } from './shellFacade.js';
|
||||||
@@ -1174,7 +1175,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
setEnabled: (next) =>
|
setEnabled: (next) =>
|
||||||
this._dataManager.setEnabled('cctv', next, { origin: 'user' }),
|
this._dataManager.setEnabled('cctv', next, { origin: 'user' }),
|
||||||
readOwnership: () => ({
|
readOwnership: () => ({
|
||||||
trackedEntity: this.viewer?.trackedEntity,
|
followed: followedOf(this.viewer),
|
||||||
cockpitActive: !!this.cockpitView?.active,
|
cockpitActive: !!this.cockpitView?.active,
|
||||||
}),
|
}),
|
||||||
shouldFocus: () =>
|
shouldFocus: () =>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { onFollowChanged } from '../data/followed.js';
|
||||||
import { isExplicitLayerStateOrigin } from '../data/layerState.js';
|
import { isExplicitLayerStateOrigin } from '../data/layerState.js';
|
||||||
import {
|
import {
|
||||||
registerCctvFocusRequestListener,
|
registerCctvFocusRequestListener,
|
||||||
@@ -80,11 +81,13 @@ export class LayerBindings {
|
|||||||
window,
|
window,
|
||||||
this._worldRequestFocusHandler,
|
this._worldRequestFocusHandler,
|
||||||
);
|
);
|
||||||
this._navigationOwnerChangedRemover =
|
this._navigationOwnerChangedRemover = onFollowChanged(
|
||||||
this.viewer.trackedEntityChanged.addEventListener((entity) => {
|
this.viewer,
|
||||||
if (entity && !this._disposed)
|
(followed) => {
|
||||||
|
if (followed && !this._disposed)
|
||||||
this._stampNavigation({ cancelPendingSelection: false });
|
this._stampNavigation({ cancelPendingSelection: false });
|
||||||
});
|
},
|
||||||
|
);
|
||||||
// Vessel/installation focus flies without ever assigning a tracked entity,
|
// Vessel/installation focus flies without ever assigning a tracked entity,
|
||||||
// so it cannot reach the listener above. It announces instead.
|
// so it cannot reach the listener above. It announces instead.
|
||||||
this._removeNavigationAuthorityListener =
|
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 * as Cesium from 'cesium';
|
||||||
import { t } from '../i18n/index.js';
|
import { t } from '../i18n/index.js';
|
||||||
import { createStateChannel } from '../app/stateChannel.js';
|
import { createStateChannel } from '../app/stateChannel.js';
|
||||||
@@ -367,9 +369,10 @@ export class LocationNavigation {
|
|||||||
} catch {
|
} catch {
|
||||||
/* best-effort release */
|
/* best-effort release */
|
||||||
}
|
}
|
||||||
this.viewer.trackedEntity = undefined;
|
stopFollowing(this.viewer);
|
||||||
this.viewer.camera.cancelFlight();
|
const camera = findCamera(this.viewer);
|
||||||
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
camera?.cancelFlight();
|
||||||
|
camera?.release();
|
||||||
this._beginWorldJumpTransition();
|
this._beginWorldJumpTransition();
|
||||||
|
|
||||||
let resolveReset;
|
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 { cancelCameraArrival } from '../data/cameraArrival.js';
|
||||||
import { createStateChannel } from '../app/stateChannel.js';
|
import { createStateChannel } from '../app/stateChannel.js';
|
||||||
import * as Cesium from 'cesium';
|
|
||||||
import {
|
import {
|
||||||
beginDeferredNavigation,
|
beginDeferredNavigation,
|
||||||
reassertNavigationHandoff,
|
reassertNavigationHandoff,
|
||||||
@@ -176,12 +177,13 @@ export class NavigationController {
|
|||||||
} catch {
|
} catch {
|
||||||
/* best-effort release */
|
/* best-effort release */
|
||||||
}
|
}
|
||||||
this.viewer.trackedEntity = undefined;
|
stopFollowing(this.viewer);
|
||||||
this.interruptCameraMotion('explicit-navigation');
|
this.interruptCameraMotion('explicit-navigation');
|
||||||
this.stopOrbit();
|
this.stopOrbit();
|
||||||
if (!preserveCameraFlight) this.viewer.camera.cancelFlight();
|
const camera = findCamera(this.viewer);
|
||||||
|
if (!preserveCameraFlight) camera?.cancelFlight();
|
||||||
try {
|
try {
|
||||||
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
camera?.release();
|
||||||
} catch {
|
} catch {
|
||||||
/* teardown race */
|
/* teardown race */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFollowing, stopFollowing } from '../data/followed.js';
|
||||||
import { interruptGlobeRotation } from '../cameraVerbs.js';
|
import { interruptGlobeRotation } from '../cameraVerbs.js';
|
||||||
import {
|
import {
|
||||||
layerSnapshot,
|
layerSnapshot,
|
||||||
@@ -396,7 +397,7 @@ export function createGevActionRunner({
|
|||||||
// Explicit navigation while TRACKING supersedes the follow camera —
|
// Explicit navigation while TRACKING supersedes the follow camera —
|
||||||
// otherwise the tracker drags the view back and "I flew there but can't
|
// otherwise the tracker drags the view back and "I flew there but can't
|
||||||
// do anything" (field finding). track_entity manages its own handoff.
|
// 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);
|
stopAllTracking(viewer, dataManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,7 +1131,7 @@ export function createGevActionRunner({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
interruptCameraMotion('nav:fly_to_location');
|
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);
|
failed.add(layerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (viewer) viewer.trackedEntity = undefined;
|
stopFollowing(viewer);
|
||||||
if (failed.size) {
|
if (failed.size) {
|
||||||
const failedLayerIds = [...failed];
|
const failedLayerIds = [...failed];
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in new issue
Block a user