Files
houseplan-card/test/pdf-collision.test.mjs
T
2026-09-07 21:07:03 +03:00

151 lines
7.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
pdfBoxInsideRing, pdfBoxTouchesGeometry, pdfInflateBox, pdfSegmentTouchesBox,
pdfSegmentTouchesGeometry,
} from '../test-build/pdf/pdf-collision.js';
const square = [[0, 0], [10, 0], [10, 10], [0, 10]];
const pointInRing = ([x, y], ring) => {
let inside = false;
for (let index = 0, previous = ring.length - 1; index < ring.length; previous = index++) {
const [xi, yi] = ring[index], [xj, yj] = ring[previous];
if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
}
return inside;
};
const squareSolid = (point) => pointInRing(point, square);
test('collision boxes expand by the requested paper clearance', () => {
assert.deepEqual(pdfInflateBox({ minX: 2, minY: 3, maxX: 7, maxY: 11 }, 1),
{ minX: 1, minY: 2, maxX: 8, maxY: 12 });
assert.deepEqual(pdfInflateBox({ minX: 2, minY: 3, maxX: 7, maxY: 11 }, -1),
{ minX: 2, minY: 3, maxX: 7, maxY: 11 }, 'invalid clearance fails closed to zero growth');
});
test('box collision catches a thin diagonal between sparse sample points', () => {
const diagonal = [[-1, 2], [11, 8], [11, 8.1], [-1, 2.1]];
const inside = ([x, y]) => {
let hit = false;
for (let index = 0, previous = diagonal.length - 1;
index < diagonal.length; previous = index++) {
const [xi, yi] = diagonal[index], [xj, yj] = diagonal[previous];
if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) hit = !hit;
}
return hit;
};
assert.equal(pdfBoxTouchesGeometry(
{ minX: 0, minY: 0, maxX: 10, maxY: 10 }, [diagonal], inside,
), true);
});
test('allowed extension start is symmetric on every outer and hole boundary', () => {
const sides = [
{ start: [0, 5], outward: [-2, 5], inward: [2, 5] },
{ start: [10, 5], outward: [12, 5], inward: [8, 5] },
{ start: [5, 0], outward: [5, -2], inward: [5, 2] },
{ start: [5, 10], outward: [5, 12], inward: [5, 8] },
];
for (const ring of [square, [...square].reverse()]) for (const side of sides) {
assert.equal(pdfSegmentTouchesGeometry(
side.start, side.outward, [ring], squareSolid, { allowStartBoundary: true },
), false, 'a dimension extension can leave any winding/face');
assert.equal(pdfSegmentTouchesGeometry(
side.start, side.inward, [ring], squareSolid, { allowStartBoundary: true },
), true, 'the same extension cannot enter masonry');
}
const outer = [[0, 0], [20, 0], [20, 20], [0, 20]];
const hole = [[5, 5], [5, 15], [15, 15], [15, 5]];
const donutSolid = (point) => pointInRing(point, outer) && !pointInRing(point, hole);
const holeSides = [
{ start: [5, 10], hole: [7, 10], wall: [3, 10] },
{ start: [15, 10], hole: [13, 10], wall: [17, 10] },
{ start: [10, 5], hole: [10, 7], wall: [10, 3] },
{ start: [10, 15], hole: [10, 13], wall: [10, 17] },
];
for (const side of holeSides) {
assert.equal(pdfSegmentTouchesGeometry(
side.start, side.hole, [outer, hole], donutSolid, { allowStartBoundary: true },
), false, 'an allowed start can leave a wall face into its opening');
assert.equal(pdfSegmentTouchesGeometry(
side.start, side.wall, [outer, hole], donutSolid, { allowStartBoundary: true },
), true, 'an extension cannot travel from an opening face into masonry');
}
});
test('external dimension extension may leave its own stepped corner but never re-enter', () => {
const step = [[0, 0], [10, 0], [10, 3], [12, 3], [12, 6], [10, 6], [10, 10], [0, 10]];
const stepSolid = (point) => pointInRing(point, step);
const ownExit = { allowStartExit: true };
assert.equal(pdfSegmentTouchesGeometry(
[10, 3], [14, 3], [step], stepSolid, ownExit,
), false, 'the source prefix may follow the incident facade step before reaching free space');
assert.equal(pdfSegmentTouchesGeometry(
[10, 3], [14, 3], [step], stepSolid, { allowStartBoundary: true },
), true, 'the ordinary strict start-boundary mode still rejects collinear overlap');
assert.equal(pdfSegmentTouchesGeometry(
[0, 5], [12, 5], [square], squareSolid, ownExit,
), false, 'the source prefix may cross its contiguous wall body once before exiting');
assert.equal(pdfSegmentTouchesGeometry(
[0, 5], [10, 5], [square], squareSolid, ownExit,
), true, 'a segment ending on the exit boundary never reaches a free interval');
const foreignWall = [[13, 2], [15, 2], [15, 4], [13, 4]];
const combinedSolid = (point) => stepSolid(point) || pointInRing(point, foreignWall);
assert.equal(pdfSegmentTouchesGeometry(
[10, 3], [16, 3], [step, foreignWall], combinedSolid, ownExit,
), true, 'a second wall after the first free interval remains a collision');
const tangentWall = [[13, 3], [15, 4], [15, 5], [13, 4]];
assert.equal(pdfSegmentTouchesGeometry(
[10, 3], [16, 3], [step, tangentWall], stepSolid, ownExit,
), true, 'a later point contact remains a collision even without a solid interval');
assert.equal(pdfSegmentTouchesGeometry(
[11, 4], [14, 4], [step], stepSolid, ownExit,
), true, 'only a segment starting at its source boundary receives the exception');
});
test('degenerate and invalid collision inputs fail deterministically', () => {
const box = { minX: 0, minY: 0, maxX: 10, maxY: 10 };
assert.equal(pdfSegmentTouchesBox([20, 20], [20, 20], box), false);
assert.equal(pdfSegmentTouchesBox([5, 5], [5, 5], box), true);
assert.equal(pdfSegmentTouchesBox([0, 5], [0, 5], box), true);
assert.equal(pdfSegmentTouchesGeometry([20, 20], [20, 20], [square], squareSolid), false);
assert.equal(pdfSegmentTouchesGeometry([5, 5], [5, 5], [square], squareSolid), true);
assert.equal(pdfSegmentTouchesGeometry([Number.NaN, 0], [1, 1], [square], squareSolid), true);
assert.equal(pdfBoxTouchesGeometry(box,
[[[0, 0], [Number.POSITIVE_INFINITY, 0], [1, 1]]], () => false), true);
assert.equal(pdfSegmentTouchesBox([20, 20], [21, 21],
{ minX: 10, minY: 0, maxX: 0, maxY: 10 }), true);
const shortOverlap = [[0, 0], [1, 0], [1, -1], [0, -1]];
assert.equal(pdfSegmentTouchesGeometry(
[0, 0], [1e9, 0], [shortOverlap], () => false, { allowStartBoundary: true },
), true, 'every positive collinear overlap remains a collision, independent of query length');
});
test('segment intersections remain stable across coordinate scales', () => {
for (const scale of [1e-6, 1, 1e6]) {
const box = { minX: 0, minY: 0, maxX: 10 * scale, maxY: 10 * scale };
assert.equal(pdfSegmentTouchesBox(
[-2 * scale, 5 * scale], [12 * scale, 5 * scale], box,
), true);
assert.equal(pdfSegmentTouchesBox(
[-2 * scale, 12 * scale], [12 * scale, 12 * scale], box,
), false);
}
});
test('inside-ring check rejects a box cut by a concave boundary', () => {
const concave = [[0, 0], [10, 0], [10, 10], [6, 10], [6, 4], [4, 4], [4, 10], [0, 10]];
const inside = ([x, y]) => x > 0 && x < 10 && y > 0 && y < 10
&& !(x > 4 && x < 6 && y > 4);
assert.equal(pdfBoxInsideRing(
{ minX: 3, minY: 3, maxX: 7, maxY: 5 }, concave, inside,
), false, 'all four corners alone are insufficient for a concave room');
assert.equal(pdfBoxInsideRing(
{ minX: 1, minY: 1, maxX: 3, maxY: 3 }, concave, inside,
), true);
});