fix(director): turn heading and roll the short way for any authored angle

A single % made the seek delta negative when the target angle was more than
540° below the start, so 350° to -350° turned 340° through 180°. Uses the
same double modulo as playback, and adds tests for exact ±180° endpoints and
a near-antipodal seek.
This commit is contained in:
Sameh Khamis committed 2026-10-14 15:05:09 -07:00
1 parent b80ef27eb9
commit cbefd529f9
3 files changed
+33 -2

No files matched your search

+3 -1
View File
@@ -399,7 +399,9 @@ camera moves with easing, duration and holds. Playback and seek share one
coordinate sampler; navigation/manual input revokes authored motion and pending
holds. Anchor heights and explicit endpoints require the WGS84 ellipsoid
reference. Legacy projects retain their ordinary flights, content and edits.
Seeking between ordinary camera poses uses the shortest longitude arc across the dateline while preserving both authored endpoint longitudes.
Seeking between ordinary camera poses uses the shortest longitude arc across the
dateline while preserving both authored endpoint longitudes, and turns heading
and roll the short way for any authored angles.
See [camera directions](DIRECTOR-CAMERA.md).
Realtime voice composes separate connection, response/tool, Radio handoff, input
@@ -24,3 +24,30 @@ test('director seek follows the short dateline arc in both directions', () => {
}
assert.equal(cameraAtProgress(camera(20), camera(24), 0.5).lon, 22);
});
test('director seek keeps exact ±180 endpoints and picks the short arc near the antipode', () => {
for (const [a, b] of [
[180, -180],
[-180, 180],
]) {
assert.equal(cameraAtProgress(camera(a), camera(b), 0).lon, a);
assert.equal(cameraAtProgress(camera(a), camera(b), 1).lon, b);
assert.equal(
Math.abs(cameraAtProgress(camera(a), camera(b), 0.5).lon),
180,
);
}
// 10°E to 169.9°W is 179.9° westward through Greenwich, not 180.1° east.
const mid = cameraAtProgress(camera(10), camera(-169.9), 0.5).lon;
assert.ok(mid > -81 && mid < -79, `short arc midpoint, got ${mid}`);
});
test('director seek turns heading and roll the short way for any authored angles', () => {
const pose = (heading, roll) => ({ ...camera(0), heading, roll });
const mid = cameraAtProgress(pose(350, 350), pose(-350, -350), 0.5);
// 350° and -350° are 20° apart through 0°/360°, not 340° through 180°.
for (const value of [mid.heading, mid.roll]) {
const wrapped = ((value % 360) + 360) % 360;
assert.ok(wrapped < 1e-9 || 360 - wrapped < 1e-9, `got ${value}`);
}
});
+3 -1
View File
@@ -41,7 +41,9 @@ export function cameraAtProgress(fromCamera, toCamera, progress) {
const lerp = (from, to) => Number(from) + (Number(to) - Number(from)) * eased;
const lerpAngle = (from, to) => {
const start = Number(from) || 0;
const delta = ((Number(to) - start + 540) % 360) - 180;
// Double modulo keeps the delta in [-180, 180) for any authored angles,
// as sampleCameraMove does; a single % goes negative below -540.
const delta = ((((Number(to) - start + 540) % 360) + 360) % 360) - 180;
return start + delta * eased;
};
return {