Files
houseplan-card/test/pdf-dimensions.test.mjs
2026-09-07 11:36:23 +03:00

187 lines
9.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
PDF_SCALE_SERIES, areOppositeDimensionEdges, choosePdfScale, compactRing,
dedupeOppositeDimensionEdges, dimensionEdges, dimensionEpsilonUnits, inwardNormalForEdge,
groupCollinearDimensionEdges, projectDimensionEdge, readableAngle, stableDimensionEdges,
} from '../test-build/pdf/pdf-dimensions.js';
import { NEAR_AXIS_MAX_SLOPE } from '../test-build/near-axis.js';
test('dimension contour compacts collinear vertices but retains every turn', () => {
assert.deepEqual(compactRing([[0, 0], [2, 0], [4, 0], [4, 3], [0, 3]]),
[[0, 0], [4, 0], [4, 3], [0, 3]]);
assert.equal(dimensionEdges([[0, 0], [4, 0], [4, 3], [0, 3]], 10, false).length, 4);
});
test('1 mm physical duplicate normalization precedes fixed-point collinear cleanup', () => {
const epsilon = dimensionEpsilonUnits(2);
assert.equal(epsilon, 0.05, '1 mm is 0.1 cm divided by cm-per-render-unit');
const input = [[0, 0], [10, 0], [10.04, 0.02], [10, 10], [0, 10]];
const snapshot = structuredClone(input);
assert.deepEqual(compactRing(input, epsilon), [[0, 0], [10, 0], [10, 10], [0, 10]],
'the near duplicate is collapsed before its two adjacent turns are inspected');
assert.deepEqual(input, snapshot, 'print normalization must not mutate config geometry');
assert.deepEqual(compactRing([
[0, 0], [10, 0], [10, 10], [0, 10], [0.02, 0.01],
], epsilon), [[0, 0], [10, 0], [10, 10], [0, 10]], 'the ring seam is normalized too');
assert.deepEqual(compactRing([
[0, 0], [10, 0], [10, 0.04], [20, 0], [20, 10], [0, 10],
], epsilon), [[0, 0], [20, 0], [20, 10], [0, 10]],
'a tiny spur is collapsed before the newly exposed straight edge is compacted');
assert.equal(compactRing([
[0, 0], [5, 0], [10, 0], [10, 5], [10, 10], [0, 10], [0, 5],
], epsilon).length, 4, 'collinear cleanup reaches a fixed point');
});
test('compaction removes only forward collinear vertices and retains a real reversal', () => {
const ring = [[0, 0], [10, 0], [5, 0], [5, 10], [0, 10]];
assert.ok(compactRing(ring, 0.01).some(([x, y]) => x === 10 && y === 0),
'a positive-length U-turn is not a redundant point');
});
test('a non-finite ring fails closed instead of bridging a phantom dimension', () => {
const ring = [[0, 0], [4, 0], [Number.NaN, 2], [4, 4], [0, 4]];
const snapshot = structuredClone(ring);
assert.deepEqual(compactRing(ring, 0.01), [],
'compaction cannot represent a broken contour and deterministically rejects the whole ring');
assert.deepEqual(compactRing(ring, 0.01), [], 'repeated compaction has the same result');
assert.deepEqual(dimensionEdges(ring, 10, false), [],
'the neighbours around the corrupt vertex must not become a synthetic edge');
assert.deepEqual(stableDimensionEdges(ring, 10, false), [],
'numbered callouts use the same fail-closed normalization');
assert.deepEqual(ring, snapshot, 'rejecting corrupt print geometry must not mutate config');
assert.deepEqual(dimensionEdges([
[0, 0], [4, 0], [4, Number.POSITIVE_INFINITY], [0, 4],
], 10, false), [], 'all non-finite coordinates invalidate the ring');
});
test('dimension candidates use the canonical 0.25 degree axis boundary only', () => {
const atBoundary = [[0, 0], [100, 100 * NEAR_AXIS_MAX_SLOPE], [100, 50], [0, 50]];
const accepted = dimensionEdges(atBoundary, 1, false, { ringIndex: 9 });
const projected = accepted.find((edge) => edge.source.edgeIndex === 0);
assert.ok(projected, 'the inclusive canonical boundary is eligible');
assert.equal(projected.axis, 'horizontal');
assert.deepEqual(projected.sourceA, atBoundary[0]);
assert.deepEqual(projected.sourceB, atBoundary[1]);
assert.equal(projected.projectedLength, 100, 'length is the major projection, not the chord');
assert.equal(projected.a[1], projected.b[1], 'near-axis print geometry passes through midpoint');
const aboveBoundary = [[0, 0], [100, 100 * NEAR_AXIS_MAX_SLOPE * 1.000001],
[100, 50], [0, 50]];
assert.equal(dimensionEdges(aboveBoundary, 1, false, { ringIndex: 9 })
.some((edge) => edge.source.edgeIndex === 0), false);
assert.equal(projectDimensionEdge([0, 0], [10, 10]), null, 'true diagonals have no projection');
const diagonalCorner = [[0, 0], [10, 10], [10, 20], [0, 20]];
const diagonalEdges = dimensionEdges(diagonalCorner, 1, false);
assert.equal(diagonalEdges.some((edge) => edge.source.edgeIndex === 0), false);
assert.equal(diagonalEdges.length, 3, 'filtering a diagonal must not connect its neighbours by a chord');
});
test('dimension candidates retain stable pre-compaction source identity', () => {
const ring = [[0, 0], [5, 0], [10, 0], [10, 10], [0, 10]];
const snapshot = structuredClone(ring);
const top = dimensionEdges(ring, 1, false, { ringIndex: 7 })
.find((edge) => edge.axis === 'horizontal' && edge.normalCoordinate === 0);
assert.ok(top);
assert.deepEqual(top.source, { ringIndex: 7, edgeIndex: 0 });
assert.deepEqual(top.sourceEdgeIndices, [0, 1]);
assert.match(top.stableKey, /^horizontal\|0\|1\|7\|0$/);
assert.deepEqual(ring, snapshot);
});
test('parallel steps on different facade lines receive independent dimension lanes', () => {
const epsilon = 0.1;
const edges = dimensionEdges([
[0, 1], [2, 1], [2, 0], [4, 0], [4, 3], [0, 3],
], 10, false, { epsilon });
const upwardFacing = edges.filter((edge) => edge.axis === 'horizontal'
&& edge.inwardNormal[1] > 0);
assert.equal(upwardFacing.length, 2, 'the stepped facade exposes two parallel top edges');
assert.equal(groupCollinearDimensionEdges(upwardFacing, epsilon).length, 2,
'parallel edges at distinct normal coordinates are not coupled into one lane');
const nearlySameLine = { ...upwardFacing[0],
normalCoordinate: upwardFacing[0].normalCoordinate + epsilon / 2 };
assert.deepEqual(groupCollinearDimensionEdges([upwardFacing[0], nearlySameLine], epsilon)
.map((group) => group.length), [2], 'sub-millimetre projection noise remains one line');
});
test('30 cm threshold belongs to internal edge labels', () => {
const edges = dimensionEdges([[0, 0], [2, 0], [2, 10], [0, 10]], 10, false);
assert.equal(edges[0].short, true);
assert.equal(edges[1].short, false);
});
test('angles stay readable and scale is selected from the standard series', () => {
assert.equal(readableAngle([1, 0], [0, 0]), 0);
assert.ok(PDF_SCALE_SERIES.includes(choosePdfScale(1000, 500, 273, 160)));
assert.ok(choosePdfScale(100000, 100000, 100, 100) > 500);
});
test('numbered callout edge order is clockwise and stable across ring rotation', () => {
const ring = [[4, 0], [4, 3], [0, 3], [0, 0]];
const rotated = [[0, 3], [0, 0], [4, 0], [4, 3]];
const signature = (value) => stableDimensionEdges(value, 10, false)
.map((edge) => `${edge.a.join(',')}>${edge.b.join(',')}:${edge.text}`);
assert.deepEqual(signature(ring), signature(rotated));
assert.deepEqual(stableDimensionEdges([...ring].reverse(), 10, false)
.map((edge) => [edge.a, edge.b]), stableDimensionEdges(ring, 10, false)
.map((edge) => [edge.a, edge.b]));
});
test('concave inward normals are selected by local point-in-ring probes', () => {
const cShape = [[0, 0], [6, 0], [6, 2], [2, 2], [2, 4], [6, 4], [6, 6], [0, 6]];
assert.deepEqual(inwardNormalForEdge(cShape, [6, 2], [2, 2], 0.01), [0, -1]);
assert.deepEqual(inwardNormalForEdge(cShape, [2, 4], [6, 4], 0.01), [0, 1]);
});
test('opposite dimension dedupe is local, geometric and placement-aware', () => {
const rectangle = [[0, 0], [10, 0], [10, 6], [0, 6]];
const edges = dimensionEdges(rectangle, 1, false, { ringIndex: 4 });
assert.equal(edges.length, 4);
assert.equal(areOppositeDimensionEdges(edges[0], edges[2], rectangle, 0.1), true);
assert.equal(areOppositeDimensionEdges(edges[0], edges[1], rectangle, 0.1), false,
'equal-looking adjacent axes are not an opposite pair');
const defaultKept = dedupeOppositeDimensionEdges(edges, { ring: rectangle, epsilon: 0.1 });
assert.equal(defaultKept.length, 2);
assert.deepEqual(new Set(defaultKept.map((edge) => edge.axis)), new Set(['horizontal', 'vertical']),
'a square keeps one value on each axis rather than globally deduping equal text');
const shuffledKept = dedupeOppositeDimensionEdges([...edges].reverse(), {
ring: rectangle, epsilon: 0.1,
});
assert.deepEqual([...defaultKept.map((edge) => edge.stableKey)].sort(),
[...shuffledKept.map((edge) => edge.stableKey)].sort(), 'full ties use stable source keys');
const preferred = dedupeOppositeDimensionEdges(edges, {
ring: rectangle,
epsilon: 0.1,
score: (edge) => ({
hardCollisions: edge.source.edgeIndex < 2 ? 0 : 1,
normalClearance: edge.source.edgeIndex < 2 ? 5 : 100,
}),
});
assert.deepEqual(preferred.map((edge) => edge.source.edgeIndex), [0, 1],
'hard collisions outrank normal clearance');
});
test('opposite dedupe preserves equal non-opposite L/C contour spans and separate rings', () => {
const cShape = [[0, 0], [6, 0], [6, 2], [2, 2], [2, 4], [6, 4], [6, 6], [0, 6]];
const cEdges = dimensionEdges(cShape, 1, false, { ringIndex: 0 });
assert.equal(dedupeOppositeDimensionEdges(cEdges, { ring: cShape, epsilon: 0.1 }).length,
cEdges.length, 'matching spans separated by the open notch are not paired');
const square = [[0, 0], [10, 0], [10, 10], [0, 10]];
const first = dedupeOppositeDimensionEdges(
dimensionEdges(square, 1, false, { ringIndex: 1 }), { ring: square, epsilon: 0.1 },
);
const second = dedupeOppositeDimensionEdges(
dimensionEdges(square, 1, false, { ringIndex: 2 }), { ring: square, epsilon: 0.1 },
);
assert.equal([...first, ...second].length, 4, 'equal dimensions in separate local rings survive');
});