Preview open passage placement

Issue: #193
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-19 10:51:07 +03:00
parent 07d0c2ef86
commit 9ec3636a42
17 changed files with 351 additions and 32 deletions
+23 -8
View File
@@ -46,6 +46,8 @@ test('golden matrix has stable unique ids and bounded comparison thresholds', ()
assert.equal(Number.isInteger(scenario.openingPreviewPixels.minChannelDelta)
&& scenario.openingPreviewPixels.minChannelDelta > 0
&& scenario.openingPreviewPixels.minChannelDelta <= 32, true, scenario.id);
assert.equal(Number.isInteger(scenario.openingPreviewPixels.minInsideWallPixels)
&& scenario.openingPreviewPixels.minInsideWallPixels > 0, true, scenario.id);
}
if (scenario.openingGeometry) {
assert.match(scenario.openingGeometry.id, /^[a-z0-9-]+$/, scenario.id);
@@ -209,7 +211,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, 27);
assert.equal(GOLDEN_MATRIX_VERSION, 28);
assert.equal(space.settings.sun_rays, true);
assert.equal(scenario.northDeg, 90,
'the sign-sensitive golden must keep a non-zero north direction');
@@ -256,15 +258,28 @@ test('value badge golden covers four positions and bottom badge with separate LQ
});
test('opening placement golden requires browser-painted preview pixels', () => {
const scenario = GOLDEN_SCENARIOS.find(
const door = GOLDEN_SCENARIOS.find(
(item) => item.id === 'opening-placement-door-thick-wall-dark',
);
assert.ok(scenario);
assert.equal(scenario.mode, 'plan');
assert.equal(scenario.openingPreview?.type, 'door');
assert.deepEqual(scenario.openingPreview?.pointer, [0.48, 0.65]);
assert.equal(scenario.openingPreviewPixels.minPixels >= 150, true);
assert.equal(scenario.openingPreviewPixels.minChannelDelta >= 4, true);
assert.ok(door);
assert.equal(door.mode, 'plan');
assert.equal(door.openingPreview?.type, 'door');
assert.deepEqual(door.openingPreview?.pointer, [0.48, 0.65]);
assert.equal(door.openingPreviewPixels.minPixels >= 150, true);
assert.equal(door.openingPreviewPixels.minChannelDelta >= 4, true);
const passages = GOLDEN_SCENARIOS.filter(
(item) => item.id.startsWith('opening-placement-passage-thick-wall-'),
);
assert.deepEqual(passages.map((scenario) => scenario.theme), ['dark', 'light']);
for (const scenario of passages) {
assert.equal(scenario.mode, 'plan');
assert.equal(scenario.openingPreview?.type, 'passage');
assert.deepEqual(scenario.openingPreview?.pointer, [0.48, 0.65]);
assert.equal(scenario.openingPreviewPixels.minPixels >= 150, true);
assert.equal(scenario.openingPreviewPixels.minInsideWallPixels >= 8, true);
assert.equal(scenario.openingPreviewPixels.minChannelDelta >= 4, true);
}
});
test('diagonal opening golden asserts a real painted 45 degree symbol', () => {
+16
View File
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const styles = readFileSync(new URL('../src/styles.ts', import.meta.url), 'utf8');
const staticRender = readFileSync(new URL('../src/space-render.ts', import.meta.url), 'utf8');
const backend = readFileSync(new URL('../custom_components/houseplan/websocket_api.py', import.meta.url), 'utf8');
const importer = readFileSync(new URL('../custom_components/houseplan/import_export.py', import.meta.url), 'utf8');
@@ -51,6 +52,21 @@ test('static passage cuts and tunnels are passage-only additions', () => {
assert.match(staticRender, /passageGlowTunnels/);
});
test('placement preview adds passage-only cut geometry without changing saved symbols', () => {
const start = card.indexOf('private _renderOpeningPlacementPreview()');
const preview = card.slice(start, card.indexOf('private _renderOpenings(', start));
assert.match(preview, /candidate\.type === 'passage'/);
assert.match(preview, /passagePlacementPreviewGeometry\(candidate, this\._gridPitch\)/);
assert.equal((preview.match(/class="passage-preview-cut"/g) || []).length, 1);
assert.equal((preview.match(/class="passage-preview-boundary"/g) || []).length, 1,
'one mapped template emits exactly two resolved boundary records');
assert.match(preview, /: renderOpeningVisibleGeometry\(visibleSpec\)/);
assert.match(preview, /aria-hidden="true" pointer-events="none"/);
assert.match(styles, /\.opening-preview\[data-kind="passage"\][\s\S]*opacity: 1/);
assert.match(styles, /\.passage-preview-cut[\s\S]*fill-opacity: 0\.35/);
assert.match(styles, /\.passage-preview-boundary[\s\S]*stroke: var\(--hp-open/);
});
test('all write/import paths invoke the semantic passage validator', () => {
assert.equal((backend.match(/validate_opening_passages\(/g) || []).length, 2);
assert.match(importer, /validate_opening_passages\(merged_config, current_config\)/);
+40
View File
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import {
openingPlacementPreset,
openingPlacementTargets,
passagePlacementPreviewGeometry,
resolveOpeningPlacement,
sameOpeningPlacementInput,
} from '../test-build/opening-placement.js';
@@ -42,6 +43,45 @@ test('Opening presets use the agreed type-specific defaults', () => {
assert.equal(openingPlacementPreset('gate', 3).lengthCm, 300);
});
test('passage preview geometry follows the resolved length and standard wall depth', () => {
const geometry = passagePlacementPreviewGeometry({
renderedLength: 90,
target: { physicalHalfWidth: 7.5 },
}, 10);
assert.deepEqual(geometry, {
rect: { x: -45, y: -7.5, width: 90, height: 15 },
boundaries: [
{ x1: -45, y1: -9.3, x2: -45, y2: 9.3 },
{ x1: 45, y1: -9.3, x2: 45, y2: 9.3 },
],
});
});
test('passage preview wall depth changes only its rect height and boundary span', () => {
const geometry = passagePlacementPreviewGeometry({
renderedLength: 90,
target: { physicalHalfWidth: 12.5 },
}, 10);
assert.deepEqual(geometry.rect, { x: -45, y: -12.5, width: 90, height: 25 });
assert.deepEqual(geometry.boundaries, [
{ x1: -45, y1: -14.3, x2: -45, y2: 14.3 },
{ x1: 45, y1: -14.3, x2: 45, y2: 14.3 },
]);
});
test('passage preview preserves boundary marks when physical wall depth is zero', () => {
const geometry = passagePlacementPreviewGeometry({
renderedLength: 60,
target: { physicalHalfWidth: 0 },
}, 5);
assert.deepEqual(geometry.rect, { x: -30, y: 0, width: 60, height: 0 });
const boundaryHalf = 5 * 0.18;
assert.deepEqual(geometry.boundaries, [
{ x1: -30, y1: -boundaryHalf, x2: -30, y2: boundaryHalf },
{ x1: 30, y1: -boundaryHalf, x2: 30, y2: boundaryHalf },
]);
});
test('Shared room-owned copies collapse to one physical target', () => {
const targets = openingPlacementTargets([
interval({ roomId: 'r1', half: 7 }),