From b18336d3d45f7c19f46d44d5dd315834739de66a Mon Sep 17 00:00:00 2001 From: Denis Groshev Date: Wed, 4 Nov 2026 22:40:57 +0100 Subject: [PATCH] fix(cockpit): keep the orbital view on a double-click at a ground point In the orbital view a double-click on a mapped ground point went on to Cesium's own double-click, which tracks the picked entity and so ended the view. The Cockpit's double-click routing now takes it in orbit: it keeps or starts the focus on that point (a point beyond the reach is left unfocused but still not tracked). A double-click anywhere else, one a tool owns, and every one outside orbit go on to Cesium's as before, and stopping the Cockpit still hands Cesium's handler back. --- docs/CURRENT-STATE.md | 5 ++- src/ui/cockpitController.js | 10 +++++ src/ui/cockpitInput.js | 6 ++- src/ui/cockpitOrbitalFocus.js | 39 +++++++++++++---- src/ui/cockpitOrbitalFocus.test.mjs | 65 +++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index cb3d128ec..cddbff85f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1723,7 +1723,10 @@ in a polite live region (`FOCUS · Camp Alpha`), and the swath reads approximate as for a tilt, scaled by the tilt onto the point. Clicks still reach Cesium's own handler and never untrack the satellite; a click a tool owns (drawing) is ignored, and the draw tool still borrows the click while -it draws. Under the readout, IN REACH · *n* (a disclosure) lists the mapped +it draws. A double-click on a mapped ground point keeps or starts the focus +on it and never reaches Cesium's double-click, which would track the point +and end the view (the Cockpit's double-click routing in +`src/ui/cockpitInput.js`); any other double-click goes on as before. Under the readout, IN REACH · *n* (a disclosure) lists the mapped points facing the satellite that are within the reach or in the frame now, nearest to nadir first, at most 12 (with NEAREST 12 OF *n* beyond that), rebuilt once a second. A row opens the point's details: position, source and diff --git a/src/ui/cockpitController.js b/src/ui/cockpitController.js index caeb3d667..ed66e8199 100644 --- a/src/ui/cockpitController.js +++ b/src/ui/cockpitController.js @@ -29,6 +29,7 @@ import { } from './cockpitOrbitalView.js'; import { onOrbitalClick, + onOrbitalDoubleClick, routeSceneClick, toggleOrbitalFocus, } from './cockpitOrbitalFocus.js'; @@ -445,6 +446,15 @@ export class CockpitViewController { onOrbitalClick(position) { return onOrbitalClick.call(this, position); } + /** + * A double-click on the orbital picture: on a mapped ground point it keeps + * or starts the focus there instead of letting Cesium track the point. + * @param {{x: number, y: number}|null|undefined} position + * @returns {boolean} True when it was taken. + */ + onOrbitalDoubleClick(position) { + return onOrbitalDoubleClick.call(this, position); + } /** * FOCUS on the reach list: focus that point, switch to it, or end the * focus held on it. diff --git a/src/ui/cockpitInput.js b/src/ui/cockpitInput.js index c5987e97a..89228c393 100644 --- a/src/ui/cockpitInput.js +++ b/src/ui/cockpitInput.js @@ -123,7 +123,8 @@ export function onSceneDoubleClick(position, nowMs = performance.now()) { * installs its own (pickAndTrackObject), which untracks whenever the pick is * not an Entity: on a followed aircraft's model, a primitive, it would drop * the very track the double-click means to ride. A double-click on the - * followed track is the Cockpit's; any other goes on to Cesium's. + * followed track is the Cockpit's, as is one on a mapped ground point in + * orbit (a focus); any other goes on to Cesium's. * @returns {() => void} Puts Cesium's handler back, unless another owner * (the draw tool borrows it) has replaced ours since. */ @@ -134,6 +135,9 @@ export function routeSceneDoubleClick() { const stock = handler.getInputAction?.(type) ?? null; const routed = (event) => { if (this.onSceneDoubleClick(event?.position)) return; + // In orbit, a double-click on a mapped ground point is a focus, never a + // retarget that would end the view. + if (this.onOrbitalDoubleClick?.(event?.position)) return; stock?.(event); }; handler.setInputAction(routed, type); diff --git a/src/ui/cockpitOrbitalFocus.js b/src/ui/cockpitOrbitalFocus.js index 8ece2e28b..dc2cfa738 100644 --- a/src/ui/cockpitOrbitalFocus.js +++ b/src/ui/cockpitOrbitalFocus.js @@ -168,6 +168,37 @@ export function toggleOrbitalFocus(key) { return record ? startFocus.call(this, record) : false; } +/** The mapped ground point drawn at a canvas position, or null. */ +function groundRecordAt(controller, position) { + let picked = null; + try { + picked = controller.viewer.scene?.pick?.(position) ?? null; + } catch { + picked = null; + } + return pickedGroundRecord(controller, picked); +} + +/** + * A double-click on the orbital picture. On a mapped ground point it keeps + * or starts the focus on it (as a single click does) and is taken: Cesium's + * own double-click would track that point, which ends the view. Any other + * double-click, one a tool owns, and every one outside orbit go on. + * @param {{x: number, y: number}|null|undefined} position Canvas position. + * @returns {boolean} True when it landed on a ground point and was taken. + */ +export function onOrbitalDoubleClick(position) { + if (!inOrbit(this) || !isPointerFree()) return false; + if (!Number.isFinite(position?.x) || !Number.isFinite(position?.y)) + return false; + const record = groundRecordAt(this, position); + if (!record) return false; + // A point beyond the reach is not focused, but still never tracked. + if (this.orbital.focus?.key !== String(record.id)) + startFocus.call(this, record); + return true; +} + /** * A click on the orbital picture. One on a mapped ground point in reach * focuses it; any other click ends a focus. A click a tool owns (drawing) @@ -179,13 +210,7 @@ export function onOrbitalClick(position) { if (!inOrbit(this) || !isPointerFree()) return false; if (!Number.isFinite(position?.x) || !Number.isFinite(position?.y)) return false; - let picked = null; - try { - picked = this.viewer.scene?.pick?.(position) ?? null; - } catch { - picked = null; - } - const record = pickedGroundRecord(this, picked); + const record = groundRecordAt(this, position); const state = this.orbital; if (record) { if (state.focus?.key === String(record.id)) return true; diff --git a/src/ui/cockpitOrbitalFocus.test.mjs b/src/ui/cockpitOrbitalFocus.test.mjs index 97370fb3f..1e1a232dd 100644 --- a/src/ui/cockpitOrbitalFocus.test.mjs +++ b/src/ui/cockpitOrbitalFocus.test.mjs @@ -109,6 +109,18 @@ function fixture(t, { reducedMotion = false } = {}) { }; handler.stock = stockClick; actions.set(Cesium.ScreenSpaceEventType.LEFT_CLICK, stockClick); + // Its double-click tracks what it picks (pickAndTrackObject): a mapped + // point, tracked, would end the orbital view. + handler.stockDoubleClicks = 0; + const stockDoubleClick = ({ position }) => { + handler.stockDoubleClicks += 1; + const picked = viewer.scene.pick(position); + viewer.trackedEntity = picked?.id + ? { ...picked.id, position: { getValue: () => null } } + : undefined; + }; + handler.stockDouble = stockDoubleClick; + actions.set(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK, stockDoubleClick); viewer.screenSpaceEventHandler = handler; let subject = { ...ISS }; @@ -229,6 +241,12 @@ function fixture(t, { reducedMotion = false } = {}) { frame, click: (position) => actions.get(Cesium.ScreenSpaceEventType.LEFT_CLICK)?.({ position }), + /** A double-click as Cesium fires it: two clicks, then the double. */ + doubleClick(position) { + actions.get(Cesium.ScreenSpaceEventType.LEFT_CLICK)?.({ position }); + actions.get(Cesium.ScreenSpaceEventType.LEFT_CLICK)?.({ position }); + actions.get(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)?.({ position }); + }, /** Move the satellite along its track by `deg` of latitude and longitude. */ move(dLat, dLon) { subject = { @@ -642,3 +660,50 @@ test('stopping the Cockpit hands the single click back to Cesium', async (t) => assert.equal(scene.handler.stockClicks, 1); assert.equal(scene.cockpit.orbital, null); }); + +test('a double-click on a mapped point focuses it and keeps the orbital view', async (t) => { + const scene = await inOrbit(t); + const { cockpit, viewer, handler } = scene; + scene.doubleClick(SITES.alpha.at); + assert.equal(handler.stockDoubleClicks, 0, 'Cesium does not track the point'); + assert.equal(viewer.trackedEntity, undefined); + scene.frame(); + assert.equal(cockpit.active, true, 'still in orbit'); + assert.equal(cockpit.orbital.focus.key, 'site-alpha'); + run(scene, LAND_FRAMES); + assert.ok(missDeg(scene.shots.at(-1), 'alpha') < 1e-5); + + // On the focused point it keeps the focus; on another it switches. + scene.doubleClick(SITES.alpha.at); + assert.equal(cockpit.orbital.focus.key, 'site-alpha'); + scene.doubleClick(SITES.bravo.at); + assert.equal(cockpit.orbital.focus.key, 'site-bravo'); + // A mapped point beyond the reach is not focused, but not tracked either. + scene.doubleClick(SITES.far.at); + assert.equal(handler.stockDoubleClicks, 0); + scene.frame(); + assert.equal(cockpit.active, true); + + // Anywhere else the double-click goes on to Cesium's, as before. + scene.doubleClick(EMPTY_GROUND); + assert.equal(handler.stockDoubleClicks, 1); + assert.equal(cockpit.orbital.focus, null, 'the clicks ended the focus'); +}); + +test('a double-click a tool owns, or one outside orbit, goes on to Cesium', async (t) => { + const scene = fixture(t); + const type = Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK; + scene.doubleClick(SITES.alpha.at); + assert.equal(scene.handler.stockDoubleClicks, 1, 'outside orbit'); + scene.viewer.trackedEntity = undefined; + assert.equal(scene.cockpit.enter(), true); + await settle(); + scene.frame(); + const lease = claimPointer('draw'); + t.after(() => releasePointer(lease)); + scene.doubleClick(SITES.alpha.at); + assert.equal(scene.handler.stockDoubleClicks, 2, 'a tool owns it'); + releasePointer(lease); + scene.cockpit.stop(); + assert.equal(scene.handler.getInputAction(type), scene.handler.stockDouble); +});