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

84 lines
3.2 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;
}