mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
151 lines
7.1 KiB
JavaScript
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);
|
|
});
|