mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Orbit, move, look and zoom through the camera port
The Cesium camera port gains move, look, rotateAround and zoom, and the browser conformance run now checks them, the lookAt-frame commands, the changed thresholds and moveEnd (timed by the clock, not a frame count). Orbit, the Bhote Koshi focus and cinematic, the launch zoom anchor and replay chase, the camera verbs' motion tick and roll levelling, and voice zoom write through the port.
This commit is contained in:
1 parent
68855b2089
commit
1d9c731c10
19 files changed
+174
-112
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
|
import { bindFakeCesiumCamera } from './testSupport/fakeCameraPort.mjs';
|
||||||
import { GLOBE_VIEW } from './locations.js';
|
import { GLOBE_VIEW } from './locations.js';
|
||||||
import { projectEarthDiscToViewport } from './celestialRing.js';
|
import { projectEarthDiscToViewport } from './celestialRing.js';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -146,13 +147,13 @@ function harness(
|
|||||||
roll: radians(roll),
|
roll: radians(roll),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const viewer = {
|
const viewer = bindFakeCesiumCamera({
|
||||||
camera,
|
camera,
|
||||||
scene,
|
scene,
|
||||||
trackedEntity: undefined,
|
trackedEntity: undefined,
|
||||||
trackedEntityChanged: new Cesium.Event(),
|
trackedEntityChanged: new Cesium.Event(),
|
||||||
clock: { onTick: new Cesium.Event() },
|
clock: { onTick: new Cesium.Event() },
|
||||||
};
|
});
|
||||||
const target = Cesium.Cartesian3.fromDegrees(
|
const target = Cesium.Cartesian3.fromDegrees(
|
||||||
longitudeDeg,
|
longitudeDeg,
|
||||||
latitude,
|
latitude,
|
||||||
|
|||||||
+23
-21
@@ -30,6 +30,8 @@ import {
|
|||||||
releaseContinuousRender,
|
releaseContinuousRender,
|
||||||
} from './renderGovernor.js';
|
} from './renderGovernor.js';
|
||||||
import { heightsFor } from './queries/heights.js';
|
import { heightsFor } from './queries/heights.js';
|
||||||
|
import { cameraOf } from './render/cesium/camera.js';
|
||||||
|
import { findCamera } from './render/rendererOf.js';
|
||||||
|
|
||||||
let _isCockpitActive = null;
|
let _isCockpitActive = null;
|
||||||
/** °/s by speed word — orbit; pan uses fractions of view height/s. */
|
/** °/s by speed word — orbit; pan uses fractions of view height/s. */
|
||||||
@@ -1004,7 +1006,7 @@ export function interruptCameraMotionIfActive(
|
|||||||
|
|
||||||
function clearLookAt() {
|
function clearLookAt() {
|
||||||
try {
|
try {
|
||||||
_viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
findCamera(_viewer)?.release();
|
||||||
} catch {
|
} catch {
|
||||||
/* teardown race */
|
/* teardown race */
|
||||||
}
|
}
|
||||||
@@ -1022,13 +1024,11 @@ function clearLookAt() {
|
|||||||
*/
|
*/
|
||||||
function levelCameraRoll() {
|
function levelCameraRoll() {
|
||||||
try {
|
try {
|
||||||
const cam = _viewer?.camera;
|
const camera = findCamera(_viewer);
|
||||||
if (!cam || typeof cam.setView !== 'function') return false;
|
if (!camera) return false;
|
||||||
const roll = Cesium.Math.negativePiToPi(cam.roll || 0);
|
const { heading, pitch, roll } = camera.pose();
|
||||||
if (Math.abs(roll) < 1e-5) return false;
|
if (Math.abs(Cesium.Math.negativePiToPi(roll || 0)) < 1e-5) return false;
|
||||||
cam.setView({
|
camera.setView({ orientation: { heading, pitch, roll: 0 } });
|
||||||
orientation: { heading: cam.heading, pitch: cam.pitch, roll: 0 },
|
|
||||||
});
|
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false; // teardown race
|
return false; // teardown race
|
||||||
@@ -1081,6 +1081,9 @@ export function getActiveCameraMotion() {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A pan's direction as the camera move it makes; any other is down. */
|
||||||
|
const PAN_MOVES = Object.freeze({ left: 'left', right: 'right', up: 'up' });
|
||||||
|
|
||||||
function onTick() {
|
function onTick() {
|
||||||
if (!_active || !_viewer) return;
|
if (!_active || !_viewer) return;
|
||||||
// Wall-clock dt: clock.currentTime FREEZES when the app clock isn't
|
// Wall-clock dt: clock.currentTime FREEZES when the app clock isn't
|
||||||
@@ -1091,6 +1094,8 @@ function onTick() {
|
|||||||
Math.max(0.001, (nowMs - (_active.lastMs ?? nowMs)) / 1000),
|
Math.max(0.001, (nowMs - (_active.lastMs ?? nowMs)) / 1000),
|
||||||
);
|
);
|
||||||
_active.lastMs = nowMs;
|
_active.lastMs = nowMs;
|
||||||
|
// Reads stay on Cesium's camera until the query port has them; every
|
||||||
|
// write goes through the camera port.
|
||||||
const cam = _viewer.camera;
|
const cam = _viewer.camera;
|
||||||
const m = _active;
|
const m = _active;
|
||||||
|
|
||||||
@@ -1106,6 +1111,7 @@ function onTick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const camera = cameraOf(_viewer);
|
||||||
if (m.kind === 'orbit' && m.pending) {
|
if (m.kind === 'orbit' && m.pending) {
|
||||||
// Armed but target-less (started mid-flight, e.g. "fly to X and orbit
|
// Armed but target-less (started mid-flight, e.g. "fly to X and orbit
|
||||||
// it"): keep trying for a ground target until the camera settles.
|
// it"): keep trying for a ground target until the camera settles.
|
||||||
@@ -1155,7 +1161,7 @@ function onTick() {
|
|||||||
} else {
|
} else {
|
||||||
m.hpr.heading += rate * dt;
|
m.hpr.heading += rate * dt;
|
||||||
}
|
}
|
||||||
cam.lookAt(m.target, m.hpr);
|
camera.lookAt(m.target, m.hpr);
|
||||||
} else if (m.kind === 'pan') {
|
} else if (m.kind === 'pan') {
|
||||||
const heightM = Math.max(50, cam.positionCartographic.height);
|
const heightM = Math.max(50, cam.positionCartographic.height);
|
||||||
const step =
|
const step =
|
||||||
@@ -1163,10 +1169,7 @@ function onTick() {
|
|||||||
? heightM * ONCE.panViewFraction * (scale - (m.lastScale || 0))
|
? heightM * ONCE.panViewFraction * (scale - (m.lastScale || 0))
|
||||||
: heightM * PAN_VIEW_FRACTION_S[m.speed] * dt;
|
: heightM * PAN_VIEW_FRACTION_S[m.speed] * dt;
|
||||||
m.lastScale = scale;
|
m.lastScale = scale;
|
||||||
if (m.direction === 'left') cam.moveLeft(step);
|
camera.move(PAN_MOVES[m.direction] ?? 'down', step);
|
||||||
else if (m.direction === 'right') cam.moveRight(step);
|
|
||||||
else if (m.direction === 'up') cam.moveUp(step);
|
|
||||||
else cam.moveDown(step);
|
|
||||||
} else if (
|
} else if (
|
||||||
m.kind === 'tilt' ||
|
m.kind === 'tilt' ||
|
||||||
m.kind === 'rotate' ||
|
m.kind === 'rotate' ||
|
||||||
@@ -1211,7 +1214,7 @@ function onTick() {
|
|||||||
interruptCameraMotion('globe-surface-limit');
|
interruptCameraMotion('globe-surface-limit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cam.rotate(m.rotationAxis, angle);
|
camera.rotateAround(m.rotationAxis, angle);
|
||||||
} else if (m.kind === 'tilt') {
|
} else if (m.kind === 'tilt') {
|
||||||
const up = m.direction === 'up';
|
const up = m.direction === 'up';
|
||||||
// Clamp pitch to [-89°, -5°]: never through the ground, never at the sky.
|
// Clamp pitch to [-89°, -5°]: never through the ground, never at the sky.
|
||||||
@@ -1220,10 +1223,8 @@ function onTick() {
|
|||||||
interruptCameraMotion('tilt-clamp');
|
interruptCameraMotion('tilt-clamp');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (up) cam.lookUp(stepRad);
|
camera.look(up ? 'up' : 'down', stepRad);
|
||||||
else cam.lookDown(stepRad);
|
} else camera.look(m.direction === 'left' ? 'left' : 'right', stepRad);
|
||||||
} else if (m.direction === 'left') cam.lookLeft(stepRad);
|
|
||||||
else cam.lookRight(stepRad);
|
|
||||||
} else if (m.kind === 'route') {
|
} else if (m.kind === 'route') {
|
||||||
// The dolly's shaping lives in advanceRouteFlight(); the tick only
|
// The dolly's shaping lives in advanceRouteFlight(); the tick only
|
||||||
// applies the pose and honours the same completion path as before.
|
// applies the pose and honours the same completion path as before.
|
||||||
@@ -1232,7 +1233,7 @@ function onTick() {
|
|||||||
// user left it rather than teleporting to an altitude derived from
|
// user left it rather than teleporting to an altitude derived from
|
||||||
// nothing. An interrupt during the arm is still instant.
|
// nothing. An interrupt during the arm is still instant.
|
||||||
if (frame.arming) return;
|
if (frame.arming) return;
|
||||||
cam.setView({
|
camera.setView({
|
||||||
destination: frame.eye,
|
destination: frame.eye,
|
||||||
orientation: { direction: frame.direction, up: frame.up },
|
orientation: { direction: frame.direction, up: frame.up },
|
||||||
});
|
});
|
||||||
@@ -1612,9 +1613,10 @@ export function moveCamera(args = {}, runNavigation = null) {
|
|||||||
if (motion === 'rotate_globe') {
|
if (motion === 'rotate_globe') {
|
||||||
// Convert an inherited ground-target frame without changing the world
|
// Convert an inherited ground-target frame without changing the world
|
||||||
// pose. Rotation must pivot about Earth, never the previous local target.
|
// pose. Rotation must pivot about Earth, never the previous local target.
|
||||||
|
const camera = cameraOf(_viewer);
|
||||||
|
camera.cancelFlight();
|
||||||
|
camera.release();
|
||||||
const cam = _viewer.camera;
|
const cam = _viewer.camera;
|
||||||
cam.cancelFlight();
|
|
||||||
cam.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
|
||||||
// Handoff may level a route's bank. Keep the caller's exact world pose.
|
// Handoff may level a route's bank. Keep the caller's exact world pose.
|
||||||
// Copy the basis directly: setView's heading/pitch conversion is singular
|
// Copy the basis directly: setView's heading/pitch conversion is singular
|
||||||
// at nadir and can change the captured orientation near the poles.
|
// at nadir and can change the captured orientation near the poles.
|
||||||
|
|||||||
+23
-18
@@ -29,6 +29,8 @@ import { formatDate, t, tn, msg, onLocaleChange } from '../i18n/index.js';
|
|||||||
import { useBhoteKoshiPanelStore } from '../ui/stores/bhoteKoshiPanel.js';
|
import { useBhoteKoshiPanelStore } from '../ui/stores/bhoteKoshiPanel.js';
|
||||||
import { applicationPinia } from '../ui/vueRuntime.js';
|
import { applicationPinia } from '../ui/vueRuntime.js';
|
||||||
import { heightsFor } from '../queries/heights.js';
|
import { heightsFor } from '../queries/heights.js';
|
||||||
|
import { cameraOf } from '../render/cesium/camera.js';
|
||||||
|
import { findCamera } from '../render/rendererOf.js';
|
||||||
|
|
||||||
export const BHOTE_KOSHI_LAYER_ID = 'bhote-koshi-2026';
|
export const BHOTE_KOSHI_LAYER_ID = 'bhote-koshi-2026';
|
||||||
export const BHOTE_KOSHI_OVERLAY_SOURCE_ID = 'bhote-koshi-witnesses';
|
export const BHOTE_KOSHI_OVERLAY_SOURCE_ID = 'bhote-koshi-witnesses';
|
||||||
@@ -888,20 +890,24 @@ function focusView(viewer, viewpoint) {
|
|||||||
viewpoint.lat,
|
viewpoint.lat,
|
||||||
viewpoint.targetElevationM,
|
viewpoint.targetElevationM,
|
||||||
);
|
);
|
||||||
const offset = new Cesium.HeadingPitchRange(
|
const offset = {
|
||||||
Cesium.Math.toRadians(viewpoint.headingDeg),
|
heading: Cesium.Math.toRadians(viewpoint.headingDeg),
|
||||||
Cesium.Math.toRadians(viewpoint.pitchDeg),
|
pitch: Cesium.Math.toRadians(viewpoint.pitchDeg),
|
||||||
viewpoint.rangeM,
|
range: viewpoint.rangeM,
|
||||||
);
|
};
|
||||||
viewer.camera.cancelFlight?.();
|
const camera = cameraOf(viewer);
|
||||||
viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(target, 80), {
|
camera.cancelFlight();
|
||||||
offset,
|
camera.flyToBounds(
|
||||||
duration: 2.2,
|
{ center: target, radius: 80 },
|
||||||
complete: () => {
|
{
|
||||||
viewer.camera.lookAt(target, offset);
|
offset,
|
||||||
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
durationS: 2.2,
|
||||||
|
onComplete: () => {
|
||||||
|
camera.lookAt(target, offset);
|
||||||
|
camera.release();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setElementProperty(element, property, value) {
|
function setElementProperty(element, property, value) {
|
||||||
@@ -1045,7 +1051,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
let _cinematicActive = false;
|
let _cinematicActive = false;
|
||||||
let _cinematicReplayArmed = false;
|
let _cinematicReplayArmed = false;
|
||||||
let _cinematicPose = { target: new Cesium.Cartesian3() };
|
let _cinematicPose = { target: new Cesium.Cartesian3() };
|
||||||
let _cinematicOffset = new Cesium.HeadingPitchRange();
|
const _cinematicOffset = { heading: 0, pitch: 0, range: 0 };
|
||||||
let _lastCinematicProgress = null;
|
let _lastCinematicProgress = null;
|
||||||
let _cameraInputTarget = null;
|
let _cameraInputTarget = null;
|
||||||
let _evidenceClickTarget = null;
|
let _evidenceClickTarget = null;
|
||||||
@@ -2626,7 +2632,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_cinematicOffset.heading = Cesium.Math.toRadians(pose.headingDeg);
|
_cinematicOffset.heading = Cesium.Math.toRadians(pose.headingDeg);
|
||||||
_cinematicOffset.pitch = Cesium.Math.toRadians(pose.pitchDeg);
|
_cinematicOffset.pitch = Cesium.Math.toRadians(pose.pitchDeg);
|
||||||
_cinematicOffset.range = pose.rangeM;
|
_cinematicOffset.range = pose.rangeM;
|
||||||
_viewer.camera.lookAt(pose.target, _cinematicOffset);
|
findCamera(_viewer)?.lookAt(pose.target, _cinematicOffset);
|
||||||
_lastCinematicProgress = _progress;
|
_lastCinematicProgress = _progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2696,7 +2702,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
_cinematicActive = true;
|
_cinematicActive = true;
|
||||||
_lastCinematicProgress = null;
|
_lastCinematicProgress = null;
|
||||||
announceNavigationAuthority('bhote-koshi-cinematic');
|
announceNavigationAuthority('bhote-koshi-cinematic');
|
||||||
_viewer?.camera?.cancelFlight?.();
|
findCamera(_viewer)?.cancelFlight();
|
||||||
}
|
}
|
||||||
syncCinematicButton();
|
syncCinematicButton();
|
||||||
return true;
|
return true;
|
||||||
@@ -2713,8 +2719,7 @@ export function createBhoteKoshiEventLayer({
|
|||||||
}
|
}
|
||||||
_cinematicActive = false;
|
_cinematicActive = false;
|
||||||
_lastCinematicProgress = null;
|
_lastCinematicProgress = null;
|
||||||
if (releaseCamera)
|
if (releaseCamera) findCamera(_viewer)?.release();
|
||||||
_viewer?.camera?.lookAtTransform?.(Cesium.Matrix4.IDENTITY);
|
|
||||||
syncCinematicButton();
|
syncCinematicButton();
|
||||||
renderHost.request('bhote-koshi-cinematic-stop');
|
renderHost.request('bhote-koshi-cinematic-stop');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { bindFakeCesiumCamera } from '../testSupport/fakeCameraPort.mjs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
@@ -239,7 +240,7 @@ function eventViewer(splitPosition = 0.37) {
|
|||||||
const canvas = new FakeElement('canvas');
|
const canvas = new FakeElement('canvas');
|
||||||
const cameraLooks = [];
|
const cameraLooks = [];
|
||||||
const cameraTransforms = [];
|
const cameraTransforms = [];
|
||||||
return {
|
return bindFakeCesiumCamera({
|
||||||
scene: { splitPosition, canvas },
|
scene: { splitPosition, canvas },
|
||||||
camera: {
|
camera: {
|
||||||
cancelFlight() {},
|
cancelFlight() {},
|
||||||
@@ -272,7 +273,7 @@ function eventViewer(splitPosition = 0.37) {
|
|||||||
cameraLooks,
|
cameraLooks,
|
||||||
cameraTransforms,
|
cameraTransforms,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('public lifecycle restores Nepal scene ownership without forwarding origin to enable', async () => {
|
test('public lifecycle restores Nepal scene ownership without forwarding origin to enable', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { t } from '../../i18n/index.js';
|
import { t } from '../../i18n/index.js';
|
||||||
import { cameraOf } from '../../render/cesium/camera.js';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
import { findCamera } from '../../render/rendererOf.js';
|
||||||
import {
|
import {
|
||||||
REPLAY_ASCENT_FALLBACK_SEC,
|
REPLAY_ASCENT_FALLBACK_SEC,
|
||||||
REPLAY_ASCENT_MIN_SEC,
|
REPLAY_ASCENT_MIN_SEC,
|
||||||
@@ -315,9 +316,9 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
layerState._replayCameraToken++;
|
layerState._replayCameraToken++;
|
||||||
if (layerState._replayCameraRemover) layerState._replayCameraRemover();
|
if (layerState._replayCameraRemover) layerState._replayCameraRemover();
|
||||||
layerState._replayCameraRemover = null;
|
layerState._replayCameraRemover = null;
|
||||||
layerState._viewer?.camera?.cancelFlight();
|
const camera = findCamera(layerState._viewer);
|
||||||
if (layerState._viewer?.camera)
|
camera?.cancelFlight();
|
||||||
layerState._viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
camera?.release();
|
||||||
layerState._replayCameraLaunchId = null;
|
layerState._replayCameraLaunchId = null;
|
||||||
layerState._replayPaused = false;
|
layerState._replayPaused = false;
|
||||||
layerState._replayPausedAtMs = null;
|
layerState._replayPausedAtMs = null;
|
||||||
@@ -354,7 +355,8 @@ 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;
|
||||||
cameraOf(layerState._viewer).cancelFlight();
|
const camera = cameraOf(layerState._viewer);
|
||||||
|
camera.cancelFlight();
|
||||||
layerState._animationStarts.set(
|
layerState._animationStarts.set(
|
||||||
launchId,
|
launchId,
|
||||||
Date.now() +
|
Date.now() +
|
||||||
@@ -374,6 +376,9 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
const state = track.beginReplayFrame();
|
const state = track.beginReplayFrame();
|
||||||
|
// Asked for each frame, not kept: a renderer bound meanwhile
|
||||||
|
// replaces the port.
|
||||||
|
const chaseCamera = cameraOf(layerState._viewer);
|
||||||
syncReplayCountdownButton(state);
|
syncReplayCountdownButton(state);
|
||||||
if (!cameraReady) return;
|
if (!cameraReady) return;
|
||||||
const path = state.ascending
|
const path = state.ascending
|
||||||
@@ -432,14 +437,11 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
)
|
)
|
||||||
: cameraView.range;
|
: cameraView.range;
|
||||||
if (state.ascending) {
|
if (state.ascending) {
|
||||||
layerState._viewer.camera.lookAt(
|
chaseCamera.lookAt(cameraTarget, {
|
||||||
cameraTarget,
|
heading: track.lastCameraHeading,
|
||||||
new Cesium.HeadingPitchRange(
|
pitch: cameraView.pitch,
|
||||||
track.lastCameraHeading,
|
range: cameraRange,
|
||||||
cameraView.pitch,
|
});
|
||||||
cameraRange,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const tangentStep = Math.max(
|
const tangentStep = Math.max(
|
||||||
0.002,
|
0.002,
|
||||||
@@ -464,12 +466,10 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
);
|
);
|
||||||
if (orbitPose) {
|
if (orbitPose) {
|
||||||
if (!track.orbitCameraWorldFrame) {
|
if (!track.orbitCameraWorldFrame) {
|
||||||
layerState._viewer.camera.lookAtTransform(
|
chaseCamera.release();
|
||||||
Cesium.Matrix4.IDENTITY,
|
|
||||||
);
|
|
||||||
track.orbitCameraWorldFrame = true;
|
track.orbitCameraWorldFrame = true;
|
||||||
}
|
}
|
||||||
layerState._viewer.camera.setView({
|
chaseCamera.setView({
|
||||||
destination: orbitPose.destination,
|
destination: orbitPose.destination,
|
||||||
orientation: {
|
orientation: {
|
||||||
direction: orbitPose.direction,
|
direction: orbitPose.direction,
|
||||||
@@ -486,7 +486,7 @@ export function createReplay({ state: layerState, services, parts, source }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cameraOf(layerState._viewer).flyToBounds(
|
camera.flyToBounds(
|
||||||
{ center: track.ascentPath[0], radius: 0 },
|
{ center: track.ascentPath[0], radius: 0 },
|
||||||
{
|
{
|
||||||
offset: {
|
offset: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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';
|
import { cameraOf } from '../../render/cesium/camera.js';
|
||||||
|
import { findCamera } from '../../render/rendererOf.js';
|
||||||
|
|
||||||
export function createSelection({
|
export function createSelection({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
@@ -94,9 +95,8 @@ export function createSelection({
|
|||||||
layerState._missionZoomAnchorRemover();
|
layerState._missionZoomAnchorRemover();
|
||||||
layerState._missionZoomAnchorRemover = null;
|
layerState._missionZoomAnchorRemover = null;
|
||||||
layerState._missionZoomAnchorId = null;
|
layerState._missionZoomAnchorId = null;
|
||||||
if (layerState._viewer?.camera && !layerState._replayCameraLaunchId) {
|
if (!layerState._replayCameraLaunchId)
|
||||||
layerState._viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
findCamera(layerState._viewer)?.release();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startMissionZoomAnchor(launch, initialRange) {
|
function startMissionZoomAnchor(launch, initialRange) {
|
||||||
@@ -109,14 +109,11 @@ export function createSelection({
|
|||||||
Number(initialRange) || MISSION_GLOBE_VIEW_RANGE_M,
|
Number(initialRange) || MISSION_GLOBE_VIEW_RANGE_M,
|
||||||
);
|
);
|
||||||
layerState._missionZoomAnchorId = launch.id;
|
layerState._missionZoomAnchorId = launch.id;
|
||||||
layerState._viewer.camera.lookAt(
|
cameraOf(layerState._viewer).lookAt(target, {
|
||||||
target,
|
heading: 0,
|
||||||
new Cesium.HeadingPitchRange(
|
pitch: parts.camera.missionZoomPitch(range),
|
||||||
0,
|
range,
|
||||||
parts.camera.missionZoomPitch(range),
|
});
|
||||||
range,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusMission(launch) {
|
function focusMission(launch) {
|
||||||
|
|||||||
+17
-13
@@ -1,4 +1,5 @@
|
|||||||
import * as Cesium from 'cesium';
|
import { toRadians } from './geo/index.js';
|
||||||
|
import { cameraOf, findCamera } from './render/rendererOf.js';
|
||||||
import {
|
import {
|
||||||
holdContinuousRender,
|
holdContinuousRender,
|
||||||
releaseContinuousRender,
|
releaseContinuousRender,
|
||||||
@@ -16,14 +17,15 @@ export class OrbitController {
|
|||||||
this.target = null;
|
this.target = null;
|
||||||
this.radius = 500;
|
this.radius = 500;
|
||||||
this.pitch = -30;
|
this.pitch = -30;
|
||||||
this.speed = Cesium.Math.toRadians(6); // ~6°/sec → full rotation in ~60s
|
this.speed = toRadians(6); // ~6°/sec → full rotation in ~60s
|
||||||
this.angle = 0;
|
this.angle = 0;
|
||||||
this._removeListener = null;
|
this._removeListener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start orbiting around a target position.
|
* Start orbiting around a target position.
|
||||||
* @param {Cesium.Cartesian3} targetCartesian - The point to orbit around
|
* @param {{x: number, y: number, z: number}} targetCartesian - The point
|
||||||
|
* to orbit around, ECEF metres
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {number} options.radius - Distance from target in meters
|
* @param {number} options.radius - Distance from target in meters
|
||||||
* @param {number} options.pitch - Tilt angle in degrees (negative = looking down)
|
* @param {number} options.pitch - Tilt angle in degrees (negative = looking down)
|
||||||
@@ -31,18 +33,21 @@ export class OrbitController {
|
|||||||
*/
|
*/
|
||||||
start(targetCartesian, options = {}) {
|
start(targetCartesian, options = {}) {
|
||||||
if (!targetCartesian) return;
|
if (!targetCartesian) return;
|
||||||
|
// Asked for each frame, not kept: a renderer bound meanwhile replaces
|
||||||
|
// the port.
|
||||||
|
const pose = cameraOf(this.viewer).pose();
|
||||||
|
|
||||||
this.target = targetCartesian;
|
this.target = targetCartesian;
|
||||||
this.radius = options.radius || this.radius;
|
this.radius = options.radius || this.radius;
|
||||||
this.pitch = options.pitch || this.pitch;
|
this.pitch = options.pitch || this.pitch;
|
||||||
this.speed = Cesium.Math.toRadians(options.speed || 6);
|
this.speed = toRadians(options.speed || 6);
|
||||||
this.active = true;
|
this.active = true;
|
||||||
// Orbit mutates the camera from preRender — without a hold the loop
|
// Orbit mutates the camera from preRender — without a hold the loop
|
||||||
// starves after its first idle frame. (perf wave 2)
|
// starves after its first idle frame. (perf wave 2)
|
||||||
holdContinuousRender('camera-orbit');
|
holdContinuousRender('camera-orbit');
|
||||||
|
|
||||||
// Start from the camera's current heading for seamless transition
|
// Start from the camera's current heading for seamless transition
|
||||||
this.angle = this.viewer.camera.heading;
|
this.angle = pose.heading;
|
||||||
|
|
||||||
let lastTime = Date.now();
|
let lastTime = Date.now();
|
||||||
this._removeListener = this.viewer.scene.preRender.addEventListener(() => {
|
this._removeListener = this.viewer.scene.preRender.addEventListener(() => {
|
||||||
@@ -54,12 +59,11 @@ export class OrbitController {
|
|||||||
|
|
||||||
this.angle += this.speed * dt;
|
this.angle += this.speed * dt;
|
||||||
|
|
||||||
const hpr = new Cesium.HeadingPitchRange(
|
cameraOf(this.viewer).lookAt(this.target, {
|
||||||
this.angle,
|
heading: this.angle,
|
||||||
Cesium.Math.toRadians(this.pitch),
|
pitch: toRadians(this.pitch),
|
||||||
this.radius,
|
range: this.radius,
|
||||||
);
|
});
|
||||||
this.viewer.camera.lookAt(this.target, hpr);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,12 +78,12 @@ export class OrbitController {
|
|||||||
this._removeListener = null;
|
this._removeListener = null;
|
||||||
}
|
}
|
||||||
// Unlock camera for free interaction
|
// Unlock camera for free interaction
|
||||||
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
findCamera(this.viewer)?.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle orbit on/off.
|
* Toggle orbit on/off.
|
||||||
* @param {Cesium.Cartesian3} targetCartesian - Required when starting
|
* @param {{x: number, y: number, z: number}} targetCartesian - Required when starting
|
||||||
* @param {object} options - Passed to start()
|
* @param {object} options - Passed to start()
|
||||||
* @returns {boolean} Whether orbit is now active
|
* @returns {boolean} Whether orbit is now active
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -339,12 +339,8 @@ const EXPECTED_MISSING = [
|
|||||||
'camera.followOffset.get',
|
'camera.followOffset.get',
|
||||||
'camera.followOffset.set',
|
'camera.followOffset.set',
|
||||||
'camera.followed',
|
'camera.followed',
|
||||||
'camera.look',
|
|
||||||
'camera.matrices',
|
'camera.matrices',
|
||||||
'camera.move',
|
|
||||||
'camera.rotateAround',
|
|
||||||
'camera.setFov',
|
'camera.setFov',
|
||||||
'camera.zoom',
|
|
||||||
'draw.atlas',
|
'draw.atlas',
|
||||||
'draw.clampState',
|
'draw.clampState',
|
||||||
'draw.on',
|
'draw.on',
|
||||||
|
|||||||
@@ -26,6 +26,25 @@ export const EASINGS = Object.freeze({
|
|||||||
sineInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
sineInOut: Cesium.EasingFunction.SINUSOIDAL_IN_OUT,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contract's moves and looks as Cesium camera method names, called by
|
||||||
|
* name: a consumer's camera may come from another copy of Cesium.
|
||||||
|
*/
|
||||||
|
const MOVES = Object.freeze({
|
||||||
|
forward: 'moveForward',
|
||||||
|
back: 'moveBackward',
|
||||||
|
left: 'moveLeft',
|
||||||
|
right: 'moveRight',
|
||||||
|
up: 'moveUp',
|
||||||
|
down: 'moveDown',
|
||||||
|
});
|
||||||
|
const LOOKS = Object.freeze({
|
||||||
|
left: 'lookLeft',
|
||||||
|
right: 'lookRight',
|
||||||
|
up: 'lookUp',
|
||||||
|
down: 'lookDown',
|
||||||
|
});
|
||||||
|
|
||||||
/** A contract destination as Cesium's: a Cartesian3, or a Rectangle. */
|
/** A contract destination as Cesium's: a Cartesian3, or a Rectangle. */
|
||||||
export function toDestination(d) {
|
export function toDestination(d) {
|
||||||
if ('rect' in d) {
|
if ('rect' in d) {
|
||||||
@@ -221,6 +240,24 @@ export function createCesiumCamera({ scene, camera, report }) {
|
|||||||
camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
},
|
},
|
||||||
|
move(direction, metres) {
|
||||||
|
camera[MOVES[direction]](metres);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
look(direction, radians) {
|
||||||
|
camera[LOOKS[direction]](radians);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
rotateAround(axis, radians) {
|
||||||
|
camera.rotate(toCartesian(axis), radians);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
|
zoom(metres) {
|
||||||
|
// Cesium's zoom, not a move: outside 3D it changes the view's width.
|
||||||
|
if (metres < 0) camera.zoomOut(-metres);
|
||||||
|
else camera.zoomIn(metres);
|
||||||
|
scene.requestRender();
|
||||||
|
},
|
||||||
viewRectangle() {
|
viewRectangle() {
|
||||||
const rect = camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
|
const rect = camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
|
||||||
if (!rect) return null;
|
if (!rect) return null;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const QUERY_RECORDS = new Set([
|
|||||||
'occluded',
|
'occluded',
|
||||||
]);
|
]);
|
||||||
/** Commands and records the port does not have yet. */
|
/** Commands and records the port does not have yet. */
|
||||||
const MISSING = new Set(['move', 'look', 'rotateAround', 'zoom', 'matrices']);
|
const MISSING = new Set(['matrices']);
|
||||||
|
|
||||||
const runnable = CAMERA_SCRIPTS.filter((script) =>
|
const runnable = CAMERA_SCRIPTS.filter((script) =>
|
||||||
script.steps.every(
|
script.steps.every(
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export const CESIUM_CHECKS = new Set([
|
|||||||
'camera.trace.setView angles',
|
'camera.trace.setView angles',
|
||||||
'camera.trace.setView keeps the position when only orientation is given',
|
'camera.trace.setView keeps the position when only orientation is given',
|
||||||
'camera.trace.lookAt sweep and release',
|
'camera.trace.lookAt sweep and release',
|
||||||
|
'camera.trace.move, look, rotate and zoom',
|
||||||
|
'camera.trace.commands inside a lookAt frame',
|
||||||
|
'camera.trace.changed fires by each listener’s threshold',
|
||||||
|
'camera.moveEnd',
|
||||||
'camera.trace.flyTo with a duration of 0 completes inside the call',
|
'camera.trace.flyTo with a duration of 0 completes inside the call',
|
||||||
'camera.trace.flyTo where the camera already is completes inside the call',
|
'camera.trace.flyTo where the camera already is completes inside the call',
|
||||||
'camera.viewRectangle',
|
'camera.viewRectangle',
|
||||||
|
|||||||
@@ -208,7 +208,6 @@
|
|||||||
"src/maps/google3d.js",
|
"src/maps/google3d.js",
|
||||||
"src/maps/imagery.js",
|
"src/maps/imagery.js",
|
||||||
"src/maps/terrain.js",
|
"src/maps/terrain.js",
|
||||||
"src/orbit.js",
|
|
||||||
"src/overlays/worldOverlay.js",
|
"src/overlays/worldOverlay.js",
|
||||||
"src/queries/heights.js",
|
"src/queries/heights.js",
|
||||||
"src/sceneCommandRelease.js",
|
"src/sceneCommandRelease.js",
|
||||||
|
|||||||
@@ -316,20 +316,30 @@ export function cameraChecks() {
|
|||||||
r.camera.on('moveStart', () => events.push(['start', h.now(r)]));
|
r.camera.on('moveStart', () => events.push(['start', h.now(r)]));
|
||||||
r.camera.on('moveEnd', () => events.push(['end', h.now(r)]));
|
r.camera.on('moveEnd', () => events.push(['end', h.now(r)]));
|
||||||
await h.frame(r);
|
await h.frame(r);
|
||||||
|
const movingAt = h.now(r);
|
||||||
r.camera.move('forward', 1000);
|
r.camera.move('forward', 1000);
|
||||||
await h.frame(r);
|
await h.frame(r);
|
||||||
equal(events[0]?.[0], 'start', 'moveStart after the move');
|
equal(events[0]?.[0], 'start', 'moveStart after the move');
|
||||||
const movedAt = h.now(r);
|
const movedAt = h.now(r);
|
||||||
for (let i = 0; i < 45; i++) await h.frame(r);
|
// By the clock, not a frame count: a page's frames come at the
|
||||||
|
// display's rate.
|
||||||
|
// The event comes on the first frame past 500 ms, so the slack is
|
||||||
|
// two of the longest frames seen, never less than two at 60 Hz.
|
||||||
|
let longestFrame = 1000 / 60;
|
||||||
|
for (let i = 0; i < 600 && h.now(r) - movedAt < 750; i++) {
|
||||||
|
const before = h.now(r);
|
||||||
|
await h.frame(r);
|
||||||
|
longestFrame = Math.max(longestFrame, h.now(r) - before);
|
||||||
|
}
|
||||||
const end = events.find(([kind]) => kind === 'end');
|
const end = events.find(([kind]) => kind === 'end');
|
||||||
ok(end, 'moveEnd fired');
|
ok(end, 'moveEnd fired');
|
||||||
ok(
|
ok(
|
||||||
end[1] - movedAt >= 500 - 1e-6,
|
end[1] - movingAt >= 500 - 1e-6,
|
||||||
`moveEnd waited 500 ms (${end[1] - movedAt})`,
|
`moveEnd waited 500 ms (${end[1] - movingAt})`,
|
||||||
);
|
);
|
||||||
ok(
|
ok(
|
||||||
end[1] - movedAt < 500 + 2 * (1000 / 60) + 1e-6,
|
end[1] - movedAt < 500 + 2 * longestFrame + 1e-6,
|
||||||
'moveEnd came within two frames of 500 ms',
|
`moveEnd came within two frames of 500 ms (${end[1] - movedAt})`,
|
||||||
);
|
);
|
||||||
r.destroy();
|
r.destroy();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* the mesh, it flattens under prefers-reduced-motion, and it stops the instant
|
* the mesh, it flattens under prefers-reduced-motion, and it stops the instant
|
||||||
* anything interrupts it.
|
* anything interrupts it.
|
||||||
*/
|
*/
|
||||||
|
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 * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
@@ -99,7 +100,7 @@ function createTickableViewer({ heightM = 700 } = {}) {
|
|||||||
if (orientation && Number.isFinite(orientation.pitch)) camera.pitch = orientation.pitch;
|
if (orientation && Number.isFinite(orientation.pitch)) camera.pitch = orientation.pitch;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const viewer = {
|
const viewer = bindFakeCesiumCamera({
|
||||||
trackedEntity: undefined,
|
trackedEntity: undefined,
|
||||||
clock: { onTick: { addEventListener: (fn) => { listeners.push(fn); return () => {}; } } },
|
clock: { onTick: { addEventListener: (fn) => { listeners.push(fn); return () => {}; } } },
|
||||||
scene: {
|
scene: {
|
||||||
@@ -111,7 +112,7 @@ function createTickableViewer({ heightM = 700 } = {}) {
|
|||||||
sampleHeight: () => 0,
|
sampleHeight: () => 0,
|
||||||
},
|
},
|
||||||
camera,
|
camera,
|
||||||
};
|
});
|
||||||
return {
|
return {
|
||||||
viewer,
|
viewer,
|
||||||
setViews,
|
setViews,
|
||||||
|
|||||||
@@ -132,8 +132,10 @@ export function bindFakeCesiumCamera(viewer) {
|
|||||||
pose: {
|
pose: {
|
||||||
value() {
|
value() {
|
||||||
const c = fake.positionCartographic;
|
const c = fake.positionCartographic;
|
||||||
|
const p = fake.positionWC;
|
||||||
return {
|
return {
|
||||||
...DEFAULT_POSE,
|
...DEFAULT_POSE,
|
||||||
|
...(p ? { position: { x: p.x, y: p.y, z: p.z } } : {}),
|
||||||
lon: c ? toDegrees(c.longitude) : NaN,
|
lon: c ? toDegrees(c.longitude) : NaN,
|
||||||
lat: c ? toDegrees(c.latitude) : NaN,
|
lat: c ? toDegrees(c.latitude) : NaN,
|
||||||
height: c ? c.height : NaN,
|
height: c ? c.height : NaN,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createGevActionRunner } from '../voice/gevActions.js';
|
|||||||
import { createGeminiSession } from '../voice/geminiSession.js';
|
import { createGeminiSession } from '../voice/geminiSession.js';
|
||||||
import { createTestVoiceUi } from './voiceUi.mjs';
|
import { createTestVoiceUi } from './voiceUi.mjs';
|
||||||
import { createNarrationScheduler } from '../voice/narration.js';
|
import { createNarrationScheduler } from '../voice/narration.js';
|
||||||
|
import { bindFakeCesiumCamera } from './fakeCameraPort.mjs';
|
||||||
|
|
||||||
export const turnOfEventLoop = () =>
|
export const turnOfEventLoop = () =>
|
||||||
new Promise((resolve) => setImmediate(resolve));
|
new Promise((resolve) => setImmediate(resolve));
|
||||||
@@ -68,7 +69,7 @@ export function nativeIntegrationFixture(t) {
|
|||||||
const acquisitions = { microphone: 0, audio: 0, token: 0, socket: 0 };
|
const acquisitions = { microphone: 0, audio: 0, token: 0, socket: 0 };
|
||||||
const state = { height: 1000, renders: 0, zooms: [], earconStarts: 0 };
|
const state = { height: 1000, renders: 0, zooms: [], earconStarts: 0 };
|
||||||
const position = { x: 6379137, y: 0, z: 0 };
|
const position = { x: 6379137, y: 0, z: 0 };
|
||||||
const viewer = {
|
const viewer = bindFakeCesiumCamera({
|
||||||
clock: { onTick: { addEventListener: () => () => {} } },
|
clock: { onTick: { addEventListener: () => () => {} } },
|
||||||
camera: {
|
camera: {
|
||||||
positionWC: position,
|
positionWC: position,
|
||||||
@@ -99,7 +100,7 @@ export function nativeIntegrationFixture(t) {
|
|||||||
state.renders++;
|
state.renders++;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
const dataManager = { layers: new Map() };
|
const dataManager = { layers: new Map() };
|
||||||
const actualRunner = createGevActionRunner({
|
const actualRunner = createGevActionRunner({
|
||||||
viewer,
|
viewer,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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 {
|
import {
|
||||||
@@ -47,7 +48,7 @@ const outputs = (ws) =>
|
|||||||
function fixture({ holdFirst = false } = {}) {
|
function fixture({ holdFirst = false } = {}) {
|
||||||
const state = { height: 1000, renders: 0, providerWrites: [], panels: [] };
|
const state = { height: 1000, renders: 0, providerWrites: [], panels: [] };
|
||||||
const position = { x: 6379137, y: 0, z: 0 };
|
const position = { x: 6379137, y: 0, z: 0 };
|
||||||
const viewer = {
|
const viewer = bindFakeCesiumCamera({
|
||||||
clock: { onTick: { addEventListener: () => () => {} } },
|
clock: { onTick: { addEventListener: () => () => {} } },
|
||||||
camera: {
|
camera: {
|
||||||
positionWC: position,
|
positionWC: position,
|
||||||
@@ -76,7 +77,7 @@ function fixture({ holdFirst = false } = {}) {
|
|||||||
state.renders++;
|
state.renders++;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
const runner = createGevActionRunner({
|
const runner = createGevActionRunner({
|
||||||
viewer,
|
viewer,
|
||||||
dataManager: { layers: new Map() },
|
dataManager: { layers: new Map() },
|
||||||
|
|||||||
@@ -2927,9 +2927,10 @@ function adjustCameraZoom(viewer, args) {
|
|||||||
}[amount];
|
}[amount];
|
||||||
if (!fraction) throw new Error(`Unknown zoom amount: ${args.amount}`);
|
if (!fraction) throw new Error(`Unknown zoom amount: ${args.amount}`);
|
||||||
|
|
||||||
const camera = viewer.camera;
|
const camera = cameraOf(viewer);
|
||||||
const beforePosition = Cesium.Cartesian3.clone(camera.positionWC);
|
const before = camera.pose();
|
||||||
const beforeHeightM = camera.positionCartographic.height;
|
const beforePosition = before.position;
|
||||||
|
const beforeHeightM = before.height;
|
||||||
const target = getViewTargetCartesian(viewer);
|
const target = getViewTargetCartesian(viewer);
|
||||||
const targetDistanceM = target
|
const targetDistanceM = target
|
||||||
? chordM(beforePosition, target)
|
? chordM(beforePosition, target)
|
||||||
@@ -2939,7 +2940,7 @@ function adjustCameraZoom(viewer, args) {
|
|||||||
|
|
||||||
camera.cancelFlight();
|
camera.cancelFlight();
|
||||||
if (direction === 'out') {
|
if (direction === 'out') {
|
||||||
camera.zoomOut(movementM);
|
camera.zoom(-movementM);
|
||||||
} else {
|
} else {
|
||||||
const safeMovementM = Math.min(
|
const safeMovementM = Math.min(
|
||||||
movementM,
|
movementM,
|
||||||
@@ -2954,13 +2955,12 @@ function adjustCameraZoom(viewer, args) {
|
|||||||
error: 'Camera is already at the minimum target distance',
|
error: 'Camera is already at the minimum target distance',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
camera.zoomIn(safeMovementM);
|
camera.zoom(safeMovementM);
|
||||||
}
|
}
|
||||||
viewer.scene.requestRender();
|
|
||||||
|
|
||||||
const afterPosition = camera.positionWC;
|
const after = camera.pose();
|
||||||
const movedM = chordM(beforePosition, afterPosition);
|
const movedM = chordM(beforePosition, after.position);
|
||||||
const afterHeightM = camera.positionCartographic.height;
|
const afterHeightM = after.height;
|
||||||
const moved = movedM >= 0.5;
|
const moved = movedM >= 0.5;
|
||||||
return {
|
return {
|
||||||
ok: moved,
|
ok: moved,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Imports intentionally target the candidate, never the immutable reference.
|
// Imports intentionally target the candidate, never the immutable reference.
|
||||||
|
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 {
|
import {
|
||||||
@@ -176,7 +177,7 @@ function compositionFixture(
|
|||||||
const canvas = new EventTarget();
|
const canvas = new EventTarget();
|
||||||
Object.assign(canvas, { clientWidth: 800, clientHeight: 600 });
|
Object.assign(canvas, { clientWidth: 800, clientHeight: 600 });
|
||||||
const position = { x: 6379137, y: 0, z: 0 };
|
const position = { x: 6379137, y: 0, z: 0 };
|
||||||
const viewer = {
|
const viewer = bindFakeCesiumCamera({
|
||||||
clock: { onTick: { addEventListener: () => () => {} } },
|
clock: { onTick: { addEventListener: () => () => {} } },
|
||||||
camera: {
|
camera: {
|
||||||
positionWC: position,
|
positionWC: position,
|
||||||
@@ -202,7 +203,7 @@ function compositionFixture(
|
|||||||
state.renders++;
|
state.renders++;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
const dataManager = { layers: new Map() };
|
const dataManager = { layers: new Map() };
|
||||||
pointer = createPointerTracker({ element: null, now: () => 1000 });
|
pointer = createPointerTracker({ element: null, now: () => 1000 });
|
||||||
const referents = createReferentRegistry();
|
const referents = createReferentRegistry();
|
||||||
|
|||||||
Reference in new issue
Block a user