Files
gods-eye-view/src/data/focusDeemphasis.js
T
Wilmer AlzateandClaude Opus 5.5 367827e742 perf(vessels): let the scene idle with AIS vessels enabled
Vessels move only when a poll lands, yet enabling the layer held the
continuous render loop for its lifetime, so a parked vessels-only scene
rendered every frame. The layer now:

- requests a frame for every forced visibility pass (new data, selection);
- leaves one trailing frame when the camera moved inside the 800 ms
  visibility cadence, so the view the camera stopped on is evaluated. It is
  keyed to camera motion alone: the pass's own label publication requests a
  frame, and scheduling on that looped at ~1.2 Hz;
- holds 'ais-vessels' only while focus emphasis is animating.

The 80 ms focus pass, which runs while anything is tracked, also skips hidden
sprites whose focus state is at rest (new spriteFocusAtRest) and whose alpha
is already 1: advancing them changes nothing, and with a 12k worldwide feed
they are most of the fleet.

Measured: a parked vessels-only scene (headless SwiftShader) went from
continuous mode and 28 frames in 5 s to idle and 0; horizon visibility had no
mismatches after the camera moved and settled. The focus pass over 12,000
vessels (11,353 hidden) went from ~4.0 ms to ~1.2 ms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 13:01:45 -05:00

592 lines
20 KiB
JavaScript

import * as Cesium from 'cesium';
/**
* Focus-aware sprite treatment shared by the live contact layers.
*
* The selected target is protected by changing ambient sprite alpha, never by
* collection ordering: Cesium's translucent distance sort is not stable for
* collections with similar bounding volumes. This is a narrow amendment to
* the normal always-visible contact rule. Ambient contacts remain present and
* never fall below `dimFloor`; only sprites competing with the tracked target
* are smoothly de-emphasized.
*
* The production consumers sample this service every 80 ms. The default
* 300 ms attack and 600 ms release deliberately span several consumer
* quanta, so a contact receives at least four distinct rendered emphasis
* values during attack instead of crossing the range in one or two ticks.
*/
export const DEFAULT_FOCUS_DEEMPHASIS_PARAMS = Object.freeze({
paddingPx: 18,
dimFloor: 0.25,
nearerBehavior: 'allow',
hysteresisPx: 6,
distanceHysteresisRatio: 0.08,
attackMs: 300,
releaseMs: 600,
writeEpsilon: 0.005,
});
let _params = { ...DEFAULT_FOCUS_DEEMPHASIS_PARAMS };
let _focusTarget = null;
const _spriteStates = new WeakMap();
const _scratchScreen = new Cesium.Cartesian2();
const _advanceResult = {
factor: 1,
changed: false,
transitioning: false,
active: false,
desired: 1,
};
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function resolvedParams(overrides) {
if (!overrides || overrides === _params) return _params;
return { ..._params, ...overrides };
}
function focusTargetEmphasisResolved(
spriteScreenPosition,
spriteCameraDistance,
target,
tuning,
previouslyCompeting,
spriteHalfWidthPx,
spriteHalfHeightPx,
previouslyFarther,
) {
if (!target?.screenRect || !spriteScreenPosition) return 1;
if (
!Number.isFinite(spriteScreenPosition.x) ||
!Number.isFinite(spriteScreenPosition.y)
)
return 1;
// Published rects already contain their publication-time padding. The
// delta keeps direct pure-function calls and runtime A/B overrides honest.
// The ambient extent makes overlap mean visual overlap, not center-point
// containment: a class-sized icon whose edge intrudes into the focus rect
// yields even when its center remains just outside it.
const paddingDelta =
tuning.paddingPx -
(Number.isFinite(target.paddingPx) ? target.paddingPx : 0);
const hysteresis = previouslyCompeting ? tuning.hysteresisPx : 0;
const expansionX =
paddingDelta +
hysteresis +
Math.max(0, Number.isFinite(spriteHalfWidthPx) ? spriteHalfWidthPx : 0);
const expansionY =
paddingDelta +
hysteresis +
Math.max(0, Number.isFinite(spriteHalfHeightPx) ? spriteHalfHeightPx : 0);
const rect = target.screenRect;
const inside =
spriteScreenPosition.x >= rect.left - expansionX &&
spriteScreenPosition.x <= rect.right + expansionX &&
spriteScreenPosition.y >= rect.top - expansionY &&
spriteScreenPosition.y <= rect.bottom + expansionY;
if (!inside) return 1;
const floor = clamp(tuning.dimFloor, 0.01, 1);
if (tuning.nearerBehavior === 'dim') return floor;
const distanceBand = Number.isFinite(target.cameraDistance)
? Math.max(0, target.cameraDistance * tuning.distanceHysteresisRatio)
: 0;
const fartherThreshold = Number.isFinite(target.cameraDistance)
? target.cameraDistance + (previouslyFarther ? -distanceBand : distanceBand)
: Number.NaN;
const farther =
!Number.isFinite(spriteCameraDistance) ||
!Number.isFinite(fartherThreshold) ||
spriteCameraDistance >= fartherThreshold;
if (farther) return floor;
if (tuning.nearerBehavior === 'partial') return floor + (1 - floor) * 0.5;
return 1;
}
function smoothFocusEmphasisResolved(current, target, elapsedMs, tuning) {
const from = clamp(
Number.isFinite(current) ? current : 1,
tuning.dimFloor,
1,
);
const to = clamp(Number.isFinite(target) ? target : 1, tuning.dimFloor, 1);
if (Math.abs(to - from) < Number.EPSILON) return to;
const duration = to < from ? tuning.attackMs : tuning.releaseMs;
if (duration <= 0) return to;
const t = clamp(
(Number.isFinite(elapsedMs) ? elapsedMs : 0) / duration,
0,
1,
);
// Cubic ease-out reaches the exact endpoint at the configured duration and
// stays continuous when a moving contact reverses direction mid-transition.
const eased = 1 - (1 - t) ** 3;
return clamp(from + (to - from) * eased, tuning.dimFloor, 1);
}
/**
* Replace the runtime tuning values used by every focus consumer.
* Unknown keys are ignored so evidence-harness overrides cannot accidentally
* grow an undocumented rendering contract.
* @param {Partial<typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS>} [patch]
* @returns {typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS} Current normalized values.
*/
export function setFocusDeemphasisParams(patch = {}) {
const next = { ..._params };
if (Number.isFinite(patch.paddingPx))
next.paddingPx = Math.max(0, patch.paddingPx);
if (Number.isFinite(patch.dimFloor))
next.dimFloor = clamp(patch.dimFloor, 0.01, 1);
if (['allow', 'dim', 'partial'].includes(patch.nearerBehavior)) {
next.nearerBehavior = patch.nearerBehavior;
}
if (Number.isFinite(patch.hysteresisPx))
next.hysteresisPx = Math.max(0, patch.hysteresisPx);
if (Number.isFinite(patch.distanceHysteresisRatio)) {
next.distanceHysteresisRatio = clamp(patch.distanceHysteresisRatio, 0, 0.5);
}
if (Number.isFinite(patch.attackMs))
next.attackMs = Math.max(0, patch.attackMs);
if (Number.isFinite(patch.releaseMs))
next.releaseMs = Math.max(0, patch.releaseMs);
if (Number.isFinite(patch.writeEpsilon))
next.writeEpsilon = Math.max(0, patch.writeEpsilon);
_params = next;
return { ..._params };
}
/** @returns {typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS} Current runtime tuning. */
export function getFocusDeemphasisParams() {
return _params;
}
/** Restore production defaults. Primarily useful for deterministic QA. */
export function resetFocusDeemphasisParams() {
_params = { ...DEFAULT_FOCUS_DEEMPHASIS_PARAMS };
}
/**
* Publish a focus target from the exact world position already cached by its
* tracking/render path. Callers must never re-derive a position here: a second
* propagation/dead-reckoning sample at a different frame phase recreates the
* tracked-target jitter class this service is designed to avoid.
*
* @param {object} input
* @param {string} input.ownerLayer
* @param {string|number} input.id
* @param {Cesium.Scene} input.scene
* @param {Cesium.Camera} input.camera
* @param {Cesium.Cartesian3} input.displayPosition Cached display position.
* @param {number} [input.widthPx=24] Projected visual width before padding.
* @param {number} [input.heightPx=24] Projected visual height before padding.
* @param {Partial<typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS>} [input.params]
* @returns {object|null} Published target, or null when it cannot be projected.
*/
export function publishFocusTargetFromCachedPosition({
ownerLayer,
id,
scene,
camera,
displayPosition,
widthPx = 24,
heightPx = 24,
params,
}) {
if (
!ownerLayer ||
id === null ||
id === undefined ||
!scene ||
!camera ||
!displayPosition
) {
clearFocusTarget(ownerLayer, id);
return null;
}
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
displayPosition,
_scratchScreen,
);
if (!screen || !Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {
clearFocusTarget(ownerLayer, id);
return null;
}
const tuning = resolvedParams(params);
const padding = tuning.paddingPx;
const halfWidth = Math.max(1, widthPx) / 2;
const halfHeight = Math.max(1, heightPx) / 2;
const appeared = !_focusTarget;
_focusTarget = {
ownerLayer,
id,
screenRect: {
left: screen.x - halfWidth - padding,
top: screen.y - halfHeight - padding,
right: screen.x + halfWidth + padding,
bottom: screen.y + halfHeight + padding,
},
paddingPx: padding,
cameraDistance: Cesium.Cartesian3.distance(
camera.positionWC,
displayPosition,
),
frameNumber: scene.frameState?.frameNumber ?? -1,
};
if (appeared) {
for (const listener of _focusAppearListeners) {
try {
listener();
} catch (e) {
console.warn('[focus] appear listener error:', e);
}
}
}
return _focusTarget;
}
const _focusAppearListeners = new Set();
/**
* Notify on the null→set edge of the shared focus target. Consumers that
* self-suspend their per-frame focus work (e.g. CCTV's projection loop) use
* this single edge to re-arm; the set→null edge needs no event because the
* consumer's own loop is still running through the relax tail and
* self-stops via focusPassIsNeeded. (perf wave 1)
* @param {() => void} listener Called when a focus target is published after none existed.
* @returns {() => void} Unsubscribe.
*/
export function onFocusTargetAppear(listener) {
_focusAppearListeners.add(listener);
return () => _focusAppearListeners.delete(listener);
}
/**
* Clear the focus only when the caller still owns it. This prevents idle
* tracked-layer ticks from erasing a target just published by another layer.
* @param {string} [ownerLayer]
* @param {string|number|null} [id]
*/
export function clearFocusTarget(ownerLayer, id = null) {
if (!_focusTarget) return;
if (ownerLayer && _focusTarget.ownerLayer !== ownerLayer) return;
if (id !== null && id !== undefined && _focusTarget.id !== id) return;
_focusTarget = null;
}
/** @returns {object|null} The current shared focus target. */
export function getFocusTarget() {
return _focusTarget;
}
/**
* Pure focus decision for one ambient sprite.
*
* `previouslyCompeting` is state, not tuning: it expands the exit edge by
* `hysteresisPx` without moving the entry edge. Farther sprites dim fully;
* nearer treatment deliberately remains a runtime tuning choice.
*
* @param {{x:number,y:number}|null} spriteScreenPosition
* @param {number} spriteCameraDistance
* @param {object|null} target
* @param {Partial<typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS>} [params]
* @param {boolean} [previouslyCompeting=false]
* @param {number} [spriteHalfWidthPx=0] Ambient visual half-width.
* @param {number} [spriteHalfHeightPx=spriteHalfWidthPx] Ambient visual half-height.
* @param {boolean} [previouslyFarther=previouslyCompeting] Prior distance-side state.
* @returns {number} Target emphasis in [dimFloor, 1].
*/
export function focusTargetEmphasis(
spriteScreenPosition,
spriteCameraDistance,
target,
params,
previouslyCompeting = false,
spriteHalfWidthPx = 0,
spriteHalfHeightPx = spriteHalfWidthPx,
previouslyFarther = previouslyCompeting,
) {
const tuning = resolvedParams(params);
return focusTargetEmphasisResolved(
spriteScreenPosition,
spriteCameraDistance,
target,
tuning,
previouslyCompeting,
spriteHalfWidthPx,
spriteHalfHeightPx,
previouslyFarther,
);
}
/**
* Pure time-based easing step. Attack means motion toward de-emphasis;
* release means restoration toward full emphasis.
* @param {number} current
* @param {number} target
* @param {number} elapsedMs
* @param {Partial<typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS>} [params]
* @returns {number}
*/
export function smoothFocusEmphasis(current, target, elapsedMs, params) {
const tuning = resolvedParams(params);
return smoothFocusEmphasisResolved(current, target, elapsedMs, tuning);
}
/**
* Advance one sprite's persistent focus state. The caller owns the eventual
* color write so focus can compose with freshness/horizon alpha at one site.
* @param {object} sprite Stable billboard/point primitive identity.
* @param {object} input
* @param {{x:number,y:number}|null} input.screenPosition
* @param {number} input.cameraDistance
* @param {number} input.nowMs
* @param {object|null} [input.target]
* @param {Partial<typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS>} [input.params]
* @param {number} [input.spriteHalfWidthPx=0]
* @param {number} [input.spriteHalfHeightPx=input.spriteHalfWidthPx]
*
* IMPORTANT: the returned object is a module-owned mutable singleton. Consume
* every needed property before the next call; never retain it as a snapshot.
* @returns {{factor:number, changed:boolean, transitioning:boolean,active:boolean,desired:number}}
*/
export function advanceSpriteFocus(
sprite,
{
screenPosition,
cameraDistance,
nowMs,
target = _focusTarget,
params,
spriteHalfWidthPx = 0,
spriteHalfHeightPx = spriteHalfWidthPx,
},
) {
const tuning = resolvedParams(params);
let state = _spriteStates.get(sprite);
if (!state) {
if (!target) {
_advanceResult.factor = 1;
_advanceResult.changed = false;
_advanceResult.transitioning = false;
_advanceResult.active = false;
_advanceResult.desired = 1;
return _advanceResult;
}
state = {
factor: 1,
desired: 1,
transitionFrom: 1,
transitionStartMs: nowMs,
wasFarther: false,
};
_spriteStates.set(sprite, state);
}
// First sample the OLD transition at this timestamp. If the desired state
// changes, the new transition must begin at the value already on screen,
// not the prior tick's stale anchor. This preserves progress during a
// retarget and, paired with range hysteresis, prevents boundary chatter
// from continually restarting at 1.0.
const oldElapsedMs = Math.max(0, nowMs - state.transitionStartMs);
const current = smoothFocusEmphasisResolved(
state.transitionFrom,
state.desired,
oldElapsedMs,
tuning,
);
const desired = focusTargetEmphasisResolved(
screenPosition,
cameraDistance,
target,
tuning,
state.desired < 1,
spriteHalfWidthPx,
spriteHalfHeightPx,
state.wasFarther,
);
if (desired !== state.desired) {
state.transitionFrom = current;
state.transitionStartMs = nowMs;
state.desired = desired;
}
const elapsedMs = Math.max(0, nowMs - state.transitionStartMs);
const next = smoothFocusEmphasisResolved(
state.transitionFrom,
desired,
elapsedMs,
tuning,
);
const changed = Math.abs(next - state.factor) > tuning.writeEpsilon;
state.factor = next;
state.desired = desired;
state.wasFarther =
tuning.nearerBehavior !== 'dim' &&
Math.abs(desired - tuning.dimFloor) <= tuning.writeEpsilon;
const transitioning = Math.abs(next - desired) > tuning.writeEpsilon;
const active = Math.abs(next - 1) > tuning.writeEpsilon;
if (!target && !transitioning && next === 1) _spriteStates.delete(sprite);
_advanceResult.factor = next;
_advanceResult.changed = changed;
_advanceResult.transitioning = transitioning;
_advanceResult.active = active;
_advanceResult.desired = desired;
return _advanceResult;
}
/**
* Project and advance one world-anchored sprite using an owning layer's
* existing render pass.
* @param {object} sprite Stable primitive identity.
* @param {Cesium.Cartesian3} position Current cached/rendered world position.
* @param {Cesium.Scene} scene
* @param {Cesium.Camera} camera
* @param {number} nowMs
* @param {object|null} [target]
* @param {Partial<typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS>} [params]
* @param {number} [spriteHalfWidthPx=0]
* @param {number} [spriteHalfHeightPx=spriteHalfWidthPx]
* @returns {{factor:number, changed:boolean, transitioning:boolean,active:boolean,desired:number}}
*/
export function advanceProjectedSpriteFocus(
sprite,
position,
scene,
camera,
nowMs,
target = _focusTarget,
params,
spriteHalfWidthPx = 0,
spriteHalfHeightPx = spriteHalfWidthPx,
) {
// Common case: nothing tracked and this sprite holds no dimming state —
// advanceSpriteFocus would return identity without reading the projection,
// so skip the worldToWindowCoordinates round trip entirely (it was N
// wasted projections per fleet tick with nothing tracked). Sprites still
// relaxing keep their state entry until fully settled (see the delete at
// the transition tail), so they keep projecting until done. (perf item 8)
if (!target && !_spriteStates.has(sprite)) {
_advanceResult.factor = 1;
_advanceResult.changed = false;
_advanceResult.transitioning = false;
_advanceResult.active = false;
_advanceResult.desired = 1;
return _advanceResult;
}
const screen =
position && scene
? Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
position,
_scratchScreen,
)
: null;
const cameraDistance =
position && camera?.positionWC
? Cesium.Cartesian3.distance(camera.positionWC, position)
: Number.NaN;
return advanceSpriteFocus(sprite, {
screenPosition: screen,
cameraDistance,
nowMs,
target,
params,
spriteHalfWidthPx,
spriteHalfHeightPx,
});
}
/**
* A gated consumer must keep ticking until every previously dimmed sprite has
* returned to identity, even when no target remains.
* @param {object|null} target
* @param {number} activeCount Count of emphasis factors outside the 1.0 deadband.
* @returns {boolean}
*/
export function focusPassIsNeeded(target, activeCount) {
return (
(target !== null && target !== undefined) ||
(Number.isFinite(activeCount) && activeCount > 0)
);
}
/**
* Match Cesium's shader-side NearFarScalar interpolation for rendered extents.
* Cesium interpolates against squared camera distance with a 0.2 exponent.
* @param {{near:number,nearValue:number,far:number,farValue:number}|null} scalar
* @param {number} cameraDistanceM
* @returns {number}
*/
export function nearFarScalarValueAtDistance(scalar, cameraDistanceM) {
if (!scalar || !Number.isFinite(cameraDistanceM)) return 1;
const nearSq = scalar.near * scalar.near;
const farSq = scalar.far * scalar.far;
if (!Number.isFinite(nearSq) || !Number.isFinite(farSq) || farSq <= nearSq)
return 1;
const rawT = (cameraDistanceM * cameraDistanceM - nearSq) / (farSq - nearSq);
const t = Math.pow(clamp(rawT, 0, 1), 0.2);
return scalar.nearValue + (scalar.farValue - scalar.nearValue) * t;
}
/** Shared virtual time used only by the DEV evidence seam. */
let _focusEvidenceNowMs = null;
/** Resolve production time unless deterministic evidence capture owns it. */
export function focusNowMs(productionNowMs) {
return _focusEvidenceNowMs ?? productionNowMs;
}
/** Set deterministic evidence time; called only through development seams. */
export function setFocusEvidenceNowMs(nowMs) {
_focusEvidenceNowMs = Number.isFinite(nowMs) ? nowMs : null;
return _focusEvidenceNowMs;
}
/** Advance deterministic evidence time without consulting a wall clock. */
export function advanceFocusEvidenceNowMs(deltaMs) {
if (_focusEvidenceNowMs === null) return null;
_focusEvidenceNowMs += Math.max(0, Number.isFinite(deltaMs) ? deltaMs : 0);
return _focusEvidenceNowMs;
}
/**
* True when a sprite holds no dimming state, or has settled at full emphasis
* with nothing pending. Advancing such a sprite without a screen position (a
* hidden sprite) changes neither its state nor its alpha, so a layer pass may
* skip it.
* @param {object} sprite
* @returns {boolean}
*/
export function spriteFocusAtRest(sprite) {
const state = _spriteStates.get(sprite);
return (
!state || (state.factor === 1 && state.desired === 1 && !state.wasFarther)
);
}
/** Forget state when a primitive is removed from its owning collection. */
export function forgetSpriteFocus(sprite) {
if (sprite) _spriteStates.delete(sprite);
}
/**
* Deadband predicate shared by consumers' final alpha write sites.
* @param {number} currentAlpha
* @param {number} nextAlpha
* @param {Partial<typeof DEFAULT_FOCUS_DEEMPHASIS_PARAMS>} [params]
* @returns {boolean}
*/
export function focusAlphaNeedsWrite(currentAlpha, nextAlpha, params) {
const tuning = resolvedParams(params);
return (
!Number.isFinite(currentAlpha) ||
Math.abs(currentAlpha - nextAlpha) > tuning.writeEpsilon
);
}