mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 13:22:18 +00:00
Fly the remaining flights through the camera port, on any viewer
The embed view, the celestial-ring focus, the ground-guard lift, the Bhote Koshi approach and orbit, launch selection and replay flights, and the cockpit's view and takeover now drive the renderer's camera port. The contract gains a sineInOut easing, pinned by a conformance trace. A viewer with no renderer bound, such as one a consumer of a public export brings, now gets a camera port too, as it already got an input port: cameraOf builds one over the viewer's own Cesium camera and keeps it for the viewer's life, until a renderer is bound. The Cesium adapter provides the factory as it loads; code that reaches Cesium takes cameraOf from the adapter so the factory comes with it, and findCamera answers null where a missing camera is not an error (HUD, share link, world focus, Recent Imagery's ZOOM IN). A test walks every package export and fails one that can reach the bridge without loading the factory. A destroyed viewer gets no camera and lets go of a built one, and the application renderer stays bound until its viewer is destroyed.
This commit is contained in:
1 parent
e3baeec223
commit
0b1c5d0f23
44 files changed
+1167
-344
No files matched your search
@@ -848,11 +848,22 @@
|
|||||||
"src/data/distanceFormat.js",
|
"src/data/distanceFormat.js",
|
||||||
"src/ui/cockpit.js",
|
"src/ui/cockpit.js",
|
||||||
"src/ui/cockpitBriefing.js",
|
"src/ui/cockpitBriefing.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/ui/cockpitBriefPages.js",
|
"src/ui/cockpitBriefPages.js",
|
||||||
"src/ui/cockpitContextState.js",
|
"src/ui/cockpitContextState.js",
|
||||||
"src/ui/cockpitInstrumentState.js",
|
"src/ui/cockpitInstrumentState.js",
|
||||||
"src/ui/cockpitRollingValue.js",
|
"src/ui/cockpitRollingValue.js",
|
||||||
"src/ui/cockpitCamera.js",
|
"src/ui/cockpitCamera.js",
|
||||||
|
"src/render/cesium/camera.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/internals.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/ui/cockpitContext.js",
|
"src/ui/cockpitContext.js",
|
||||||
"src/ui/cockpitController.js",
|
"src/ui/cockpitController.js",
|
||||||
"src/ui/cockpitDisplayPortal.js",
|
"src/ui/cockpitDisplayPortal.js",
|
||||||
@@ -968,7 +979,11 @@
|
|||||||
"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/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
|
"src/render/camera/events.js",
|
||||||
|
"src/render/cesium/internals.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/ui/localePicker.js",
|
"src/ui/localePicker.js",
|
||||||
"src/ui/cyberSonarControls.js",
|
"src/ui/cyberSonarControls.js",
|
||||||
"src/ui/locationNavigation.js",
|
"src/ui/locationNavigation.js",
|
||||||
@@ -1711,10 +1726,12 @@
|
|||||||
"src/layers/launches/camera.js",
|
"src/layers/launches/camera.js",
|
||||||
"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/layers/launches/ingestion.js",
|
"src/layers/launches/ingestion.js",
|
||||||
"src/layers/launches/launchPad.js",
|
"src/layers/launches/launchPad.js",
|
||||||
"src/layers/launches/lifecycle.js",
|
"src/layers/launches/lifecycle.js",
|
||||||
"src/layers/launches/model.js",
|
"src/layers/launches/model.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/layers/launches/orbitRendering.js",
|
"src/layers/launches/orbitRendering.js",
|
||||||
"src/layers/launches/overlays.js",
|
"src/layers/launches/overlays.js",
|
||||||
"src/layers/launches/panel.js",
|
"src/layers/launches/panel.js",
|
||||||
@@ -1722,6 +1739,14 @@
|
|||||||
"src/layers/launches/policy.js",
|
"src/layers/launches/policy.js",
|
||||||
"src/layers/launches/policyHelpers.js",
|
"src/layers/launches/policyHelpers.js",
|
||||||
"src/render/rendererOf.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/internals.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/layers/launches/selection.js",
|
"src/layers/launches/selection.js",
|
||||||
@@ -1761,7 +1786,17 @@
|
|||||||
"src/data/militaryInstallationData.js",
|
"src/data/militaryInstallationData.js",
|
||||||
"src/data/installationNames.js",
|
"src/data/installationNames.js",
|
||||||
"src/layers/installations/controls.js",
|
"src/layers/installations/controls.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/render/rendererOf.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/internals.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/layers/installations/index.js",
|
"src/layers/installations/index.js",
|
||||||
"src/layers/installations/ingestion.js",
|
"src/layers/installations/ingestion.js",
|
||||||
"src/layers/installations/lifecycle.js",
|
"src/layers/installations/lifecycle.js",
|
||||||
@@ -1825,11 +1860,21 @@
|
|||||||
"src/cockpitMath.js",
|
"src/cockpitMath.js",
|
||||||
"src/data/iconOrientation.js",
|
"src/data/iconOrientation.js",
|
||||||
"src/data/militaryAwarenessEngine.js",
|
"src/data/militaryAwarenessEngine.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/data/installationNames.js",
|
"src/data/installationNames.js",
|
||||||
"src/layers/awareness/controls.js",
|
"src/layers/awareness/controls.js",
|
||||||
"src/layers/awareness/dependencies.js",
|
"src/layers/awareness/dependencies.js",
|
||||||
"src/layers/awareness/focus.js",
|
"src/layers/awareness/focus.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.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/internals.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/layers/awareness/history.js",
|
"src/layers/awareness/history.js",
|
||||||
"src/layers/awareness/index.js",
|
"src/layers/awareness/index.js",
|
||||||
"src/layers/awareness/ingestion.js",
|
"src/layers/awareness/ingestion.js",
|
||||||
@@ -1987,16 +2032,26 @@
|
|||||||
"src/layers/cctv/geometryQueue.js",
|
"src/layers/cctv/geometryQueue.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/layers/cctv/health.js",
|
"src/layers/cctv/health.js",
|
||||||
"src/layers/cctv/hover.js",
|
"src/layers/cctv/hover.js",
|
||||||
"src/layers/cctv/index.js",
|
"src/layers/cctv/index.js",
|
||||||
|
"src/geo/frames.js",
|
||||||
"src/layers/cctv/ingestion.js",
|
"src/layers/cctv/ingestion.js",
|
||||||
"src/layers/cctv/lifecycle.js",
|
"src/layers/cctv/lifecycle.js",
|
||||||
"src/layers/cctv/model.js",
|
"src/layers/cctv/model.js",
|
||||||
"src/geoDistance.js",
|
"src/geoDistance.js",
|
||||||
"src/layers/cctv/modelSpecs.js",
|
"src/layers/cctv/modelSpecs.js",
|
||||||
"src/layers/cctv/navigation.js",
|
"src/layers/cctv/navigation.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
|
"src/render/camera/events.js",
|
||||||
|
"src/geo/celestial.js",
|
||||||
|
"src/geo/quaternion.js",
|
||||||
|
"src/geo/ellipsoid.js",
|
||||||
|
"src/geo/vector.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
|
"src/render/cesium/internals.js",
|
||||||
"src/layers/cctv/policy.js",
|
"src/layers/cctv/policy.js",
|
||||||
"src/layers/cctv/presentation.js",
|
"src/layers/cctv/presentation.js",
|
||||||
"src/layers/cctv/projection.js",
|
"src/layers/cctv/projection.js",
|
||||||
@@ -2052,11 +2107,21 @@
|
|||||||
],
|
],
|
||||||
"modules": [
|
"modules": [
|
||||||
"src/layers/streetLevel/index.js",
|
"src/layers/streetLevel/index.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/layers/streetLevel/credits.js",
|
"src/layers/streetLevel/credits.js",
|
||||||
"src/layers/streetLevel/providers/mapillary/index.js",
|
"src/layers/streetLevel/providers/mapillary/index.js",
|
||||||
"src/layers/streetLevel/viewerHost.js",
|
"src/layers/streetLevel/viewerHost.js",
|
||||||
"src/layers/streetLevel/cameraFraming.js",
|
"src/layers/streetLevel/cameraFraming.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.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/internals.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/layers/streetLevel/marker.js",
|
"src/layers/streetLevel/marker.js",
|
||||||
"src/layers/streetLevel/view.js",
|
"src/layers/streetLevel/view.js",
|
||||||
"src/layers/streetLevel/uiState.js",
|
"src/layers/streetLevel/uiState.js",
|
||||||
@@ -2129,10 +2194,20 @@
|
|||||||
"modules": [
|
"modules": [
|
||||||
"src/data/headingDegrees.js",
|
"src/data/headingDegrees.js",
|
||||||
"src/layers/alpr/index.js",
|
"src/layers/alpr/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",
|
||||||
"src/layers/alpr/presentation.js",
|
"src/layers/alpr/presentation.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.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/common.js",
|
||||||
|
"src/render/cesium/internals.js",
|
||||||
"src/layers/alpr/visuals.js",
|
"src/layers/alpr/visuals.js",
|
||||||
"src/layers/alpr/overlay.js",
|
"src/layers/alpr/overlay.js",
|
||||||
"src/layers/alpr/markerState.js",
|
"src/layers/alpr/markerState.js",
|
||||||
@@ -2183,6 +2258,7 @@
|
|||||||
"src/data/radioCountry.js",
|
"src/data/radioCountry.js",
|
||||||
"src/layers/radio/catalogModel.js",
|
"src/layers/radio/catalogModel.js",
|
||||||
"src/layers/radio/categories.js",
|
"src/layers/radio/categories.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/layers/radio/clustering.js",
|
"src/layers/radio/clustering.js",
|
||||||
"src/layers/radio/controls.js",
|
"src/layers/radio/controls.js",
|
||||||
"src/layers/radio/index.js",
|
"src/layers/radio/index.js",
|
||||||
@@ -2192,7 +2268,15 @@
|
|||||||
"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/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.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/common.js",
|
||||||
"src/layers/radio/playback.js",
|
"src/layers/radio/playback.js",
|
||||||
"src/layers/radio/policy.js",
|
"src/layers/radio/policy.js",
|
||||||
"src/layers/radio/presentation.js",
|
"src/layers/radio/presentation.js",
|
||||||
@@ -2237,8 +2321,18 @@
|
|||||||
"src/layers/submarineCables/geometry.js",
|
"src/layers/submarineCables/geometry.js",
|
||||||
"src/layers/submarineCables/index.js",
|
"src/layers/submarineCables/index.js",
|
||||||
"src/layers/submarineCables/ingestion.js",
|
"src/layers/submarineCables/ingestion.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/layers/submarineCables/interaction.js",
|
"src/layers/submarineCables/interaction.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.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/internals.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/layers/submarineCables/pickState.js",
|
"src/layers/submarineCables/pickState.js",
|
||||||
"src/data/entityRecords.js",
|
"src/data/entityRecords.js",
|
||||||
"src/layers/submarineCables/lifecycle.js",
|
"src/layers/submarineCables/lifecycle.js",
|
||||||
@@ -3481,6 +3575,7 @@
|
|||||||
"src/maps/imageryComparison.js",
|
"src/maps/imageryComparison.js",
|
||||||
"src/data/bhoteKoshiEmbeddedMedia.js",
|
"src/data/bhoteKoshiEmbeddedMedia.js",
|
||||||
"src/data/bhoteKoshiLocator.js",
|
"src/data/bhoteKoshiLocator.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
"src/data/bhoteKoshiIncidentPlaces.js",
|
"src/data/bhoteKoshiIncidentPlaces.js",
|
||||||
"src/data/bhoteKoshiFloodPath.js",
|
"src/data/bhoteKoshiFloodPath.js",
|
||||||
"src/data/bhoteKoshiShotPaths.js",
|
"src/data/bhoteKoshiShotPaths.js",
|
||||||
@@ -3819,6 +3914,8 @@
|
|||||||
"src/layers/vessels/tracking.js",
|
"src/layers/vessels/tracking.js",
|
||||||
"src/locations.js",
|
"src/locations.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
|
"src/render/camera/events.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/cityPois.js",
|
"src/cityPois.js",
|
||||||
"src/navigationPolicy.js",
|
"src/navigationPolicy.js",
|
||||||
"src/overlays/worldOverlay.js",
|
"src/overlays/worldOverlay.js",
|
||||||
@@ -4121,7 +4218,16 @@
|
|||||||
"src/keylessGeocoder.js",
|
"src/keylessGeocoder.js",
|
||||||
"src/layers/firms/anchors.js",
|
"src/layers/firms/anchors.js",
|
||||||
"src/locations.js",
|
"src/locations.js",
|
||||||
|
"src/render/cesium/camera.js",
|
||||||
"src/render/rendererOf.js",
|
"src/render/rendererOf.js",
|
||||||
|
"src/render/camera/events.js",
|
||||||
|
"src/geo/index.js",
|
||||||
|
"src/geo/celestial.js",
|
||||||
|
"src/geo/quaternion.js",
|
||||||
|
"src/geo/frames.js",
|
||||||
|
"src/geo/ellipsoid.js",
|
||||||
|
"src/geo/vector.js",
|
||||||
|
"src/render/cesium/common.js",
|
||||||
"src/cityPois.js",
|
"src/cityPois.js",
|
||||||
"src/cameraGroundGuard.js",
|
"src/cameraGroundGuard.js",
|
||||||
"src/search/alpsGeocoder.js",
|
"src/search/alpsGeocoder.js",
|
||||||
@@ -4713,6 +4819,15 @@
|
|||||||
"src/data/headingDegrees.js",
|
"src/data/headingDegrees.js",
|
||||||
"src/cyberSonar.js",
|
"src/cyberSonar.js",
|
||||||
"src/celestialRing.js",
|
"src/celestialRing.js",
|
||||||
|
"src/render/cesium/camera.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/common.js",
|
||||||
"src/data/approxDistance.js",
|
"src/data/approxDistance.js",
|
||||||
"src/data/cameraSensitivity.js",
|
"src/data/cameraSensitivity.js",
|
||||||
"src/data/contactPlayback.js",
|
"src/data/contactPlayback.js",
|
||||||
@@ -4733,6 +4848,7 @@
|
|||||||
"src/data/spriteOrder.js",
|
"src/data/spriteOrder.js",
|
||||||
"src/data/terrainHeights.js",
|
"src/data/terrainHeights.js",
|
||||||
"src/data/transit.js",
|
"src/data/transit.js",
|
||||||
|
"src/geo/index.js",
|
||||||
"src/data/transitFeeds.js",
|
"src/data/transitFeeds.js",
|
||||||
"src/data/transitIcons.js",
|
"src/data/transitIcons.js",
|
||||||
"src/data/transitPresetStyle.js",
|
"src/data/transitPresetStyle.js",
|
||||||
|
|||||||
+13
-14
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { announceNavigationAuthority } from '../navigationPolicy.js';
|
import { announceNavigationAuthority } from '../navigationPolicy.js';
|
||||||
|
import { cameraOf } from '../render/cesium/camera.js';
|
||||||
import { annotationsFromParams, createView } from '../view/index.js';
|
import { annotationsFromParams, createView } from '../view/index.js';
|
||||||
|
|
||||||
const EMBED_VIEW_MESSAGE = 'gev:view';
|
const EMBED_VIEW_MESSAGE = 'gev:view';
|
||||||
@@ -147,21 +148,19 @@ const delay = (ms, signal) =>
|
|||||||
function flyTo(viewer, camera) {
|
function flyTo(viewer, camera) {
|
||||||
announceNavigationAuthority('embed-view');
|
announceNavigationAuthority('embed-view');
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
viewer.camera.flyTo({
|
cameraOf(viewer).flyTo(
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
{ lon: camera.lon, lat: camera.lat, height: camera.altitude_m },
|
||||||
camera.lon,
|
{
|
||||||
camera.lat,
|
orientation: {
|
||||||
camera.altitude_m,
|
heading: Cesium.Math.toRadians(camera.heading_deg),
|
||||||
),
|
pitch: Cesium.Math.toRadians(camera.pitch_deg),
|
||||||
orientation: {
|
roll: 0,
|
||||||
heading: Cesium.Math.toRadians(camera.heading_deg),
|
},
|
||||||
pitch: Cesium.Math.toRadians(camera.pitch_deg),
|
durationS: FLIGHT_SECONDS,
|
||||||
roll: 0,
|
onComplete: () => resolve(true),
|
||||||
|
onCancel: () => resolve(false),
|
||||||
},
|
},
|
||||||
duration: FLIGHT_SECONDS,
|
);
|
||||||
complete: () => resolve(true),
|
|
||||||
cancel: () => resolve(false),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +13,7 @@ import { createView } from '../view/index.js';
|
|||||||
|
|
||||||
const fakeViewer = () => {
|
const fakeViewer = () => {
|
||||||
const flights = [];
|
const flights = [];
|
||||||
return {
|
return bindFakeCesiumCamera({
|
||||||
flights,
|
flights,
|
||||||
camera: {
|
camera: {
|
||||||
flyTo(options) {
|
flyTo(options) {
|
||||||
@@ -20,7 +21,7 @@ const fakeViewer = () => {
|
|||||||
options.complete();
|
options.complete();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
const fakeLayers = (enabled) => ({
|
const fakeLayers = (enabled) => ({
|
||||||
getAll: () =>
|
getAll: () =>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
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';
|
||||||
|
import { cameraOf } from './render/cesium/camera.js';
|
||||||
import { cameraFlightActive } from './render/cesium/internals.js';
|
import { cameraFlightActive } from './render/cesium/internals.js';
|
||||||
|
|
||||||
/** Minimum eye height above the rendered surface for a usable view. */
|
/** Minimum eye height above the rendered surface for a usable view. */
|
||||||
@@ -196,20 +197,22 @@ export function guardCameraAboveGround(viewer, at, options = {}) {
|
|||||||
);
|
);
|
||||||
if (deficit > 0) {
|
if (deficit > 0) {
|
||||||
stop();
|
stop();
|
||||||
camera.flyTo({
|
cameraOf(viewer).flyTo(
|
||||||
destination: Cesium.Cartesian3.fromRadians(
|
Cesium.Cartesian3.fromRadians(
|
||||||
carto.longitude,
|
carto.longitude,
|
||||||
carto.latitude,
|
carto.latitude,
|
||||||
carto.height + deficit,
|
carto.height + deficit,
|
||||||
),
|
),
|
||||||
orientation: {
|
{
|
||||||
heading: camera.heading,
|
orientation: {
|
||||||
pitch: camera.pitch,
|
heading: camera.heading,
|
||||||
roll: camera.roll,
|
pitch: camera.pitch,
|
||||||
|
roll: camera.roll,
|
||||||
|
},
|
||||||
|
durationS: 0.9,
|
||||||
|
easing: 'cubicInOut',
|
||||||
},
|
},
|
||||||
duration: 0.9,
|
);
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
onLift?.(deficit);
|
onLift?.(deficit);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Arrival framing is computed before the destination's tiles exist, so it
|
// Arrival framing is computed before the destination's tiles exist, so it
|
||||||
// works from a predicted ground height. The guard measures the real surface
|
// works from a predicted ground height. The guard measures the real surface
|
||||||
// after arrival and lifts the eye; these tests pin when it acts.
|
// after arrival and lifts the eye; these tests pin when it acts.
|
||||||
|
import { bindFakeCesiumCamera } from './testSupport/fakeCameraPort.mjs';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
@@ -49,8 +50,12 @@ test('the clearance frames a subject without turning into an overflight', () =>
|
|||||||
|
|
||||||
function fakeViewer({ cameraHeightM, surfaceM }) {
|
function fakeViewer({ cameraHeightM, surfaceM }) {
|
||||||
const flights = [];
|
const flights = [];
|
||||||
const position = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, cameraHeightM);
|
const position = Cesium.Cartographic.fromDegrees(
|
||||||
return {
|
-97.7603,
|
||||||
|
30.3141,
|
||||||
|
cameraHeightM,
|
||||||
|
);
|
||||||
|
return bindFakeCesiumCamera({
|
||||||
flights,
|
flights,
|
||||||
scene: {
|
scene: {
|
||||||
canvas: null,
|
canvas: null,
|
||||||
@@ -71,7 +76,7 @@ function fakeViewer({ cameraHeightM, surfaceM }) {
|
|||||||
this._currentFlight = {};
|
this._currentFlight = {};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('the guard lifts a buried arrival once the surface answers', async () => {
|
test('the guard lifts a buried arrival once the surface answers', async () => {
|
||||||
@@ -111,7 +116,11 @@ test('the guard waits for streaming tiles and yields to a newer arrival', async
|
|||||||
|
|
||||||
test('the guard leaves a clear view untouched', async () => {
|
test('the guard leaves a clear view untouched', async () => {
|
||||||
const viewer = fakeViewer({ cameraHeightM: 400, surfaceM: 171 });
|
const viewer = fakeViewer({ cameraHeightM: 400, surfaceM: 171 });
|
||||||
guardCameraAboveGround(viewer, { lat: 30.3125, lon: -97.765 }, { intervalMs: 1 });
|
guardCameraAboveGround(
|
||||||
|
viewer,
|
||||||
|
{ lat: 30.3125, lon: -97.765 },
|
||||||
|
{ intervalMs: 1 },
|
||||||
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
assert.equal(viewer.flights.length, 0);
|
assert.equal(viewer.flights.length, 0);
|
||||||
});
|
});
|
||||||
@@ -142,27 +151,58 @@ function handoffFixture(t) {
|
|||||||
const originalFly = viewer.camera.flyTo;
|
const originalFly = viewer.camera.flyTo;
|
||||||
const navigation = new NavigationController({
|
const navigation = new NavigationController({
|
||||||
viewer,
|
viewer,
|
||||||
tracking: Object.fromEntries(['flightsLayer', 'militaryFlightsLayer',
|
tracking: Object.fromEntries(
|
||||||
'satellitesLayer', 'aisLiveVesselsLayer', 'militaryAwarenessLayer',
|
[
|
||||||
'rocketLaunchesLayer'].map((key) => [key, {}])),
|
'flightsLayer',
|
||||||
cancelOrientation() {}, clearLocation() {}, cancelShareSelection() {},
|
'militaryFlightsLayer',
|
||||||
getDataManager() {}, interruptCameraMotion() {}, stopOrbit() {},
|
'satellitesLayer',
|
||||||
isCockpitActive: () => false, showToast() {},
|
'aisLiveVesselsLayer',
|
||||||
|
'militaryAwarenessLayer',
|
||||||
|
'rocketLaunchesLayer',
|
||||||
|
].map((key) => [key, {}]),
|
||||||
|
),
|
||||||
|
cancelOrientation() {},
|
||||||
|
clearLocation() {},
|
||||||
|
cancelShareSelection() {},
|
||||||
|
getDataManager() {},
|
||||||
|
interruptCameraMotion() {},
|
||||||
|
stopOrbit() {},
|
||||||
|
isCockpitActive: () => false,
|
||||||
|
showToast() {},
|
||||||
});
|
});
|
||||||
guardCameraAboveGround(viewer, { lat: 30.3, lon: -97.7 });
|
guardCameraAboveGround(viewer, { lat: 30.3, lon: -97.7 });
|
||||||
return { viewer, navigation, originalFly };
|
return { viewer, navigation, originalFly };
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const handoff of ['Director', 'tracking', 'follow', 'keyboard', 'UI',
|
for (const handoff of [
|
||||||
'flyTo', 'flyToBoundingSphere', 'setView', 'layer-disable', 'layer-destroy', 'app-stop']) {
|
'Director',
|
||||||
|
'tracking',
|
||||||
|
'follow',
|
||||||
|
'keyboard',
|
||||||
|
'UI',
|
||||||
|
'flyTo',
|
||||||
|
'flyToBoundingSphere',
|
||||||
|
'setView',
|
||||||
|
'layer-disable',
|
||||||
|
'layer-destroy',
|
||||||
|
'app-stop',
|
||||||
|
]) {
|
||||||
test(`arrival guard relinquishes and removes hooks on ${handoff}`, async (t) => {
|
test(`arrival guard relinquishes and removes hooks on ${handoff}`, async (t) => {
|
||||||
const { viewer, navigation, originalFly } = handoffFixture(t);
|
const { viewer, navigation, originalFly } = handoffFixture(t);
|
||||||
const methods = ['flyTo', 'flyToBoundingSphere', 'setView', 'lookAt', 'cancelFlight']
|
const methods = [
|
||||||
.map((name) => viewer.camera[name]);
|
'flyTo',
|
||||||
|
'flyToBoundingSphere',
|
||||||
|
'setView',
|
||||||
|
'lookAt',
|
||||||
|
'cancelFlight',
|
||||||
|
].map((name) => viewer.camera[name]);
|
||||||
if (handoff === 'Director') {
|
if (handoff === 'Director') {
|
||||||
SceneDirector.prototype._claimCameraOwnership.call({
|
SceneDirector.prototype._claimCameraOwnership.call({
|
||||||
viewer, styleManager: { runImmediateNavigation: (noun, fn) =>
|
viewer,
|
||||||
navigation._runExplicitNavigation(noun, fn) },
|
styleManager: {
|
||||||
|
runImmediateNavigation: (noun, fn) =>
|
||||||
|
navigation._runExplicitNavigation(noun, fn),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else if (handoff === 'tracking') {
|
} else if (handoff === 'tracking') {
|
||||||
viewer.trackedEntityChanged.raiseEvent({ id: 'contact' });
|
viewer.trackedEntityChanged.raiseEvent({ id: 'contact' });
|
||||||
@@ -173,19 +213,31 @@ for (const handoff of ['Director', 'tracking', 'follow', 'keyboard', 'UI',
|
|||||||
} else if (handoff === 'app-stop') navigation.stop();
|
} else if (handoff === 'app-stop') navigation.stop();
|
||||||
else if (handoff.startsWith('layer-')) {
|
else if (handoff.startsWith('layer-')) {
|
||||||
const lifecycle = new LayerLifecycle(viewer);
|
const lifecycle = new LayerLifecycle(viewer);
|
||||||
lifecycle.register({ id: 'test', init() {}, enable() {}, disable() {}, destroy() {} });
|
lifecycle.register({
|
||||||
|
id: 'test',
|
||||||
|
init() {},
|
||||||
|
enable() {},
|
||||||
|
disable() {},
|
||||||
|
destroy() {},
|
||||||
|
});
|
||||||
await lifecycle.setEnabled('test', true);
|
await lifecycle.setEnabled('test', true);
|
||||||
if (handoff === 'layer-disable') await lifecycle.setEnabled('test', false);
|
if (handoff === 'layer-disable')
|
||||||
|
await lifecycle.setEnabled('test', false);
|
||||||
else await lifecycle.destroyLayer('test');
|
else await lifecycle.destroyLayer('test');
|
||||||
} else viewer.camera[handoff]({});
|
} else viewer.camera[handoff]({});
|
||||||
// A direct flyTo is itself one recorded flight; the guard adds none.
|
// A direct flyTo is itself one recorded flight; the guard adds none.
|
||||||
const priorFlights = viewer.flights.length;
|
const priorFlights = viewer.flights.length;
|
||||||
t.mock.timers.tick(10000);
|
t.mock.timers.tick(10000);
|
||||||
assert.equal(viewer.flights.length, priorFlights, 'the old probe cannot fly');
|
assert.equal(
|
||||||
|
viewer.flights.length,
|
||||||
|
priorFlights,
|
||||||
|
'the old probe cannot fly',
|
||||||
|
);
|
||||||
assert.equal(viewer.camera.flyTo, originalFly);
|
assert.equal(viewer.camera.flyTo, originalFly);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
['flyTo', 'flyToBoundingSphere', 'setView', 'lookAt', 'cancelFlight']
|
['flyTo', 'flyToBoundingSphere', 'setView', 'lookAt', 'cancelFlight'].map(
|
||||||
.map((name) => viewer.camera[name]),
|
(name) => viewer.camera[name],
|
||||||
|
),
|
||||||
methods,
|
methods,
|
||||||
'the guard never replaces camera methods',
|
'the guard never replaces camera methods',
|
||||||
);
|
);
|
||||||
@@ -196,7 +248,11 @@ for (const handoff of ['Director', 'tracking', 'follow', 'keyboard', 'UI',
|
|||||||
|
|
||||||
test('the guard yields to a camera moved without any camera method', async () => {
|
test('the guard yields to a camera moved without any camera method', async () => {
|
||||||
const viewer = fakeViewer({ cameraHeightM: 172, surfaceM: 171 });
|
const viewer = fakeViewer({ cameraHeightM: 172, surfaceM: 171 });
|
||||||
guardCameraAboveGround(viewer, { lat: 30.3125, lon: -97.765 }, { intervalMs: 5 });
|
guardCameraAboveGround(
|
||||||
|
viewer,
|
||||||
|
{ lat: 30.3125, lon: -97.765 },
|
||||||
|
{ intervalMs: 5 },
|
||||||
|
);
|
||||||
// A cockpit or follow frame writes the pose directly.
|
// A cockpit or follow frame writes the pose directly.
|
||||||
viewer.camera.directionWC = new Cesium.Cartesian3(1, 0, 0);
|
viewer.camera.directionWC = new Cesium.Cartesian3(1, 0, 0);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
@@ -205,20 +261,30 @@ test('the guard yields to a camera moved without any camera method', async () =>
|
|||||||
|
|
||||||
test('Cesium lifting a buried eye straight up is not another owner', async () => {
|
test('Cesium lifting a buried eye straight up is not another owner', async () => {
|
||||||
const viewer = fakeViewer({ cameraHeightM: 104, surfaceM: 171 });
|
const viewer = fakeViewer({ cameraHeightM: 104, surfaceM: 171 });
|
||||||
guardCameraAboveGround(viewer, { lat: 30.3125, lon: -97.765 }, { intervalMs: 5 });
|
guardCameraAboveGround(
|
||||||
|
viewer,
|
||||||
|
{ lat: 30.3125, lon: -97.765 },
|
||||||
|
{ intervalMs: 5 },
|
||||||
|
);
|
||||||
// Terrain collision raises the eye to just above the loaded ground.
|
// Terrain collision raises the eye to just above the loaded ground.
|
||||||
const raised = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, 172);
|
const raised = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, 172);
|
||||||
viewer.camera.positionCartographic = raised;
|
viewer.camera.positionCartographic = raised;
|
||||||
viewer.camera.positionWC = Cesium.Cartographic.toCartesian(raised);
|
viewer.camera.positionWC = Cesium.Cartographic.toCartesian(raised);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||||
assert.equal(viewer.flights.length, 1, 'the guard still lifts the eye');
|
assert.equal(viewer.flights.length, 1, 'the guard still lifts the eye');
|
||||||
const lifted = Cesium.Cartographic.fromCartesian(viewer.flights[0].destination);
|
const lifted = Cesium.Cartographic.fromCartesian(
|
||||||
|
viewer.flights[0].destination,
|
||||||
|
);
|
||||||
assert.ok(Math.abs(lifted.height - (171 + MIN_EYE_CLEARANCE_M)) < 0.5);
|
assert.ok(Math.abs(lifted.height - (171 + MIN_EYE_CLEARANCE_M)) < 0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('another owner lowering the eye is not corrected', async () => {
|
test('another owner lowering the eye is not corrected', async () => {
|
||||||
const viewer = fakeViewer({ cameraHeightM: 172, surfaceM: 171 });
|
const viewer = fakeViewer({ cameraHeightM: 172, surfaceM: 171 });
|
||||||
guardCameraAboveGround(viewer, { lat: 30.3125, lon: -97.765 }, { intervalMs: 5 });
|
guardCameraAboveGround(
|
||||||
|
viewer,
|
||||||
|
{ lat: 30.3125, lon: -97.765 },
|
||||||
|
{ intervalMs: 5 },
|
||||||
|
);
|
||||||
const lowered = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, 150);
|
const lowered = Cesium.Cartographic.fromDegrees(-97.7603, 30.3141, 150);
|
||||||
viewer.camera.positionCartographic = lowered;
|
viewer.camera.positionCartographic = lowered;
|
||||||
viewer.camera.positionWC = Cesium.Cartographic.toCartesian(lowered);
|
viewer.camera.positionWC = Cesium.Cartographic.toCartesian(lowered);
|
||||||
|
|||||||
+320
-146
@@ -1,7 +1,13 @@
|
|||||||
import { LayerBindings } from './ui/layerBindings.js';
|
import { LayerBindings } from './ui/layerBindings.js';
|
||||||
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
|
import {
|
||||||
|
readShellSource,
|
||||||
|
shellMethod,
|
||||||
|
} from './testSupport/readShellSource.mjs';
|
||||||
import { readLayerSource } from './testSupport/readLayerSource.mjs';
|
import { readLayerSource } from './testSupport/readLayerSource.mjs';
|
||||||
import { enter as cockpitEnter, navigateContext } from './ui/cockpitTrackingController.js';
|
import {
|
||||||
|
enter as cockpitEnter,
|
||||||
|
navigateContext,
|
||||||
|
} from './ui/cockpitTrackingController.js';
|
||||||
import { CockpitViewController } from './ui/cockpitController.js';
|
import { CockpitViewController } from './ui/cockpitController.js';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@@ -11,11 +17,24 @@ import { fileURLToPath } from 'node:url';
|
|||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const ui = readShellSource();
|
const ui = readShellSource();
|
||||||
const firms = readLayerSource(path.join(ROOT, 'src', 'data', 'firmsHeatmap.js'));
|
const firms = readLayerSource(
|
||||||
const vessels = readLayerSource(path.join(ROOT, 'src', 'data', 'aisLiveVessels.js'));
|
path.join(ROOT, 'src', 'data', 'firmsHeatmap.js'),
|
||||||
const voice = fs.readFileSync(path.join(ROOT, 'src', 'voice', 'gevActions.js'), 'utf8');
|
);
|
||||||
const cameraVerbs = fs.readFileSync(path.join(ROOT, 'src', 'cameraVerbs.js'), 'utf8');
|
const vessels = readLayerSource(
|
||||||
const cockpitTracking = fs.readFileSync(path.join(ROOT, 'src', 'cockpitTracking.js'), 'utf8');
|
path.join(ROOT, 'src', 'data', 'aisLiveVessels.js'),
|
||||||
|
);
|
||||||
|
const voice = fs.readFileSync(
|
||||||
|
path.join(ROOT, 'src', 'voice', 'gevActions.js'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const cameraVerbs = fs.readFileSync(
|
||||||
|
path.join(ROOT, 'src', 'cameraVerbs.js'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const cockpitTracking = fs.readFileSync(
|
||||||
|
path.join(ROOT, 'src', 'cockpitTracking.js'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
function body(source, pattern, label) {
|
function body(source, pattern, label) {
|
||||||
const name = pattern.source.match(/^(\w+)/)?.[1];
|
const name = pattern.source.match(/^(\w+)/)?.[1];
|
||||||
@@ -31,8 +50,14 @@ function body(source, pattern, label) {
|
|||||||
function ordered(source, needles, label) {
|
function ordered(source, needles, label) {
|
||||||
let previous = -1;
|
let previous = -1;
|
||||||
for (const needle of needles) {
|
for (const needle of needles) {
|
||||||
const match = needle instanceof RegExp ? needle.exec(source.slice(previous + 1)) : null;
|
const match =
|
||||||
const index = needle instanceof RegExp ? (match ? previous + 1 + match.index : -1) : source.indexOf(needle, previous + 1);
|
needle instanceof RegExp ? needle.exec(source.slice(previous + 1)) : null;
|
||||||
|
const index =
|
||||||
|
needle instanceof RegExp
|
||||||
|
? match
|
||||||
|
? previous + 1 + match.index
|
||||||
|
: -1
|
||||||
|
: source.indexOf(needle, previous + 1);
|
||||||
assert.ok(index >= 0, `${label}: missing ${needle}`);
|
assert.ok(index >= 0, `${label}: missing ${needle}`);
|
||||||
assert.ok(index > previous, `${label}: ${needle} is out of order`);
|
assert.ok(index > previous, `${label}: ${needle} is out of order`);
|
||||||
previous = index;
|
previous = index;
|
||||||
@@ -41,12 +66,16 @@ function ordered(source, needles, label) {
|
|||||||
|
|
||||||
test('Cockpit takeover invalidates deferred work before camera cancellation', () => {
|
test('Cockpit takeover invalidates deferred work before camera cancellation', () => {
|
||||||
const enter = cockpitEnter.toString();
|
const enter = cockpitEnter.toString();
|
||||||
ordered(enter, [
|
ordered(
|
||||||
'if (!info || !entity?.position) return false;',
|
enter,
|
||||||
'this.onCameraTakeover?.();',
|
[
|
||||||
'this.viewer.camera.cancelFlight();',
|
'if (!info || !entity?.position) return false;',
|
||||||
'this.viewer.trackedEntity = undefined;',
|
'this.onCameraTakeover?.();',
|
||||||
], 'Cockpit takeover');
|
'cameraOf(this.viewer).cancelFlight();',
|
||||||
|
'this.viewer.trackedEntity = undefined;',
|
||||||
|
],
|
||||||
|
'Cockpit takeover',
|
||||||
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
ui,
|
ui,
|
||||||
/onCameraTakeover: \(\) =>\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\),/,
|
/onCameraTakeover: \(\) =>\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\),/,
|
||||||
@@ -80,11 +109,23 @@ test('navigation clears dormant tracker IDs without aborting unrelated layer res
|
|||||||
);
|
);
|
||||||
assert.doesNotMatch(stamp, /cancelPendingRestores\(\)/);
|
assert.doesNotMatch(stamp, /cancelPendingRestores\(\)/);
|
||||||
assert.match(stamp, /flightsLayer\.cancelPendingTrackingRestore\?\.\(\)/);
|
assert.match(stamp, /flightsLayer\.cancelPendingTrackingRestore\?\.\(\)/);
|
||||||
assert.match(stamp, /militaryFlightsLayer\.cancelPendingTrackingRestore\?\.\(\)/);
|
assert.match(
|
||||||
|
stamp,
|
||||||
|
/militaryFlightsLayer\.cancelPendingTrackingRestore\?\.\(\)/,
|
||||||
|
);
|
||||||
assert.match(stamp, /satellitesLayer\.cancelPendingTrackingRestore\?\.\(\)/);
|
assert.match(stamp, /satellitesLayer\.cancelPendingTrackingRestore\?\.\(\)/);
|
||||||
assert.match(stamp, /if \(\s*!passivelyClearedShareSelection\s*&&\s*!flightsLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedFlightsTrackingId: null/);
|
assert.match(
|
||||||
assert.match(stamp, /if \(\s*!passivelyClearedShareSelection\s*&&\s*!militaryFlightsLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedMilitaryTrackingId: null/);
|
stamp,
|
||||||
assert.match(stamp, /if \(\s*!passivelyClearedShareSelection\s*&&\s*!satellitesLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedSatTrackingId: null/);
|
/if \(\s*!passivelyClearedShareSelection\s*&&\s*!flightsLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedFlightsTrackingId: null/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
stamp,
|
||||||
|
/if \(\s*!passivelyClearedShareSelection\s*&&\s*!militaryFlightsLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedMilitaryTrackingId: null/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
stamp,
|
||||||
|
/if \(\s*!passivelyClearedShareSelection\s*&&\s*!satellitesLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedSatTrackingId: null/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('voice Cockpit entry reaches the camera only through stamping seams', () => {
|
test('voice Cockpit entry reaches the camera only through stamping seams', () => {
|
||||||
@@ -104,10 +145,14 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () =>
|
|||||||
/export function enterCockpitWithTracking\(\{[\s\S]*?\n\}\) \{([\s\S]*?)\n\}\n/,
|
/export function enterCockpitWithTracking\(\{[\s\S]*?\n\}\) \{([\s\S]*?)\n\}\n/,
|
||||||
'Cockpit entry transaction',
|
'Cockpit entry transaction',
|
||||||
);
|
);
|
||||||
ordered(transaction, [
|
ordered(
|
||||||
/if \(\s*!selectedLayer\.trackById\?\.\(selectedTarget\.id, \{\s*origin: selectionOrigin,?\s*\}\)\s*\) \{/,
|
transaction,
|
||||||
'if (!entryError) entered = Boolean(cockpitView.enter());',
|
[
|
||||||
], 'Cockpit entry transaction');
|
/if \(\s*!selectedLayer\.trackById\?\.\(selectedTarget\.id, \{\s*origin: selectionOrigin,?\s*\}\)\s*\) \{/,
|
||||||
|
'if (!entryError) entered = Boolean(cockpitView.enter());',
|
||||||
|
],
|
||||||
|
'Cockpit entry transaction',
|
||||||
|
);
|
||||||
// No direct camera control: every mutation goes through a layer tracker or
|
// No direct camera control: every mutation goes through a layer tracker or
|
||||||
// the cockpit controller, both of which stamp.
|
// the cockpit controller, both of which stamp.
|
||||||
assert.doesNotMatch(transaction, /\b(?:viewer\.)?camera\s*(?:\.|\[|=)/);
|
assert.doesNotMatch(transaction, /\b(?:viewer\.)?camera\s*(?:\.|\[|=)/);
|
||||||
@@ -146,27 +191,41 @@ test('voice Cockpit next/previous shares the manual Context navigation path', ()
|
|||||||
/this\._listen\(this\.contextNext, 'click', \(\) =>\s*this\.navigateContext\(1, \{ origin: 'user' \}\),?\s*\);/,
|
/this\._listen\(this\.contextNext, 'click', \(\) =>\s*this\.navigateContext\(1, \{ origin: 'user' \}\),?\s*\);/,
|
||||||
);
|
);
|
||||||
const funnel = navigateContext.toString();
|
const funnel = navigateContext.toString();
|
||||||
assert.match(funnel, /const navigationOptions = wasActive\s*\? \{ \.\.\.options, aircraftOnly: true \}\s*: options;/);
|
assert.match(
|
||||||
ordered(funnel, [
|
funnel,
|
||||||
"const method = direction < 0 ? 'navigatePrevious' : 'navigateNext';",
|
/const navigationOptions = wasActive\s*\? \{ \.\.\.options, aircraftOnly: true \}\s*: options;/,
|
||||||
'const navigationOptions = wasActive',
|
);
|
||||||
'militaryAwarenessLayer?.[method]?.(navigationOptions)',
|
ordered(
|
||||||
'this._adoptTrackedEntity(performance.now());',
|
funnel,
|
||||||
], 'Cockpit Context navigation funnel');
|
[
|
||||||
|
"const method = direction < 0 ? 'navigatePrevious' : 'navigateNext';",
|
||||||
|
'const navigationOptions = wasActive',
|
||||||
|
'militaryAwarenessLayer?.[method]?.(navigationOptions)',
|
||||||
|
'this._adoptTrackedEntity(performance.now());',
|
||||||
|
],
|
||||||
|
'Cockpit Context navigation funnel',
|
||||||
|
);
|
||||||
const navigate = body(
|
const navigate = body(
|
||||||
ui,
|
ui,
|
||||||
/if \(normalized === 'next' \|\| normalized === 'previous'\) \{([\s\S]*?)\n \}/,
|
/if \(normalized === 'next' \|\| normalized === 'previous'\) \{([\s\S]*?)\n \}/,
|
||||||
'controlCockpit navigation branch',
|
'controlCockpit navigation branch',
|
||||||
);
|
);
|
||||||
ordered(navigate, [
|
ordered(
|
||||||
'this.cockpitView.navigateContext(',
|
navigate,
|
||||||
"normalized === 'next' ? 1 : -1,",
|
['this.cockpitView.navigateContext(', "normalized === 'next' ? 1 : -1,"],
|
||||||
], 'controlCockpit navigation branch');
|
'controlCockpit navigation branch',
|
||||||
|
);
|
||||||
// No private camera route around the awareness layer.
|
// No private camera route around the awareness layer.
|
||||||
assert.doesNotMatch(navigate, /flyTo|camera|trackById|_runExplicitNavigation/);
|
assert.doesNotMatch(
|
||||||
|
navigate,
|
||||||
|
/flyTo|camera|trackById|_runExplicitNavigation/,
|
||||||
|
);
|
||||||
// An exhausted cohort is an honest failure, not a silent success.
|
// An exhausted cohort is an honest failure, not a silent success.
|
||||||
assert.match(navigate, /ok: changed,/);
|
assert.match(navigate, /ok: changed,/);
|
||||||
assert.match(navigate, /error: changed \? null : 'No further context target was available',/);
|
assert.match(
|
||||||
|
navigate,
|
||||||
|
/error: changed \? null : 'No further context target was available',/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('accepted navigation releases through PR15-aware ownership before flight', () => {
|
test('accepted navigation releases through PR15-aware ownership before flight', () => {
|
||||||
@@ -175,37 +234,69 @@ test('accepted navigation releases through PR15-aware ownership before flight',
|
|||||||
/_runExplicitNavigation\(noun, navigate, releaseOptions = undefined\) \{([\s\S]*?)\n \}/,
|
/_runExplicitNavigation\(noun, navigate, releaseOptions = undefined\) \{([\s\S]*?)\n \}/,
|
||||||
'explicit navigation',
|
'explicit navigation',
|
||||||
);
|
);
|
||||||
ordered(run, [
|
ordered(
|
||||||
'cockpitActive: this.isCockpitActive()',
|
run,
|
||||||
'stamp: () => this._stampNavigation()',
|
[
|
||||||
'release: () => this._releaseFollowCamera(releaseOptions)',
|
'cockpitActive: this.isCockpitActive()',
|
||||||
'navigate,',
|
'stamp: () => this._stampNavigation()',
|
||||||
], 'explicit navigation');
|
'release: () => this._releaseFollowCamera(releaseOptions)',
|
||||||
|
'navigate,',
|
||||||
|
],
|
||||||
|
'explicit navigation',
|
||||||
|
);
|
||||||
const release = body(
|
const release = body(
|
||||||
ui,
|
ui,
|
||||||
/_releaseFollowCamera\(\{[\s\S]*?\} = \{\}\) \{([\s\S]*?)\n \}/,
|
/_releaseFollowCamera\(\{[\s\S]*?\} = \{\}\) \{([\s\S]*?)\n \}/,
|
||||||
'follow release',
|
'follow release',
|
||||||
);
|
);
|
||||||
ordered(release, [
|
ordered(
|
||||||
'origin: trackingOrigin',
|
release,
|
||||||
'satellitesLayer.stopTracking?.({ origin: trackingOrigin })',
|
[
|
||||||
'rocketLaunchesLayer.releaseCameraOwnership?.()',
|
'origin: trackingOrigin',
|
||||||
'this.viewer.trackedEntity = undefined;',
|
'satellitesLayer.stopTracking?.({ origin: trackingOrigin })',
|
||||||
"interruptCameraMotion('explicit-navigation')",
|
'rocketLaunchesLayer.releaseCameraOwnership?.()',
|
||||||
'this.viewer.camera.cancelFlight();',
|
'this.viewer.trackedEntity = undefined;',
|
||||||
'this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);',
|
"interruptCameraMotion('explicit-navigation')",
|
||||||
], 'follow release');
|
'this.viewer.camera.cancelFlight();',
|
||||||
assert.match(release, /flightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/);
|
'this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);',
|
||||||
assert.match(release, /militaryFlightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/);
|
],
|
||||||
|
'follow release',
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
release,
|
||||||
|
/flightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
release,
|
||||||
|
/militaryFlightsLayer\.stopTracking\?\.\(\{ origin: trackingOrigin \}\)/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('validated voice camera destinations share the UI navigation authority facade', () => {
|
test('validated voice camera destinations share the UI navigation authority facade', () => {
|
||||||
assert.match(ui, /runImmediateNavigation\(noun, navigate, releaseOptions = undefined\) \{\s*return this\._runExplicitNavigation\(noun, navigate, releaseOptions\);/);
|
assert.match(
|
||||||
assert.match(voice, /runManagedVoiceNavigation\(\s*styleManager,\s*'camera',\s*'move_camera',\s*navigate,\s*releaseOptions/);
|
ui,
|
||||||
assert.match(voice, /runManagedVoiceNavigation\(\s*styleManager,\s*'route',\s*'fly_route',\s*navigate/);
|
/runImmediateNavigation\(noun, navigate, releaseOptions = undefined\) \{\s*return this\._runExplicitNavigation\(noun, navigate, releaseOptions\);/,
|
||||||
assert.match(voice, /runManagedVoiceNavigation\(\s*styleManager,\s*'fire',\s*'track_entity'/);
|
);
|
||||||
assert.match(voice, /runManagedVoiceNavigation\(\s*styleManager,\s*family\.kind,\s*'track_entity'/);
|
assert.match(
|
||||||
assert.match(voice, /runManagedVoiceNavigation\(\s*styleManager,\s*'frame',\s*'frame_overhead'/);
|
voice,
|
||||||
|
/runManagedVoiceNavigation\(\s*styleManager,\s*'camera',\s*'move_camera',\s*navigate,\s*releaseOptions/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
voice,
|
||||||
|
/runManagedVoiceNavigation\(\s*styleManager,\s*'route',\s*'fly_route',\s*navigate/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
voice,
|
||||||
|
/runManagedVoiceNavigation\(\s*styleManager,\s*'fire',\s*'track_entity'/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
voice,
|
||||||
|
/runManagedVoiceNavigation\(\s*styleManager,\s*family\.kind,\s*'track_entity'/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
voice,
|
||||||
|
/runManagedVoiceNavigation\(\s*styleManager,\s*'frame',\s*'frame_overhead'/,
|
||||||
|
);
|
||||||
const trackedVoice = voice.slice(
|
const trackedVoice = voice.slice(
|
||||||
voice.indexOf('async function trackEntity'),
|
voice.indexOf('async function trackEntity'),
|
||||||
voice.indexOf('async function frameOverhead'),
|
voice.indexOf('async function frameOverhead'),
|
||||||
@@ -222,47 +313,80 @@ test('validated voice camera destinations share the UI navigation authority faca
|
|||||||
/export function moveCamera\(args = \{\}, runNavigation = null\) \{([\s\S]*?)\n\}/,
|
/export function moveCamera\(args = \{\}, runNavigation = null\) \{([\s\S]*?)\n\}/,
|
||||||
'move camera',
|
'move camera',
|
||||||
);
|
);
|
||||||
ordered(move, [
|
ordered(
|
||||||
"if (!['orbit', 'pan', 'tilt', 'rotate', 'rotate_globe'].includes(motion))",
|
move,
|
||||||
'const start = () => {',
|
[
|
||||||
/const preserveCameraFlight =\s*motion === 'orbit'/,
|
"if (!['orbit', 'pan', 'tilt', 'rotate', 'rotate_globe'].includes(motion))",
|
||||||
'runNavigation(start, { preserveCameraFlight })',
|
'const start = () => {',
|
||||||
], 'move validation before handoff');
|
/const preserveCameraFlight =\s*motion === 'orbit'/,
|
||||||
|
'runNavigation(start, { preserveCameraFlight })',
|
||||||
|
],
|
||||||
|
'move validation before handoff',
|
||||||
|
);
|
||||||
|
|
||||||
const route = body(
|
const route = body(
|
||||||
cameraVerbs,
|
cameraVerbs,
|
||||||
/export function flyRoute\(\s*annoList,\s*args = \{\},\s*floorFn = null,\s*runNavigation = null,\s*warmFn = null,?\s*\) \{([\s\S]*?)\n\}/,
|
/export function flyRoute\(\s*annoList,\s*args = \{\},\s*floorFn = null,\s*runNavigation = null,\s*warmFn = null,?\s*\) \{([\s\S]*?)\n\}/,
|
||||||
'fly route',
|
'fly route',
|
||||||
);
|
);
|
||||||
ordered(route, [
|
ordered(
|
||||||
'if (!routes.length)',
|
route,
|
||||||
'const pts = route.path.map',
|
[
|
||||||
'const start = () => {',
|
'if (!routes.length)',
|
||||||
/return typeof runNavigation === 'function'\s*\? runNavigation\(start\)\s*: start\(\);/,
|
'const pts = route.path.map',
|
||||||
], 'route validation before handoff');
|
'const start = () => {',
|
||||||
|
/return typeof runNavigation === 'function'\s*\? runNavigation\(start\)\s*: start\(\);/,
|
||||||
|
],
|
||||||
|
'route validation before handoff',
|
||||||
|
);
|
||||||
// The corridor warm is injected the same way the floor READ is — the dolly
|
// The corridor warm is injected the same way the floor READ is — the dolly
|
||||||
// never reaches into the data layer itself, and the voice dispatch is the one
|
// never reaches into the data layer itself, and the voice dispatch is the one
|
||||||
// place that binds both.
|
// place that binds both.
|
||||||
assert.match(voice, /\(lat, lon\) => floorServices\.cachedGroundFloor\(lat, lon\),[\s\S]{0,200}?\(cells\) => floorServices\.warmGroundFloor\(cells\),/);
|
assert.match(
|
||||||
|
voice,
|
||||||
|
/\(lat, lon\) => floorServices\.cachedGroundFloor\(lat, lon\),[\s\S]{0,200}?\(cells\) => floorServices\.warmGroundFloor\(cells\),/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('deferred search releases only after its final authority check', () => {
|
test('deferred search releases only after its final authority check', () => {
|
||||||
const search = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'locationSearch.js'), 'utf8');
|
const search = fs.readFileSync(
|
||||||
ordered(search, [
|
path.join(ROOT, 'src', 'ui', 'locationSearch.js'),
|
||||||
'const authority = this.begin();',
|
'utf8',
|
||||||
'this.onStart?.(authority);',
|
);
|
||||||
'await this.search(query',
|
ordered(
|
||||||
'beforeFly: () => current() && this.beforeFly(authority)',
|
search,
|
||||||
'if (!current() || controller.signal.aborted) return;',
|
[
|
||||||
'destination?.cancelled',
|
'const authority = this.begin();',
|
||||||
'finally',
|
'this.onStart?.(authority);',
|
||||||
'this.onSettled?.(authority)',
|
'await this.search(query',
|
||||||
], 'deferred search component');
|
'beforeFly: () => current() && this.beforeFly(authority)',
|
||||||
assert.match(ui, /begin: \(\) => this\._beginDeferredNavigation\('location'\)/);
|
'if (!current() || controller.signal.aborted) return;',
|
||||||
assert.match(ui, /beforeFly: \(generation\) => this\._reassertNavigationHandoff\(generation\)/);
|
'destination?.cancelled',
|
||||||
assert.match(ui, /isCurrent: \(generation\) =>\s*!this\._disposed && generation === this\._navigationGeneration/);
|
'finally',
|
||||||
assert.match(ui, /change\.type === 'settled'\)\s*this\._settleLocationSearchUi\(change\.generation, \{\s*keepQuery: state\.status === 'missing' \|\| state\.status === 'failed',?\s*\}\)/);
|
'this.onSettled?.(authority)',
|
||||||
assert.doesNotMatch(search.slice(0, search.indexOf('await this.search')), /_releaseFollowCamera/);
|
],
|
||||||
|
'deferred search component',
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
ui,
|
||||||
|
/begin: \(\) => this\._beginDeferredNavigation\('location'\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
ui,
|
||||||
|
/beforeFly: \(generation\) => this\._reassertNavigationHandoff\(generation\)/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
ui,
|
||||||
|
/isCurrent: \(generation\) =>\s*!this\._disposed && generation === this\._navigationGeneration/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
ui,
|
||||||
|
/change\.type === 'settled'\)\s*this\._settleLocationSearchUi\(change\.generation, \{\s*keepQuery: state\.status === 'missing' \|\| state\.status === 'failed',?\s*\}\)/,
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
search.slice(0, search.indexOf('await this.search')),
|
||||||
|
/_releaseFollowCamera/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a direct globe gesture retires delayed camera and selection restore only', () => {
|
test('a direct globe gesture retires delayed camera and selection restore only', () => {
|
||||||
@@ -287,77 +411,115 @@ test('a direct globe gesture retires delayed camera and selection restore only',
|
|||||||
/_stampNavigation\(\{ cancelPendingSelection = true[^)]*\} = \{\}\) \{([\s\S]*?)\n \}/,
|
/_stampNavigation\(\{ cancelPendingSelection = true[^)]*\} = \{\}\) \{([\s\S]*?)\n \}/,
|
||||||
'navigation stamp',
|
'navigation stamp',
|
||||||
);
|
);
|
||||||
assert.match(stamp, /if \(cancelPendingSelection\) \{[\s\S]*?cancelPendingTrackingRestore/);
|
assert.match(
|
||||||
|
stamp,
|
||||||
|
/if \(cancelPendingSelection\) \{[\s\S]*?cancelPendingTrackingRestore/,
|
||||||
|
);
|
||||||
assert.doesNotMatch(stamp, /cancelPendingRestores\(\)/);
|
assert.doesNotMatch(stamp, /cancelPendingRestores\(\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('newer navigation, reset, Cockpit, and teardown share one generation', () => {
|
test('newer navigation, reset, Cockpit, and teardown share one generation', () => {
|
||||||
assert.equal((ui.match(/_navigationGeneration \+= 1/g) || []).length, 1);
|
assert.equal((ui.match(/_navigationGeneration \+= 1/g) || []).length, 1);
|
||||||
const reset = body(ui, /resetToGlobeView\(\) \{([\s\S]*?)\n \}/, 'reset');
|
const reset = body(ui, /resetToGlobeView\(\) \{([\s\S]*?)\n \}/, 'reset');
|
||||||
ordered(reset, [
|
ordered(
|
||||||
'if (this._globeResetPromise) return this._globeResetPromise;',
|
reset,
|
||||||
'this._stampNavigation();',
|
[
|
||||||
"interruptCameraMotion('reset-globe')",
|
'if (this._globeResetPromise) return this._globeResetPromise;',
|
||||||
], 'reset supersession');
|
'this._stampNavigation();',
|
||||||
|
"interruptCameraMotion('reset-globe')",
|
||||||
|
],
|
||||||
|
'reset supersession',
|
||||||
|
);
|
||||||
const dispose = body(ui, /async dispose\(\) \{([\s\S]*?)\n \}/, 'dispose');
|
const dispose = body(ui, /async dispose\(\) \{([\s\S]*?)\n \}/, 'dispose');
|
||||||
ordered(dispose, [
|
ordered(
|
||||||
'this._disposed = true;',
|
dispose,
|
||||||
'this._layerBindings.stop();',
|
[
|
||||||
'this._navigation.destroy();',
|
'this._disposed = true;',
|
||||||
'await this._contextControls.restoreForDisposal();',
|
'this._layerBindings.stop();',
|
||||||
], 'dispose invalidation');
|
'this._navigation.destroy();',
|
||||||
|
'await this._contextControls.restoreForDisposal();',
|
||||||
|
],
|
||||||
|
'dispose invalidation',
|
||||||
|
);
|
||||||
assert.match(shellMethod('destroy').toString(), /this\._stampNavigation\(\)/);
|
assert.match(shellMethod('destroy').toString(), /this\._stampNavigation\(\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('teardown synchronously closes immediate camera entry points', () => {
|
test('teardown synchronously closes immediate camera entry points', () => {
|
||||||
const dispose = body(ui, /async dispose\(\) \{([\s\S]*?)\n \}/, 'dispose');
|
const dispose = body(ui, /async dispose\(\) \{([\s\S]*?)\n \}/, 'dispose');
|
||||||
ordered(dispose, [
|
ordered(
|
||||||
'this._disposed = true;',
|
dispose,
|
||||||
'this._layerBindings.stop();',
|
[
|
||||||
'await this._contextControls.restoreForDisposal();',
|
'this._disposed = true;',
|
||||||
], 'synchronous teardown barrier');
|
'this._layerBindings.stop();',
|
||||||
ordered(LayerBindings.prototype.stop.toString(), [
|
'await this._contextControls.restoreForDisposal();',
|
||||||
'this._disposed = true;',
|
],
|
||||||
'this._removeCctvRequestFocusListener?.();',
|
'synchronous teardown barrier',
|
||||||
'this._removeWorldRequestFocusListener?.();',
|
);
|
||||||
'this._navigationOwnerChangedRemover?.();',
|
ordered(
|
||||||
], 'camera bindings close synchronously');
|
LayerBindings.prototype.stop.toString(),
|
||||||
|
[
|
||||||
|
'this._disposed = true;',
|
||||||
|
'this._removeCctvRequestFocusListener?.();',
|
||||||
|
'this._removeWorldRequestFocusListener?.();',
|
||||||
|
'this._navigationOwnerChangedRemover?.();',
|
||||||
|
],
|
||||||
|
'camera bindings close synchronously',
|
||||||
|
);
|
||||||
|
|
||||||
const navigation = body(
|
const navigation = body(
|
||||||
ui,
|
ui,
|
||||||
/_runExplicitNavigation\(noun, navigate, releaseOptions = undefined\) \{([\s\S]*?)\n \}/,
|
/_runExplicitNavigation\(noun, navigate, releaseOptions = undefined\) \{([\s\S]*?)\n \}/,
|
||||||
'explicit navigation',
|
'explicit navigation',
|
||||||
);
|
);
|
||||||
ordered(navigation, [
|
ordered(
|
||||||
'disposed: this._disposed',
|
navigation,
|
||||||
'release: () => this._releaseFollowCamera(releaseOptions)',
|
[
|
||||||
'navigate,',
|
'disposed: this._disposed',
|
||||||
], 'disposed navigation guard');
|
'release: () => this._releaseFollowCamera(releaseOptions)',
|
||||||
|
'navigate,',
|
||||||
|
],
|
||||||
|
'disposed navigation guard',
|
||||||
|
);
|
||||||
|
|
||||||
const cctvFocus = body(
|
const cctvFocus = body(
|
||||||
ui,
|
ui,
|
||||||
/_runExplicitCctvFocus\(activate, focus\) \{([\s\S]*?)\n \}/,
|
/_runExplicitCctvFocus\(activate, focus\) \{([\s\S]*?)\n \}/,
|
||||||
'explicit CCTV focus',
|
'explicit CCTV focus',
|
||||||
);
|
);
|
||||||
ordered(cctvFocus, [
|
ordered(
|
||||||
'if (this._disposed) return false;',
|
cctvFocus,
|
||||||
'const cameraId = activate();',
|
['if (this._disposed) return false;', 'const cameraId = activate();'],
|
||||||
], 'disposed CCTV activation guard');
|
'disposed CCTV activation guard',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('teardown refuses deferred location work before geocoding begins', () => {
|
test('teardown refuses deferred location work before geocoding begins', () => {
|
||||||
const deferred = body(ui, /_beginDeferredNavigation\(noun = 'location', \{ cancelPendingSelection = true \} = \{\}\) \{([\s\S]*?)\n \}/, 'deferred navigation');
|
const deferred = body(
|
||||||
|
ui,
|
||||||
|
/_beginDeferredNavigation\(noun = 'location', \{ cancelPendingSelection = true \} = \{\}\) \{([\s\S]*?)\n \}/,
|
||||||
|
'deferred navigation',
|
||||||
|
);
|
||||||
assert.match(deferred, /disposed: this\._disposed/);
|
assert.match(deferred, /disposed: this\._disposed/);
|
||||||
const search = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'locationSearch.js'), 'utf8');
|
const search = fs.readFileSync(
|
||||||
ordered(search, [
|
path.join(ROOT, 'src', 'ui', 'locationSearch.js'),
|
||||||
'if (!query || this.destroyed) return;',
|
'utf8',
|
||||||
'const authority = this.begin();',
|
);
|
||||||
'if (authority === false)',
|
ordered(
|
||||||
'this.input?.blur();',
|
search,
|
||||||
'this.onStart?.(authority);',
|
[
|
||||||
'await this.search(query',
|
'if (!query || this.destroyed) return;',
|
||||||
], 'disposed search refusal');
|
'const authority = this.begin();',
|
||||||
assert.match(search, /if \(authority === false\) \{[\s\S]*?return;[\s\S]*?\}\s*this\.controller/);
|
'if (authority === false)',
|
||||||
|
'this.input?.blur();',
|
||||||
|
'this.onStart?.(authority);',
|
||||||
|
'await this.search(query',
|
||||||
|
],
|
||||||
|
'disposed search refusal',
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
search,
|
||||||
|
/if \(authority === false\) \{[\s\S]*?return;[\s\S]*?\}\s*this\.controller/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('refused canned destinations commit no location or POI state', () => {
|
test('refused canned destinations commit no location or POI state', () => {
|
||||||
@@ -366,11 +528,15 @@ test('refused canned destinations commit no location or POI state', () => {
|
|||||||
['poi', /_onPoiClick\(cityId, poiIndex\) \{([\s\S]*?)\n \}/],
|
['poi', /_onPoiClick\(cityId, poiIndex\) \{([\s\S]*?)\n \}/],
|
||||||
]) {
|
]) {
|
||||||
const handler = body(ui, pattern, name);
|
const handler = body(ui, pattern, name);
|
||||||
ordered(handler, [
|
ordered(
|
||||||
'this._flyWithTransition(',
|
handler,
|
||||||
'if (result === false) return;',
|
[
|
||||||
'this._setActiveLocation(cityId);',
|
'this._flyWithTransition(',
|
||||||
], name);
|
'if (result === false) return;',
|
||||||
|
'this._setActiveLocation(cityId);',
|
||||||
|
],
|
||||||
|
name,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,11 +558,15 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
|
|||||||
/inputFor\(viewer\)\.onClick\(\(event\) => \{([\s\S]*?)\n \}\);/,
|
/inputFor\(viewer\)\.onClick\(\(event\) => \{([\s\S]*?)\n \}\);/,
|
||||||
'vessel click',
|
'vessel click',
|
||||||
);
|
);
|
||||||
ordered(vesselClick, [
|
ordered(
|
||||||
"isOwnedByOtherLayer('ais-live-vessels', pickedId)",
|
vesselClick,
|
||||||
'_vesselOverlayHost.hitTest?.(',
|
[
|
||||||
'selectAndFocusVessel(record)',
|
"isOwnedByOtherLayer('ais-live-vessels', pickedId)",
|
||||||
], 'vessel sibling ownership');
|
'_vesselOverlayHost.hitTest?.(',
|
||||||
|
'selectAndFocusVessel(record)',
|
||||||
|
],
|
||||||
|
'vessel sibling ownership',
|
||||||
|
);
|
||||||
const vesselFocus = body(
|
const vesselFocus = body(
|
||||||
vessels,
|
vessels,
|
||||||
/function selectAndFocusVessel\(record\) \{([\s\S]*?)\n \}/,
|
/function selectAndFocusVessel\(record\) \{([\s\S]*?)\n \}/,
|
||||||
@@ -408,9 +578,13 @@ test('vessel and fire layers announce valid clicks and never fly cameras', () =>
|
|||||||
/input\.onClick\(\(event\) => \{([\s\S]*?)\n \}\);/,
|
/input\.onClick\(\(event\) => \{([\s\S]*?)\n \}\);/,
|
||||||
'fire click',
|
'fire click',
|
||||||
);
|
);
|
||||||
ordered(fireClick, [
|
ordered(
|
||||||
'isOwnedByOtherLayer(id, pickedId)',
|
fireClick,
|
||||||
'overlayHost.hitTest?.(',
|
[
|
||||||
'selectAndFocusFire(carded)',
|
'isOwnedByOtherLayer(id, pickedId)',
|
||||||
], 'fire sibling ownership');
|
'overlayHost.hitTest?.(',
|
||||||
|
'selectAndFocusFire(carded)',
|
||||||
|
],
|
||||||
|
'fire sibling ownership',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
+18
-14
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { governorRequestRender } from './renderGovernor.js';
|
import { governorRequestRender } from './renderGovernor.js';
|
||||||
|
import { cameraOf } from './render/cesium/camera.js';
|
||||||
|
|
||||||
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
|
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
|
||||||
export const KEYHOLE_OUTER_RADIUS = 1.05;
|
export const KEYHOLE_OUTER_RADIUS = 1.05;
|
||||||
@@ -576,30 +577,33 @@ export class CelestialRing {
|
|||||||
const distance = earthRadius / Math.max(Math.sin(angularRadius), 1e-4);
|
const distance = earthRadius / Math.max(Math.sin(angularRadius), 1e-4);
|
||||||
const altitude = Math.max(earthRadius * 1.55, distance - earthRadius);
|
const altitude = Math.max(earthRadius * 1.55, distance - earthRadius);
|
||||||
|
|
||||||
|
const camera = cameraOf(this.viewer);
|
||||||
this._focusInProgress = true;
|
this._focusInProgress = true;
|
||||||
const finishFocus = () => {
|
const finishFocus = () => {
|
||||||
this._focusInProgress = false;
|
this._focusInProgress = false;
|
||||||
this.viewer.scene.requestRender?.();
|
this.viewer.scene.requestRender?.();
|
||||||
};
|
};
|
||||||
this.viewer.camera.flyTo({
|
camera.flyTo(
|
||||||
destination: Cesium.Cartesian3.fromRadians(
|
Cesium.Cartesian3.fromRadians(
|
||||||
cartographic.longitude,
|
cartographic.longitude,
|
||||||
cartographic.latitude,
|
cartographic.latitude,
|
||||||
altitude,
|
altitude,
|
||||||
),
|
),
|
||||||
orientation: {
|
{
|
||||||
heading: this.viewer.camera.heading,
|
orientation: {
|
||||||
pitch: -Cesium.Math.PI_OVER_TWO,
|
heading: camera.pose().heading,
|
||||||
roll: 0,
|
pitch: -Cesium.Math.PI_OVER_TWO,
|
||||||
|
roll: 0,
|
||||||
|
},
|
||||||
|
durationS: duration,
|
||||||
|
easing: 'cubicInOut',
|
||||||
|
onComplete: finishFocus,
|
||||||
|
onCancel: () => {
|
||||||
|
finishFocus();
|
||||||
|
if (!this.isGlobeFullyVisible()) this._autoDisable();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
duration,
|
);
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
||||||
complete: finishFocus,
|
|
||||||
cancel: () => {
|
|
||||||
finishFocus();
|
|
||||||
if (!this.isGlobeFullyVisible()) this._autoDisable();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
setOverlayEntries,
|
setOverlayEntries,
|
||||||
} from '../overlays/worldOverlay.js';
|
} from '../overlays/worldOverlay.js';
|
||||||
import { governorRequestRender } from '../renderGovernor.js';
|
import { governorRequestRender } from '../renderGovernor.js';
|
||||||
|
import { cameraOf } from '../render/cesium/camera.js';
|
||||||
import {
|
import {
|
||||||
BHOTE_KOSHI_LOCATOR_CREDIT,
|
BHOTE_KOSHI_LOCATOR_CREDIT,
|
||||||
registerDynamicCredit,
|
registerDynamicCredit,
|
||||||
@@ -620,6 +621,7 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
requestRender('bhote-koshi-trigger-record-reveal');
|
requestRender('bhote-koshi-trigger-record-reveal');
|
||||||
const camera = _viewer?.camera;
|
const camera = _viewer?.camera;
|
||||||
if (!camera?.flyTo) return;
|
if (!camera?.flyTo) return;
|
||||||
|
const port = cameraOf(_viewer);
|
||||||
const generation = _generation;
|
const generation = _generation;
|
||||||
_triggerApproachInFlight = true;
|
_triggerApproachInFlight = true;
|
||||||
const finish = () => {
|
const finish = () => {
|
||||||
@@ -627,50 +629,54 @@ export function createBhoteKoshiLocatorLayer({
|
|||||||
attempt.phase = 'finished';
|
attempt.phase = 'finished';
|
||||||
_triggerApproachInFlight = false;
|
_triggerApproachInFlight = false;
|
||||||
};
|
};
|
||||||
camera.flyTo({
|
port.flyTo(
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
{
|
||||||
TRIGGER_APPROACH_CAMERA.lon,
|
lon: TRIGGER_APPROACH_CAMERA.lon,
|
||||||
TRIGGER_APPROACH_CAMERA.lat,
|
lat: TRIGGER_APPROACH_CAMERA.lat,
|
||||||
TRIGGER_APPROACH_CAMERA.alt,
|
height: TRIGGER_APPROACH_CAMERA.alt,
|
||||||
),
|
|
||||||
orientation: {
|
|
||||||
heading: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.heading),
|
|
||||||
pitch: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.pitch),
|
|
||||||
roll: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.roll),
|
|
||||||
},
|
},
|
||||||
duration: TRIGGER_APPROACH_DURATION_SEC,
|
{
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
orientation: {
|
||||||
complete: () => {
|
heading: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.heading),
|
||||||
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'flying')
|
pitch: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.pitch),
|
||||||
return;
|
roll: Cesium.Math.toRadians(TRIGGER_APPROACH_CAMERA.roll),
|
||||||
if (
|
},
|
||||||
!_enabled ||
|
durationS: TRIGGER_APPROACH_DURATION_SEC,
|
||||||
generation !== _generation ||
|
easing: 'cubicInOut',
|
||||||
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
onComplete: () => {
|
||||||
) {
|
if (_triggerApproachAttempt !== attempt || attempt.phase !== 'flying')
|
||||||
finish();
|
return;
|
||||||
return;
|
if (
|
||||||
}
|
!_enabled ||
|
||||||
attempt.phase = 'orbiting';
|
generation !== _generation ||
|
||||||
camera.flyTo({
|
_presentation !== BHOTE_KOSHI_TRIGGER_RECORD_PRESENTATION
|
||||||
destination: Cesium.Cartesian3.fromDegrees(
|
) {
|
||||||
TRIGGER_ORBIT_CAMERA.lon,
|
finish();
|
||||||
TRIGGER_ORBIT_CAMERA.lat,
|
return;
|
||||||
TRIGGER_ORBIT_CAMERA.alt,
|
}
|
||||||
),
|
attempt.phase = 'orbiting';
|
||||||
orientation: {
|
port.flyTo(
|
||||||
heading: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.heading),
|
{
|
||||||
pitch: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.pitch),
|
lon: TRIGGER_ORBIT_CAMERA.lon,
|
||||||
roll: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.roll),
|
lat: TRIGGER_ORBIT_CAMERA.lat,
|
||||||
},
|
height: TRIGGER_ORBIT_CAMERA.alt,
|
||||||
duration: TRIGGER_ORBIT_DURATION_SEC,
|
},
|
||||||
easingFunction: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
{
|
||||||
complete: finish,
|
orientation: {
|
||||||
cancel: finish,
|
heading: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.heading),
|
||||||
});
|
pitch: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.pitch),
|
||||||
|
roll: Cesium.Math.toRadians(TRIGGER_ORBIT_CAMERA.roll),
|
||||||
|
},
|
||||||
|
durationS: TRIGGER_ORBIT_DURATION_SEC,
|
||||||
|
easing: 'sineInOut',
|
||||||
|
onComplete: finish,
|
||||||
|
onCancel: finish,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onCancel: finish,
|
||||||
},
|
},
|
||||||
cancel: finish,
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function armTriggerApproach() {
|
function armTriggerApproach() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
@@ -647,6 +648,7 @@ test('Bhote Koshi trigger-record waits for the scene camera, zooms, then orbits'
|
|||||||
},
|
},
|
||||||
cancelFlight() {},
|
cancelFlight() {},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
const layer = createBhoteKoshiLocatorLayer({
|
const layer = createBhoteKoshiLocatorLayer({
|
||||||
boundaryResolver: async () => null,
|
boundaryResolver: async () => null,
|
||||||
requestRender() {},
|
requestRender() {},
|
||||||
@@ -703,6 +705,9 @@ for (const phase of ['timer', 'move-end', 'approach', 'orbit']) {
|
|||||||
},
|
},
|
||||||
cancelFlight() {},
|
cancelFlight() {},
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
|
// The renderer's camera port relays these; the layer's own come on top.
|
||||||
|
const portListeners = [moveStart, moveEnd].map((e) => e.numberOfListeners);
|
||||||
const layer = createBhoteKoshiLocatorLayer({
|
const layer = createBhoteKoshiLocatorLayer({
|
||||||
boundaryResolver: async () => null,
|
boundaryResolver: async () => null,
|
||||||
overlayHost: overlayHostFixture(),
|
overlayHost: overlayHostFixture(),
|
||||||
@@ -745,8 +750,10 @@ for (const phase of ['timer', 'move-end', 'approach', 'orbit']) {
|
|||||||
const oldFlights = [...flights];
|
const oldFlights = [...flights];
|
||||||
SceneDirector.prototype.stopScene.call(director);
|
SceneDirector.prototype.stopScene.call(director);
|
||||||
assert.equal(timers.size, 0, 'Stop cancels the actual delayed work');
|
assert.equal(timers.size, 0, 'Stop cancels the actual delayed work');
|
||||||
assert.equal(moveStart.numberOfListeners, 0);
|
assert.deepEqual(
|
||||||
assert.equal(moveEnd.numberOfListeners, 0);
|
[moveStart, moveEnd].map((e) => e.numberOfListeners),
|
||||||
|
portListeners,
|
||||||
|
);
|
||||||
callbacks[0]();
|
callbacks[0]();
|
||||||
moveEnd.raiseEvent();
|
moveEnd.raiseEvent();
|
||||||
for (const flight of oldFlights) flight.complete();
|
for (const flight of oldFlights) flight.complete();
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ import { applicationServices } from './services/application.js';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { toDegrees, toRadians } from './geo/index.js';
|
import { toDegrees, toRadians } from './geo/index.js';
|
||||||
import { rendererOf } from './render/rendererOf.js';
|
import { findCamera } from './render/rendererOf.js';
|
||||||
import { greatCircleKm } from './geoDistance.js';
|
import { greatCircleKm } from './geoDistance.js';
|
||||||
import { forward as toMGRS } from 'mgrs';
|
import { forward as toMGRS } from 'mgrs';
|
||||||
import { CITY_POIS } from './cityPois.js';
|
import { CITY_POIS } from './cityPois.js';
|
||||||
@@ -241,7 +241,7 @@ export class IntelHUD {
|
|||||||
basemapContext = {},
|
basemapContext = {},
|
||||||
summaryService = applicationServices.summary,
|
summaryService = applicationServices.summary,
|
||||||
publish = () => {},
|
publish = () => {},
|
||||||
camera = rendererOf(viewer)?.camera,
|
camera = findCamera(viewer),
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
if (!camera) throw new Error('IntelHUD needs a camera port');
|
if (!camera) throw new Error('IntelHUD needs a camera port');
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
|
import { entityRecord, readEntityRecord } from '../../data/entityRecords.js';
|
||||||
import { alprMarkers } from './markerState.js';
|
import { alprMarkers } from './markerState.js';
|
||||||
import { heightsFor } from '../../queries/heights.js';
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
export function createAlprPresentation({ state, services, source }) {
|
export function createAlprPresentation({ state, services, source }) {
|
||||||
const { governorRequestRender } = services.render;
|
const { governorRequestRender } = services.render;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { VESSEL_FOCUS_RADIUS_M } from './policy.js';
|
import { VESSEL_FOCUS_RADIUS_M } from './policy.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
export function createFocus({ state: layerState, services, parts, source }) {
|
export function createFocus({ state: layerState, services, parts, source }) {
|
||||||
const flightsLayer = services.flights;
|
const flightsLayer = services.flights;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
export function createNavigation({
|
export function createNavigation({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
translateKnown,
|
translateKnown,
|
||||||
} from '../../i18n/index.js';
|
} from '../../i18n/index.js';
|
||||||
import { formatQuantity } from '../../ui/unitLabels.js';
|
import { formatQuantity } from '../../ui/unitLabels.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
|
// "10-12 15:00 UTC" never breaks across lines: no-break spaces, and a word
|
||||||
// joiner after the hyphen (JetBrains Mono has no U+2011 non-breaking hyphen).
|
// joiner after the hyphen (JetBrains Mono has no U+2011 non-breaking hyphen).
|
||||||
|
|||||||
Vendored
+1
-1
@@ -6,7 +6,7 @@ import {
|
|||||||
ANCHOR_REFRESH_M,
|
ANCHOR_REFRESH_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
import { formatNumber, t, tn } from '../../i18n/index.js';
|
import { formatNumber, t, tn } from '../../i18n/index.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
|
// installationFeedback.js is shared with the HUD (src/loadingFeedback.js) and
|
||||||
// hands back msg()-marked English plus values; getStats translates them here.
|
// hands back msg()-marked English plus values; getStats translates them here.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
import {
|
import {
|
||||||
SATELLITE_STANDALONE_DEFAULTS,
|
SATELLITE_STANDALONE_DEFAULTS,
|
||||||
LAUNCH_PAD_ZONE_MAX_CAMERA_HEIGHT_M,
|
LAUNCH_PAD_ZONE_MAX_CAMERA_HEIGHT_M,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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 {
|
import {
|
||||||
REPLAY_ASCENT_FALLBACK_SEC,
|
REPLAY_ASCENT_FALLBACK_SEC,
|
||||||
REPLAY_ASCENT_MIN_SEC,
|
REPLAY_ASCENT_MIN_SEC,
|
||||||
@@ -353,7 +354,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
parts.overlays.syncMissionOverlayEntries();
|
parts.overlays.syncMissionOverlayEntries();
|
||||||
parts.policyHelpers.releaseAircraftTracking(layerState._dataManager);
|
parts.policyHelpers.releaseAircraftTracking(layerState._dataManager);
|
||||||
layerState._viewer.trackedEntity = undefined;
|
layerState._viewer.trackedEntity = undefined;
|
||||||
layerState._viewer.camera.cancelFlight();
|
cameraOf(layerState._viewer).cancelFlight();
|
||||||
layerState._animationStarts.set(
|
layerState._animationStarts.set(
|
||||||
launchId,
|
launchId,
|
||||||
Date.now() +
|
Date.now() +
|
||||||
@@ -485,20 +486,20 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
layerState._viewer.camera.flyToBoundingSphere(
|
cameraOf(layerState._viewer).flyToBounds(
|
||||||
new Cesium.BoundingSphere(track.ascentPath[0], 0),
|
{ center: track.ascentPath[0], radius: 0 },
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: {
|
||||||
initialHeading,
|
heading: initialHeading,
|
||||||
Cesium.Math.toRadians(-20),
|
pitch: Cesium.Math.toRadians(-20),
|
||||||
REPLAY_INITIAL_RANGE_M,
|
range: REPLAY_INITIAL_RANGE_M,
|
||||||
),
|
},
|
||||||
duration: 1.4,
|
durationS: 1.4,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easing: 'cubicInOut',
|
||||||
complete: () => {
|
onComplete: () => {
|
||||||
cameraReady = true;
|
cameraReady = true;
|
||||||
},
|
},
|
||||||
cancel: () => {
|
onCancel: () => {
|
||||||
if (
|
if (
|
||||||
token === layerState._replayCameraToken &&
|
token === layerState._replayCameraToken &&
|
||||||
layerState._replayCameraLaunchId === launchId
|
layerState._replayCameraLaunchId === launchId
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { MISSION_GLOBE_VIEW_RANGE_M, MISSION_FOCUS_RANGE_M } from './policy.js';
|
import { MISSION_GLOBE_VIEW_RANGE_M, MISSION_FOCUS_RANGE_M } from './policy.js';
|
||||||
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
export function createSelection({
|
export function createSelection({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -32,20 +33,23 @@ export function createSelection({
|
|||||||
);
|
);
|
||||||
const distance = earthRadius / Math.max(Math.sin(angularRadius), 1e-4);
|
const distance = earthRadius / Math.max(Math.sin(angularRadius), 1e-4);
|
||||||
const altitude = Math.max(earthRadius * 1.55, distance - earthRadius);
|
const altitude = Math.max(earthRadius * 1.55, distance - earthRadius);
|
||||||
viewer.camera.flyTo({
|
const camera = cameraOf(viewer);
|
||||||
destination: Cesium.Cartesian3.fromRadians(
|
camera.flyTo(
|
||||||
|
Cesium.Cartesian3.fromRadians(
|
||||||
cartographic.longitude,
|
cartographic.longitude,
|
||||||
cartographic.latitude,
|
cartographic.latitude,
|
||||||
altitude,
|
altitude,
|
||||||
),
|
),
|
||||||
orientation: {
|
{
|
||||||
heading: viewer.camera.heading,
|
orientation: {
|
||||||
pitch: -Cesium.Math.PI_OVER_TWO,
|
heading: camera.pose().heading,
|
||||||
roll: 0,
|
pitch: -Cesium.Math.PI_OVER_TWO,
|
||||||
|
roll: 0,
|
||||||
|
},
|
||||||
|
durationS: duration,
|
||||||
|
easing: 'cubicInOut',
|
||||||
},
|
},
|
||||||
duration,
|
);
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function entityLaunchId(entity) {
|
function entityLaunchId(entity) {
|
||||||
@@ -187,11 +191,10 @@ export function createSelection({
|
|||||||
Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()),
|
Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()),
|
||||||
new Cesium.Cartesian3(),
|
new Cesium.Cartesian3(),
|
||||||
);
|
);
|
||||||
layerState._viewer.camera.flyTo({
|
cameraOf(layerState._viewer).flyTo(destination, {
|
||||||
destination,
|
|
||||||
orientation: { direction, up },
|
orientation: { direction, up },
|
||||||
duration: 1.1,
|
durationS: 1.1,
|
||||||
complete: () => {
|
onComplete: () => {
|
||||||
if (layerState._selectedLaunchId === launch.id)
|
if (layerState._selectedLaunchId === launch.id)
|
||||||
startMissionZoomAnchor(launch, range);
|
startMissionZoomAnchor(launch, range);
|
||||||
},
|
},
|
||||||
@@ -202,24 +205,20 @@ export function createSelection({
|
|||||||
if (!layerState._viewer || !launch) return;
|
if (!layerState._viewer || !launch) return;
|
||||||
parts.replay.stopMissionReplay();
|
parts.replay.stopMissionReplay();
|
||||||
stopMissionZoomAnchor();
|
stopMissionZoomAnchor();
|
||||||
const launchPosition = Cesium.Cartesian3.fromDegrees(
|
|
||||||
launch.lon,
|
|
||||||
launch.lat,
|
|
||||||
);
|
|
||||||
layerState._viewer.selectedEntity = layerState._dataSource.entities.getById(
|
layerState._viewer.selectedEntity = layerState._dataSource.entities.getById(
|
||||||
`rocket-launch:${launch.id}`,
|
`rocket-launch:${launch.id}`,
|
||||||
);
|
);
|
||||||
layerState._viewer.camera.flyToBoundingSphere(
|
cameraOf(layerState._viewer).flyToBounds(
|
||||||
new Cesium.BoundingSphere(launchPosition, 0),
|
{ center: { lon: launch.lon, lat: launch.lat }, radius: 0 },
|
||||||
{
|
{
|
||||||
offset: new Cesium.HeadingPitchRange(
|
offset: {
|
||||||
0,
|
heading: 0,
|
||||||
Cesium.Math.toRadians(-42),
|
pitch: Cesium.Math.toRadians(-42),
|
||||||
MISSION_FOCUS_RANGE_M,
|
range: MISSION_FOCUS_RANGE_M,
|
||||||
),
|
},
|
||||||
duration: 1.4,
|
durationS: 1.4,
|
||||||
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
|
easing: 'cubicInOut',
|
||||||
complete: () => {
|
onComplete: () => {
|
||||||
if (layerState._selectedLaunchId === launch.id) {
|
if (layerState._selectedLaunchId === launch.id) {
|
||||||
startMissionZoomAnchor(launch, MISSION_FOCUS_RANGE_M);
|
startMissionZoomAnchor(launch, MISSION_FOCUS_RANGE_M);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
GLOBAL_RADIO_ALTITUDE_M,
|
GLOBAL_RADIO_ALTITUDE_M,
|
||||||
RADIO_GLOBE_RECENTER_MAX_HEIGHT_M,
|
RADIO_GLOBE_RECENTER_MAX_HEIGHT_M,
|
||||||
} from './policy.js';
|
} from './policy.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
export function createNavigation({
|
export function createNavigation({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import {
|
|||||||
} from './model.js';
|
} from './model.js';
|
||||||
import { msg, t } from '../../i18n/index.js';
|
import { msg, t } from '../../i18n/index.js';
|
||||||
import { READOUT_WORDS, displayDay, displayMetres } from './display.js';
|
import { READOUT_WORDS, displayDay, displayMetres } from './display.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { findCamera } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quiet time after the last arrow key before the focused day previews, so a
|
* Quiet time after the last arrow key before the focused day previews, so a
|
||||||
@@ -980,15 +980,17 @@ export function createRecentImageryLayer({
|
|||||||
const target = _boxError ? _refusalTarget : null;
|
const target = _boxError ? _refusalTarget : null;
|
||||||
const camera = _viewer?.camera;
|
const camera = _viewer?.camera;
|
||||||
const ellipsoid = ellipsoidOf(_viewer);
|
const ellipsoid = ellipsoidOf(_viewer);
|
||||||
if (!target || !camera?.flyTo || !ellipsoid?.cartographicToCartesian)
|
// No camera port: a viewer of the caller's own whose renderer did not
|
||||||
return null;
|
// bring the camera factory (GEV's own imagery renderer does).
|
||||||
|
const port = findCamera(_viewer);
|
||||||
|
if (!target || !port || !ellipsoid?.cartographicToCartesian) return null;
|
||||||
const canvas = _viewer.scene?.canvas;
|
const canvas = _viewer.scene?.canvas;
|
||||||
const height = fitViewHeightM({
|
const height = fitViewHeightM({
|
||||||
width: canvas?.clientWidth,
|
width: canvas?.clientWidth,
|
||||||
height: canvas?.clientHeight,
|
height: canvas?.clientHeight,
|
||||||
fovy: camera.frustum?.fovy,
|
fovy: camera?.frustum?.fovy,
|
||||||
});
|
});
|
||||||
cameraOf(_viewer).flyTo(
|
port.flyTo(
|
||||||
ellipsoid.cartographicToCartesian({
|
ellipsoid.cartographicToCartesian({
|
||||||
longitude: toRadians(target.lon),
|
longitude: toRadians(target.lon),
|
||||||
latitude: toRadians(target.lat),
|
latitude: toRadians(target.lat),
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { withTileRequestHook } from '../../render/cesium/tileRequests.js';
|
import { withTileRequestHook } from '../../render/cesium/tileRequests.js';
|
||||||
|
// The layer is Cesium-free and flies through `cameraOf`; its Cesium renderer
|
||||||
|
// brings the factory a viewer with no renderer bound gets a camera from.
|
||||||
|
import '../../render/cesium/camera.js';
|
||||||
import { PRODUCTS, gibsTemplate } from './model.js';
|
import { PRODUCTS, gibsTemplate } from './model.js';
|
||||||
|
|
||||||
const SLOT_IDS = ['a', 'b'];
|
const SLOT_IDS = ['a', 'b'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
|
import { plausibleSurfaceHeight, sampledSurfaceHeight } from './view.js';
|
||||||
import { heightsFor } from '../../queries/heights.js';
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
/** Fly the globe camera once to frame a photo after it opens. */
|
/** Fly the globe camera once to frame a photo after it opens. */
|
||||||
export function createCameraFraming({ state }) {
|
export function createCameraFraming({ state }) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { cablePicks } from './pickState.js';
|
import { cablePicks } from './pickState.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
export function createInteraction({ state, inputFor }) {
|
export function createInteraction({ state, inputFor }) {
|
||||||
function registerPickEntity(entity, info) {
|
function registerPickEntity(entity, info) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from './clock.js';
|
} from './clock.js';
|
||||||
import { formatUtcDayMonth, t, msg } from '../../i18n/index.js';
|
import { formatUtcDayMonth, t, msg } from '../../i18n/index.js';
|
||||||
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
import { formatScaleLabel } from '../../ui/unitLabels.js';
|
||||||
import { cameraOf } from '../../render/rendererOf.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stored (English) failure in the language. HTTP failures from source.js
|
* A stored (English) failure in the language. HTTP failures from source.js
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import { cancelCameraArrival } from './data/cameraArrival.js';
|
|||||||
import { guardCameraAboveGround } from './cameraGroundGuard.js';
|
import { guardCameraAboveGround } from './cameraGroundGuard.js';
|
||||||
import { CITY_POIS } from './cityPois.js';
|
import { CITY_POIS } from './cityPois.js';
|
||||||
import { heightsFor } from './queries/heights.js';
|
import { heightsFor } from './queries/heights.js';
|
||||||
import { cameraOf } from './render/rendererOf.js';
|
import { cameraOf } from './render/cesium/camera.js';
|
||||||
|
|
||||||
/** Newest landmark arrival; an older post-arrival ground guard yields to it. */
|
/** Newest landmark arrival; an older post-arrival ground guard yields to it. */
|
||||||
let arrivalGeneration = 0;
|
let arrivalGeneration = 0;
|
||||||
|
|||||||
@@ -121,11 +121,13 @@ export function createCesiumRendererPlugin({
|
|||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
try {
|
try {
|
||||||
unbind();
|
|
||||||
input.dispose();
|
input.dispose();
|
||||||
ports.dispose();
|
ports.dispose();
|
||||||
if (!viewer.isDestroyed()) viewer.destroy();
|
if (!viewer.isDestroyed()) viewer.destroy();
|
||||||
} finally {
|
} finally {
|
||||||
|
// Bound until the viewer is gone: a late `cameraOf` must not
|
||||||
|
// build a camera of its own over the application's viewer.
|
||||||
|
unbind();
|
||||||
creditContainer.remove();
|
creditContainer.remove();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,8 +6,15 @@
|
|||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { createCameraEvents } from '../camera/events.js';
|
import { createCameraEvents } from '../camera/events.js';
|
||||||
import { listenerList, toCartesian } from './common.js';
|
import { listenerList, toCartesian, uncaught } from './common.js';
|
||||||
import { cameraFlightActive } from './internals.js';
|
import { cameraFlightActive } from './internals.js';
|
||||||
|
import { cameraOf, 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.
|
||||||
|
*/
|
||||||
|
export { cameraOf };
|
||||||
|
|
||||||
/** The contract's easings as Cesium's. */
|
/** The contract's easings as Cesium's. */
|
||||||
export const EASINGS = Object.freeze({
|
export const EASINGS = Object.freeze({
|
||||||
@@ -16,6 +23,7 @@ export const EASINGS = Object.freeze({
|
|||||||
cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT,
|
cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||||
cubicOut: Cesium.EasingFunction.CUBIC_OUT,
|
cubicOut: Cesium.EasingFunction.CUBIC_OUT,
|
||||||
quinticInOut: Cesium.EasingFunction.QUINTIC_IN_OUT,
|
quinticInOut: Cesium.EasingFunction.QUINTIC_IN_OUT,
|
||||||
|
sineInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** A contract destination as Cesium's: a Cartesian3, or a Rectangle. */
|
/** A contract destination as Cesium's: a Cartesian3, or a Rectangle. */
|
||||||
@@ -265,3 +273,19 @@ export function createCesiumCamera({ scene, camera, report }) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Cesium viewer of a consumer's own, with no renderer bound to it, gets a
|
||||||
|
// camera port over its camera when code first asks `cameraOf` for one.
|
||||||
|
provideUnboundCamera((viewer) =>
|
||||||
|
// By shape, not `instanceof`: a consumer's viewer may come from another
|
||||||
|
// copy of Cesium.
|
||||||
|
typeof viewer.camera?.flyTo === 'function' &&
|
||||||
|
typeof viewer.camera.moveStart?.addEventListener === 'function' &&
|
||||||
|
typeof viewer.scene?.postUpdate?.addEventListener === 'function'
|
||||||
|
? createCesiumCamera({
|
||||||
|
scene: viewer.scene,
|
||||||
|
camera: viewer.camera,
|
||||||
|
report: uncaught,
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
);
|
||||||
@@ -29,6 +29,7 @@ const EASINGS = {
|
|||||||
cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT,
|
cubicInOut: Cesium.EasingFunction.CUBIC_IN_OUT,
|
||||||
cubicOut: Cesium.EasingFunction.CUBIC_OUT,
|
cubicOut: Cesium.EasingFunction.CUBIC_OUT,
|
||||||
quinticInOut: Cesium.EasingFunction.QUINTIC_IN_OUT,
|
quinticInOut: Cesium.EasingFunction.QUINTIC_IN_OUT,
|
||||||
|
sineInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** One frame of the virtual clock, ms. */
|
/** One frame of the virtual clock, ms. */
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { FRAME_MS, headlessCamera, withVirtualClock } from './cameraGoldens.js';
|
import { FRAME_MS, headlessCamera, withVirtualClock } from './cameraGoldens.js';
|
||||||
import { createCesiumCamera } from './camera.js';
|
import { createCesiumCamera } from './camera.js';
|
||||||
import { createCesiumPorts } from './ports.js';
|
import { createCesiumPorts } from './ports.js';
|
||||||
|
import { cameraOf } from '../rendererOf.js';
|
||||||
|
|
||||||
/** Records the headless scene cannot answer: they need the query port. */
|
/** Records the headless scene cannot answer: they need the query port. */
|
||||||
const QUERY_RECORDS = new Set([
|
const QUERY_RECORDS = new Set([
|
||||||
@@ -208,3 +209,26 @@ test('a flight Cesium does not start ends cancelled rather than never', async ()
|
|||||||
assert.equal(await flight.done, 'cancelled');
|
assert.equal(await flight.done, 'cancelled');
|
||||||
assert.equal(port.flying, false);
|
assert.equal(port.flying, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a Cesium viewer with no renderer bound gets a camera port over its own camera', async () => {
|
||||||
|
await withVirtualClock(async () => {
|
||||||
|
const { scene, camera } = headlessCamera();
|
||||||
|
scene.postUpdate = new Cesium.Event();
|
||||||
|
scene.requestRender = () => {};
|
||||||
|
const viewer = { scene, camera };
|
||||||
|
const port = cameraOf(viewer);
|
||||||
|
assert.equal(cameraOf(viewer), port, 'one port per viewer');
|
||||||
|
let outcome = null;
|
||||||
|
port.flyTo(
|
||||||
|
{ lon: 2.35, lat: 48.86, height: 1500 },
|
||||||
|
{ durationS: 0, onComplete: () => (outcome = 'complete') },
|
||||||
|
);
|
||||||
|
assert.equal(outcome, 'complete');
|
||||||
|
const pose = port.pose();
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(pose.lon - 2.35) < 1e-9 && Math.abs(pose.lat - 48.86) < 1e-9,
|
||||||
|
);
|
||||||
|
assert.ok(Math.abs(pose.height - 1500) < 1e-6);
|
||||||
|
assert.throws(() => cameraOf({ scene, camera: {} }), /no renderer camera/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -50,3 +50,18 @@ export function toCartesian(pos) {
|
|||||||
if ('x' in pos) return new Cesium.Cartesian3(pos.x, pos.y, pos.z);
|
if ('x' in pos) return new Cesium.Cartesian3(pos.x, pos.y, pos.z);
|
||||||
return Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.height ?? 0);
|
return Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, pos.height ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a listener's throw goes with no `error` listener: the platform's
|
||||||
|
* `reportError` (or a rethrow on a microtask), as a browser reports an event
|
||||||
|
* listener's exception, loud, while the frame and the other listeners carry
|
||||||
|
* on.
|
||||||
|
*/
|
||||||
|
export function uncaught(error) {
|
||||||
|
if (typeof globalThis.reportError === 'function')
|
||||||
|
globalThis.reportError(error);
|
||||||
|
else
|
||||||
|
queueMicrotask(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { heightsFor } from '../../queries/heights.js';
|
import { heightsFor } from '../../queries/heights.js';
|
||||||
import { createCesiumCamera } from './camera.js';
|
import { createCesiumCamera } from './camera.js';
|
||||||
import { listenerList, toCartesian } from './common.js';
|
import { listenerList, toCartesian, uncaught } from './common.js';
|
||||||
import {
|
import {
|
||||||
atEndOfTick,
|
atEndOfTick,
|
||||||
billboardLoadError,
|
billboardLoadError,
|
||||||
@@ -25,21 +25,6 @@ import {
|
|||||||
|
|
||||||
export { listenerList, toCartesian };
|
export { listenerList, toCartesian };
|
||||||
|
|
||||||
/**
|
|
||||||
* Where a listener's throw goes with no `error` listener: the platform's
|
|
||||||
* `reportError` (or a rethrow on a microtask), as a browser reports an event
|
|
||||||
* listener's exception, loud, while the frame and the other listeners carry
|
|
||||||
* on.
|
|
||||||
*/
|
|
||||||
function uncaught(error) {
|
|
||||||
if (typeof globalThis.reportError === 'function')
|
|
||||||
globalThis.reportError(error);
|
|
||||||
else
|
|
||||||
queueMicrotask(() => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The contract's colour as Cesium's. */
|
/** The contract's colour as Cesium's. */
|
||||||
export function toColor(color, alpha = 1) {
|
export function toColor(color, alpha = 1) {
|
||||||
if (color === undefined || color === null)
|
if (color === undefined || color === null)
|
||||||
|
|||||||
@@ -392,6 +392,29 @@ export const CAMERA_SCRIPTS = Object.freeze([
|
|||||||
{ record: 'pose' },
|
{ record: 'pose' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'flyTo sineInOut',
|
||||||
|
steps: [
|
||||||
|
START,
|
||||||
|
{
|
||||||
|
do: 'flyTo',
|
||||||
|
args: [
|
||||||
|
{ ...AUSTIN, height: 4000 },
|
||||||
|
{
|
||||||
|
durationS: 1,
|
||||||
|
easing: 'sineInOut',
|
||||||
|
orientation: { heading: 0.3, pitch: -0.5, roll: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ do: 'frames', count: 15 },
|
||||||
|
{ record: 'pose' },
|
||||||
|
{ do: 'frames', count: 30 },
|
||||||
|
{ record: 'pose' },
|
||||||
|
{ do: 'frames', count: 30 },
|
||||||
|
{ record: 'pose' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'a cancelled flight stops where it was',
|
name: 'a cancelled flight stops where it was',
|
||||||
steps: [
|
steps: [
|
||||||
|
|||||||
@@ -2144,6 +2144,110 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"flyTo sineInOut": {
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"kind": "pose",
|
||||||
|
"value": {
|
||||||
|
"position": {
|
||||||
|
"x": -2264202.3786493177,
|
||||||
|
"y": -16652052.298267456,
|
||||||
|
"z": 9785772.494992554
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"x": 0.1297004555930398,
|
||||||
|
"y": 0.9146885584650551,
|
||||||
|
"z": -0.38278274886948277
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
"x": 0.08931821646822154,
|
||||||
|
"y": 0.3736965407891975,
|
||||||
|
"z": 0.9232405708205872
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"x": 0.9875221759666432,
|
||||||
|
"y": -0.1539341950812336,
|
||||||
|
"z": -0.0332297390720854
|
||||||
|
},
|
||||||
|
"heading": 0.03852827617838983,
|
||||||
|
"pitch": -1.433276538096362,
|
||||||
|
"roll": 6.283185307179586,
|
||||||
|
"height": 13074099.901429506,
|
||||||
|
"frameIsIdentity": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 5,
|
||||||
|
"kind": "pose",
|
||||||
|
"value": {
|
||||||
|
"position": {
|
||||||
|
"x": -1031979.3952742261,
|
||||||
|
"y": -7589681.480280083,
|
||||||
|
"z": 4448442.5808664905
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"x": 0.31556045134531424,
|
||||||
|
"y": 0.8875905643040791,
|
||||||
|
"z": 0.33556607651118137
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
"x": 0.10445908107755925,
|
||||||
|
"y": -0.38397902960887687,
|
||||||
|
"z": 0.9174139770033249
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"x": 0.9431383259772522,
|
||||||
|
"y": -0.254446644660509,
|
||||||
|
"z": -0.21388549061082934
|
||||||
|
},
|
||||||
|
"heading": 0.25036959095382905,
|
||||||
|
"pitch": -0.6771468656798949,
|
||||||
|
"roll": 6.283185307179586,
|
||||||
|
"height": 2484858.7135209497,
|
||||||
|
"frameIsIdentity": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 7,
|
||||||
|
"kind": "pose",
|
||||||
|
"value": {
|
||||||
|
"position": {
|
||||||
|
"x": -743292.8811925538,
|
||||||
|
"y": -5466539.584651379,
|
||||||
|
"z": 3198007.239048282
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"x": 0.36970163659367444,
|
||||||
|
"y": 0.7940777354746522,
|
||||||
|
"z": 0.48245336554262613
|
||||||
|
},
|
||||||
|
"up": {
|
||||||
|
"x": 0.0693712174939781,
|
||||||
|
"y": -0.5413817815775576,
|
||||||
|
"z": 0.8379101388331048
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"x": 0.9265572481413991,
|
||||||
|
"y": -0.2763083722932726,
|
||||||
|
"z": -0.2552358699659599
|
||||||
|
},
|
||||||
|
"heading": 0.2999999999999998,
|
||||||
|
"pitch": -0.5,
|
||||||
|
"roll": 6.283185307179586,
|
||||||
|
"height": 3999.9999999988663,
|
||||||
|
"frameIsIdentity": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flights": [
|
||||||
|
{
|
||||||
|
"step": 1,
|
||||||
|
"outcome": "complete",
|
||||||
|
"frames": 62
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"a cancelled flight stops where it was": {
|
"a cancelled flight stops where it was": {
|
||||||
"records": [
|
"records": [
|
||||||
{
|
{
|
||||||
|
|||||||
Vendored
+6
-1
@@ -78,7 +78,12 @@ export interface ViewSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Easing =
|
export type Easing =
|
||||||
'linear' | 'quadraticInOut' | 'cubicInOut' | 'cubicOut' | 'quinticInOut';
|
| 'linear'
|
||||||
|
| 'quadraticInOut'
|
||||||
|
| 'cubicInOut'
|
||||||
|
| 'cubicOut'
|
||||||
|
| 'quinticInOut'
|
||||||
|
| 'sineInOut';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flight's omitted orientation is the same as `setView`'s. Its omitted
|
* A flight's omitted orientation is the same as `setView`'s. Its omitted
|
||||||
|
|||||||
@@ -9,13 +9,36 @@
|
|||||||
const renderers = new WeakMap();
|
const renderers = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind a renderer to its scene object.
|
* Camera ports built for scene objects no renderer is bound to (a viewer of
|
||||||
|
* a consumer's own, say), each `{port, dispose}` and kept for as long as its
|
||||||
|
* scene object lives, or until a renderer is bound to it.
|
||||||
|
*/
|
||||||
|
const unboundCameras = new WeakMap();
|
||||||
|
let buildUnboundCamera = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the factory `cameraOf` builds a camera port with for a scene
|
||||||
|
* object no renderer is bound to. The renderer adapter provides it as it
|
||||||
|
* loads; a later call replaces it.
|
||||||
|
* @param {(host: object) => ({port: object, dispose: () => void} | null)} build
|
||||||
|
*/
|
||||||
|
export function provideUnboundCamera(build) {
|
||||||
|
buildUnboundCamera = build;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a renderer to its scene object. A camera `cameraOf` built for it
|
||||||
|
* while it had no renderer is disposed: its listeners hear nothing more,
|
||||||
|
* and `cameraOf` answers with the renderer's camera from then on.
|
||||||
* @param {object} host The scene object callers hold.
|
* @param {object} host The scene object callers hold.
|
||||||
* @param {object} renderer
|
* @param {object} renderer
|
||||||
* @returns {() => void} Unbinds, if the binding is still this one.
|
* @returns {() => void} Unbinds, if the binding is still this one.
|
||||||
*/
|
*/
|
||||||
export function bindRenderer(host, renderer) {
|
export function bindRenderer(host, renderer) {
|
||||||
if (renderers.has(host)) throw new Error('This scene already has a renderer');
|
if (renderers.has(host)) throw new Error('This scene already has a renderer');
|
||||||
|
// The renderer's camera supersedes one built while there was none.
|
||||||
|
unboundCameras.get(host)?.dispose();
|
||||||
|
unboundCameras.delete(host);
|
||||||
renderers.set(host, renderer);
|
renderers.set(host, renderer);
|
||||||
return () => {
|
return () => {
|
||||||
if (renderers.get(host) === renderer) renderers.delete(host);
|
if (renderers.get(host) === renderer) renderers.delete(host);
|
||||||
@@ -29,13 +52,43 @@ export function bindRenderer(host, renderer) {
|
|||||||
export const rendererOf = (host) => (host && renderers.get(host)) || null;
|
export const rendererOf = (host) => (host && renderers.get(host)) || null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The camera port of the renderer bound to `host`.
|
* The camera port of the renderer bound to `host`; with no renderer bound,
|
||||||
|
* one built over `host` by the provided factory and kept with it while
|
||||||
|
* `host` lives.
|
||||||
|
* @param {object|null|undefined} host
|
||||||
|
* @returns {import('./contract/camera').CameraPort | null} Null when `host`
|
||||||
|
* has no renderer and no camera can be built for it, or is destroyed.
|
||||||
|
*/
|
||||||
|
export function findCamera(host) {
|
||||||
|
const camera = rendererOf(host)?.camera;
|
||||||
|
if (camera) return camera;
|
||||||
|
if (!host || typeof host !== 'object') return null;
|
||||||
|
if (host.isDestroyed?.()) {
|
||||||
|
// Gone with its scene: let the built camera go with it.
|
||||||
|
unboundCameras.get(host)?.dispose();
|
||||||
|
unboundCameras.delete(host);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let built = unboundCameras.get(host);
|
||||||
|
if (!built && buildUnboundCamera) {
|
||||||
|
built = buildUnboundCamera(host);
|
||||||
|
if (built) unboundCameras.set(host, built);
|
||||||
|
}
|
||||||
|
return built?.port ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `findCamera`, for code that needs the camera.
|
||||||
* @param {object} host
|
* @param {object} host
|
||||||
* @returns {import('./contract/camera').CameraPort}
|
* @returns {import('./contract/camera').CameraPort}
|
||||||
* @throws When no renderer is bound to `host`.
|
* @throws When there is no camera for `host`.
|
||||||
*/
|
*/
|
||||||
export function cameraOf(host) {
|
export function cameraOf(host) {
|
||||||
const camera = rendererOf(host)?.camera;
|
const camera = findCamera(host);
|
||||||
if (!camera) throw new Error('This scene has no renderer camera');
|
if (camera) return camera;
|
||||||
return camera;
|
throw new Error(
|
||||||
|
host?.isDestroyed?.()
|
||||||
|
? 'This scene has been destroyed'
|
||||||
|
: 'This scene has no renderer camera',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { bindRenderer, cameraOf, rendererOf } from './rendererOf.js';
|
import {
|
||||||
|
bindRenderer,
|
||||||
|
cameraOf,
|
||||||
|
findCamera,
|
||||||
|
provideUnboundCamera,
|
||||||
|
rendererOf,
|
||||||
|
} from './rendererOf.js';
|
||||||
|
|
||||||
test('a scene finds its renderer until it is unbound', () => {
|
test('a scene finds its renderer until it is unbound', () => {
|
||||||
const scene = {};
|
const scene = {};
|
||||||
@@ -32,3 +38,52 @@ test('cameraOf names the missing renderer instead of returning nothing', () => {
|
|||||||
assert.equal(cameraOf(scene), camera);
|
assert.equal(cameraOf(scene), camera);
|
||||||
unbind();
|
unbind();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('with no renderer bound, cameraOf builds one camera per scene until a renderer is bound', (t) => {
|
||||||
|
t.after(() => provideUnboundCamera(null));
|
||||||
|
const built = [];
|
||||||
|
provideUnboundCamera((host) => {
|
||||||
|
if (!host.camera) return null;
|
||||||
|
const entry = { port: { id: built.length }, disposed: 0 };
|
||||||
|
entry.dispose = () => (entry.disposed += 1);
|
||||||
|
built.push(entry);
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
const viewer = { camera: {} };
|
||||||
|
assert.equal(cameraOf(viewer), built[0].port);
|
||||||
|
assert.equal(cameraOf(viewer), built[0].port, 'kept, not rebuilt');
|
||||||
|
assert.equal(built.length, 1);
|
||||||
|
assert.throws(
|
||||||
|
() => cameraOf({}),
|
||||||
|
/no renderer camera/,
|
||||||
|
'nothing to build over',
|
||||||
|
);
|
||||||
|
assert.equal(findCamera({}), null);
|
||||||
|
assert.equal(findCamera(null), null);
|
||||||
|
|
||||||
|
const camera = { pose() {} };
|
||||||
|
const unbind = bindRenderer(viewer, { camera });
|
||||||
|
assert.equal(
|
||||||
|
built[0].disposed,
|
||||||
|
1,
|
||||||
|
'the renderer supersedes the built camera',
|
||||||
|
);
|
||||||
|
assert.equal(cameraOf(viewer), camera);
|
||||||
|
unbind();
|
||||||
|
assert.equal(cameraOf(viewer), built[1].port, 'unbound again: a fresh one');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a destroyed scene gets no built camera, and lets go of the one it had', (t) => {
|
||||||
|
t.after(() => provideUnboundCamera(null));
|
||||||
|
let disposed = 0;
|
||||||
|
provideUnboundCamera(() => ({ port: {}, dispose: () => (disposed += 1) }));
|
||||||
|
let destroyed = false;
|
||||||
|
const viewer = { isDestroyed: () => destroyed };
|
||||||
|
cameraOf(viewer);
|
||||||
|
destroyed = true;
|
||||||
|
assert.throws(() => cameraOf(viewer), /destroyed/);
|
||||||
|
assert.equal(disposed, 1);
|
||||||
|
assert.equal(findCamera(viewer), null, 'findCamera answers null instead');
|
||||||
|
assert.throws(() => cameraOf(viewer), /destroyed/);
|
||||||
|
assert.equal(disposed, 1, 'once');
|
||||||
|
});
|
||||||
@@ -61,6 +61,7 @@ const EASINGS = {
|
|||||||
cubicInOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2),
|
cubicInOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2),
|
||||||
quinticInOut: (t) => (t < 0.5 ? 16 * t ** 5 : 1 - (-2 * t + 2) ** 5 / 2),
|
quinticInOut: (t) => (t < 0.5 ? 16 * t ** 5 : 1 - (-2 * t + 2) ** 5 / 2),
|
||||||
cubicOut: (t) => (t - 1) ** 3 + 1,
|
cubicOut: (t) => (t - 1) ** 3 + 1,
|
||||||
|
sineInOut: (t) => 0.5 * (1 - Math.cos(Math.PI * t)),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Above this height, a descending flight eases out rather than in and out. */
|
/** Above this height, a descending flight eases out rather than in and out. */
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* A public entry that reaches the renderer bridge (`rendererOf.js`) may hand
|
||||||
|
* `cameraOf` a viewer of the consumer's own, with no renderer bound. The
|
||||||
|
* bridge builds a camera port for it with the factory the Cesium adapter
|
||||||
|
* provides as `render/cesium/camera.js` loads, so every such entry must load
|
||||||
|
* that module too.
|
||||||
|
*
|
||||||
|
* The graph is every static import, re-export and literal dynamic import of
|
||||||
|
* the entry's module and those it reaches, read by the parsers the Cesium
|
||||||
|
* ratchet uses; bare specifiers (packages) end the walk.
|
||||||
|
*/
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { parsers } from 'prettier/plugins/babel.mjs';
|
||||||
|
import { parse as parseSfc } from 'vue/compiler-sfc';
|
||||||
|
|
||||||
|
const root = path.resolve(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
'../..',
|
||||||
|
);
|
||||||
|
const BRIDGE = 'src/render/rendererOf.js';
|
||||||
|
const FACTORY = 'src/render/cesium/camera.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entries that take their Cesium code injected: the module named here is
|
||||||
|
* GEV's Cesium side for them, and must load the factory. Given another, the
|
||||||
|
* entry's flight finds no camera (`findCamera`) and does nothing.
|
||||||
|
*/
|
||||||
|
const INJECTED = {
|
||||||
|
'./layers/recent-imagery': 'src/layers/recentImagery/rendering.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. */
|
||||||
|
function browserTarget(declaration) {
|
||||||
|
if (typeof declaration === 'string') return declaration;
|
||||||
|
if (!declaration || typeof declaration !== 'object') return null;
|
||||||
|
for (const condition of ['browser', 'import', 'default']) {
|
||||||
|
const target = browserTarget(declaration[condition]);
|
||||||
|
if (target) return target;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function specifiersOf(file) {
|
||||||
|
let code = readFileSync(path.join(root, file), 'utf8');
|
||||||
|
if (file.endsWith('.vue')) {
|
||||||
|
const { descriptor } = parseSfc(code, { filename: file });
|
||||||
|
code = [descriptor.script, descriptor.scriptSetup]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((block) => block.content)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
const found = [];
|
||||||
|
const walk = (node) => {
|
||||||
|
if (!node || typeof node !== 'object') return;
|
||||||
|
if (Array.isArray(node)) return node.forEach(walk);
|
||||||
|
if (
|
||||||
|
(node.type === 'ImportDeclaration' ||
|
||||||
|
node.type === 'ExportNamedDeclaration' ||
|
||||||
|
node.type === 'ExportAllDeclaration') &&
|
||||||
|
node.source
|
||||||
|
)
|
||||||
|
found.push({ specifier: node.source.value, lazy: false });
|
||||||
|
if (
|
||||||
|
node.type === 'ImportExpression' &&
|
||||||
|
node.source?.type === 'StringLiteral'
|
||||||
|
)
|
||||||
|
found.push({ specifier: node.source.value, lazy: true });
|
||||||
|
if (
|
||||||
|
node.type === 'CallExpression' &&
|
||||||
|
node.callee?.type === 'Import' &&
|
||||||
|
node.arguments[0]?.type === 'StringLiteral'
|
||||||
|
)
|
||||||
|
found.push({ specifier: node.arguments[0].value, lazy: true });
|
||||||
|
for (const key of Object.keys(node))
|
||||||
|
if (key !== 'loc' && key !== 'start' && key !== 'end') walk(node[key]);
|
||||||
|
};
|
||||||
|
walk(parsers.babel.parse(code));
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A relative specifier as a repository file, or null when it is a package. */
|
||||||
|
function resolve(from, specifier) {
|
||||||
|
const bare = specifier.split('?')[0];
|
||||||
|
if (!bare.startsWith('.')) return null;
|
||||||
|
const base = path.join(path.dirname(from), bare);
|
||||||
|
const candidates = ['', '.js', '.mjs', '.vue', '/index.js', '/index.mjs'];
|
||||||
|
for (const suffix of candidates) {
|
||||||
|
const full = path.join(root, base + suffix);
|
||||||
|
if (existsSync(full) && statSync(full).isFile())
|
||||||
|
return path.normalize(base + suffix);
|
||||||
|
}
|
||||||
|
// A subgraph dropped here would hide a missing factory.
|
||||||
|
throw new Error(`${from}: cannot resolve ${specifier}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The modules `entry` reaches. With `eager`, only through static imports:
|
||||||
|
* what has loaded once the entry has, before any lazy import resolves.
|
||||||
|
*/
|
||||||
|
function reachable(entry, { eager = false } = {}) {
|
||||||
|
const seen = new Set([entry]);
|
||||||
|
const queue = [entry];
|
||||||
|
while (queue.length) {
|
||||||
|
const file = queue.pop();
|
||||||
|
if (!/\.(m?js|vue)$/.test(file)) continue;
|
||||||
|
for (const { specifier, lazy } of specifiersOf(file)) {
|
||||||
|
if (eager && lazy) continue;
|
||||||
|
const next = resolve(file, specifier);
|
||||||
|
if (next && !seen.has(next)) {
|
||||||
|
seen.add(next);
|
||||||
|
queue.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('every public entry that reaches the renderer bridge loads the unbound camera factory', () => {
|
||||||
|
const missing = [];
|
||||||
|
let bridged = 0;
|
||||||
|
for (const [key, declaration] of Object.entries(pkg.exports)) {
|
||||||
|
const target = browserTarget(declaration);
|
||||||
|
if (typeof target !== 'string' || !target.startsWith('./src/')) continue;
|
||||||
|
const entry = path.normalize(target.slice(2));
|
||||||
|
const graph = reachable(entry);
|
||||||
|
// Any path to the bridge counts, lazy or not; only an eager one loads
|
||||||
|
// the factory in time for the first `cameraOf`.
|
||||||
|
if (!graph.has(BRIDGE)) continue;
|
||||||
|
bridged += 1;
|
||||||
|
const carrier = INJECTED[key];
|
||||||
|
const loads = (file) => reachable(file, { eager: true }).has(FACTORY);
|
||||||
|
if (!loads(entry) && !(carrier && loads(carrier))) missing.push(key);
|
||||||
|
}
|
||||||
|
assert.ok(bridged > 0, 'no entry reaches the bridge: the walk is broken');
|
||||||
|
assert.deepEqual(
|
||||||
|
missing,
|
||||||
|
[],
|
||||||
|
`These entries can reach cameraOf without ${FACTORY}; import it where the entry first reaches the bridge.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { toDegrees, toRadians } from './geo/index.js';
|
import { toDegrees, toRadians } from './geo/index.js';
|
||||||
import { rendererOf } from './render/rendererOf.js';
|
import { findCamera } from './render/rendererOf.js';
|
||||||
import { BLOOM_INTENSITY_DEFAULT, BLOOM_SCALE_VERSION } from './bloom.js';
|
import { BLOOM_INTENSITY_DEFAULT, BLOOM_SCALE_VERSION } from './bloom.js';
|
||||||
import {
|
import {
|
||||||
migrateDetectionState,
|
migrateDetectionState,
|
||||||
@@ -69,7 +69,7 @@ export class ShareLinkManager {
|
|||||||
isNavigationCurrent,
|
isNavigationCurrent,
|
||||||
cancelOwnedNavigation,
|
cancelOwnedNavigation,
|
||||||
initialShareCode = null,
|
initialShareCode = null,
|
||||||
camera = rendererOf(viewer)?.camera,
|
camera = findCamera(viewer),
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
if (!camera) throw new Error('ShareLinkManager needs a camera port');
|
if (!camera) throw new Error('ShareLinkManager needs a camera port');
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
} from './cyberSonarControls.js';
|
} from './cyberSonarControls.js';
|
||||||
|
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { cameraOf } from '../render/rendererOf.js';
|
import { cameraOf } from '../render/cesium/camera.js';
|
||||||
|
|
||||||
import { aircraftTrackingTarget } from '../cockpitTracking.js';
|
import { aircraftTrackingTarget } from '../cockpitTracking.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** Camera placement, ground acquisition and motion correction for the Cockpit controller. */
|
/** Camera placement, ground acquisition and motion correction for the Cockpit controller. */
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
|
import { cameraOf } from '../render/cesium/camera.js';
|
||||||
import {
|
import {
|
||||||
cockpitAnchorCorrectionStep,
|
cockpitAnchorCorrectionStep,
|
||||||
cockpitGroundSafeHeight,
|
cockpitGroundSafeHeight,
|
||||||
@@ -349,7 +350,7 @@ export function update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewer.camera.setView({
|
cameraOf(this.viewer).setView({
|
||||||
destination: this.scratchCamera,
|
destination: this.scratchCamera,
|
||||||
orientation: { direction: this.scratchForward, up: this.scratchUp },
|
orientation: { direction: this.scratchForward, up: this.scratchUp },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { CockpitViewController } from './cockpitController.js';
|
|||||||
import { useCockpitStore } from './stores/cockpit.js';
|
import { useCockpitStore } from './stores/cockpit.js';
|
||||||
import CockpitView from './views/CockpitView.vue';
|
import CockpitView from './views/CockpitView.vue';
|
||||||
import ContextView from './views/ContextView.vue';
|
import ContextView from './views/ContextView.vue';
|
||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
|
|
||||||
const INFO = Object.freeze({
|
const INFO = Object.freeze({
|
||||||
icao24: 'abc123',
|
icao24: 'abc123',
|
||||||
@@ -51,6 +52,7 @@ function fixture(t) {
|
|||||||
},
|
},
|
||||||
trackedEntityChanged: listeners(),
|
trackedEntityChanged: listeners(),
|
||||||
};
|
};
|
||||||
|
bindFakeCesiumCamera(viewer);
|
||||||
const cockpit = new CockpitViewController(viewer, {
|
const cockpit = new CockpitViewController(viewer, {
|
||||||
services: {
|
services: {
|
||||||
flightsLayer: { getTrackedInfo: () => INFO, refreshTr3b() {} },
|
flightsLayer: { getTrackedInfo: () => INFO, refreshTr3b() {} },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/** Tracking identity, entry, exit and contact navigation for the Cockpit controller. */
|
/** Tracking identity, entry, exit and contact navigation for the Cockpit controller. */
|
||||||
import { t } from '../i18n/index.js';
|
import { t } from '../i18n/index.js';
|
||||||
|
import { cameraOf } from '../render/rendererOf.js';
|
||||||
import {
|
import {
|
||||||
formatHeadingReadout,
|
formatHeadingReadout,
|
||||||
normalizeHeading,
|
normalizeHeading,
|
||||||
@@ -150,7 +151,7 @@ export function enter() {
|
|||||||
: null;
|
: null;
|
||||||
// Retire deferred navigation before cancelFlight can run its callbacks.
|
// Retire deferred navigation before cancelFlight can run its callbacks.
|
||||||
this.onCameraTakeover?.();
|
this.onCameraTakeover?.();
|
||||||
this.viewer.camera.cancelFlight();
|
cameraOf(this.viewer).cancelFlight();
|
||||||
this.trackedEntity = entity;
|
this.trackedEntity = entity;
|
||||||
this.trackedEntityWasShown = entity.show;
|
this.trackedEntityWasShown = entity.show;
|
||||||
entity.show = false;
|
entity.show = false;
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
* One-click camera transfer for layer-owned world targets.
|
* One-click camera transfer for layer-owned world targets.
|
||||||
*/
|
*/
|
||||||
import { WGS84, toRadians } from './geo/index.js';
|
import { WGS84, toRadians } from './geo/index.js';
|
||||||
import { rendererOf } from './render/rendererOf.js';
|
import { findCamera } from './render/rendererOf.js';
|
||||||
|
|
||||||
export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus';
|
export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus';
|
||||||
export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9;
|
export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9;
|
||||||
@@ -69,7 +69,7 @@ export function routeWorldFocusRequest(event, runExplicitFocus, fly) {
|
|||||||
|
|
||||||
/** Fly to a world target after ownership has been released. */
|
/** Fly to a world target after ownership has been released. */
|
||||||
export function flyToWorldTarget(viewer, target = {}) {
|
export function flyToWorldTarget(viewer, target = {}) {
|
||||||
const camera = rendererOf(viewer)?.camera;
|
const camera = findCamera(viewer);
|
||||||
const framing = WORLD_FOCUS_FRAMING[target.kind];
|
const framing = WORLD_FOCUS_FRAMING[target.kind];
|
||||||
if (!camera || !framing || !isValidWorldFocusTarget(target)) return false;
|
if (!camera || !framing || !isValidWorldFocusTarget(target)) return false;
|
||||||
const poseHeading = camera.pose().heading;
|
const poseHeading = camera.pose().heading;
|
||||||
|
|||||||
Reference in new issue
Block a user