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.
This commit is contained in:
Denis Groshev committed 2026-11-09 01:56:30 +01:00
1 parent dd063b0e83
commit b18336d3d4
5 files changed
+116 -9

No files matched your search

+4 -1
View File
@@ -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
+10
View File
@@ -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.
+5 -1
View File
@@ -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);
+32 -7
View File
@@ -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;
+65
View File
@@ -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);
});