fix: preserve stepped PDF dimensions

Issue: #484
User-Visible: yes
This commit is contained in:
Matysh
2026-09-07 21:05:57 +03:00
parent 66f7bdd0aa
commit e88ef93c3c
55 changed files with 775 additions and 480 deletions
+17 -1
View File
@@ -394,12 +394,28 @@ test('doorway spill golden exposes the opaque-fill failure mode from issue 71',
assert.equal(scenario.warmPixelRegion?.x >= 0.5, true);
});
test('stepped exterior PDF golden pins the complete reconstructable dimension chain', () => {
const scenario = GOLDEN_SCENARIOS.find((item) =>
item.id === 'pdf-export-stepped-dimensions-light');
assert.ok(scenario);
assert.equal(scenario.pdfExport, true);
assert.equal(scenario.pdfSteppedExterior, true);
assert.deepEqual(scenario.pdfSemantic.requiredDimensionLabels,
['1.20 m', '2.40 m', '3.60 m', '3.75 m', '7.35 m', '9.75 m']);
assert.equal(scenario.pdfSemantic.maxDimensionAxisErrorDeg, 0.5);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(space.rooms[0].poly.length, 8);
assert.equal(space.wall_segments.length, 8);
assert.equal(space.wall_segments.every((wall) => wall.cm === 15), true);
});
test('sun-ray golden requires browser-painted light from a state-only sun entity', () => {
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'lighting-sun-window-state-only-dark');
assert.ok(scenario);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(GOLDEN_MATRIX_VERSION, 58);
assert.equal(GOLDEN_MATRIX_VERSION, 59);
assert.equal(space.settings.sun_rays, true);
assert.equal(scenario.northDeg, 90,
'the sign-sensitive golden must keep a non-zero north direction');
+32
View File
@@ -74,6 +74,38 @@ test('allowed extension start is symmetric on every outer and hole boundary', ()
}
});
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], combinedSolid, 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);
+38
View File
@@ -187,6 +187,44 @@ test('dimensions switch removes the external chain from physical walls', () => {
assert.deepEqual(texts(false), [], 'the option removes every dimension value');
});
test('stepped exterior keeps the complete reconstructable dimension chain', () => {
const step = [
[0.1, 0.1], [0.7, 0.1], [0.7, 0.3], [0.8, 0.3],
[0.8, 0.6], [0.7, 0.6], [0.7, 0.9], [0.1, 0.9],
];
const externalLabels = (poly) => {
const raw = polygonRaw('stepped', poly, { cellCm: 5, wallCm: 15, name: '' });
const output = buildRawPage(raw,
{ dimensions: true, roomNames: false, decor: false, backdrop: false });
const architecture = output.commands.find((command) => command.kind === 'path' && command.fill);
assert.ok(architecture, 'the stepped physical wall body reaches the PDF scene');
const box = commandBox(architecture);
const inPlanField = (command) => command.x >= output.planField.minX
&& command.x <= output.planField.maxX && command.y >= output.planField.minY
&& command.y <= output.planField.maxY;
const outsideWallBox = (command) => command.x < box.minX || command.x > box.maxX
|| command.y < box.minY || command.y > box.maxY;
return output.commands.filter((command) => command.kind === 'text'
&& /^\d+(?:[.,]\d+)?\s*(?:m|cm)$/.test(command.text)
&& inPlanField(command) && outsideWallBox(command));
};
for (const poly of [step, [...step].reverse()]) {
const labels = externalLabels(poly);
const values = labels.map((command) => command.text);
for (const required of ['1.20 m', '2.40 m', '3.60 m', '3.75 m']) {
assert.ok(values.includes(required),
`${required} remains in the reconstructable protrusion chain`);
}
assert.equal(values.filter((value) => value === '1.20 m').length, 1,
'the equal upper/lower protrusion depths are locally deduplicated');
assert.equal(values.length, 6,
'the four protrusion dimensions coexist with the overall width and height');
assert.ok(labels.every((command) => Math.abs(command.angle % 90) < 1e-8),
'the exterior chain contains no diagonal labels');
}
});
test('backdrop is below physical architecture and disappears with its option', () => {
const raster = {
id: 'backdrop', bytes: new Uint8Array([0xff, 0xd8, 0xff, 0xd9]),