mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Review follow-ups for the sonar pulses. Wake on user input only: a camera move wakes the pulses only within 1.5 s of a pointer, wheel or touch on the Cesium canvas, where its camera controller listens. Tracking, the cockpit view, an orbit or a fly-to move the camera on every frame and no longer keep the pulses running. Keys don't count: the camera controller reads none. Below the disc: with the camera under 2.5 km the pulses rest completely, with no disc, ring flash, echoes or frame requests. Echo brightness: echoes scale by the contact bracket's drawn alpha, which includes the Cyber sonar sweep, rounded to the nearest band with no floor, so an echo is never brighter than its bracket. Disc follow: the pulse disc follows the subject by a model-matrix rotation about the Earth's centre and is rebuilt only every 20 km instead of every 500 m. Tests: a cap on burst length; teardown through disable(); the echo's spread and alpha on the canvas; echoes under the Cyber sonar never brighter than their bracket; waking by pointer, wheel, touch and drag but not by keys, app camera moves or stale input; the rest below the disc; and tighter asserts. CURRENT-STATE notes that terrain above 2.5 km still cuts through the disc and ring.
173 lines
6.6 KiB
JavaScript
173 lines
6.6 KiB
JavaScript
/**
|
|
* @module awarenessEcho
|
|
* @description The Global Context sonar echo field. The awareness layer
|
|
* publishes it while its range pulses run; the detection overlay reads it to
|
|
* pulse the brackets of contacts a pulse front has just crossed. It holds no
|
|
* awareness policy of its own: the publisher passes radius, pulse timing and
|
|
* echo styling in, so the overlay never imports awareness internals.
|
|
*/
|
|
import * as Cesium from 'cesium';
|
|
|
|
/**
|
|
* @typedef {object} AwarenessEchoField
|
|
* @property {Cesium.Cartesian3} center Subject's ground point. The publisher
|
|
* may move it in place; it is read on every query.
|
|
* @property {number} radiusM Range the pulses grow to.
|
|
* @property {{count: number, periodMs: number, startFraction: number, fadeInFraction?: number}} pulse
|
|
* @property {{fadeMs: number, layerIds: string[]}} echo
|
|
* @property {(nowMs: number) => number} [level] How far the pulses are
|
|
* switched on, 0 to 1; echoes scale with it, so they rest with the pulses.
|
|
*/
|
|
|
|
/** @type {AwarenessEchoField|null} */
|
|
let field = null;
|
|
let layerIds = new Set();
|
|
const groundScratch = new Cesium.Cartesian3();
|
|
|
|
/**
|
|
* Start (or replace) the echo field.
|
|
* @param {AwarenessEchoField} next
|
|
* @returns {void}
|
|
*/
|
|
export function setAwarenessEchoField(next) {
|
|
field = next;
|
|
layerIds = new Set(next.echo.layerIds);
|
|
}
|
|
|
|
/** Stop all echoes. Safe when none were set. */
|
|
export function clearAwarenessEchoField() {
|
|
field = null;
|
|
layerIds = new Set();
|
|
}
|
|
|
|
/** @returns {AwarenessEchoField|null} The field the overlay styles from. */
|
|
export function getAwarenessEchoField() {
|
|
return field;
|
|
}
|
|
|
|
/**
|
|
* Whether any echo can light at `nowMs`: a field is set and its pulses are
|
|
* running. The overlay checks this once per frame, so resting pulses cost no
|
|
* per-contact work.
|
|
* @param {number} nowMs Monotonic clock in milliseconds.
|
|
* @returns {boolean}
|
|
*/
|
|
export function awarenessEchoActive(nowMs) {
|
|
return Boolean(field) && (field.level?.(nowMs) ?? 1) > 0;
|
|
}
|
|
|
|
/**
|
|
* How lit a contact's echo is at `nowMs`: 1 the moment a pulse front reaches
|
|
* its distance, fading as (1 - elapsed / `fadeMs`) squared, 0 otherwise. The
|
|
* strongest pulse wins. A front that has wrapped back to the centre still
|
|
* counts for the contact it last crossed, so rim contacts fade out instead of
|
|
* cutting off. Nothing nearer than the birth ring `startFraction` out echoes,
|
|
* and within the pulse's `fadeInFraction` the echo is scaled down with it.
|
|
* @param {number} nowMs Monotonic clock in milliseconds.
|
|
* @param {number} distanceM Contact's distance from the subject.
|
|
* @param {number} radiusM Range the pulses grow to.
|
|
* @param {{count: number, periodMs: number, startFraction: number, fadeInFraction?: number}} pulse
|
|
* @param {{fadeMs: number}} echo
|
|
* @returns {number} 0 to 1.
|
|
*/
|
|
export function awarenessEchoStrength(nowMs, distanceM, radiusM, pulse, echo) {
|
|
// Pulses are born `startFraction` out, so nothing nearer (the subject
|
|
// itself, its wingmen) is ever crossed; without this it lit on every birth.
|
|
if (
|
|
!Number.isFinite(distanceM) ||
|
|
distanceM < radiusM * pulse.startFraction ||
|
|
distanceM > radiusM
|
|
)
|
|
return 0;
|
|
// The phase at which a pulse front sits at this distance.
|
|
const touchPhase =
|
|
(distanceM / radiusM - pulse.startFraction) / (1 - pulse.startFraction);
|
|
let strength = 0;
|
|
for (let index = 0; index < pulse.count; index++) {
|
|
const cycle = nowMs / pulse.periodMs + index / pulse.count;
|
|
let since = cycle - Math.floor(cycle) - touchPhase;
|
|
// Rounding at the exact crossing must not read as a whole period ago.
|
|
if (since < -1e-9) since += 1;
|
|
else if (since < 0) since = 0;
|
|
const elapsedMs = since * pulse.periodMs;
|
|
if (elapsedMs < echo.fadeMs)
|
|
strength = Math.max(strength, (1 - elapsedMs / echo.fadeMs) ** 2);
|
|
}
|
|
// A front still fading in over `fadeInFraction` of its travel echoes no
|
|
// brighter than it shows.
|
|
const fadeIn = pulse.fadeInFraction ?? 0;
|
|
return fadeIn > 0 ? strength * Math.min(1, touchPhase / fadeIn) : strength;
|
|
}
|
|
|
|
/**
|
|
* The echo for one contact of `layerId` at world `position`, or 0 when no
|
|
* field is set or the layer does not echo. Distance is measured between
|
|
* ground points, so an aircraft's altitude does not shift its echo.
|
|
* @param {string} layerId Owning layer of the contact.
|
|
* @param {Cesium.Cartesian3} position Contact's world position.
|
|
* @param {number} nowMs Monotonic clock in milliseconds.
|
|
* @returns {number} 0 to 1.
|
|
*/
|
|
export function awarenessEchoAt(layerId, position, nowMs) {
|
|
if (!field || !position || !layerIds.has(layerId)) return 0;
|
|
const level = field.level?.(nowMs) ?? 1;
|
|
if (!(level > 0)) return 0;
|
|
const ground = Cesium.Ellipsoid.WGS84.scaleToGeodeticSurface(
|
|
position,
|
|
groundScratch,
|
|
);
|
|
if (!ground) return 0;
|
|
return (
|
|
level *
|
|
awarenessEchoStrength(
|
|
nowMs,
|
|
Cesium.Cartesian3.distance(field.center, ground),
|
|
field.radiusM,
|
|
field.pulse,
|
|
field.echo,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The echo bracket the detection overlay draws around one contact, or null
|
|
* when it has no echo. `band` is the alpha band, 1 to `steps`, the echo's
|
|
* strength times the contact bracket's own `bracketAlpha` rounded to the
|
|
* nearest band, so an echo is never drawn brighter than its bracket; one
|
|
* that rounds to nothing is not drawn. `spreadPx` is how far the echo sits
|
|
* outside the contact's bracket, 0 as the front arrives, growing to the
|
|
* field's `spreadPx` as it fades.
|
|
* @param {string} layerId Owning layer of the contact.
|
|
* @param {Cesium.Cartesian3} position Contact's world position.
|
|
* @param {number} nowMs Monotonic clock in milliseconds.
|
|
* @param {number} bracketAlpha The contact bracket's drawn alpha, 0 to 1.
|
|
* @param {number} steps Alpha bands the overlay batches by.
|
|
* @returns {{band: number, spreadPx: number}|null}
|
|
*/
|
|
export function awarenessEchoBracket(
|
|
layerId,
|
|
position,
|
|
nowMs,
|
|
bracketAlpha,
|
|
steps,
|
|
) {
|
|
const echo = awarenessEchoAt(layerId, position, nowMs);
|
|
if (!(echo > 0.01)) return null;
|
|
const band = Math.min(steps, Math.round(echo * bracketAlpha * steps));
|
|
if (!(band >= 1)) return null;
|
|
return { band, spreadPx: field.echo.spreadPx * (1 - echo) };
|
|
}
|
|
|
|
/**
|
|
* The echo's stroke colour: the field's own colour on the default detection
|
|
* style, and the style's line colour under the others (NVG, FLIR, CRT), so
|
|
* an echo never stands out of a monochrome palette.
|
|
* @param {string} styleLine The active detection style's line colour.
|
|
* @param {boolean} defaultStyle Whether the default detection style is active.
|
|
* @returns {string|null} Null when no field is set.
|
|
*/
|
|
export function awarenessEchoColor(styleLine, defaultStyle) {
|
|
if (!field) return null;
|
|
return defaultStyle ? field.echo.color : styleLine;
|
|
}
|