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": [
"src/data/infrastructure.js",
"src/data/localGeojsonCore.js",
"src/render/cesium/camera.js",
"src/data/localGeojsonLod.js",
"src/data/inputOwnership.js",
"src/data/pickRegistry.js",
"src/data/scenePick.js",
"src/input/dispatcher.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/ctrlWheelZoom.js",
"src/render/input/router.js",
@@ -1242,6 +1253,17 @@
"src/data/bhoteKoshiFloodPath.js",
"src/maps/catalog.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/playbackAdapter.js",
"src/director/playback.js",
+39 -31
View File
@@ -1,6 +1,7 @@
import { isUnavailableCapability } from '../sources/capability.js';
import { defaultGeospatial } from '../search/defaults.js';
import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import { greatCircleMeters } from '../geoDistance.js';
import {
holdContinuousRender,
@@ -1700,14 +1701,18 @@ export function createAnnotationEngine({
Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height || 0),
);
const sphere = Cesium.BoundingSphere.fromPoints(cart);
viewer.camera.flyToBoundingSphere(sphere, {
offset: new Cesium.HeadingPitchRange(
viewer.camera.heading,
Cesium.Math.toRadians(-35),
Math.max(900, sphere.radius * 2.6),
),
duration: 1.6,
});
const camera = cameraOf(viewer);
camera.flyToBounds(
{ center: sphere.center, radius: sphere.radius },
{
offset: {
heading: camera.pose().heading,
pitch: Cesium.Math.toRadians(-35),
range: Math.max(900, sphere.radius * 2.6),
},
durationS: 1.6,
},
);
} catch {
/* camera assist is best-effort */
}
@@ -1757,22 +1762,23 @@ export function createAnnotationEngine({
: anno.ring,
)
: viewportRange(anno.viewport) || 600;
viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(
Cesium.Cartesian3.fromDegrees(
target.lon,
target.lat,
anno.anchor.height || 0,
),
range,
),
const camera = cameraOf(viewer);
camera.flyToBounds(
{
offset: new Cesium.HeadingPitchRange(
viewer.camera.heading,
Cesium.Math.toRadians(-35),
range * 2.4,
),
duration: 1.8,
center: {
lon: target.lon,
lat: target.lat,
height: anno.anchor.height || 0,
},
radius: range,
},
{
offset: {
heading: camera.pose().heading,
pitch: Cesium.Math.toRadians(-35),
range: range * 2.4,
},
durationS: 1.8,
},
);
} catch {
@@ -1978,15 +1984,17 @@ export function createAnnotationEngine({
}) {
if (destroyed) return;
try {
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lon, lat, height),
orientation: {
heading: Cesium.Math.toRadians(heading),
pitch: Cesium.Math.toRadians(pitch),
roll: 0,
cameraOf(viewer).flyTo(
{ lon, lat, height },
{
orientation: {
heading: Cesium.Math.toRadians(heading),
pitch: Cesium.Math.toRadians(pitch),
roll: 0,
},
durationS: duration,
},
duration,
});
);
} catch {
/* camera flight best-effort */
}
+4 -3
View File
@@ -1,3 +1,4 @@
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
import { createStandalonePlaceSearch } from '../standalone/placeSearch.js';
// 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 flights = [];
const engine = createAnnotationEngine({
viewer: { camera: { heading: 0, flyToBoundingSphere: (sphere) => flights.push(sphere) } },
viewer: bindFakeCesiumCamera({ camera: { heading: 0, flyToBoundingSphere: (sphere) => flights.push(sphere) } }),
renderer,
});
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 flights = [];
const engine = createAnnotationEngine({
viewer: {
viewer: bindFakeCesiumCamera({
camera: {
heading: 0,
flyToBoundingSphere: (sphere) => flights.push(sphere),
},
},
}),
renderer,
});
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 sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
'camera.js',
'cameraInput.js',
'celestial.js',
'common.js',
'ctrlWheelZoom.js',
'dispatcher.js',
'events.js',
'heights.js',
'infrastructure.js',
'infrastructureData.js',
'infrastructureOverlayEntry.js',
'input.js',
'inputOwnership.js',
'internals.js',
'localGeojsonCore.js',
'localGeojsonLod.js',
'pickRegistry.js',
'rendererOf.js',
'router.js',
'scenePick.js',
'sensorPick.js',
'vector.js',
]);
assert.deepEqual(
entry.imports,
+11 -6
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import {
layerTitle,
mapAnalystRecord,
@@ -820,6 +821,8 @@ export function createLocalGeoJsonLayer(
if (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.
// A hold, not a flag: the flight ending must not hand input
@@ -828,16 +831,18 @@ export function createLocalGeoJsonLayer(
'local-geojson-flight',
);
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromRadians(
camera.flyTo(
Cesium.Cartesian3.fromRadians(
carto.longitude,
carto.latitude,
5000,
),
duration: 1.5,
complete: releaseCamera,
cancel: releaseCamera,
});
{
durationS: 1.5,
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 { createScenePlaybackAdapter } from './playbackAdapter.js';
import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import { findCamera } from '../render/rendererOf.js';
import {
SCENE_APPEND_RECIPES,
SCENE_RECIPES,
@@ -263,7 +265,8 @@ export class SceneDirector {
this.stopScene(msg('Stopped'));
this._loadAbort?.abort();
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();
await Promise.allSettled(this._pendingWork || []);
this._clock.destroy();
@@ -1746,16 +1749,16 @@ export class SceneDirector {
/** Place the camera at an authored replay start pose without an intermediate flight. */
_setCameraView(cameraState) {
if (!cameraState || typeof this.viewer?.camera?.setView !== 'function')
return false;
const camera = findCamera(this.viewer);
if (!cameraState || !camera) return false;
this.styleManager?.clearSearchedLocation?.();
this.viewer.camera.cancelFlight?.();
this.viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(
cameraState.lon,
cameraState.lat,
cameraState.alt,
),
camera.cancelFlight();
camera.setView({
destination: {
lon: cameraState.lon,
lat: cameraState.lat,
height: cameraState.alt,
},
orientation: {
heading: Cesium.Math.toRadians(cameraState.heading || 0),
pitch: Cesium.Math.toRadians(cameraState.pitch ?? -35),
@@ -2022,7 +2025,8 @@ export class SceneDirector {
this._loadGeneration++;
this._clock.stopShot();
this._cancelActiveSceneTravel();
this.viewer.camera.cancelFlight();
// Stop runs in teardown too, after the viewer may be gone.
findCamera(this.viewer)?.cancelFlight();
this._clock.stop();
if (!this._running || !this._runToken) return;
this._runToken.cancelled = true;
@@ -2464,11 +2468,11 @@ export class SceneDirector {
0.2,
Number(durationSec) || DEFAULT_SHOT_DURATION_SEC,
);
const destination = Cesium.Cartesian3.fromDegrees(
cameraState.lon,
cameraState.lat,
cameraState.alt,
);
const destination = {
lon: cameraState.lon,
lat: cameraState.lat,
height: cameraState.alt,
};
await new Promise((resolve) => {
// Guard against double-resolve from both callback and timeout
@@ -2481,17 +2485,16 @@ export class SceneDirector {
resolve();
};
this.viewer.camera.flyTo({
destination,
cameraOf(this.viewer).flyTo(destination, {
orientation: {
heading: Cesium.Math.toRadians(cameraState.heading || 0),
pitch: Cesium.Math.toRadians(cameraState.pitch ?? -35),
roll: Cesium.Math.toRadians(cameraState.roll || 0),
},
duration,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
complete: finish,
cancel: finish,
durationS: duration,
easing: 'cubicInOut',
onComplete: finish,
onCancel: finish,
});
// 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 { useStatusStore } from '../ui/stores/status.js';
import { applicationPinia } from '../ui/vueRuntime.js';
@@ -1072,7 +1073,7 @@ function fakeViewer() {
const flights = [];
const views = [];
let cancelled = 0;
return {
return bindFakeCesiumCamera({
flights,
views,
get cancelledFlights() { return cancelled; },
@@ -1086,7 +1087,7 @@ function fakeViewer() {
},
cancelFlight() { cancelled++; },
},
};
});
}
/** 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);
let finishVisual;
const dataManager = fakeDataManager();
const viewer = { camera: { cancelFlight() {} } };
const viewer = bindFakeCesiumCamera({ camera: { cancelFlight() {} } });
const director = new SceneDirector(viewer, {
applyVisualState: () => new Promise((resolve) => { finishVisual = resolve; }),
}, dataManager);
+13 -8
View File
@@ -12,6 +12,7 @@ import { msg } from '../i18n/index.js';
import { useProviderSettingsStore } from '../ui/stores/providerSettings.js';
import { applicationPinia } from '../ui/vueRuntime.js';
import * as Cesium from 'cesium';
import { cameraOf } from '../render/cesium/camera.js';
import { distance as chordM, toLonLatH } from '../geo/index.js';
import { CITY_POIS } from '../cityPois.js';
import { greatCircleKm } from '../geoDistance.js';
@@ -2654,14 +2655,18 @@ async function frameOverhead(viewer, dataManager, styleManager, args = {}) {
'frame',
'frame_overhead',
() => {
viewer.camera.flyToBoundingSphere(sphere, {
duration: 2.0,
offset: new Cesium.HeadingPitchRange(
viewer.camera.heading,
pitch,
sphere.radius * 2.4,
),
});
const camera = cameraOf(viewer);
camera.flyToBounds(
{ center: sphere.center, radius: sphere.radius },
{
durationS: 2.0,
offset: {
heading: camera.pose().heading,
pitch,
range: sphere.radius * 2.4,
},
},
);
let detectionEnabled = false;
try {