/* Small shared helpers. No three.js, no DOM — safe to import anywhere. */ /** HTML-escape for interpolation into innerHTML — text AND attribute contexts. * Escapes the five significant characters incl. both quotes, so a value is safe * whether it lands in element content or inside a "…"/'…' attribute. Node fields * (label/table/id/…) are attacker-controlled (any `?graph=` URL/data: URI), so * every innerHTML sink that interpolates them MUST wrap them in this. */ export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); /** Deterministic PRNG. Same seed always yields the same galaxy. */ export function mulberry32(a) { return function () { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } export const clamp = (x, a, b) => Math.max(a, Math.min(b, x)); export const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2); export const easeOut = (t) => 1 - Math.pow(1 - t, 3); export const lerp = (a, b, t) => a + (b - a) * t; /** #rrggbb or [r,g,b] (0..1) -> [r,g,b] floats. */ export function toRGB(c) { if (Array.isArray(c)) return c; const h = String(c).replace('#', ''); const n = parseInt(h.length === 3 ? h.split('').map((x) => x + x).join('') : h, 16); return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255]; } export function rgbToCss(c, alpha) { const [r, g, b] = toRGB(c); const v = (x) => Math.round(x * 255); return alpha == null ? `rgb(${v(r)},${v(g)},${v(b)})` : `rgba(${v(r)},${v(g)},${v(b)},${alpha})`; } /** Compact magnitude formatter: 1_200_000 -> "$1.2M". Override via config.formatMagnitude. */ export function formatCompact(v, prefix = '$') { if (v == null || !isFinite(v)) return '—'; const a = Math.abs(v); if (a >= 1e12) return prefix + (v / 1e12).toFixed(a >= 1e13 ? 0 : 1) + 'T'; if (a >= 1e9) return prefix + (v / 1e9).toFixed(a >= 1e10 ? 0 : 1) + 'B'; if (a >= 1e6) return prefix + (v / 1e6).toFixed(a >= 1e7 ? 0 : 1) + 'M'; if (a >= 1e3) return prefix + Math.round(v / 1e3) + 'K'; return prefix + String(Math.round(v)); } export function truncate(s, n) { if (!s) return ''; return s.length > n ? s.slice(0, n) + '…' : s; } /** Resolve a config value that may be a literal, a key string, or a function. */ export function accessor(spec, fallback) { if (spec == null) return fallback; if (typeof spec === 'function') return spec; return (d) => d[spec]; }