Stage 0. Zero-dependency serve.py (static + allowlisted horizons/sbdb/cad proxy, sha256 disk cache with past-span immutability). Three.js via pinned importmap, logarithmic depth, procedural starfield sky (+ Milky Way panorama swap-in), Sun with additive glow, OrbitControls, brass-and-void HUD, scrubbable time bar, central bodyWorld position pass, focus/camera system, hash state. Boots clean, no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.3 KiB
JavaScript
102 lines
3.3 KiB
JavaScript
// SOLARGOD HUD controller — layer rows with per-layer status lines, LIVE/SCRUBBED
|
|
// chip, breadcrumb, date readout. Ports GODSIGH's ui.js contract so layer modules
|
|
// use the identical addLayer/setStatus/setLiveChip API.
|
|
|
|
const rows = new Map();
|
|
|
|
// Register a layer row. Returns a handle so callers can flip the checkbox.
|
|
export function addLayer(id, name, defaultOn, onToggle) {
|
|
const row = document.createElement('div');
|
|
row.className = 'layer-row';
|
|
row.innerHTML = `
|
|
<label>
|
|
<input type="checkbox" ${defaultOn ? 'checked' : ''} />
|
|
<span class="dot" data-state="${defaultOn ? 'ok' : 'off'}"></span>
|
|
<span class="lname"></span>
|
|
</label>
|
|
<div class="lstatus"></div>`;
|
|
row.querySelector('.lname').textContent = name;
|
|
const cb = row.querySelector('input');
|
|
cb.addEventListener('change', () => onToggle(cb.checked));
|
|
document.getElementById('layers').appendChild(row);
|
|
rows.set(id, row);
|
|
return { setChecked: (v) => { cb.checked = v; } };
|
|
}
|
|
|
|
export function getLayerIds() { return [...rows.keys()]; }
|
|
|
|
export function getLayerChecked(id) {
|
|
const row = rows.get(id);
|
|
return row ? row.querySelector('input').checked : false;
|
|
}
|
|
|
|
// Set checkbox AND fire onToggle (replays a real user toggle) — used to restore
|
|
// hash state. No-op if unchanged.
|
|
export function setLayerChecked(id, on) {
|
|
const row = rows.get(id);
|
|
if (!row) return;
|
|
const cb = row.querySelector('input');
|
|
if (cb.checked !== on) {
|
|
cb.checked = on;
|
|
cb.dispatchEvent(new Event('change'));
|
|
}
|
|
}
|
|
|
|
// state ∈ 'ok' | 'warn' | 'err' | 'off'
|
|
export function setStatus(id, text, state = 'ok') {
|
|
const row = rows.get(id);
|
|
if (!row) return;
|
|
row.querySelector('.lstatus').textContent = text;
|
|
row.querySelector('.dot').dataset.state = state;
|
|
}
|
|
|
|
export function setLiveChip(isLive) {
|
|
const chip = document.getElementById('live-chip');
|
|
if (!chip) return;
|
|
chip.textContent = isLive ? 'LIVE' : 'SCRUBBED';
|
|
chip.title = isLive ? 'Clock is at wall-clock now' : 'Clock scrubbed off wall-clock now';
|
|
chip.dataset.state = isLive ? 'live' : 'scrub';
|
|
}
|
|
|
|
// Breadcrumb: array of {id, name} from Sun down to the focused body.
|
|
export function setBreadcrumb(trail, onClick) {
|
|
const el = document.getElementById('breadcrumb');
|
|
if (!el) return;
|
|
el.innerHTML = '';
|
|
trail.forEach((node, i) => {
|
|
if (i > 0) {
|
|
const sep = document.createElement('span');
|
|
sep.className = 'crumb-sep';
|
|
sep.textContent = '▸';
|
|
el.appendChild(sep);
|
|
}
|
|
const c = document.createElement('span');
|
|
c.className = 'crumb' + (i === trail.length - 1 ? ' current' : '');
|
|
c.textContent = node.name.toUpperCase();
|
|
if (i !== trail.length - 1 && onClick) {
|
|
c.style.cursor = 'pointer';
|
|
c.addEventListener('click', () => onClick(node.id));
|
|
}
|
|
el.appendChild(c);
|
|
});
|
|
}
|
|
|
|
export function setDateReadout(text, isLive) {
|
|
const el = document.getElementById('date-readout');
|
|
if (el) {
|
|
el.textContent = text;
|
|
el.classList.toggle('scrubbed', !isLive);
|
|
}
|
|
}
|
|
|
|
// Small transient toast (bottom-center) for one-shot notices.
|
|
let toastTimer = null;
|
|
export function toast(text, ms = 2200) {
|
|
let el = document.getElementById('toast');
|
|
if (!el) return;
|
|
el.textContent = text;
|
|
el.classList.add('show');
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => el.classList.remove('show'), ms);
|
|
}
|