mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(missions): throttle hover picks and clear stale cursors
This commit is contained in:
1 parent
de2138e84c
commit
08e90e8182
3 files changed
+175
-8
No files matched your search
@@ -1,5 +1,7 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
Mission hover picking is limited to one drill-pick per 120 ms and suppressed during camera movement, hidden tabs, disabled layers, and pointer-owned tools. Motion, hiding, disable, and destroy clear the cursor. Click selection keeps its full overlap-aware drill-pick behavior.
|
||||
|
||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Event } from 'cesium';
|
||||
import { createLaunchHover } from './lifecycle.js';
|
||||
|
||||
test('mission hover throttles, clears on motion/hidden, and releases listeners', () => {
|
||||
let at = 0,
|
||||
picks = 0,
|
||||
enabled = true;
|
||||
const listeners = new Set();
|
||||
const doc = {
|
||||
hidden: false,
|
||||
addEventListener: (_n, fn) => listeners.add(fn),
|
||||
removeEventListener: (_n, fn) => listeners.delete(fn),
|
||||
};
|
||||
const viewer = {
|
||||
camera: { moveStart: new Event(), moveEnd: new Event() },
|
||||
scene: {
|
||||
canvas: { style: {} },
|
||||
drillPick() {
|
||||
picks++;
|
||||
return [{ id: 'launch' }];
|
||||
},
|
||||
},
|
||||
};
|
||||
const hover = createLaunchHover({
|
||||
viewer,
|
||||
enabled: () => enabled,
|
||||
entityLaunchId: (id) => id === 'launch',
|
||||
now: () => at,
|
||||
documentRef: doc,
|
||||
pointerFree: () => true,
|
||||
});
|
||||
for (at = 0; at < 120; at++) hover.move({ endPosition: {} });
|
||||
assert.equal(picks, 1);
|
||||
assert.equal(viewer.scene.canvas.style.cursor, 'pointer');
|
||||
hover.move({ endPosition: {} });
|
||||
assert.equal(picks, 2);
|
||||
viewer.camera.moveStart.raiseEvent();
|
||||
at = 500;
|
||||
hover.move({ endPosition: {} });
|
||||
assert.equal(picks, 2);
|
||||
assert.equal(viewer.scene.canvas.style.cursor, '');
|
||||
viewer.camera.moveEnd.raiseEvent();
|
||||
hover.move({ endPosition: {} });
|
||||
assert.equal(picks, 3);
|
||||
doc.hidden = true;
|
||||
for (const fn of listeners) fn();
|
||||
hover.move({ endPosition: {} });
|
||||
assert.equal(picks, 3);
|
||||
assert.equal(viewer.scene.canvas.style.cursor, '');
|
||||
doc.hidden = false;
|
||||
enabled = false;
|
||||
hover.move({ endPosition: {} });
|
||||
assert.equal(picks, 3);
|
||||
hover.destroy();
|
||||
assert.equal(listeners.size, 0);
|
||||
assert.equal(viewer.camera.moveStart.numberOfListeners, 0);
|
||||
assert.equal(viewer.camera.moveEnd.numberOfListeners, 0);
|
||||
});
|
||||
|
||||
test('mission hover preserves the cursor of a pointer-owning tool', () => {
|
||||
let free = true,
|
||||
picks = 0;
|
||||
const viewer = {
|
||||
camera: { moveStart: new Event(), moveEnd: new Event() },
|
||||
scene: {
|
||||
canvas: { style: { cursor: '' } },
|
||||
drillPick() {
|
||||
picks++;
|
||||
return [{ id: 'launch' }];
|
||||
},
|
||||
},
|
||||
};
|
||||
const hover = createLaunchHover({
|
||||
viewer,
|
||||
enabled: () => true,
|
||||
entityLaunchId: () => true,
|
||||
documentRef: null,
|
||||
pointerFree: () => free,
|
||||
});
|
||||
hover.move({ endPosition: {} });
|
||||
assert.equal(viewer.scene.canvas.style.cursor, 'pointer');
|
||||
free = false;
|
||||
viewer.scene.canvas.style.cursor = 'crosshair';
|
||||
hover.move({ endPosition: {} });
|
||||
viewer.camera.moveStart.raiseEvent();
|
||||
hover.destroy();
|
||||
assert.equal(picks, 1);
|
||||
assert.equal(viewer.scene.canvas.style.cursor, 'crosshair');
|
||||
});
|
||||
@@ -1,6 +1,75 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { isPointerFree } from '../../data/inputOwnership.js';
|
||||
|
||||
/** Bound hover picking independently of precise click selection. */
|
||||
export function createLaunchHover({
|
||||
viewer,
|
||||
enabled,
|
||||
entityLaunchId,
|
||||
now = () => performance.now(),
|
||||
documentRef = globalThis.document,
|
||||
pointerFree = isPointerFree,
|
||||
}) {
|
||||
let moving = false;
|
||||
let lastPick = -Infinity;
|
||||
let ownsCursor = false;
|
||||
const clearCursor = () => {
|
||||
if (ownsCursor && viewer.scene.canvas.style.cursor === 'pointer')
|
||||
viewer.scene.canvas.style.cursor = '';
|
||||
ownsCursor = false;
|
||||
};
|
||||
const reset = () => {
|
||||
clearCursor();
|
||||
lastPick = -Infinity;
|
||||
};
|
||||
const start = () => {
|
||||
moving = true;
|
||||
reset();
|
||||
};
|
||||
const end = () => {
|
||||
moving = false;
|
||||
reset();
|
||||
};
|
||||
const visibility = () => {
|
||||
if (documentRef?.hidden || documentRef?.visibilityState === 'hidden')
|
||||
reset();
|
||||
};
|
||||
const removeStart = viewer.camera?.moveStart?.addEventListener(start);
|
||||
const removeEnd = viewer.camera?.moveEnd?.addEventListener(end);
|
||||
documentRef?.addEventListener?.('visibilitychange', visibility);
|
||||
return {
|
||||
move(movement) {
|
||||
if (
|
||||
!enabled() ||
|
||||
moving ||
|
||||
documentRef?.hidden ||
|
||||
documentRef?.visibilityState === 'hidden' ||
|
||||
!pointerFree()
|
||||
) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
const at = now();
|
||||
if (at - lastPick < 120) return;
|
||||
lastPick = at;
|
||||
const found = viewer.scene
|
||||
.drillPick(movement.endPosition, 12)
|
||||
.some((picked) => entityLaunchId(picked?.id));
|
||||
if (found) {
|
||||
viewer.scene.canvas.style.cursor = 'pointer';
|
||||
ownsCursor = true;
|
||||
} else clearCursor();
|
||||
},
|
||||
reset,
|
||||
destroy() {
|
||||
reset();
|
||||
removeStart?.();
|
||||
removeEnd?.();
|
||||
documentRef?.removeEventListener?.('visibilitychange', visibility);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createLifecycle({
|
||||
state: layerState,
|
||||
services,
|
||||
@@ -8,6 +77,7 @@ export function createLifecycle({
|
||||
source,
|
||||
}) {
|
||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||
let hover = null;
|
||||
|
||||
const methods = {
|
||||
init(viewer) {
|
||||
@@ -45,14 +115,15 @@ export function createLifecycle({
|
||||
layerState._moveHandler = new Cesium.ScreenSpaceEventHandler(
|
||||
viewer.scene.canvas,
|
||||
);
|
||||
layerState._moveHandler.setInputAction((movement) => {
|
||||
if (!layerState._enabled || !layerState._dataSource?.show) return;
|
||||
const missionEntity = viewer.scene
|
||||
.drillPick(movement.endPosition, 12)
|
||||
.map((picked) => picked?.id)
|
||||
.find((candidate) => parts.selection.entityLaunchId(candidate));
|
||||
viewer.scene.canvas.style.cursor = missionEntity ? 'pointer' : '';
|
||||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
||||
hover = createLaunchHover({
|
||||
viewer,
|
||||
enabled: () => layerState._enabled && layerState._dataSource?.show,
|
||||
entityLaunchId: parts.selection.entityLaunchId,
|
||||
});
|
||||
layerState._moveHandler.setInputAction(
|
||||
hover.move,
|
||||
Cesium.ScreenSpaceEventType.MOUSE_MOVE,
|
||||
);
|
||||
// Apply horizon visibility before Cesium traverses and picks the scene.
|
||||
// A postRender write is one frame late and can remain visibly stale when
|
||||
// request-on-demand rendering stops after a globe camera move.
|
||||
@@ -100,6 +171,7 @@ export function createLifecycle({
|
||||
},
|
||||
|
||||
async disable() {
|
||||
hover?.reset();
|
||||
layerState._sourceController.abort();
|
||||
layerState._enabled = false;
|
||||
releaseContinuousRender('rocket-launches');
|
||||
@@ -122,6 +194,8 @@ export function createLifecycle({
|
||||
},
|
||||
|
||||
async destroy(viewer) {
|
||||
hover?.destroy();
|
||||
hover = null;
|
||||
layerState._sourceController.abort();
|
||||
releaseContinuousRender('rocket-launches'); // direct-destroy path (perf wave 2 fix)
|
||||
layerState._enabled = false;
|
||||
|
||||
Reference in new issue
Block a user