mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
163 lines
6.3 KiB
JavaScript
163 lines
6.3 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?.kind === 'wall'));
|
|
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 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 296 removes a shorter outer duplicate which is entirely hidden', () => {
|
|
const partial = clone(source);
|
|
partial.spaces[0].partitions[0].b = [0.4, 0];
|
|
const result = optimizePlans(partial, {});
|
|
assert.equal(result.config.spaces[0].partitions
|
|
?.some((partition) => partition.id === 'top-left') ?? false, false);
|
|
});
|
|
|
|
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');
|
|
});
|