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 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 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 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, 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), 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 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'; } from './cockpitOrbitalView.js';
import { import {
onOrbitalClick, onOrbitalClick,
onOrbitalDoubleClick,
routeSceneClick, routeSceneClick,
toggleOrbitalFocus, toggleOrbitalFocus,
} from './cockpitOrbitalFocus.js'; } from './cockpitOrbitalFocus.js';
@@ -445,6 +446,15 @@ export class CockpitViewController {
onOrbitalClick(position) { onOrbitalClick(position) {
return onOrbitalClick.call(this, 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 on the reach list: focus that point, switch to it, or end the
* focus held on it. * 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 * installs its own (pickAndTrackObject), which untracks whenever the pick is
* not an Entity: on a followed aircraft's model, a primitive, it would drop * 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 * 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 * @returns {() => void} Puts Cesium's handler back, unless another owner
* (the draw tool borrows it) has replaced ours since. * (the draw tool borrows it) has replaced ours since.
*/ */
@@ -134,6 +135,9 @@ export function routeSceneDoubleClick() {
const stock = handler.getInputAction?.(type) ?? null; const stock = handler.getInputAction?.(type) ?? null;
const routed = (event) => { const routed = (event) => {
if (this.onSceneDoubleClick(event?.position)) return; 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); stock?.(event);
}; };
handler.setInputAction(routed, type); handler.setInputAction(routed, type);
+32 -7
View File
@@ -168,6 +168,37 @@ export function toggleOrbitalFocus(key) {
return record ? startFocus.call(this, record) : false; 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 * 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) * 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 (!inOrbit(this) || !isPointerFree()) return false;
if (!Number.isFinite(position?.x) || !Number.isFinite(position?.y)) if (!Number.isFinite(position?.x) || !Number.isFinite(position?.y))
return false; return false;
let picked = null; const record = groundRecordAt(this, position);
try {
picked = this.viewer.scene?.pick?.(position) ?? null;
} catch {
picked = null;
}
const record = pickedGroundRecord(this, picked);
const state = this.orbital; const state = this.orbital;
if (record) { if (record) {
if (state.focus?.key === String(record.id)) return true; if (state.focus?.key === String(record.id)) return true;
+65
View File
@@ -109,6 +109,18 @@ function fixture(t, { reducedMotion = false } = {}) {
}; };
handler.stock = stockClick; handler.stock = stockClick;
actions.set(Cesium.ScreenSpaceEventType.LEFT_CLICK, 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; viewer.screenSpaceEventHandler = handler;
let subject = { ...ISS }; let subject = { ...ISS };
@@ -229,6 +241,12 @@ function fixture(t, { reducedMotion = false } = {}) {
frame, frame,
click: (position) => click: (position) =>
actions.get(Cesium.ScreenSpaceEventType.LEFT_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 the satellite along its track by `deg` of latitude and longitude. */
move(dLat, dLon) { move(dLat, dLon) {
subject = { 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.handler.stockClicks, 1);
assert.equal(scene.cockpit.orbital, null); 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);
});