diff --git a/test/plan-optimizer.test.mjs b/test/plan-optimizer.test.mjs index 8ae88d16..893c7c9e 100644 --- a/test/plan-optimizer.test.mjs +++ b/test/plan-optimizer.test.mjs @@ -124,6 +124,41 @@ test('micro-interval cleanup preserves ambiguous and topological boundaries', () ); }); +test('micro-interval cleanup is endpoint, input-order and room-order invariant without mutation', () => { + const fixture = microIntervalFixture(); + const detached = { + id: 'r2', poly: [[0.85, 0.85], [0.95, 0.85], [0.95, 0.95], [0.85, 0.95]], + }; + const semantic = (walls) => walls.map((wall) => { + const ends = [wall.a, wall.b] + .map((point) => point.map((value) => Number(value.toFixed(12))).join(',')) + .sort(); + return `${ends[0]}|${ends[1]}|${wall.cm}`; + }).sort(); + + const baseline = collapseIsolatedWallThicknessIslands( + [fixture.rooms[0], detached], fixture.walls, [], S, 5, S, + ); + const reversedMiddle = { + ...fixture.walls[1], + a: [...fixture.walls[1].b], + b: [...fixture.walls[1].a], + }; + const rooms = [detached, { + ...fixture.rooms[0], poly: [...fixture.rooms[0].poly].reverse().map((point) => [...point]), + }]; + const walls = [fixture.walls[2], reversedMiddle, fixture.walls[0]] + .map((wall) => ({ ...wall, a: [...wall.a], b: [...wall.b] })); + const beforeRooms = structuredClone(rooms); + const beforeWalls = structuredClone(walls); + + const result = collapseIsolatedWallThicknessIslands(rooms, walls, [], S, 5, S); + assert.deepEqual(rooms, beforeRooms, 'helper must not mutate rooms or their winding'); + assert.deepEqual(walls, beforeWalls, 'helper must not mutate the walls array or entries'); + assert.deepEqual(semantic(result), semantic(baseline)); + assert.equal(result.find((wall) => wall.a?.[0] === 0.5 || wall.b?.[0] === 0.5)?.cm, 22); +}); + test('optimizePlans migrates, aligns and canonicalises idempotently', () => { const config = { spaces: [{ diff --git a/test/wall-thickness.test.mjs b/test/wall-thickness.test.mjs index 129ee5c9..bfadc737 100644 --- a/test/wall-thickness.test.mjs +++ b/test/wall-thickness.test.mjs @@ -365,6 +365,27 @@ test('different solid thicknesses remain separate atomic keys', () => { assert.ok(!next.some((w) => w.key === wallKey([5, 0], [5, 10], pitch))); }); +test('lossless wall helpers preserve an isolated sub-half-step thickness island outside Optimize', () => { + const y = 0.2, split = 0.5, length = pitch / 3; + const rooms = [{ id: 'r1', poly: [[0.2, y], [0.8, y], [0.8, 0.8], [0.2, 0.8]] }]; + const walls = [ + { key: wallKey([0.2, y], [split, y], pitch), a: [0.2, y], b: [split, y], cm: 22 }, + { key: wallKey([split, y], [split + length, y], pitch), + a: [split, y], b: [split + length, y], cm: 15 }, + { key: wallKey([split + length, y], [0.8, y], pitch), + a: [split + length, y], b: [0.8, y], cm: 22 }, + ]; + const before = structuredClone(walls); + const byKey = (entries) => structuredClone(entries) + .sort((left, right) => left.key.localeCompare(right.key)); + + const normalized = normalizeWallIntervals(rooms, walls, [], pitch, cellCm, GRID_PITCH); + const degraded = degradeWalls(walls, rooms, pitch); + assert.deepEqual(byKey(normalized), byKey(before), 'runtime/editor normalization remains lossless'); + assert.deepEqual(byKey(degraded), byKey(before), 'runtime/editor degradation must not infer island removal'); + assert.deepEqual(walls, before, 'direct lossless helpers must not mutate persisted input'); +}); + test('closing the sole geometric split preserves different thicknesses', () => { const rooms = [ { id: 'a', poly: [[0, 0], [5, 0], [5, 10], [0, 10]] },