From cbefd529f9ca08d55e874240142d495cdf1d342f Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 15:05:05 -0700 Subject: [PATCH] fix(director): turn heading and roll the short way for any authored angle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/CURRENT-STATE.md | 4 ++- src/director/director-dateline-seek.test.mjs | 27 ++++++++++++++++++++ src/director/timeline.js | 4 ++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index fa48b7bfe..3a7f7b94f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -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 diff --git a/src/director/director-dateline-seek.test.mjs b/src/director/director-dateline-seek.test.mjs index f950814f3..b0dd2b24b 100644 --- a/src/director/director-dateline-seek.test.mjs +++ b/src/director/director-dateline-seek.test.mjs @@ -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}`); + } +}); diff --git a/src/director/timeline.js b/src/director/timeline.js index fbaca4301..f7cffcece 100644 --- a/src/director/timeline.js +++ b/src/director/timeline.js @@ -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 {