Files
gods-eye-view/src/worldFocus.js
T

97 lines
3.3 KiB
JavaScript

/**
* One-click camera transfer for layer-owned world targets.
*/
import * as Cesium from 'cesium';
export const WORLD_FOCUS_REQUEST_EVENT = 'gev:world-request-focus';
export const WORLD_CLICK_FOCUS_DURATION_SEC = 1.9;
export const WORLD_FOCUS_FRAMING = Object.freeze({
vessel: Object.freeze({ radiusM: 150, rangeM: 1200, pitchDeg: -30 }),
fire: Object.freeze({ radiusM: 400, rangeM: 3000, pitchDeg: -35 }),
});
/** Validate a layer-owned focus target before camera policy can release tracking. */
export function isValidWorldFocusTarget(detail) {
if (!detail || !WORLD_FOCUS_FRAMING[detail.kind]) return false;
if (!String(detail.id || '').trim()) return false;
const { position } = detail;
if (
!position ||
!Number.isFinite(position.x) ||
!Number.isFinite(position.y) ||
!Number.isFinite(position.z)
)
return false;
// Vessel and fire targets are surface-anchored Earth positions. Merely
// finite coordinates near the ECEF origin cannot be flown to, and must be
// rejected before the camera policy releases a current follow owner.
const magnitude = Cesium.Cartesian3.magnitude(position);
return (
Number.isFinite(magnitude) &&
magnitude >= Cesium.Ellipsoid.WGS84.minimumRadius * 0.95
);
}
/** Announce a valid user-click focus request. */
export function requestWorldFocus(detail, eventTarget = globalThis.window) {
if (!isValidWorldFocusTarget(detail)) return false;
if (typeof eventTarget?.dispatchEvent !== 'function') return false;
eventTarget.dispatchEvent(
new CustomEvent(WORLD_FOCUS_REQUEST_EVENT, { detail }),
);
return true;
}
/** Register one listener and return an idempotent disposer. */
export function registerWorldFocusRequestListener(eventTarget, listener) {
if (
!eventTarget?.addEventListener ||
!eventTarget?.removeEventListener ||
typeof listener !== 'function'
)
return () => {};
eventTarget.addEventListener(WORLD_FOCUS_REQUEST_EVENT, listener);
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
eventTarget.removeEventListener(WORLD_FOCUS_REQUEST_EVENT, listener);
};
}
/** Route a valid request through the UI-owned camera policy. */
export function routeWorldFocusRequest(event, runExplicitFocus, fly) {
const detail = event?.detail;
if (!isValidWorldFocusTarget(detail)) return false;
if (typeof runExplicitFocus !== 'function' || typeof fly !== 'function')
return false;
return runExplicitFocus(detail, () => fly(detail));
}
/** Fly to a world target after ownership has been released. */
export function flyToWorldTarget(viewer, target = {}) {
const camera = viewer?.camera;
const framing = WORLD_FOCUS_FRAMING[target.kind];
if (!camera || !framing || !isValidWorldFocusTarget(target)) return false;
const heading = Number.isFinite(camera.heading) ? camera.heading : 0;
const duration =
target.durationSec > 0
? target.durationSec
: WORLD_CLICK_FOCUS_DURATION_SEC;
camera.cancelFlight?.();
camera.flyToBoundingSphere(
new Cesium.BoundingSphere(target.position, framing.radiusM),
{
offset: new Cesium.HeadingPitchRange(
heading,
Cesium.Math.toRadians(framing.pitchDeg),
framing.rangeM,
),
duration,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
},
);
return true;
}