feat(hud): Cyber HUD theme with GPU contact sonar (#706)

Add the opt-in Cyber HUD layout with coordinated map and cockpit panel
styling, GPU-only contact sonar (rings, range, power, opacity, sector) with
native-rendering fallback, and set_cyber_sonar voice controls. Sonar visual
style inspired by kk376's tactical naval sonar (#214).

Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-09-23 20:31:56 -07:00
1 parent f913066cf2
commit 513dc87a0e
74 files changed
+7320 -90

No files matched your search

+28 -3
View File
@@ -1,4 +1,9 @@
import * as Cesium from 'cesium';
import {
createCyberSonarSampler,
isCyberContactSonarActive as isCyberSonarActive,
CYBER_SONAR_RENDER_INTERVAL_MS,
} from '../cyberSonar.js';
import {
getKeyholeFadeTuning,
getKeyholeGeometry,
@@ -170,6 +175,7 @@ let _resizeObserver = null;
let _mutationObserver = null;
let _observedOccluderElements = new WeakSet();
let _occluderRefreshTimer = null;
let _sonarRenderTimer = null;
let _cockpitModeHandler = null;
let _windowResizeHandler = null;
let _cockpitActive = false;
@@ -2443,7 +2449,7 @@ function paintCustomLane(lane) {
}
}
function paintEntryItem(item, keyhole) {
function paintEntryItem(item, keyhole, sonar) {
const { record, placement } = item;
const entry = record.entry;
let keyholeAlpha = 1;
@@ -2468,7 +2474,8 @@ function paintEntryItem(item, keyhole) {
item.temporalAlpha *
record.distanceAlpha *
record.altitudeAlpha *
keyholeAlpha;
keyholeAlpha *
(sonar ? sonar.label(sonar.at(placement.anchorX, placement.anchorY)) : 1);
if (finalAlpha <= 0.001) return;
if (record.paintScale === 1) {
paintOverlayEntry(
@@ -2508,6 +2515,9 @@ function paintEntryItem(item, keyhole) {
function paintFrame(keyhole) {
const started = nowMs();
const sonar = isCyberSonarActive()
? createCyberSonarSampler(_canvasWidth, _canvasHeight, started)
: null;
clearCanvas(true, false);
_detectionSurfacePrepared = false;
if (
@@ -2528,7 +2538,7 @@ function paintFrame(keyhole) {
// retains its former z5 position below every ordinary host entry at z6.
paintCustomLane(lane);
while (itemIndex < _paintCount && _paintQueue[itemIndex].lane === lane) {
paintEntryItem(_paintQueue[itemIndex], keyhole);
paintEntryItem(_paintQueue[itemIndex], keyhole, sonar);
itemIndex++;
}
}
@@ -2581,6 +2591,10 @@ function resetFrameDiagnostics() {
}
function drawWorldOverlay() {
if (_sonarRenderTimer !== null) {
clearTimeout(_sonarRenderTimer);
_sonarRenderTimer = null;
}
if (_destroyed || !_viewer || !_canvas || !_ctx) return;
const timestamp = nowMs();
if (!overlayHasPaintWork(timestamp)) {
@@ -2622,6 +2636,15 @@ function drawWorldOverlay() {
_diagnostics.projectionMs = nowMs() - projectionStarted;
solveDomains(timestamp);
paintFrame(keyhole);
// Labels-only layers need a bounded sweep refresh even with detection off.
// Empty/hidden hosts and teardown never retain scanner render demand.
if (_paintRectCount > 0 && isCyberSonarActive()) {
_sonarRenderTimer = setTimeout(() => {
_sonarRenderTimer = null;
if (!_destroyed && isCyberSonarActive())
_viewer?.scene?.requestRender?.();
}, CYBER_SONAR_RENDER_INTERVAL_MS);
}
const animationsRemaining =
activeFadeCount(timestamp) + activeLeaderAnimationCount(timestamp);
if (animationsRemaining > 0) _viewer.scene.requestRender?.();
@@ -2679,6 +2702,8 @@ export function initWorldOverlay(viewer) {
/** Remove all listeners, observers, entries, diagnostics, and overlay DOM. */
export function destroyWorldOverlay() {
if (_sonarRenderTimer !== null) clearTimeout(_sonarRenderTimer);
_sonarRenderTimer = null;
// Tearing down a host that was never initialized must not arm the
// post-destroy guard: sources are allowed to publish before the first
// `initWorldOverlay`, and that buffering has to survive a stray destroy.