// Geometry unit tests for the DEEP SCAN pulse (js/scan/ScanGeometry.js). // // node dev/scan.test.mjs // // Verifies the circle-circle window math (arcInCircle) against brute-force // angle sampling, plus the degenerate cases: concentric circles, full // containment, no overlap, near-tangent, and the contact/far radii the // sweep's timeline is built from. import assert from 'node:assert'; import { arcInCircle, contactRadius, farRadius } from '../js/scan/ScanGeometry.js'; let passed = 0; const check = (label, cond) => { assert.ok(cond, 'FAIL: ' + label); passed++; console.log(` ok - ${label}`); }; /** Brute force: the fraction of circle `a`'s circumference that lies inside * circle `b` (`a` centered at the origin, `b` centered at (d, 0)). */ const fracInside = (a, b, d, N = 7200) => { let hits = 0; for (let i = 0; i < N; i++) { const th = (i / N) * Math.PI * 2; const px = a * Math.cos(th); const py = a * Math.sin(th); if (Math.hypot(px - d, py) <= b + 1e-9) hits++; } return hits / N; }; // 1) Partial overlap — the predicted window fraction must match the // measured fraction (the window is centered on a-center → b-center). { const a = 100, b = 60, d = 100; const half = arcInCircle(a, b, d); const measured = fracInside(a, b, d); const predicted = (2 * half) / (2 * Math.PI); check('partial overlap: half-angle in (0, pi)', half > 0 && half < Math.PI); check(`window fraction ${predicted.toFixed(3)} ~ sampled ${measured.toFixed(3)}`, Math.abs(predicted - measured) < 0.02); } // 2) The window's BOUNDARY points sit exactly on circle b's rim. { const a = 100, b = 60, d = 100; const half = arcInCircle(a, b, d); for (const sgn of [1, -1]) { const px = a * Math.cos(sgn * half); const py = a * Math.sin(sgn * half); check(`boundary point (sign ${sgn}) lies on circle b's rim`, Math.abs(Math.hypot(px - d, py) - b) < 1e-6); } } // 3) Concentric: the whole of `a` inside / outside `b`. check('concentric: a < b → whole circle', arcInCircle(50, 60, 0) === Math.PI); check('concentric: a > b → none', arcInCircle(70, 60, 0) === -1); check('concentric: a = b → whole circle', arcInCircle(60, 60, 0) === Math.PI); // 4) Full containment (offset): d + a ≤ b → whole circle. check('offset containment (d + a < b) → whole circle', arcInCircle(20, 60, 35) === Math.PI); check('just contained (d + a = b) → whole circle', arcInCircle(20, 60, 40) === Math.PI); // 5) No overlap. check('far apart → none', arcInCircle(100, 60, 300) === -1); check('b inside a hole (a > b, close) → none', arcInCircle(120, 60, 20) === -1); check('zero radius → none', arcInCircle(0, 60, 10) === -1); // 6) Near-tangent: the window shrinks to ~0 as the circles nearly miss. { const a = 100, b = 60; const half = arcInCircle(a, b, a + b - 0.001); check('near-tangent (inside) → tiny window', half > 0 && half < 0.05); const half2 = arcInCircle(a, b, a + b + 0.001); check('near-tangent (outside) → none', half2 === -1); } // 7) The contact radius — where the front first touches the rim. check('ship outside: contact = d - R', contactRadius(60, 100) === 40); check('ship inside: contact = R - d', contactRadius(60, 20) === 40); check('concentric: contact = R', contactRadius(60, 0) === 60); check('ship on the rim: contact = 0', contactRadius(60, 60) === 0); // 8) The far radius — the last point absorbed. check('far rim = R + d', farRadius(60, 100) === 160); check('far rim (concentric) = R', farRadius(60, 0) === 60); console.log(`\nscan-geometry: ${passed} checks passed`);