mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -1,9 +1,11 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
PDF_SCALE_SERIES, choosePdfScale, compactRing, dimensionEdges, outsideNormal, readableAngle,
|
||||
stableDimensionEdges,
|
||||
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]]),
|
||||
@@ -11,6 +13,99 @@ test('dimension contour compacts collinear vertices but retains every turn', ()
|
||||
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.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);
|
||||
@@ -34,7 +129,54 @@ test('numbered callout edge order is clockwise and stable across ring rotation',
|
||||
.map((edge) => [edge.a, edge.b]));
|
||||
});
|
||||
|
||||
test('external dimension normal points outside the room contour', () => {
|
||||
assert.deepEqual(outsideNormal([0, 0], [10, 0], [5, 5]), [0, -1]);
|
||||
assert.deepEqual(outsideNormal([10, 10], [0, 10], [5, 5]), [0, 1]);
|
||||
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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user