90 lines
3.5 KiB
JavaScript
90 lines
3.5 KiB
JavaScript
// 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`);
|