fix(camera): judge a voice tilt's frame limit by the way its pitch moves

The pre-check picked the in-place look from args.direction alone, while the tilt itself turns down for any direction but up. A tilt sent as left or right at the frame limit was checked as a turn left or right, refused as losing the globe, though it turns down toward the planet. A rotate sent as up or down was checked as a look up or down, though it turns right.

A tilt's look now comes from its goal pitch against the current pitch (tiltLook), and the tick tilts in place through tiltInPlaceWithinFrame(cam, m, goalPitch). Neither depends on the direction word, so a tilt to an absolute angle, or a tilt with no direction, can use them as they are.
This commit is contained in:
Sameh Khamis committed 2026-11-05 15:55:52 -08:00
1 parent f53342932d
commit a20076fc5e
2 files changed
+51 -9

No files matched your search

+20
View File
@@ -1426,6 +1426,26 @@ test('voice tilt from orbit stops with the planet still on screen and then says
assert.ok(camera.pitch > radians(-68.5) && camera.pitch < radians(-66));
});
// The frame limit judges a tilt by the way its pitch moves, not by the
// direction word: a tilt sent as left or right turns down.
for (const direction of ['left', 'right']) {
test(`a tilt ${direction} from the frame limit is judged as the look down it is`, (t) => {
withScope(t, false);
const { camera, finishOnce } = harness(t, {
pitch: -80,
viewportWidth: 1600,
viewportHeight: 900,
});
assert.equal(moveCamera({ motion: 'tilt', direction: 'up' }).ok, true);
finishOnce();
const atLimit = camera.pitch;
const result = moveCamera({ motion: 'tilt', direction });
assert.equal(result.ok, true, result.error);
finishOnce();
assert.ok(camera.pitch < atLimit - radians(5), 'the tilt turned down');
});
}
test('voice turn in place from orbit stops with the planet still on screen', (t) => {
withScope(t, false); // the whole frame is visible
const { camera, tick } = harness(t, { pitch: -75 });
+31 -9
View File
@@ -1090,6 +1090,23 @@ function lookWithinFrame(cam, m, look, angle) {
if (allowed < angle) interruptCameraMotion('globe-out-of-frame');
}
/**
* The in-place look a pitch change from `fromPitch` to `goalPitch` makes:
* 'lookUp', 'lookDown', or null when it does not move. A tilt is judged by
* the way its pitch moves, whatever form or direction word it came in.
*/
function tiltLook(fromPitch, goalPitch) {
if (!Number.isFinite(fromPitch) || !Number.isFinite(goalPitch)) return null;
if (goalPitch > fromPitch) return 'lookUp';
return goalPitch < fromPitch ? 'lookDown' : null;
}
/** Tilt `m` in place to `goalPitch`, as far as the frame limit allows. */
function tiltInPlaceWithinFrame(cam, m, goalPitch) {
const look = tiltLook(cam.pitch, goalPitch);
if (look) lookWithinFrame(cam, m, look, Math.abs(goalPitch - cam.pitch));
}
/** True when the pre-check proved a half-degree `look` from `cam`'s pose. */
function lookProven(cam, look) {
return (
@@ -1358,7 +1375,7 @@ function onTick() {
interruptCameraMotion('tilt-clamp');
return;
}
lookWithinFrame(cam, m, up ? 'lookUp' : 'lookDown', stepRad);
tiltInPlaceWithinFrame(cam, m, next);
} else {
lookWithinFrame(
cam,
@@ -1662,15 +1679,20 @@ export function moveCamera(args = {}, runNavigation = null) {
};
}
}
// A tilt is judged by the way its pitch will move (any direction other
// than up tilts down); a rotate turns left only when told to, as it runs.
const look =
motion === 'tilt' || motion === 'rotate'
? {
up: 'lookUp',
down: 'lookDown',
left: 'lookLeft',
right: 'lookRight',
}[direction]
: null;
motion === 'tilt'
? tiltLook(
_viewer.camera.pitch,
_viewer.camera.pitch +
Cesium.Math.toRadians(ONCE.tiltDeg) * (direction === 'up' ? 1 : -1),
)
: motion === 'rotate'
? direction === 'left'
? 'lookLeft'
: 'lookRight'
: null;
// Honest limits from orbit too: a stopped turn rests within 0.05° of its
// limit, so one that cannot go half a degree further is refused rather than
// run and reported as done, and one that can is then turned at least that