mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
test: prove coincident partition acceptance
Issue: #276 User-Visible: no
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { optimizePlans } from '../test-build/plan-optimizer.js';
|
||||
import { checkOptimizeGeometry } from '../test-build/plan-geometry-preflight.js';
|
||||
import { reconcileCoincidentPartitions } from '../test-build/coincident-partitions.js';
|
||||
import { GRID_PITCH, GRID_STEP_N as S } from '../test-build/space-geometry.js';
|
||||
import { wallIntervals } from '../test-build/wall-thickness.js';
|
||||
|
||||
@@ -68,6 +71,91 @@ test('issue 276 exact proof ignores endpoint direction and room order', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('issue 276 rehosts three non-overlapping door/window/gate openings atomically', () => {
|
||||
const input = clone(fixture);
|
||||
const space = input.spaces[0];
|
||||
space.openings = [
|
||||
{ id: 'hosted-door', type: 'door', x: 0, y: 0, angle: 0, length: 0.12,
|
||||
contact: 'binary_sensor.door', host: { kind: 'partition', id: 'redundant', t: 59 / 238 } },
|
||||
{ id: 'hosted-window', type: 'window', x: 0, y: 0, angle: 0, length: 0.12,
|
||||
cover: 'cover.window', host: { kind: 'partition', id: 'redundant', t: 0.50 } },
|
||||
{ id: 'hosted-gate', type: 'gate', x: 0, y: 0, angle: 0, length: 0.12,
|
||||
future_field: { keep: true }, host: { kind: 'partition', id: 'redundant', t: 179 / 238 } },
|
||||
];
|
||||
|
||||
const result = optimize(input);
|
||||
assert.equal(result.report.partitionsReconciled, 1);
|
||||
assert.equal(result.report.openingsRehosted, 3);
|
||||
const openings = result.config.spaces[0].openings;
|
||||
assert.deepEqual(openings.map((opening) => opening.id),
|
||||
['hosted-door', 'hosted-window', 'hosted-gate']);
|
||||
assert.deepEqual(openings.map((opening) => opening.type), ['door', 'window', 'gate']);
|
||||
assert.ok(openings.every((opening) => opening.host === undefined));
|
||||
assert.ok(openings.every((opening) => opening.x === 0.504166667));
|
||||
assert.ok(openings[0].y < openings[1].y && openings[1].y < openings[2].y);
|
||||
assert.equal(openings[0].contact, 'binary_sensor.door');
|
||||
assert.equal(openings[1].cover, 'cover.window');
|
||||
assert.deepEqual(openings[2].future_field, { keep: true });
|
||||
assert.equal(checkOptimizeGeometry(result.config).ok, true);
|
||||
|
||||
const second = optimize(result.config);
|
||||
assert.equal(second.changed, false);
|
||||
assert.equal(second.report.openingsRehosted, 0);
|
||||
assert.deepEqual(second.config, result.config);
|
||||
});
|
||||
|
||||
test('issue 276 fails closed when two hosted openings would overlap after rehost', () => {
|
||||
const input = clone(fixture);
|
||||
const space = input.spaces[0];
|
||||
space.openings = [
|
||||
{ id: 'overlap-door', type: 'door', x: 0, y: 0, angle: 0, length: 0.2,
|
||||
host: { kind: 'partition', id: 'redundant', t: 0.45 } },
|
||||
{ id: 'overlap-window', type: 'window', x: 0, y: 0, angle: 0, length: 0.2,
|
||||
host: { kind: 'partition', id: 'redundant', t: 0.55 } },
|
||||
];
|
||||
const result = optimize(input);
|
||||
assert.equal(result.report.partitionsReconciled, 0);
|
||||
assert.equal(result.report.openingsRehosted, 0);
|
||||
assert.equal(result.config.spaces[0].partitions.length, 1);
|
||||
assert.deepEqual(result.config.spaces[0].openings.map((opening) => opening.id),
|
||||
['overlap-door', 'overlap-window']);
|
||||
assert.ok(result.config.spaces[0].openings.every((opening) => opening.host?.id === 'redundant'));
|
||||
});
|
||||
|
||||
test('issue 276 reconciliation is owned by explicit Optimize and called once per valid space', () => {
|
||||
const input = clone(fixture);
|
||||
const secondSpace = clone(input.spaces[0]);
|
||||
secondSpace.id = 'offset-shared-wall-second';
|
||||
input.spaces.push(secondSpace);
|
||||
let calls = 0;
|
||||
const instrumented = (...args) => {
|
||||
calls++;
|
||||
return reconcileCoincidentPartitions(...args);
|
||||
};
|
||||
const result = optimizePlans(input, {}, {}, {
|
||||
reconcileCoincidentPartitions: instrumented,
|
||||
});
|
||||
assert.equal(calls, 2, 'one explicit pass per valid space in one Optimize candidate');
|
||||
assert.equal(result.report.partitionsReconciled, 2);
|
||||
|
||||
const sourceRoot = fileURLToPath(new URL('../src/', import.meta.url));
|
||||
const sourceFiles = [];
|
||||
const walk = (directory) => {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(path);
|
||||
else if (entry.isFile() && entry.name.endsWith('.ts')) sourceFiles.push(path);
|
||||
}
|
||||
};
|
||||
walk(sourceRoot);
|
||||
const owners = sourceFiles
|
||||
.filter((path) => readFileSync(path, 'utf8').includes('reconcileCoincidentPartitions'))
|
||||
.map((path) => relative(sourceRoot, path).replaceAll('\\', '/'))
|
||||
.sort();
|
||||
assert.deepEqual(owners, ['coincident-partitions.ts', 'plan-optimizer.ts'],
|
||||
'render/pointer modules must not import or invoke the Optimize-only pass');
|
||||
});
|
||||
|
||||
test('issue 276 fails closed for an orphan host, overlap, draft, column and unknown partition data', () => {
|
||||
const variants = [];
|
||||
|
||||
|
||||
@@ -255,6 +255,36 @@ test('corner Split golden captures before, thin and thick facade states', () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('issue 276 golden captures 5 cm offsets and hosted door before/after 10/30/virtual', () => {
|
||||
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.coincidentPartition);
|
||||
assert.deepEqual(
|
||||
scenarios.map((scenario) => scenario.coincidentPartition),
|
||||
['before', 'thin', 'thick', 'virtual'],
|
||||
);
|
||||
for (const scenario of scenarios) {
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.ok(space);
|
||||
assert.equal(space.cell_cm, 5);
|
||||
assert.ok(space.rooms[0].poly.some((point) => point[0] === 0.5
|
||||
&& point[1] === 0.004166666666666667), 'short 5 cm offset must be visible');
|
||||
assert.equal(space.openings.length, 1);
|
||||
if (scenario.coincidentPartition === 'before') {
|
||||
assert.equal(space.partitions.length, 1);
|
||||
assert.equal(space.openings[0].host.id, 'redundant');
|
||||
continue;
|
||||
}
|
||||
assert.equal(space.partitions, undefined);
|
||||
assert.equal(space.openings[0].host, undefined);
|
||||
assert.equal(space.openings[0].x, 0.504166667);
|
||||
if (scenario.coincidentPartition === 'virtual') {
|
||||
assert.equal(space.open_spans.length, 1);
|
||||
} else {
|
||||
assert.equal(space.walls[0].cm, scenario.coincidentPartition === 'thin' ? 10 : 30);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('filled opening golden has a pixel-level seam detector', () => {
|
||||
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'openings-filled-tunnel-dark');
|
||||
assert.ok(scenario);
|
||||
@@ -322,7 +352,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
|
||||
assert.ok(scenario);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 41);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 42);
|
||||
assert.equal(space.settings.sun_rays, true);
|
||||
assert.equal(scenario.northDeg, 90,
|
||||
'the sign-sensitive golden must keep a non-zero north direction');
|
||||
|
||||
Reference in New Issue
Block a user