Fly the director, annotations, voice framing and local GeoJSON through the camera port

The scene director's shot flights, start pose and stops, the annotation
framings and fly-to, the voice overhead framing and the local GeoJSON
click flight now drive the renderer's camera port. The director's stops
look the camera up with findCamera: teardown can run after the viewer is
gone, and then there is no flight to stop. The local GeoJSON flight,
part of the public infrastructure export, works on a plain Cesium viewer
through the unbound camera, and looks it up before taking its input
hold. Voice zoom stays for the move slice.
This commit is contained in:
Sameh Khamis committed 2026-11-06 09:49:30 -08:00
1 parent 0b1c5d0f23
commit bc69e8a4ee
8 files changed
+125 -73

No files matched your search

+22
View File
@@ -8,12 +8,23 @@
"modules": [ "modules": [
"src/data/infrastructure.js", "src/data/infrastructure.js",
"src/data/localGeojsonCore.js", "src/data/localGeojsonCore.js",
"src/render/cesium/camera.js",
"src/data/localGeojsonLod.js", "src/data/localGeojsonLod.js",
"src/data/inputOwnership.js", "src/data/inputOwnership.js",
"src/data/pickRegistry.js", "src/data/pickRegistry.js",
"src/data/scenePick.js", "src/data/scenePick.js",
"src/input/dispatcher.js", "src/input/dispatcher.js",
"src/render/cesium/cameraInput.js", "src/render/cesium/cameraInput.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/rendererOf.js",
"src/render/cesium/common.js",
"src/render/cesium/internals.js",
"src/render/cesium/input.js", "src/render/cesium/input.js",
"src/render/cesium/ctrlWheelZoom.js", "src/render/cesium/ctrlWheelZoom.js",
"src/render/input/router.js", "src/render/input/router.js",
@@ -1242,6 +1253,17 @@
"src/data/bhoteKoshiFloodPath.js", "src/data/bhoteKoshiFloodPath.js",
"src/maps/catalog.js", "src/maps/catalog.js",
"src/scenes/director.js", "src/scenes/director.js",
"src/render/cesium/camera.js",
"src/geo/index.js",
"src/render/rendererOf.js",
"src/render/camera/events.js",
"src/geo/celestial.js",
"src/geo/quaternion.js",
"src/geo/frames.js",
"src/geo/ellipsoid.js",
"src/geo/vector.js",
"src/render/cesium/internals.js",
"src/render/cesium/common.js",
"src/scenes/sceneSaveError.js", "src/scenes/sceneSaveError.js",
"src/scenes/playbackAdapter.js", "src/scenes/playbackAdapter.js",
"src/director/playback.js", "src/director/playback.js",
+39 -31
View File
@@ -1,6 +1,7 @@
import { isUnavailableCapability } from '../sources/capability.js'; import { isUnavailableCapability } from '../sources/capability.js';
import { defaultGeospatial } from '../search/defaults.js'; import { defaultGeospatial } from '../search/defaults.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import { greatCircleMeters } from '../geoDistance.js'; import { greatCircleMeters } from '../geoDistance.js';
import { import {
holdContinuousRender, holdContinuousRender,
@@ -1700,14 +1701,18 @@ export function createAnnotationEngine({
Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height || 0), Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height || 0),
); );
const sphere = Cesium.BoundingSphere.fromPoints(cart); const sphere = Cesium.BoundingSphere.fromPoints(cart);
viewer.camera.flyToBoundingSphere(sphere, { const camera = cameraOf(viewer);
offset: new Cesium.HeadingPitchRange( camera.flyToBounds(
viewer.camera.heading, { center: sphere.center, radius: sphere.radius },
Cesium.Math.toRadians(-35), {
Math.max(900, sphere.radius * 2.6), offset: {
), heading: camera.pose().heading,
duration: 1.6, pitch: Cesium.Math.toRadians(-35),
}); range: Math.max(900, sphere.radius * 2.6),
},
durationS: 1.6,
},
);
} catch { } catch {
/* camera assist is best-effort */ /* camera assist is best-effort */
} }
@@ -1757,22 +1762,23 @@ export function createAnnotationEngine({
: anno.ring, : anno.ring,
) )
: viewportRange(anno.viewport) || 600; : viewportRange(anno.viewport) || 600;
viewer.camera.flyToBoundingSphere( const camera = cameraOf(viewer);
new Cesium.BoundingSphere( camera.flyToBounds(
Cesium.Cartesian3.fromDegrees(
target.lon,
target.lat,
anno.anchor.height || 0,
),
range,
),
{ {
offset: new Cesium.HeadingPitchRange( center: {
viewer.camera.heading, lon: target.lon,
Cesium.Math.toRadians(-35), lat: target.lat,
range * 2.4, height: anno.anchor.height || 0,
), },
duration: 1.8, radius: range,
},
{
offset: {
heading: camera.pose().heading,
pitch: Cesium.Math.toRadians(-35),
range: range * 2.4,
},
durationS: 1.8,
}, },
); );
} catch { } catch {
@@ -1978,15 +1984,17 @@ export function createAnnotationEngine({
}) { }) {
if (destroyed) return; if (destroyed) return;
try { try {
viewer.camera.flyTo({ cameraOf(viewer).flyTo(
destination: Cesium.Cartesian3.fromDegrees(lon, lat, height), { lon, lat, height },
orientation: { {
heading: Cesium.Math.toRadians(heading), orientation: {
pitch: Cesium.Math.toRadians(pitch), heading: Cesium.Math.toRadians(heading),
roll: 0, pitch: Cesium.Math.toRadians(pitch),
roll: 0,
},
durationS: duration,
}, },
duration, );
});
} catch { } catch {
/* camera flight best-effort */ /* camera flight best-effort */
} }
+4 -3
View File
@@ -1,3 +1,4 @@
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
import { createStandalonePlaceSearch } from '../standalone/placeSearch.js'; import { createStandalonePlaceSearch } from '../standalone/placeSearch.js';
// Voice-annotation resilience contract tests — pure logic, no network, no browser. // Voice-annotation resilience contract tests — pure logic, no network, no browser.
// //
@@ -1262,7 +1263,7 @@ test('hand-drawn marks: one mark can be hidden, renamed, recoloured, framed and
const { renderer, calls } = fakeRenderer(); const { renderer, calls } = fakeRenderer();
const flights = []; const flights = [];
const engine = createAnnotationEngine({ const engine = createAnnotationEngine({
viewer: { camera: { heading: 0, flyToBoundingSphere: (sphere) => flights.push(sphere) } }, viewer: bindFakeCesiumCamera({ camera: { heading: 0, flyToBoundingSphere: (sphere) => flights.push(sphere) } }),
renderer, renderer,
}); });
let changes = 0; let changes = 0;
@@ -1331,12 +1332,12 @@ test('hand-drawn marks: remove, hide, clear, expiry and framing have their effec
const { renderer, calls } = fakeRenderer(); const { renderer, calls } = fakeRenderer();
const flights = []; const flights = [];
const engine = createAnnotationEngine({ const engine = createAnnotationEngine({
viewer: { viewer: bindFakeCesiumCamera({
camera: { camera: {
heading: 0, heading: 0,
flyToBoundingSphere: (sphere) => flights.push(sphere), flyToBoundingSphere: (sphere) => flights.push(sphere),
}, },
}, }),
renderer, renderer,
}); });
let changes = 0; let changes = 0;
+7
View File
@@ -186,21 +186,28 @@ test('consumer build includes only infrastructure code and resolves assets under
const entry = output.find((item) => item.type === 'chunk' && item.isEntry); const entry = output.find((item) => item.type === 'chunk' && item.isEntry);
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js')); const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [ assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
'camera.js',
'cameraInput.js', 'cameraInput.js',
'celestial.js',
'common.js',
'ctrlWheelZoom.js', 'ctrlWheelZoom.js',
'dispatcher.js', 'dispatcher.js',
'events.js',
'heights.js', 'heights.js',
'infrastructure.js', 'infrastructure.js',
'infrastructureData.js', 'infrastructureData.js',
'infrastructureOverlayEntry.js', 'infrastructureOverlayEntry.js',
'input.js', 'input.js',
'inputOwnership.js', 'inputOwnership.js',
'internals.js',
'localGeojsonCore.js', 'localGeojsonCore.js',
'localGeojsonLod.js', 'localGeojsonLod.js',
'pickRegistry.js', 'pickRegistry.js',
'rendererOf.js',
'router.js', 'router.js',
'scenePick.js', 'scenePick.js',
'sensorPick.js', 'sensorPick.js',
'vector.js',
]); ]);
assert.deepEqual( assert.deepEqual(
entry.imports, entry.imports,
+11 -6
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import { import {
layerTitle, layerTitle,
mapAnalystRecord, mapAnalystRecord,
@@ -820,6 +821,8 @@ export function createLocalGeoJsonLayer(
if (targetPos) { if (targetPos) {
const carto = Cesium.Cartographic.fromCartesian(targetPos); const carto = Cesium.Cartographic.fromCartesian(targetPos);
// Before the hold: with no camera, nothing would end it.
const camera = cameraOf(viewer);
// Hold camera input so Cesium doesn't cancel the flight. // Hold camera input so Cesium doesn't cancel the flight.
// A hold, not a flag: the flight ending must not hand input // A hold, not a flag: the flight ending must not hand input
@@ -828,16 +831,18 @@ export function createLocalGeoJsonLayer(
'local-geojson-flight', 'local-geojson-flight',
); );
viewer.camera.flyTo({ camera.flyTo(
destination: Cesium.Cartesian3.fromRadians( Cesium.Cartesian3.fromRadians(
carto.longitude, carto.longitude,
carto.latitude, carto.latitude,
5000, 5000,
), ),
duration: 1.5, {
complete: releaseCamera, durationS: 1.5,
cancel: releaseCamera, onComplete: releaseCamera,
}); onCancel: releaseCamera,
},
);
} }
} }
}); });
+25 -22
View File
@@ -21,6 +21,8 @@ import { createToast } from '../ui/toast.js';
import { buildPlaybackQueue, playSceneQueue } from '../director/playback.js'; import { buildPlaybackQueue, playSceneQueue } from '../director/playback.js';
import { createScenePlaybackAdapter } from './playbackAdapter.js'; import { createScenePlaybackAdapter } from './playbackAdapter.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import { findCamera } from '../render/rendererOf.js';
import { import {
SCENE_APPEND_RECIPES, SCENE_APPEND_RECIPES,
SCENE_RECIPES, SCENE_RECIPES,
@@ -263,7 +265,8 @@ export class SceneDirector {
this.stopScene(msg('Stopped')); this.stopScene(msg('Stopped'));
this._loadAbort?.abort(); this._loadAbort?.abort();
this._loadGeneration++; this._loadGeneration++;
this.viewer.camera.cancelFlight(); // The viewer may be gone by now: then there is no flight to stop.
findCamera(this.viewer)?.cancelFlight();
this._storageToast?.cancel(); this._storageToast?.cancel();
await Promise.allSettled(this._pendingWork || []); await Promise.allSettled(this._pendingWork || []);
this._clock.destroy(); this._clock.destroy();
@@ -1746,16 +1749,16 @@ export class SceneDirector {
/** Place the camera at an authored replay start pose without an intermediate flight. */ /** Place the camera at an authored replay start pose without an intermediate flight. */
_setCameraView(cameraState) { _setCameraView(cameraState) {
if (!cameraState || typeof this.viewer?.camera?.setView !== 'function') const camera = findCamera(this.viewer);
return false; if (!cameraState || !camera) return false;
this.styleManager?.clearSearchedLocation?.(); this.styleManager?.clearSearchedLocation?.();
this.viewer.camera.cancelFlight?.(); camera.cancelFlight();
this.viewer.camera.setView({ camera.setView({
destination: Cesium.Cartesian3.fromDegrees( destination: {
cameraState.lon, lon: cameraState.lon,
cameraState.lat, lat: cameraState.lat,
cameraState.alt, height: cameraState.alt,
), },
orientation: { orientation: {
heading: Cesium.Math.toRadians(cameraState.heading || 0), heading: Cesium.Math.toRadians(cameraState.heading || 0),
pitch: Cesium.Math.toRadians(cameraState.pitch ?? -35), pitch: Cesium.Math.toRadians(cameraState.pitch ?? -35),
@@ -2022,7 +2025,8 @@ export class SceneDirector {
this._loadGeneration++; this._loadGeneration++;
this._clock.stopShot(); this._clock.stopShot();
this._cancelActiveSceneTravel(); this._cancelActiveSceneTravel();
this.viewer.camera.cancelFlight(); // Stop runs in teardown too, after the viewer may be gone.
findCamera(this.viewer)?.cancelFlight();
this._clock.stop(); this._clock.stop();
if (!this._running || !this._runToken) return; if (!this._running || !this._runToken) return;
this._runToken.cancelled = true; this._runToken.cancelled = true;
@@ -2464,11 +2468,11 @@ export class SceneDirector {
0.2, 0.2,
Number(durationSec) || DEFAULT_SHOT_DURATION_SEC, Number(durationSec) || DEFAULT_SHOT_DURATION_SEC,
); );
const destination = Cesium.Cartesian3.fromDegrees( const destination = {
cameraState.lon, lon: cameraState.lon,
cameraState.lat, lat: cameraState.lat,
cameraState.alt, height: cameraState.alt,
); };
await new Promise((resolve) => { await new Promise((resolve) => {
// Guard against double-resolve from both callback and timeout // Guard against double-resolve from both callback and timeout
@@ -2481,17 +2485,16 @@ export class SceneDirector {
resolve(); resolve();
}; };
this.viewer.camera.flyTo({ cameraOf(this.viewer).flyTo(destination, {
destination,
orientation: { orientation: {
heading: Cesium.Math.toRadians(cameraState.heading || 0), heading: Cesium.Math.toRadians(cameraState.heading || 0),
pitch: Cesium.Math.toRadians(cameraState.pitch ?? -35), pitch: Cesium.Math.toRadians(cameraState.pitch ?? -35),
roll: Cesium.Math.toRadians(cameraState.roll || 0), roll: Cesium.Math.toRadians(cameraState.roll || 0),
}, },
duration, durationS: duration,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT, easing: 'cubicInOut',
complete: finish, onComplete: finish,
cancel: finish, onCancel: finish,
}); });
// Safety timeout in case Cesium callbacks fail to fire // Safety timeout in case Cesium callbacks fail to fire
+4 -3
View File
@@ -1,3 +1,4 @@
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
import { readShellSource } from '../testSupport/readShellSource.mjs'; import { readShellSource } from '../testSupport/readShellSource.mjs';
import { useStatusStore } from '../ui/stores/status.js'; import { useStatusStore } from '../ui/stores/status.js';
import { applicationPinia } from '../ui/vueRuntime.js'; import { applicationPinia } from '../ui/vueRuntime.js';
@@ -1072,7 +1073,7 @@ function fakeViewer() {
const flights = []; const flights = [];
const views = []; const views = [];
let cancelled = 0; let cancelled = 0;
return { return bindFakeCesiumCamera({
flights, flights,
views, views,
get cancelledFlights() { return cancelled; }, get cancelledFlights() { return cancelled; },
@@ -1086,7 +1087,7 @@ function fakeViewer() {
}, },
cancelFlight() { cancelled++; }, cancelFlight() { cancelled++; },
}, },
}; });
} }
/** Yield enough turns for the director's pending awaits to advance. */ /** Yield enough turns for the director's pending awaits to advance. */
@@ -1776,7 +1777,7 @@ test('destroy drains cancelled LOAD work before its viewer can be discarded', as
t.after(restore); t.after(restore);
let finishVisual; let finishVisual;
const dataManager = fakeDataManager(); const dataManager = fakeDataManager();
const viewer = { camera: { cancelFlight() {} } }; const viewer = bindFakeCesiumCamera({ camera: { cancelFlight() {} } });
const director = new SceneDirector(viewer, { const director = new SceneDirector(viewer, {
applyVisualState: () => new Promise((resolve) => { finishVisual = resolve; }), applyVisualState: () => new Promise((resolve) => { finishVisual = resolve; }),
}, dataManager); }, dataManager);
+13 -8
View File
@@ -12,6 +12,7 @@ import { msg } from '../i18n/index.js';
import { useProviderSettingsStore } from '../ui/stores/providerSettings.js'; import { useProviderSettingsStore } from '../ui/stores/providerSettings.js';
import { applicationPinia } from '../ui/vueRuntime.js'; import { applicationPinia } from '../ui/vueRuntime.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import { distance as chordM, toLonLatH } from '../geo/index.js'; import { distance as chordM, toLonLatH } from '../geo/index.js';
import { CITY_POIS } from '../cityPois.js'; import { CITY_POIS } from '../cityPois.js';
import { greatCircleKm } from '../geoDistance.js'; import { greatCircleKm } from '../geoDistance.js';
@@ -2654,14 +2655,18 @@ async function frameOverhead(viewer, dataManager, styleManager, args = {}) {
'frame', 'frame',
'frame_overhead', 'frame_overhead',
() => { () => {
viewer.camera.flyToBoundingSphere(sphere, { const camera = cameraOf(viewer);
duration: 2.0, camera.flyToBounds(
offset: new Cesium.HeadingPitchRange( { center: sphere.center, radius: sphere.radius },
viewer.camera.heading, {
pitch, durationS: 2.0,
sphere.radius * 2.4, offset: {
), heading: camera.pose().heading,
}); pitch,
range: sphere.radius * 2.4,
},
},
);
let detectionEnabled = false; let detectionEnabled = false;
try { try {