From 940050d4e5abd244b291408f9de8cb5e8277de4a Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:06:07 +0530 Subject: [PATCH] =?UTF-8?q?fix(cctv):=20show=20a=20camera=20facing=20359.6?= =?UTF-8?q?=C2=B0=20as=20HDG=200=C2=B0,=20not=20HDG=20360=C2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CCTV HUD heading token and the camera panel's meta line rounded the bearing without wrapping, so a camera facing 359.5° or more (a feed bearing, or a heading calibrated just west of north) read "HDG 360°". Both now wrap after rounding: 359.6° reads "HDG 0°", 359.4° "HDG 359°". --- docs/CURRENT-STATE.md | 4 +++- src/layers/cctv/headingConfidence.js | 3 ++- src/layers/cctv/headingConfidence.test.mjs | 9 ++++++++ src/ui/cctvControls.test.mjs | 24 ++++++++++++++++++++++ src/ui/cctvPresentation.js | 2 +- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c55031a6a..c3e87e8d6 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4788,7 +4788,9 @@ silently demoting every later lookup for the session. wireframe dashed; colours, widths and active/idle emphasis are unchanged. A manual calibration (`calSource: 'manual'`) or curated pose (`poseSource: 'curated'`) is never presented as estimated, matching the CAL badge. Public camera state carries `headingConfidence` and - `headingEstimated` (`src/layers/cctv/headingConfidence.js`, #639). + `headingEstimated` (`src/layers/cctv/headingConfidence.js`, #639). The HUD token and the + camera panel's meta line wrap the bearing after rounding to whole degrees, so a camera + facing 359.5° or more reads `HDG 0°` rather than `HDG 360°`. - **CCTV identified hardware:** a camera whose source or seed names a hardware `model` that resolves in `src/layers/cctv/modelSpecs.js` (vendored CC0 records from the CCTV Camera Database) gets the model's specs appended to the HUD meta line. Only fixed single-lens diff --git a/src/layers/cctv/headingConfidence.js b/src/layers/cctv/headingConfidence.js index 26a877558..fad0867de 100644 --- a/src/layers/cctv/headingConfidence.js +++ b/src/layers/cctv/headingConfidence.js @@ -44,7 +44,8 @@ export function isHeadingEstimated(camera) { * @returns {string} */ export function headingHudToken(camera) { - const hdg = Math.round(Number(camera?.headingDeg) || 0); + // 359.5° and above rounds to north; modulo keeps it from printing 360°. + const hdg = Math.round(Number(camera?.headingDeg) || 0) % 360; return isHeadingEstimated(camera) ? t('HDG {value}° (ESTIMATED)', { value: hdg }) : t('HDG {value}°', { value: hdg }); diff --git a/src/layers/cctv/headingConfidence.test.mjs b/src/layers/cctv/headingConfidence.test.mjs index 513bf0d1b..3cf673861 100644 --- a/src/layers/cctv/headingConfidence.test.mjs +++ b/src/layers/cctv/headingConfidence.test.mjs @@ -44,6 +44,15 @@ test('HUD token tags a synthetic bearing and rounds the degrees', () => { ); }); +test('HUD token reads a bearing that rounds to north as 0°, not 360°', () => { + assert.equal(headingHudToken({ headingDeg: 359.4 }), 'HDG 359°'); + assert.equal(headingHudToken({ headingDeg: 359.6 }), 'HDG 0°'); + assert.equal( + headingHudToken({ headingDeg: 359.5, headingConfidence: 'low' }), + 'HDG 0° (ESTIMATED)', + ); +}); + test('HUD token stays untagged for surveyed and calibrated bearings', () => { assert.equal( headingHudToken({ headingDeg: 67.5, headingConfidence: 'high' }), diff --git a/src/ui/cctvControls.test.mjs b/src/ui/cctvControls.test.mjs index 6a47ff91f..23e5ea75e 100644 --- a/src/ui/cctvControls.test.mjs +++ b/src/ui/cctvControls.test.mjs @@ -47,6 +47,30 @@ function fixture(t) { return { controls, requests }; } +test('the panel meta line reads a bearing that rounds to north as HDG 0°', (t) => { + const { controls } = fixture(t); + controls.actions.setPanelCollapsed = () => {}; + controls._cctvMeta = element(); + const meta = (headingDeg) => { + const camera = { + id: 'a', + city: 'Austin', + headingDeg, + fovDeg: 74, + rangeM: 700, + }; + controls._renderCctvState({ + enabled: true, + activeCameraId: camera.id, + activeCamera: camera, + cameras: [camera], + }); + return controls._cctvMeta.textContent; + }; + assert.match(meta(359.4), /· HDG 359° ·/); + assert.match(meta(359.6), /· HDG 0° ·/); +}); + test('a late image completion cannot replace a newer camera preview', (t) => { const { controls, requests } = fixture(t); controls._queueCctvFrame('first.jpg', 'a', true); diff --git a/src/ui/cctvPresentation.js b/src/ui/cctvPresentation.js index 070c47f64..e4dffa98e 100644 --- a/src/ui/cctvPresentation.js +++ b/src/ui/cctvPresentation.js @@ -171,7 +171,7 @@ export function _renderCctvState(state) { '{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}', { city: activeCamera.city, - heading: `${Math.round(activeCamera.headingDeg)}°`, + heading: `${Math.round(activeCamera.headingDeg) % 360}°`, fov: `${Math.round(activeCamera.fovDeg)}°`, range: Math.round(activeCamera.rangeM), projection: projLabel,