Files
gods-eye-view/src/data/cctvViewshed.test.mjs
T
Bilawal Sidhu b6da93b8ac release: v0.1.0 — one-click install, keyless boot, in-app Provider Settings (#142)
The first formal release.

- One-click install via Pinokio; keyless boot lands on a live Esri World
  Imagery satellite globe with keyless terrain, with automatic OSM fallback
  and graceful degradation when terrain is unavailable.
- Provider Settings (the POWER UP panel): add, replace, or remove API keys
  inside the app; credential files made owner-only before any secret is
  written; external keys shown read-only; the panel refuses shared or proxied
  servers.
- Keyless capability responses for the optional HUD summary and place search.
- Aircraft-identity voice answers cover operator, type, and route, and say so
  plainly when enrichment is unavailable.
- README rewritten keyless-first; concise 0.1.0 changelog section; CI
  workflow included.

Gates: 2,672 unit tests pass / 0 fail, build clean, tracking regression
108/108, map-source tray QA pass, setup doctor ready.
2026-08-31 17:49:45 -05:00

129 lines
5.4 KiB
JavaScript

// src/data/cctvViewshed.test.mjs — viewshed hue assignment + frustum volume
// geometry (docs/superpowers/specs/2026-07-05-cctv-viewshed-gizmo-design.md §3a/§3b).
//
// Locks:
// - cameraHue is golden-angle spaced and deterministic (color identity is
// stable across sessions for a stable catalog);
// - nearby catalog indices get well-separated hues (the whole point: a local
// cluster of neighbor cameras must be tellable-apart);
// - frustumVolumeGeometryData is welded BY CONSTRUCTION to the wireframe:
// its 5 vertices are exactly the caller's frustumCartesians positions and
// its 18 indices only reference those 5 vertices (4 side faces + 2 cap
// triangles) — no independent geometry recompute anywhere.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import {
cameraHue,
viewshedColors,
frustumVolumeGeometryData,
} from './cctvViewshed.js';
const GOLDEN_ANGLE = 137.50776405003785;
function circularDeltaDeg(a, b) {
const d = Math.abs(a - b) % 360;
return Math.min(d, 360 - d);
}
test('cameraHue: deterministic golden-angle spacing in [0, 360)', () => {
assert.equal(cameraHue(0), 0);
assert.ok(Math.abs(cameraHue(1) - GOLDEN_ANGLE) < 1e-9);
for (let i = 0; i < 300; i++) {
const hue = cameraHue(i);
assert.ok(hue >= 0 && hue < 360, `hue ${hue} out of range at index ${i}`);
assert.equal(hue, cameraHue(i), 'must be pure/deterministic');
}
});
// NB: 20 points on a 360° wheel cap the best-possible min pairwise gap at 18°
// (pigeonhole), so "≥20°" is unachievable for any assignment. Golden-angle
// gives 12.4° min over the first 20 (measured) — assert ≥12° so a regression
// to a worse spacing scheme (e.g. raw id-hash clustering) fails loudly, and
// separately assert CONSECUTIVE indices (the likeliest co-visible neighbors)
// stay far apart.
test('cameraHue: first 20 indices pairwise separated by >= 12 degrees', () => {
const hues = Array.from({ length: 20 }, (_, i) => cameraHue(i));
for (let i = 0; i < hues.length; i++) {
for (let j = i + 1; j < hues.length; j++) {
const delta = circularDeltaDeg(hues[i], hues[j]);
assert.ok(delta >= 12, `hue(${i})=${hues[i].toFixed(1)} vs hue(${j})=${hues[j].toFixed(1)} only ${delta.toFixed(1)}° apart`);
}
}
});
test('cameraHue: consecutive indices separated by >= 80 degrees', () => {
for (let i = 0; i < 40; i++) {
const delta = circularDeltaDeg(cameraHue(i), cameraHue(i + 1));
assert.ok(delta >= 80, `consecutive hues ${i}/${i + 1} only ${delta.toFixed(1)}° apart`);
}
});
test('viewshedColors: fill/line pairs with the designed alphas', () => {
const colors = viewshedColors(210);
for (const key of ['fill', 'fillActive', 'line', 'lineActive']) {
assert.ok(colors[key] instanceof Cesium.Color, `${key} must be a Cesium.Color`);
}
assert.ok(Math.abs(colors.fill.alpha - 0.12) < 1e-6);
assert.ok(Math.abs(colors.fillActive.alpha - 0.22) < 1e-6);
assert.ok(Math.abs(colors.line.alpha - 0.85) < 1e-6);
assert.ok(Math.abs(colors.lineActive.alpha - 1.0) < 1e-6);
// Same hue family: fill and line of the same hue must not be gray.
assert.ok(colors.line.red !== colors.line.green || colors.line.green !== colors.line.blue);
});
/** Minimal frustumCartesians-shaped fixture. */
function positionsFixture() {
return {
mount: new Cesium.Cartesian3(1000, 2000, 3000),
tl: new Cesium.Cartesian3(1100, 2100, 3050),
tr: new Cesium.Cartesian3(1200, 2050, 3050),
br: new Cesium.Cartesian3(1200, 2050, 2950),
bl: new Cesium.Cartesian3(1100, 2100, 2950),
};
}
test('frustumVolumeGeometryData: 5 vertices are exactly the input Cartesians', () => {
const positions = positionsFixture();
const { positions: flat, indices } = frustumVolumeGeometryData(positions);
assert.equal(flat.length, 15);
assert.equal(indices.length, 18);
const order = [positions.mount, positions.tl, positions.tr, positions.br, positions.bl];
order.forEach((p, i) => {
assert.equal(flat[i * 3], p.x);
assert.equal(flat[i * 3 + 1], p.y);
assert.equal(flat[i * 3 + 2], p.z);
});
});
test('frustumVolumeGeometryData: indices form 4 side faces + 2 cap triangles over the 5 vertices', () => {
const { indices } = frustumVolumeGeometryData(positionsFixture());
const counts = new Map();
for (const idx of indices) {
assert.ok(idx >= 0 && idx <= 4, `index ${idx} out of vertex range`);
counts.set(idx, (counts.get(idx) || 0) + 1);
}
// Apex (0) appears in exactly the 4 side triangles; every corner in >= 2.
assert.equal(counts.get(0), 4, 'apex must appear in exactly 4 side triangles');
for (const corner of [1, 2, 3, 4]) {
assert.ok(counts.get(corner) >= 2, `corner ${corner} underused`);
}
// 6 triangles, none degenerate (three distinct vertices each).
for (let t = 0; t < indices.length; t += 3) {
const tri = new Set([indices[t], indices[t + 1], indices[t + 2]]);
assert.equal(tri.size, 3, `triangle at ${t} is degenerate`);
}
});
test('frustumVolumeGeometryData: no NaN for a tight (probe-clamped) pyramid', () => {
const near = {
mount: new Cesium.Cartesian3(0, 0, 0),
tl: new Cesium.Cartesian3(12, 1, 1),
tr: new Cesium.Cartesian3(12, -1, 1),
br: new Cesium.Cartesian3(12, -1, -1),
bl: new Cesium.Cartesian3(12, 1, -1),
};
const { positions: flat } = frustumVolumeGeometryData(near);
for (const v of flat) assert.ok(Number.isFinite(v));
});