mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
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]>
212 lines
7.5 KiB
JavaScript
212 lines
7.5 KiB
JavaScript
import * as Cesium from 'cesium';
|
|
import { createCyberSonarGpu } from './cyberSonarGpu.js';
|
|
import {
|
|
applyCyberSonarPrimitive,
|
|
createCyberSonarSampler,
|
|
isCyberContactThemeActive,
|
|
isCyberContactSonarActive,
|
|
isCyberSonarActive,
|
|
readCyberSonarSettings,
|
|
cyberSonarAngleDeg,
|
|
syncCyberSonarAnimation,
|
|
CYBER_SONAR_RENDER_INTERVAL_MS,
|
|
} from './cyberSonar.js';
|
|
|
|
const alive = (value) => value && !value.isDestroyed?.();
|
|
const diagnostics = new WeakMap();
|
|
export const getCyberSonarSceneDiagnostics = (viewer) => ({
|
|
...diagnostics.get(viewer),
|
|
});
|
|
|
|
export function visitCyberSonarModels(primitive, visit, visible = true) {
|
|
if (!alive(primitive)) return;
|
|
visible = visible && primitive.show !== false;
|
|
if (primitive instanceof Cesium.PrimitiveCollection) {
|
|
for (let i = 0; i < primitive.length; i++)
|
|
visitCyberSonarModels(primitive.get(i), visit, visible);
|
|
} else if (primitive instanceof Cesium.Model) {
|
|
visit(primitive, primitive, 'model', visible && primitive.ready);
|
|
}
|
|
}
|
|
|
|
/** Native dots, icons and glyphs use GPU draw-command treatment. Models retain
|
|
* their existing reversible CPU color treatment without walking native records.
|
|
*/
|
|
export function createCyberSonarScene(viewer, manager) {
|
|
const scene = viewer?.scene;
|
|
if (
|
|
!scene?.postUpdate?.addEventListener ||
|
|
!scene?.preRender?.addEventListener
|
|
)
|
|
return () => {};
|
|
const tracked = new Map();
|
|
const matrix = new Cesium.Matrix4();
|
|
const position = new Cesium.Cartesian3();
|
|
let epoch = 0,
|
|
timer = null,
|
|
disposed = false;
|
|
let sampler, width, height, active, count;
|
|
const stats = { contacts: 0, updateMs: 0, frames: 0 };
|
|
const root = globalThis.document?.documentElement;
|
|
const gpu = createCyberSonarGpu(scene, () => ({
|
|
...readCyberSonarSettings(),
|
|
enabled: isCyberContactThemeActive(),
|
|
active: isCyberContactSonarActive(),
|
|
angle: cyberSonarAngleDeg(performance.now()),
|
|
}));
|
|
stats.gpu = gpu.stats;
|
|
let gpuSupported = gpu.stats.supported;
|
|
diagnostics.set(viewer, stats);
|
|
const restore = (visual, state) => {
|
|
if (!alive(state.owner)) return;
|
|
for (const key of state.keys)
|
|
applyCyberSonarPrimitive(visual, 1, false, undefined, key);
|
|
};
|
|
const visit = (visual, owner, kind, visible) => {
|
|
let state = tracked.get(visual);
|
|
if (!visible) {
|
|
if (state) {
|
|
restore(visual, state);
|
|
tracked.delete(visual);
|
|
}
|
|
return;
|
|
}
|
|
let p = visual.position;
|
|
if (kind === 'model')
|
|
p = Cesium.Matrix4.getTranslation(visual.modelMatrix, position);
|
|
else if (
|
|
p &&
|
|
owner.modelMatrix &&
|
|
!Cesium.Matrix4.equals(owner.modelMatrix, Cesium.Matrix4.IDENTITY)
|
|
)
|
|
p = Cesium.Matrix4.multiplyByPoint(owner.modelMatrix, p, position);
|
|
if (!p) return;
|
|
const w = matrix[3] * p.x + matrix[7] * p.y + matrix[11] * p.z + matrix[15];
|
|
const x =
|
|
(matrix[0] * p.x + matrix[4] * p.y + matrix[8] * p.z + matrix[12]) / w;
|
|
const y =
|
|
(matrix[1] * p.x + matrix[5] * p.y + matrix[9] * p.z + matrix[13]) / w;
|
|
if (w <= 0 || Math.abs(x) > 1.1 || Math.abs(y) > 1.1) return;
|
|
if (!state) {
|
|
state = {
|
|
owner,
|
|
keys:
|
|
kind === 'label'
|
|
? ['fillColor', 'outlineColor', 'backgroundColor']
|
|
: kind === 'marker' && visual.outlineColor
|
|
? ['color', 'outlineColor']
|
|
: ['color'],
|
|
epoch,
|
|
};
|
|
tracked.set(visual, state);
|
|
}
|
|
state.epoch = epoch;
|
|
const factor = active
|
|
? sampler.at((x + 1) * width * 0.5, (1 - y) * height * 0.5)
|
|
: sampler.neutral;
|
|
for (const key of state.keys) {
|
|
if (key === 'outlineColor' && visual.outlineWidth === 0) continue;
|
|
if (key === 'backgroundColor' && !visual.showBackground) continue;
|
|
applyCyberSonarPrimitive(
|
|
visual,
|
|
kind === 'label' ? sampler.label(factor) : factor,
|
|
true,
|
|
sampler,
|
|
key,
|
|
kind === 'model',
|
|
);
|
|
}
|
|
count++;
|
|
};
|
|
const update = () => {
|
|
if (disposed) return;
|
|
if (timer !== null) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
if (!isCyberContactThemeActive()) {
|
|
for (const [visual, state] of tracked) restore(visual, state);
|
|
tracked.clear();
|
|
stats.contacts = 0;
|
|
stats.updateMs = 0;
|
|
return;
|
|
}
|
|
const started = performance.now();
|
|
width = scene.canvas.clientWidth;
|
|
height = scene.canvas.clientHeight;
|
|
sampler = createCyberSonarSampler(width, height, started);
|
|
active = isCyberContactSonarActive();
|
|
Cesium.Matrix4.multiply(
|
|
scene.camera.frustum.projectionMatrix,
|
|
scene.camera.viewMatrix,
|
|
matrix,
|
|
);
|
|
epoch++;
|
|
count = 0;
|
|
visitCyberSonarModels(scene.primitives, visit);
|
|
for (const [visual, state] of tracked)
|
|
if (state.epoch !== epoch) {
|
|
restore(visual, state);
|
|
tracked.delete(visual);
|
|
}
|
|
stats.contacts = count;
|
|
stats.updateMs = performance.now() - started;
|
|
stats.frames++;
|
|
// No contacts, no scanner-owned render demand. In continuous scenes the
|
|
// next rendered frame cancels this timer; parked scenes refresh at fleet
|
|
// cadence. Idle update-only ticks must not continually postpone the timer.
|
|
};
|
|
let removeRender;
|
|
// Aircraft fleet writers run in preRender, while tracked models and entity
|
|
// visualizers update earlier. Compose AFTER all those owners and BEFORE
|
|
// Cesium uploads primitive attributes for this draw. Requeue at postUpdate
|
|
// so writers installed later (enable/re-enable/track) cannot overwrite sonar.
|
|
// This only reorders one callback; the contact walk runs once per actual draw.
|
|
const removeUpdate = scene.postUpdate.addEventListener(() => {
|
|
if (isCyberSonarActive()) syncCyberSonarAnimation();
|
|
const supportChanged = gpuSupported !== gpu.stats.supported;
|
|
gpuSupported = gpu.stats.supported;
|
|
const support = gpu.stats.supported ? 'supported' : 'unsupported';
|
|
if (root?.dataset && root.dataset.cyberSonarGpu !== support)
|
|
root.dataset.cyberSonarGpu = support;
|
|
// Shader setup runs after preRender's model tint. If it fails while the
|
|
// scene is parked (including Sonar OFF/reduced motion), draw once more so
|
|
// the now-unsupported contact path restores models before becoming idle.
|
|
if (supportChanged) scene.requestRender();
|
|
const status = globalThis.document?.getElementById?.('cyber-sonar-status');
|
|
const message = gpu.stats.supported
|
|
? ''
|
|
: 'Contact GPU unavailable. Native contacts remain visible.';
|
|
if (status && status.textContent !== message) status.textContent = message;
|
|
removeRender?.();
|
|
removeRender = scene.preRender.addEventListener(update);
|
|
});
|
|
const removePostRender = scene.postRender?.addEventListener(() => {
|
|
if (timer !== null) clearTimeout(timer);
|
|
timer = null;
|
|
if (isCyberContactSonarActive() && (count || gpu.stats.visibleCommands))
|
|
timer = setTimeout(() => {
|
|
timer = null;
|
|
if (!disposed && isCyberContactSonarActive()) scene.requestRender();
|
|
}, CYBER_SONAR_RENDER_INTERVAL_MS);
|
|
});
|
|
const unsubscribe = manager?.subscribe?.((change) => {
|
|
if (change.type !== 'visibility') return;
|
|
scene.requestRender();
|
|
});
|
|
return () => {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
removeUpdate();
|
|
removeRender?.();
|
|
removePostRender?.();
|
|
gpu.destroy();
|
|
if (root?.dataset) delete root.dataset.cyberSonarGpu;
|
|
unsubscribe?.();
|
|
if (timer !== null) clearTimeout(timer);
|
|
for (const [visual, state] of tracked) restore(visual, state);
|
|
tracked.clear();
|
|
diagnostics.delete(viewer);
|
|
};
|
|
}
|