mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
fix: preserve wall ownership through compaction
Issue: #299 User-Visible: yes
This commit is contained in:
@@ -13,6 +13,7 @@ import { GRID_PITCH, GRID_STEP_N as S, NORM_W } from '../test-build/space-geomet
|
||||
import {
|
||||
wallBodiesGeometry, wallIntervals, wallKey,
|
||||
} from '../test-build/wall-thickness.js';
|
||||
import { checkMixedRoleRecords, checkWallKeys } from '../scripts/model-invariants.mjs';
|
||||
|
||||
const room = (id, x0, x1, openTo) => ({
|
||||
id,
|
||||
@@ -36,6 +37,10 @@ const wallKeyRoundtripFixture = JSON.parse(readFileSync(
|
||||
new URL('./fixtures/258-wall-key-roundtrip.json', import.meta.url),
|
||||
'utf8',
|
||||
));
|
||||
const realFirstFloorFixture = JSON.parse(readFileSync(
|
||||
new URL('./fixtures/real-plan-first-floor.json', import.meta.url),
|
||||
'utf8',
|
||||
));
|
||||
|
||||
const assertNoPersistedChanges = (result) => {
|
||||
assert.equal(result.changed, false);
|
||||
@@ -235,15 +240,20 @@ test('issue 273 Optimize collapses the beta.5 island beside one T-node', () => {
|
||||
assert.deepEqual(config, before, 'preview must not mutate the T-node source');
|
||||
assert.equal(first.changed, true);
|
||||
assert.equal(first.report.canonicalized, 1);
|
||||
assert.equal(first.report.wallsMerged, 2);
|
||||
assert.equal(first.report.wallsMerged, 0,
|
||||
'role breakpoints may keep the record count even after the micro island is repaired');
|
||||
const canonicalBefore = canonicalizeConfigGeometry(before);
|
||||
assert.deepEqual(first.config.spaces[0].rooms, canonicalBefore.spaces[0].rooms,
|
||||
'T coordinate and perpendicular incident room stay byte-equivalent');
|
||||
assert.equal(first.config.spaces[0].walls.length, 1);
|
||||
assert.equal(first.config.spaces[0].walls[0].cm, 22);
|
||||
assert.equal(first.config.spaces[0].walls.length, 3);
|
||||
assert.ok(first.config.spaces[0].walls.every((wall) => wall.cm === 22),
|
||||
'the micro thickness is repaired without merging outer/shared owner roles');
|
||||
const exactNode = 83 / 240;
|
||||
assert.deepEqual(first.config.spaces[0].walls[0].a, [0.8, exactNode]);
|
||||
assert.deepEqual(first.config.spaces[0].walls[0].b, [0.95, exactNode]);
|
||||
const roleBreaks = first.config.spaces[0].walls
|
||||
.flatMap((wall) => [wall.a?.[0], wall.b?.[0]])
|
||||
.filter(Number.isFinite);
|
||||
assert.ok(roleBreaks.includes(203 / 240), 'outer→shared owner boundary stays exact');
|
||||
assert.ok(roleBreaks.includes(213 / 240), 'shared→outer owner boundary stays exact');
|
||||
|
||||
const afterIntervals = wallIntervals(
|
||||
first.config.spaces[0].rooms, first.config.spaces[0].walls, [], S, 5, S,
|
||||
@@ -264,6 +274,33 @@ test('issue 273 Optimize collapses the beta.5 island beside one T-node', () => {
|
||||
assert.deepEqual(second.config, first.config);
|
||||
});
|
||||
|
||||
test('#299 Optimize keeps the real first-floor shared/outer role breakpoint', () => {
|
||||
const config = {
|
||||
model_version: PLAN_MODEL_VERSION,
|
||||
spaces: [structuredClone(realFirstFloorFixture.space)],
|
||||
markers: [], settings: {},
|
||||
};
|
||||
const before = structuredClone(config);
|
||||
const first = optimizePlans(config, {});
|
||||
assert.deepEqual(config, before, 'Optimize preview must not mutate the real fixture');
|
||||
assert.equal(first.changed, true);
|
||||
assert.deepEqual(checkMixedRoleRecords(first.config), []);
|
||||
assert.deepEqual(checkWallKeys(first.config, { notes: [] }), []);
|
||||
|
||||
const y = 83 / 240;
|
||||
const line = first.config.spaces[0].walls.filter((wall) => wall.a && wall.b
|
||||
&& Math.abs(wall.a[1] - y) < 1e-9 && Math.abs(wall.b[1] - y) < 1e-9);
|
||||
assert.equal(line.length, 2, 'micro cleanup keeps one exact record per owner role');
|
||||
assert.ok(line.every((wall) => wall.cm === 22));
|
||||
assert.ok(line.every((wall) => wall.a[0] === 213 / 240 || wall.b[0] === 213 / 240),
|
||||
'both runs stop at the exact shared/outer boundary');
|
||||
|
||||
const second = optimizePlans(first.config, first.layout);
|
||||
assertNoPersistedChanges(second);
|
||||
assert.deepEqual(second.config, first.config);
|
||||
assert.deepEqual(second.layout, first.layout);
|
||||
});
|
||||
|
||||
test('Optimize canonicalizes the six-room ULP source without claiming a visible move', () => {
|
||||
const config = {
|
||||
model_version: PLAN_MODEL_VERSION,
|
||||
|
||||
@@ -30,7 +30,7 @@ import { polygonArea, paperRoomShapes, splitRoomPath, sharedBoundary } from '../
|
||||
import { resolveOpenCuts } from '../test-build/open-spans.js';
|
||||
import { GRID_PITCH, NORM_W } from '../test-build/space-geometry.js';
|
||||
import { geometryArea } from '../test-build/physical-geometry.js';
|
||||
import { checkWallRecordsPreserved } from '../scripts/model-invariants.mjs';
|
||||
import { checkMixedRoleRecords, checkWallRecordsPreserved } from '../scripts/model-invariants.mjs';
|
||||
import { difference, intersection, union } from 'polyclip-ts';
|
||||
|
||||
const closeTo = (got, want, tol = 1e-6) =>
|
||||
@@ -918,18 +918,57 @@ test('a compacted exact wall covers a shorter collinear side in another room', (
|
||||
assert.ok(right && right.half > 0, 'hover/body profile must use the real inner face');
|
||||
});
|
||||
|
||||
test('equal solid atomic pieces compact back to one whole-wall key', () => {
|
||||
test('#299 equal thickness does not compact across a shared-to-outer role boundary', () => {
|
||||
const rooms = partialRooms();
|
||||
const walls = [
|
||||
{ key: wallKey([5, 0], [5, 4], pitch), cm: 30 },
|
||||
{ key: wallKey([5, 4], [5, 10], pitch), cm: 30 },
|
||||
];
|
||||
const next = normalizeWallIntervals(rooms, walls, [], pitch, cellCm, GRID_PITCH);
|
||||
assert.equal(next.length, 2);
|
||||
assert.ok(next.some((wall) => wall.key === wallKey([5, 0], [5, 4], pitch)));
|
||||
assert.ok(next.some((wall) => wall.key === wallKey([5, 4], [5, 10], pitch)));
|
||||
assert.ok(next.every((wall) => wall.cm === 30));
|
||||
assert.deepEqual(checkMixedRoleRecords({ spaces: [{ id: 'roles', rooms, walls: next }] }), []);
|
||||
|
||||
const reversed = normalizeWallIntervals(
|
||||
[...rooms].reverse(), [...walls].reverse(), [], pitch, cellCm, GRID_PITCH,
|
||||
);
|
||||
assert.deepEqual(
|
||||
reversed.map((wall) => wall.key).sort(),
|
||||
next.map((wall) => wall.key).sort(),
|
||||
'room/input order must not change the physical role breakpoint',
|
||||
);
|
||||
});
|
||||
|
||||
test('#299 the shared owner pair is part of the compaction role', () => {
|
||||
const rooms = [
|
||||
{ id: 'a', poly: [[0, 0], [5, 0], [5, 10], [0, 10]] },
|
||||
{ id: 'b', poly: [[5, 0], [10, 0], [10, 5], [5, 5]] },
|
||||
{ id: 'c', poly: [[5, 5], [10, 5], [10, 10], [5, 10]] },
|
||||
];
|
||||
const walls = [
|
||||
{ key: wallKey([5, 0], [5, 5], pitch), cm: 22 },
|
||||
{ key: wallKey([5, 5], [5, 10], pitch), cm: 22 },
|
||||
];
|
||||
const next = normalizeWallIntervals(rooms, walls, [], pitch, cellCm, GRID_PITCH);
|
||||
const vertical = next.filter((wall) => wall.a?.[0] === 5 && wall.b?.[0] === 5);
|
||||
assert.equal(vertical.length, 2);
|
||||
assert.ok(vertical.some((wall) => wall.key === wallKey([5, 0], [5, 5], pitch)));
|
||||
assert.ok(vertical.some((wall) => wall.key === wallKey([5, 5], [5, 10], pitch)));
|
||||
assert.deepEqual(checkMixedRoleRecords({ spaces: [{ id: 'pairs', rooms, walls: next }] }), []);
|
||||
});
|
||||
|
||||
test('#299 equal neighbouring outer atoms of one room still compact', () => {
|
||||
const rooms = [{ id: 'a', poly: [[0, 0], [5, 0], [5, 10], [0, 10]] }];
|
||||
const walls = [
|
||||
{ key: wallKey([5, 0], [5, 4], pitch), a: [5, 0], b: [5, 4], cm: 30 },
|
||||
{ key: wallKey([5, 4], [5, 10], pitch), a: [5, 4], b: [5, 10], cm: 30 },
|
||||
];
|
||||
const next = normalizeWallIntervals(rooms, walls, [], pitch, cellCm, GRID_PITCH);
|
||||
assert.equal(next.length, 1);
|
||||
assert.equal(next[0].key, wallKey([5, 0], [5, 10], pitch));
|
||||
assert.equal(next[0].cm, 30);
|
||||
assert.deepEqual(next[0].a, [5, 0]);
|
||||
assert.deepEqual(next[0].b, [5, 10]);
|
||||
});
|
||||
|
||||
test('different solid thicknesses remain separate atomic keys', () => {
|
||||
|
||||
Reference in New Issue
Block a user