mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +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
|
# 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
|
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
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 * as Cesium from 'cesium';
|
||||||
import { isPointerFree } from '../../data/inputOwnership.js';
|
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({
|
export function createLifecycle({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
services,
|
services,
|
||||||
@@ -8,6 +77,7 @@ export function createLifecycle({
|
|||||||
source,
|
source,
|
||||||
}) {
|
}) {
|
||||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||||
|
let hover = null;
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
init(viewer) {
|
init(viewer) {
|
||||||
@@ -45,14 +115,15 @@ export function createLifecycle({
|
|||||||
layerState._moveHandler = new Cesium.ScreenSpaceEventHandler(
|
layerState._moveHandler = new Cesium.ScreenSpaceEventHandler(
|
||||||
viewer.scene.canvas,
|
viewer.scene.canvas,
|
||||||
);
|
);
|
||||||
layerState._moveHandler.setInputAction((movement) => {
|
hover = createLaunchHover({
|
||||||
if (!layerState._enabled || !layerState._dataSource?.show) return;
|
viewer,
|
||||||
const missionEntity = viewer.scene
|
enabled: () => layerState._enabled && layerState._dataSource?.show,
|
||||||
.drillPick(movement.endPosition, 12)
|
entityLaunchId: parts.selection.entityLaunchId,
|
||||||
.map((picked) => picked?.id)
|
});
|
||||||
.find((candidate) => parts.selection.entityLaunchId(candidate));
|
layerState._moveHandler.setInputAction(
|
||||||
viewer.scene.canvas.style.cursor = missionEntity ? 'pointer' : '';
|
hover.move,
|
||||||
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
Cesium.ScreenSpaceEventType.MOUSE_MOVE,
|
||||||
|
);
|
||||||
// Apply horizon visibility before Cesium traverses and picks the scene.
|
// Apply horizon visibility before Cesium traverses and picks the scene.
|
||||||
// A postRender write is one frame late and can remain visibly stale when
|
// A postRender write is one frame late and can remain visibly stale when
|
||||||
// request-on-demand rendering stops after a globe camera move.
|
// request-on-demand rendering stops after a globe camera move.
|
||||||
@@ -100,6 +171,7 @@ export function createLifecycle({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async disable() {
|
async disable() {
|
||||||
|
hover?.reset();
|
||||||
layerState._sourceController.abort();
|
layerState._sourceController.abort();
|
||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
releaseContinuousRender('rocket-launches');
|
releaseContinuousRender('rocket-launches');
|
||||||
@@ -122,6 +194,8 @@ export function createLifecycle({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async destroy(viewer) {
|
async destroy(viewer) {
|
||||||
|
hover?.destroy();
|
||||||
|
hover = null;
|
||||||
layerState._sourceController.abort();
|
layerState._sourceController.abort();
|
||||||
releaseContinuousRender('rocket-launches'); // direct-destroy path (perf wave 2 fix)
|
releaseContinuousRender('rocket-launches'); // direct-destroy path (perf wave 2 fix)
|
||||||
layerState._enabled = false;
|
layerState._enabled = false;
|
||||||
|
|||||||
Reference in new issue
Block a user