Files
houseplan-card/test/resize-optimize.test.mjs
T
2026-08-24 10:43:33 +03:00

158 lines
6.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { existsSync } from 'node:fs';
import { optimizePlans } from '../test-build/plan-optimizer.js';
import {
applySafeResize, clampSafeResize, resolveSafeResize,
} from '../test-build/resize.js';
import { GRID_PITCH, NORM_W } from '../test-build/space-geometry.js';
import { thicknessCmAt, wallCmToUnits } from '../test-build/wall-thickness.js';
const source = JSON.parse(readFileSync(
new URL('./fixtures/281-resize-outer-partitions.json', import.meta.url), 'utf8',
));
const expected = JSON.parse(readFileSync(
new URL('./fixtures/281-resize-outer-candidate.json', import.meta.url), 'utf8',
));
const clone = (value) => structuredClone(value);
const resizeInputs = (space) => ({
rooms: space.rooms.map((room) => ({
id: room.id,
poly: room.poly.map(([x, y]) => [x * NORM_W, y * NORM_W]),
})),
openings: (space.openings || []).map((opening) => ({
id: opening.id,
x: opening.x * NORM_W,
y: opening.y * NORM_W,
length: opening.length * NORM_W,
angle: opening.angle,
hosted: !!opening.host,
})),
obstacles: (space.partitions || []).map((partition) => ({
kind: 'segment',
a: partition.a.map((value) => value * NORM_W),
b: partition.b.map((value) => value * NORM_W),
half: wallCmToUnits(partition.cm, space.cell_cm, GRID_PITCH) / 2,
})),
});
const options = (obstacles) => ({
minDim: wallCmToUnits(30, 5, GRID_PITCH),
eps: GRID_PITCH * 0.05,
step: GRID_PITCH,
movingHalf: wallCmToUnits(20, 5, GRID_PITCH) / 2,
obstacles,
});
test('issue 281 Optimize removes exact outer duplicates and rehosts their windows', () => {
const input = clone(source);
const before = clone(input);
const result = optimizePlans(input, {});
assert.deepEqual(input, before, 'Optimize preview must stay immutable');
assert.equal(result.report.partitionsReconciled, 3);
assert.equal(result.report.openingsRehosted, 2);
assert.deepEqual(result.config, expected);
assert.ok(result.config.spaces[0].openings.every((opening) => !opening.host));
const second = optimizePlans(result.config, result.layout);
assert.equal(second.changed, false);
assert.deepEqual(second.config, result.config);
});
test('issue 281 disables the old zero-range handle and enables it after Optimize', () => {
const before = resizeInputs(source.spaces[0]);
const blocked = resolveSafeResize(
before.rooms, before.openings, 'left', 1, options(before.obstacles),
);
assert.deepEqual(blocked, { enabled: false, reason: 'duplicate-physical-wall' });
const optimized = optimizePlans(clone(source), {}).config.spaces[0];
const after = resizeInputs(optimized);
const opts = options(after.obstacles);
const resolution = resolveSafeResize(after.rooms, after.openings, 'left', 1, opts);
assert.equal(resolution.enabled, true);
const plan = resolution.plan;
assert.equal(clampSafeResize(
after.rooms, after.openings, plan, GRID_PITCH, GRID_PITCH, opts,
), GRID_PITCH);
assert.equal(clampSafeResize(
after.rooms, after.openings, plan, -GRID_PITCH, GRID_PITCH, opts,
), -GRID_PITCH);
const moved = applySafeResize(after.rooms, after.openings, plan, GRID_PITCH);
assert.deepEqual(Object.keys(moved.polys).sort(), ['left', 'right']);
assert.ok(Object.values(moved.polys).every((poly) => poly.length === 4));
});
test('issue 281 leaves unsafe outer duplicates as hard stops', () => {
const variants = [];
const partial = clone(source);
partial.spaces[0].partitions[0].b = [0.4, 0];
variants.push(partial);
const unknown = clone(source);
unknown.spaces[0].partitions[0].future_semantics = true;
variants.push(unknown);
const overlap = clone(source);
overlap.spaces[0].openings.push({
id: 'ordinary-overlap', type: 'window',
x: 0.25, y: 0, angle: 0, length: 0.2,
});
variants.push(overlap);
for (const input of variants) {
const result = optimizePlans(input, {});
assert.ok(
result.config.spaces[0].partitions?.some((partition) => partition.id === 'top-left'),
'unsafe outer partition must remain explicit',
);
}
});
test('issue 281 private exact fixture has no enabled zero-range handle', (t) => {
const path = 'C:\\Temp\\44.json';
if (!existsSync(path)) return t.skip('private #281 fixture is not present');
const raw = JSON.parse(readFileSync(path, 'utf8'));
const previous = raw.payload?.config || raw.config || raw;
const optimized = optimizePlans(previous, {}).config.spaces[0];
const inputs = resizeInputs(optimized);
const openings = inputs.openings;
const obstacles = inputs.obstacles;
let enabled = 0;
let zeroRange = 0;
let target = null;
for (const room of inputs.rooms) {
for (let edge = 0; edge < room.poly.length; edge++) {
const a = room.poly[edge], b = room.poly[(edge + 1) % room.poly.length];
const cm = thicknessCmAt(optimized.walls, a, b, 1 / 240, NORM_W) || 15;
const opts = {
minDim: wallCmToUnits(30, optimized.cell_cm, GRID_PITCH),
eps: GRID_PITCH * 0.05,
step: GRID_PITCH,
movingHalf: wallCmToUnits(cm, optimized.cell_cm, GRID_PITCH) / 2,
obstacles,
};
const resolution = resolveSafeResize(inputs.rooms, openings, room.id, edge, opts);
if (!resolution.enabled) continue;
enabled++;
const negative = clampSafeResize(
inputs.rooms, openings, resolution.plan, -GRID_PITCH, GRID_PITCH, opts,
);
const positive = clampSafeResize(
inputs.rooms, openings, resolution.plan, GRID_PITCH, GRID_PITCH, opts,
);
if (negative === 0 && positive === 0) zeroRange++;
const configA = a.map((value) => value / NORM_W);
const configB = b.map((value) => value / NORM_W);
if ([configA, configB].every((point) =>
Math.abs(point[0] - 0.4) < 1e-8
&& point[1] >= -0.208333334 && point[1] <= 1.266666668)) {
target = { negative, positive };
}
}
}
assert.equal(zeroRange, 0, `${zeroRange}/${enabled} enabled handles cannot move`);
assert.ok(target && target.negative < 0 && target.positive > 0,
'the reported shared wall must move at least one step both ways');
});