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