mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
fix(camera): leave flights and scenes to their framing
Review follow-ups for the globe-in-view recovery and voice limits. - Leave a camera flight in progress and a playing Director scene, holds included, to their own framing. The recovery is now installed with the scene tools, which own the Director, and reads its running state. - Reuse a voice turn's half-degree proof only from the position and direction it was proven at, so a release that moves the camera checks the turn again. - Share one free-globe-camera check between the recovery, the voice turn limit and the globe rotation. - Keep an enableLook setting other code makes after the free-look limit is installed, instead of writing back the install-time value. - Name the nudging test for what it asserts, the 1% floor, and document that a view losing its surface without a camera move waits for the next one.
This commit is contained in:
1 parent
d36b28f352
commit
64377db91a
8 files changed
+137
-44
No files matched your search
@@ -100,8 +100,11 @@ away, such as a saved or shared view that did, or a drag
|
||||
that Cesium turns into an in-place look below its 7,500 km trackball height (one
|
||||
that starts on the stars, or a tilt drag with the screen centre off the planet).
|
||||
Views with more surface than that, such as the oblique orbital Director shots,
|
||||
are left as they are, and so are tracked or locked cameras and the 2D and
|
||||
Columbus modes. A voice turn requested while the camera is tracked is judged
|
||||
are left as they are, and so are tracked or locked cameras, a camera flight in
|
||||
progress, a Director scene while it plays (holds included) and the 2D and
|
||||
Columbus modes. A view that loses its surface without a camera move (a resize,
|
||||
or turning the scope on) is left as it is until the next move. A voice turn
|
||||
requested while the camera is tracked is judged
|
||||
by the pose voice's release of the follow camera will leave, before that
|
||||
release, so a refused turn keeps the tracking.
|
||||
|
||||
|
||||
@@ -399,6 +399,26 @@ test('free look follows camera height and is restored on dispose', () => {
|
||||
assert.equal(f.listeners.size, 0);
|
||||
});
|
||||
|
||||
test('free look keeps a setting other code makes after it is installed', () => {
|
||||
const f = freeLookFixture({ height: 500 });
|
||||
const dispose = installFreeLookHeightLimit(f.viewer);
|
||||
f.controller.enableLook = false;
|
||||
f.frame(10);
|
||||
assert.equal(f.controller.enableLook, false, 'turned off stays off');
|
||||
f.frame(FREE_LOOK_MAX_HEIGHT_METERS * 20);
|
||||
f.frame(10);
|
||||
assert.equal(f.controller.enableLook, false, 'and stays off after orbit');
|
||||
f.controller.enableLook = true;
|
||||
f.frame(10);
|
||||
assert.equal(f.controller.enableLook, true, 'turned back on');
|
||||
f.frame(FREE_LOOK_MAX_HEIGHT_METERS * 20);
|
||||
assert.equal(f.controller.enableLook, false, 'still limited from orbit');
|
||||
f.frame(10);
|
||||
f.controller.enableLook = false;
|
||||
dispose();
|
||||
assert.equal(f.controller.enableLook, false, 'dispose keeps a later setting');
|
||||
});
|
||||
|
||||
test('free look stays off when the viewer had it disabled', () => {
|
||||
const f = freeLookFixture({ enableLook: false, height: 500 });
|
||||
installFreeLookHeightLimit(f.viewer);
|
||||
@@ -418,6 +438,7 @@ function realCamera({ width = 1600, height = 900, mode } = {}) {
|
||||
pixelRatio: 1,
|
||||
ellipsoid: Cesium.Ellipsoid.WGS84,
|
||||
mapMode2D: Cesium.MapMode2D.INFINITE_SCROLL,
|
||||
tweens: new Cesium.TweenCollection(),
|
||||
renders: 0,
|
||||
requestRender() {
|
||||
this.renders += 1;
|
||||
@@ -674,6 +695,29 @@ test('the recovery follows the scope: corner-only views are turned back only beh
|
||||
}
|
||||
});
|
||||
|
||||
test('the recovery leaves a camera flight and an authored scene alone', () => {
|
||||
let playing = true;
|
||||
const { camera, scene, viewer } = realCamera({ width: 1225, height: 923 });
|
||||
scene.camera = camera;
|
||||
scene.screenSpaceCameraController = { enableInputs: true };
|
||||
installGlobeInViewRecovery(viewer, { paused: () => playing });
|
||||
aim(camera, reportedStuckView);
|
||||
camera.moveEnd.raiseEvent();
|
||||
assert.equal(globeOutOfFrame(camera), true, 'a playing scene is left alone');
|
||||
playing = false;
|
||||
|
||||
// A flight that pauses looking away is left alone until it ends.
|
||||
camera.flyTo({
|
||||
destination: Cesium.Cartesian3.fromDegrees(10, 20, 18_000_000),
|
||||
duration: 5,
|
||||
});
|
||||
camera.moveEnd.raiseEvent();
|
||||
assert.equal(globeOutOfFrame(camera), true, 'a flight is left alone');
|
||||
camera.cancelFlight();
|
||||
camera.moveEnd.raiseEvent();
|
||||
assert.ok(Cesium.Math.equalsEpsilon(camera.pitch, -Math.PI / 2, 1e-6));
|
||||
});
|
||||
|
||||
function fakeTimers() {
|
||||
let next = 1;
|
||||
const pending = new Map();
|
||||
|
||||
@@ -3,12 +3,9 @@ import * as Cesium from 'cesium';
|
||||
import {
|
||||
createApplicationViewer,
|
||||
installFreeLookHeightLimit,
|
||||
installGlobeInViewRecovery,
|
||||
installTrackpadPinchZoom,
|
||||
} from '../app/viewer.js';
|
||||
import { registerDataCredits } from '../data/dataCredits.js';
|
||||
import { KEYHOLE_OUTER_RADIUS } from '../celestialRing.js';
|
||||
import { isScopeMaskEnabled } from '../scopeMask.js';
|
||||
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
|
||||
import { MapStackController } from '../mapStackController.js';
|
||||
import { loadPhotorealisticTileset } from '../mapStartup.js';
|
||||
@@ -63,12 +60,6 @@ export async function createApplicationScene({
|
||||
});
|
||||
defer(installTrackpadPinchZoom(viewer));
|
||||
defer(installFreeLookHeightLimit(viewer));
|
||||
defer(
|
||||
installGlobeInViewRecovery(viewer, {
|
||||
// The scope mask hides everything outside its keyhole.
|
||||
keyholeRadius: () => (isScopeMaskEnabled() ? KEYHOLE_OUTER_RADIUS : 0),
|
||||
}),
|
||||
);
|
||||
registerDataCredits(viewer, credits);
|
||||
configureCreditKeyboardAccess(document);
|
||||
defer(onLocaleChange(() => configureCreditKeyboardAccess(document)));
|
||||
|
||||
+15
-1
@@ -8,7 +8,13 @@ import { initImageryBoxTool } from '../ui/imageryBoxTool.js';
|
||||
import { createRecentImageryPanel } from '../ui/recentImagery.js';
|
||||
import { initGevVoiceCommands } from '../voice/gevRealtime.js';
|
||||
import { installViews, isEmbeddedInline } from './embed.js';
|
||||
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
|
||||
import {
|
||||
installScopeMask,
|
||||
destroyScopeMask,
|
||||
isScopeMaskEnabled,
|
||||
} from '../scopeMask.js';
|
||||
import { KEYHOLE_OUTER_RADIUS } from '../celestialRing.js';
|
||||
import { installGlobeInViewRecovery } from './viewer.js';
|
||||
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
|
||||
import {
|
||||
installRenderGovernor,
|
||||
@@ -44,6 +50,14 @@ export function createApplicationTools({
|
||||
.get('bhote-koshi-2026')
|
||||
?.module.attachSceneController(sceneDirector);
|
||||
defer(() => sceneDirector.destroy());
|
||||
defer(
|
||||
installGlobeInViewRecovery(viewer, {
|
||||
// The scope mask hides everything outside its keyhole.
|
||||
keyholeRadius: () => (isScopeMaskEnabled() ? KEYHOLE_OUTER_RADIUS : 0),
|
||||
// An authored scene frames its own shots, holds included.
|
||||
paused: () => sceneDirector.running,
|
||||
}),
|
||||
);
|
||||
onSceneDirector?.(sceneDirector);
|
||||
const annotations = initAnnotations({
|
||||
viewer,
|
||||
|
||||
+18
-9
@@ -4,6 +4,7 @@ import { StarrySky } from '../sky/starrySky.js';
|
||||
import {
|
||||
FREE_LOOK_MAX_HEIGHT_METERS,
|
||||
globeOutOfFrame,
|
||||
isFreeGlobeCamera,
|
||||
} from '../globeInView.js';
|
||||
|
||||
const PINCH_ZOOM_MULTIPLIER = 8;
|
||||
@@ -109,7 +110,9 @@ export function installTrackpadPinchZoom(
|
||||
|
||||
/**
|
||||
* Allow Cesium's free look only below `maxHeight` and return its disposer.
|
||||
* Orbit, tilt and zoom stay available at every height. `enableLook` gates the
|
||||
* Orbit, tilt and zoom stay available at every height. A value other code
|
||||
* sets later is kept as the one to allow below that height, and restored on
|
||||
* dispose. `enableLook` gates the
|
||||
* Shift+drag binding only; a drag that starts on the stars below Cesium's
|
||||
* trackball height still turns the camera in place, which
|
||||
* `installGlobeInViewRecovery` catches.
|
||||
@@ -124,11 +127,14 @@ export function installFreeLookHeightLimit(
|
||||
if (!controller || !camera || !scene?.preUpdate)
|
||||
throw new TypeError('A complete Cesium viewer is required');
|
||||
|
||||
const originalEnableLook = controller.enableLook;
|
||||
let wanted = controller.enableLook;
|
||||
let written;
|
||||
const sync = () => {
|
||||
if (written !== undefined && controller.enableLook !== written)
|
||||
wanted = controller.enableLook;
|
||||
const height = camera.positionCartographic?.height;
|
||||
controller.enableLook =
|
||||
originalEnableLook && !(Number.isFinite(height) && height > maxHeight);
|
||||
written = wanted && !(Number.isFinite(height) && height > maxHeight);
|
||||
controller.enableLook = written;
|
||||
};
|
||||
sync();
|
||||
const removeListener = scene.preUpdate.addEventListener(sync);
|
||||
@@ -138,7 +144,7 @@ export function installFreeLookHeightLimit(
|
||||
if (!active) return;
|
||||
active = false;
|
||||
removeListener();
|
||||
controller.enableLook = originalEnableLook;
|
||||
if (controller.enableLook === written) controller.enableLook = wanted;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,7 +165,9 @@ const SETTLE_GIVE_UP_MS = 5 * 60_000;
|
||||
* catches a saved or shared view that looked away, and a drag that Cesium turns
|
||||
* into an in-place look below its trackball height whatever `enableLook` says.
|
||||
* Heading and position are kept. Tracked cameras, locked ones (non-identity
|
||||
* transform) and the 2D and Columbus modes are left alone. `keyholeRadius`
|
||||
* transform), a camera flight in progress, the 2D and Columbus modes, and any
|
||||
* time `paused` returns true (an authored scene playing) are left alone.
|
||||
* `keyholeRadius`
|
||||
* returns the radius of the visible circle when a scope mask hides the rest of
|
||||
* the frame (see `globeFrameShare`), or 0; it is read at every check.
|
||||
*
|
||||
@@ -179,6 +187,7 @@ export function installGlobeInViewRecovery(
|
||||
{
|
||||
minHeight = FREE_LOOK_MAX_HEIGHT_METERS,
|
||||
keyholeRadius = () => 0,
|
||||
paused = () => false,
|
||||
setTimeout: schedule = globalThis.setTimeout,
|
||||
clearTimeout: unschedule = globalThis.clearTimeout,
|
||||
} = {},
|
||||
@@ -189,9 +198,9 @@ export function installGlobeInViewRecovery(
|
||||
throw new TypeError('A complete Cesium viewer is required');
|
||||
|
||||
const recover = () => {
|
||||
if (scene.mode !== Cesium.SceneMode.SCENE3D || viewer.trackedEntity) return;
|
||||
if (!Cesium.Matrix4.equals(camera.transform, Cesium.Matrix4.IDENTITY))
|
||||
return;
|
||||
if (!isFreeGlobeCamera(viewer)) return;
|
||||
// Cesium has no public running-flight flag.
|
||||
if (camera._currentFlight || paused()) return;
|
||||
const height = camera.positionCartographic?.height;
|
||||
if (
|
||||
!(height > minHeight) ||
|
||||
|
||||
@@ -1526,7 +1526,7 @@ test('a voice turn after something else turned the camera starts over', (t) => {
|
||||
assert.ok(globeFrameShare(camera) >= start - 2 / (32 * 18) - 1e-9);
|
||||
});
|
||||
|
||||
test('nudging the camera between voice turns cannot slide the planet away', (t) => {
|
||||
test('nudging the camera between voice turns cannot take the planet below the 1% floor', (t) => {
|
||||
withScope(t, false); // the whole frame is visible
|
||||
// A wheel notch or an orbit drag between requests starts each turn afresh;
|
||||
// the 1% floor still holds, and below it a turn may only keep what it has.
|
||||
@@ -1810,6 +1810,32 @@ test('a refused voice turn leaves a tracked camera tracking', (t) => {
|
||||
}
|
||||
});
|
||||
|
||||
test('a turn proven at one position is checked again after a release moves the camera', (t) => {
|
||||
withScope(t, false);
|
||||
const { camera, tick } = harness(t, {
|
||||
pitch: -80,
|
||||
viewportWidth: 1600,
|
||||
viewportHeight: 900,
|
||||
});
|
||||
// Tilt up to the limit, then check from 4,000 km closer along the same view
|
||||
// direction, where the next tilt fits; the release moves the camera back.
|
||||
for (let i = 0; i < 40; i += 1) {
|
||||
if (!moveCamera({ motion: 'tilt', direction: 'up', mode: 'once' }).ok)
|
||||
break;
|
||||
for (let j = 0; j < 600 && getActiveCameraMotion(); j += 1) tick(1 / 60);
|
||||
}
|
||||
camera.moveForward(4_000_000);
|
||||
const result = moveCamera(
|
||||
{ motion: 'tilt', direction: 'up', mode: 'once' },
|
||||
(start) => {
|
||||
camera.moveBackward(4_000_000);
|
||||
return start();
|
||||
},
|
||||
);
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error, /globe out of view/);
|
||||
});
|
||||
|
||||
test('the voice frame limit leaves Columbus view alone', (t) => {
|
||||
// At its limit in 3D a further tilt up is refused; the same tilt in Columbus
|
||||
// view is not limited by the frame.
|
||||
|
||||
+16
-22
@@ -22,6 +22,7 @@ import * as Cesium from 'cesium';
|
||||
import { GLOBE_VIEW } from './locations.js';
|
||||
import {
|
||||
globeFrameShare,
|
||||
isFreeGlobeCamera,
|
||||
lookAngleWithinFrame,
|
||||
lookStepLosesGlobe,
|
||||
} from './globeInView.js';
|
||||
@@ -1015,18 +1016,6 @@ export function interruptCameraMotionIfActive(
|
||||
return interruptCameraMotion(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* A free globe camera: 3D, not tracking, not locked to a frame. The frame
|
||||
* limit for in-place turns applies to it alone, as the move-end recovery does.
|
||||
*/
|
||||
function isFreeGlobeCamera(cam) {
|
||||
return (
|
||||
_viewer.scene.mode === Cesium.SceneMode.SCENE3D &&
|
||||
!_viewer.trackedEntity &&
|
||||
Cesium.Matrix4.equals(cam.transform, Cesium.Matrix4.IDENTITY)
|
||||
);
|
||||
}
|
||||
|
||||
// The frame share that in-place voice turns measure their limit against, with
|
||||
// the pose the last voice turn left the camera in. A turn that starts where
|
||||
// the last one left off keeps that baseline (raised if the planet now fills
|
||||
@@ -1036,7 +1025,7 @@ function isFreeGlobeCamera(cam) {
|
||||
let _lookBaseline = null;
|
||||
|
||||
// The pre-check's proof that a half-degree turn from this pose keeps the planet
|
||||
// in view: which way, from which direction. A motion that starts from that
|
||||
// in view: which way, from which position and direction. A motion that starts from that
|
||||
// pose turns at least that far, so a request reported as done really moves
|
||||
// even when a smaller first step lands in a dip of the sampled share.
|
||||
const PROVEN_TURN_RAD = Cesium.Math.toRadians(0.5);
|
||||
@@ -1079,7 +1068,7 @@ function lookBaselineShare(cam) {
|
||||
*/
|
||||
function lookWithinFrame(cam, m, look, angle) {
|
||||
let allowed = angle;
|
||||
if (isFreeGlobeCamera(cam)) {
|
||||
if (isFreeGlobeCamera(_viewer)) {
|
||||
if (m.globeShareAtStart === undefined) {
|
||||
m.globeShareAtStart = lookBaselineShare(cam);
|
||||
m.provenTurnRad = lookProven(cam, look) ? PROVEN_TURN_RAD : 0;
|
||||
@@ -1105,6 +1094,12 @@ function lookWithinFrame(cam, m, look, angle) {
|
||||
function lookProven(cam, look) {
|
||||
return (
|
||||
_provenLook?.look === look &&
|
||||
Cesium.Cartesian3.equalsEpsilon(
|
||||
_provenLook.position,
|
||||
cam.positionWC,
|
||||
0,
|
||||
1,
|
||||
) &&
|
||||
Cesium.Cartesian3.equalsEpsilon(
|
||||
_provenLook.direction,
|
||||
cam.directionWC,
|
||||
@@ -1129,7 +1124,7 @@ function checkLookFromOrbit(look, { released = false } = {}) {
|
||||
_provenLook = null;
|
||||
if (_viewer.scene.mode !== Cesium.SceneMode.SCENE3D) return null;
|
||||
const cam = _viewer.camera;
|
||||
if (!released && !isFreeGlobeCamera(cam)) return null;
|
||||
if (!released && !isFreeGlobeCamera(_viewer)) return null;
|
||||
if (
|
||||
lookStepLosesGlobe(cam, look, PROVEN_TURN_RAD, {
|
||||
...visibleFrame(),
|
||||
@@ -1142,7 +1137,11 @@ function checkLookFromOrbit(look, { released = false } = {}) {
|
||||
error:
|
||||
'From this height that would turn the globe out of view — zoom in first, or turn the other way.',
|
||||
};
|
||||
_provenLook = { look, direction: Cesium.Cartesian3.clone(cam.directionWC) };
|
||||
_provenLook = {
|
||||
look,
|
||||
position: Cesium.Cartesian3.clone(cam.positionWC),
|
||||
direction: Cesium.Cartesian3.clone(cam.directionWC),
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1329,12 +1328,7 @@ function onTick() {
|
||||
// Tracking can acquire the camera without going through navigation.
|
||||
// Yield without resetting its pose/frame: the world-space arc proof
|
||||
// only describes Cesium's rotation while the frame is identity.
|
||||
if (
|
||||
_viewer.scene.mode !== Cesium.SceneMode.SCENE3D ||
|
||||
_viewer.trackedEntity ||
|
||||
_isCockpitActive?.() ||
|
||||
!Cesium.Matrix4.equals(cam.transform, Cesium.Matrix4.IDENTITY)
|
||||
) {
|
||||
if (!isFreeGlobeCamera(_viewer) || _isCockpitActive?.()) {
|
||||
interruptCameraMotion('globe-camera-owner-changed');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,18 @@ export const FREE_LOOK_MAX_HEIGHT_METERS = 1_000_000;
|
||||
// the planet's surface.
|
||||
export const MIN_GLOBE_FRAME_SHARE = 0.1;
|
||||
|
||||
/**
|
||||
* A free globe camera: the viewer is in 3D, follows no tracked entity, and its
|
||||
* camera is not locked to a reference frame.
|
||||
*/
|
||||
export function isFreeGlobeCamera(viewer) {
|
||||
return (
|
||||
viewer.scene.mode === Cesium.SceneMode.SCENE3D &&
|
||||
!viewer.trackedEntity &&
|
||||
Cesium.Matrix4.equals(viewer.camera.transform, Cesium.Matrix4.IDENTITY)
|
||||
);
|
||||
}
|
||||
|
||||
const GRID_COLUMNS = 32;
|
||||
const GRID_ROWS = 18;
|
||||
// A turn across a sliver of planet moves its edge through sample points, so
|
||||
|
||||
Reference in new issue
Block a user