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

142 lines
4.4 KiB
JavaScript

/**
* Camera-ownership policy for explicit and deferred navigation.
*
* Immediate destinations refuse Cockpit before mutation, then stamp, release,
* and fly. Deferred destinations stamp without releasing; after resolution
* they must recheck the stamp and Cockpit immediately before releasing and
* flying.
*/
export const NAVIGATION_AUTHORITY_EVENT = 'gev:navigation-authority-taken';
/**
* Announce that a layer-owned camera flight is taking navigation authority.
*
* Aircraft focus reaches the stamp for free: it assigns `viewer.trackedEntity`,
* and the UI stamps on `trackedEntityChanged`. Vessel and installation focus
* fly the camera WITHOUT ever setting a tracked entity, so they have no such
* seam — an earlier deferred geocode would still match the generation it
* captured and could resolve on top of the new Context focus. This is that
* missing seam, kept explicit so the flight and the stamp cannot drift apart.
* @param {string} reason Diagnostic label for the taking path.
* @param {object} [options] Authority options.
* @param {EventTarget} [options.eventTarget=globalThis.window] Dispatch target.
* @param {boolean} [options.cancelPendingSelection=true] Whether this is newer
* direct intent that may supersede a passive selected-entity restore.
* @returns {boolean} Whether the announcement was dispatched.
*/
export function announceNavigationAuthority(
reason,
{ eventTarget = globalThis.window, cancelPendingSelection = true } = {},
) {
if (typeof eventTarget?.dispatchEvent !== 'function') return false;
eventTarget.dispatchEvent(
new CustomEvent(NAVIGATION_AUTHORITY_EVENT, {
detail: {
reason: String(reason || 'layer-focus'),
cancelPendingSelection: Boolean(cancelPendingSelection),
},
}),
);
return true;
}
/**
* Register one authority listener and return an idempotent disposer.
* @param {EventTarget} eventTarget Listener host.
* @param {Function} listener Authority handler.
* @returns {() => void} Idempotent disposer.
*/
export function registerNavigationAuthorityListener(eventTarget, listener) {
if (
!eventTarget?.addEventListener ||
!eventTarget?.removeEventListener ||
typeof listener !== 'function'
)
return () => {};
eventTarget.addEventListener(NAVIGATION_AUTHORITY_EVENT, listener);
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
eventTarget.removeEventListener(NAVIGATION_AUTHORITY_EVENT, listener);
};
}
/**
* Let a physical globe gesture supersede the delayed shared camera and Follow.
* Ordinary layer visibility/options remain authoritative, but camera intent
* from the recipient always wins over a passive selected-subject restore.
* @param {Function} stamp Navigation stamp callback.
* @returns {*} The callback result, when present.
*/
export function stampInitialShareGesture(stamp) {
return stamp?.({ cancelPendingSelection: true });
}
/**
* Run an immediate explicit camera navigation.
* @param {Object} options
* @returns {*} Navigation result, or false when disposed or Cockpit refuses.
*/
export function runExplicitNavigation({
disposed = false,
cockpitActive = false,
noun = 'target',
showToast,
stamp,
release,
navigate,
} = {}) {
if (disposed) return false;
if (cockpitActive) {
showToast?.(`Exit cockpit to fly to a ${noun}`);
return false;
}
const generation = stamp?.();
release?.();
return navigate?.(generation);
}
/**
* Accept a deferred navigation intent without releasing the current owner.
* @param {Object} options
* @returns {number|false} Generation stamp, or false when disposed or Cockpit refuses.
*/
export function beginDeferredNavigation({
disposed = false,
cockpitActive = false,
noun = 'location',
showToast,
stamp,
} = {}) {
if (disposed) return false;
if (cockpitActive) {
showToast?.(`Exit cockpit to fly to a ${noun}`);
return false;
}
return stamp?.();
}
/**
* Re-assert authority immediately before a deferred flight.
* @param {Object} options
* @returns {boolean} Whether the deferred flight still owns the camera.
*/
export function reassertNavigationHandoff({
generation,
currentGeneration,
cockpitActive = false,
disposed = false,
showToast,
release,
} = {}) {
if (disposed || generation !== currentGeneration) return false;
if (cockpitActive) {
showToast?.('Exit cockpit to fly to a location');
return false;
}
release?.();
return true;
}