mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
File diff suppressed because one or more lines are too long
@@ -447,7 +447,7 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
}
|
||||
if (scenario.openingPreview) {
|
||||
const { type, pointer } = scenario.openingPreview;
|
||||
if (!['window', 'door', 'gate'].includes(type)
|
||||
if (!['window', 'door', 'passage', 'gate'].includes(type)
|
||||
|| !Array.isArray(pointer) || pointer.length !== 2
|
||||
|| !pointer.every(Number.isFinite)) {
|
||||
throw new Error(`invalid golden openingPreview: ${scenario.id}`);
|
||||
@@ -471,7 +471,11 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
await card.updateComplete;
|
||||
await frame();
|
||||
const preview = card.renderRoot.querySelector(`.opening-preview[data-kind="${type}"]`);
|
||||
if (!preview || !preview.querySelector('.op-leaf')) {
|
||||
const expectedGeometry = type === 'passage'
|
||||
? !!preview?.querySelector('.passage-preview-cut')
|
||||
&& preview.querySelectorAll('.passage-preview-boundary').length === 2
|
||||
: !!preview?.querySelector('.op-leaf');
|
||||
if (!preview || !expectedGeometry) {
|
||||
const intervals = card._openingPlacementIntervalsCache?.value || [];
|
||||
const nearest = intervals.map((interval) => {
|
||||
const [px, py] = card._cursorPt || [0, 0];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
|
||||
|
||||
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
|
||||
export const GOLDEN_MATRIX_VERSION = 27;
|
||||
export const GOLDEN_MATRIX_VERSION = 28;
|
||||
|
||||
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
|
||||
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
|
||||
@@ -104,6 +104,14 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
mode: 'plan', openingPreview: { type: 'door', pointer: [0.48, 0.65] },
|
||||
openingPreviewPixels: { minPixels: 150, minInsideWallPixels: 8, minChannelDelta: 4 },
|
||||
theme: 'dark', viewport: { width: 1180, height: 900 }, ...page },
|
||||
{ id: 'opening-placement-passage-thick-wall-dark', fixture: 'visual', space: 'golden-geometry',
|
||||
mode: 'plan', openingPreview: { type: 'passage', pointer: [0.48, 0.65] },
|
||||
openingPreviewPixels: { minPixels: 150, minInsideWallPixels: 8, minChannelDelta: 4 },
|
||||
theme: 'dark', viewport: { width: 1180, height: 900 }, ...page },
|
||||
{ id: 'opening-placement-passage-thick-wall-light', fixture: 'visual', space: 'golden-geometry',
|
||||
mode: 'plan', openingPreview: { type: 'passage', pointer: [0.48, 0.65] },
|
||||
openingPreviewPixels: { minPixels: 150, minInsideWallPixels: 8, minChannelDelta: 4 },
|
||||
theme: 'light', viewport: { width: 1180, height: 900 }, ...page },
|
||||
{ id: 'geometry-devices-editor-dark', fixture: 'visual', space: 'golden-geometry', mode: 'devices',
|
||||
theme: 'dark', viewport: { width: 1180, height: 900 }, ...page },
|
||||
{ id: 'geometry-decor-editor-dark', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
|
||||
|
||||
@@ -91,14 +91,104 @@ const res = await page.evaluate(async () => {
|
||||
card._cursorPt = wallMid;
|
||||
await settle();
|
||||
const passagePreview = root().querySelector('.opening-preview[data-kind="passage"]');
|
||||
out.passagePreviewHasNoSymbol = !!passagePreview
|
||||
&& passagePreview.querySelectorAll('.op-leaf,.op-arc,.op-glass,line').length === 0;
|
||||
const passageCandidate = card._resolveOpeningPlacement(wallMid);
|
||||
const passageCut = passagePreview?.querySelector('.passage-preview-cut');
|
||||
const passageBoundaries = [...(passagePreview?.querySelectorAll('.passage-preview-boundary') || [])];
|
||||
const passageHalfLength = (passageCandidate?.renderedLength || 0) / 2;
|
||||
const passageHalfDepth = passageCandidate?.target?.physicalHalfWidth || 0;
|
||||
const passageBoundaryHalf = passageHalfDepth + card._gridPitch * 0.18;
|
||||
const near = (value, expected) => Math.abs(Number.parseFloat(value) - expected) < 1e-6;
|
||||
out.passagePreviewGeometry = !!passagePreview && !!passageCandidate && !!passageCut
|
||||
&& passageBoundaries.length === 2
|
||||
&& near(passageCut.getAttribute('x'), -passageHalfLength)
|
||||
&& near(passageCut.getAttribute('y'), -passageHalfDepth)
|
||||
&& near(passageCut.getAttribute('width'), passageCandidate.renderedLength)
|
||||
&& near(passageCut.getAttribute('height'), passageHalfDepth * 2)
|
||||
&& passageBoundaries.every((boundary, index) => {
|
||||
const x = index === 0 ? -passageHalfLength : passageHalfLength;
|
||||
return near(boundary.getAttribute('x1'), x) && near(boundary.getAttribute('x2'), x)
|
||||
&& near(boundary.getAttribute('y1'), -passageBoundaryHalf)
|
||||
&& near(boundary.getAttribute('y2'), passageBoundaryHalf);
|
||||
});
|
||||
const wallFill = root().querySelector('.wallbody-fill');
|
||||
const passageDot = root().querySelector('.opening-preview-dot');
|
||||
const passagePreviewStyle = passagePreview ? getComputedStyle(passagePreview) : null;
|
||||
const passageCutStyle = passageCut ? getComputedStyle(passageCut) : null;
|
||||
const wallFillStyle = wallFill ? getComputedStyle(wallFill) : null;
|
||||
const passageDotStyle = passageDot ? getComputedStyle(passageDot) : null;
|
||||
const inheritedWallFill = passageCutStyle?.getPropertyValue('--wall-fill').trim() || '';
|
||||
out._passagePreviewThemeDiagnostics = {
|
||||
opacity: passagePreviewStyle?.opacity,
|
||||
fillOpacity: passageCutStyle?.fillOpacity,
|
||||
cutFill: passageCutStyle?.fill,
|
||||
wallFill: wallFillStyle?.fill,
|
||||
inheritedWallFill,
|
||||
configuredWallFill: card._fillColors.wall_fill.c,
|
||||
boundaryStrokes: passageBoundaries.map((boundary) => getComputedStyle(boundary).stroke),
|
||||
dotFill: passageDotStyle?.fill,
|
||||
};
|
||||
out.passagePreviewTheme = !!passagePreview && !!passageCut
|
||||
&& Math.abs(Number.parseFloat(passagePreviewStyle.opacity) - 1) < 0.001
|
||||
&& Math.abs(Number.parseFloat(passageCutStyle.fillOpacity) - 0.35) < 0.001
|
||||
&& inheritedWallFill.toLowerCase() === card._fillColors.wall_fill.c.toLowerCase()
|
||||
&& passageCutStyle.fill !== 'none'
|
||||
&& passageBoundaries.every((boundary) => getComputedStyle(boundary).stroke
|
||||
=== passageDotStyle.fill);
|
||||
out.passagePreviewInert = passagePreview?.getAttribute('aria-hidden') === 'true'
|
||||
&& passagePreview?.getAttribute('pointer-events') === 'none'
|
||||
&& passageCut?.getAttribute('pointer-events') === 'none'
|
||||
&& passageBoundaries.every((boundary) => boundary.getAttribute('pointer-events') === 'none');
|
||||
out.passagePreviewKeepsRulers = root().querySelectorAll('.measurelabel.opshoulder').length === 2
|
||||
&& !!root().querySelector('.opcentertick');
|
||||
out.passagePreviewHasNoCommittedSymbolParts = !!passagePreview
|
||||
&& passagePreview.querySelectorAll('.op-leaf,.op-arc,.op-glass').length === 0;
|
||||
out.otherPreviewHasNoPassageGeometry = windowPreview
|
||||
?.querySelectorAll('.passage-preview-cut,.passage-preview-boundary').length === 0;
|
||||
out.typeChoicesDoNotMutateGeometry = card._cfgEpoch === epochBeforeMenu
|
||||
&& JSON.stringify(card._curSpaceCfg.openings || []) === geometryBeforeMenu;
|
||||
|
||||
// Passage click and save use the same candidate, but the committed opening
|
||||
// keeps only the canonical masonry cut and never the transient overlay.
|
||||
const passageWallMid = [
|
||||
(poly[2][0] + poly[3][0]) / 2,
|
||||
(poly[2][1] + poly[3][1]) / 2,
|
||||
];
|
||||
card._cursorPt = passageWallMid;
|
||||
await settle();
|
||||
const passageSaveCandidate = card._resolveOpeningPlacement(passageWallMid);
|
||||
const passageIdsBeforeSave = new Set((card._curSpaceCfg.openings || []).map((item) => item.id));
|
||||
card._openingClick(passageWallMid);
|
||||
await settle();
|
||||
out.passageDialogMatchesPreview = !!passageSaveCandidate && card._openingDialog?.type === 'passage'
|
||||
&& near(card._openingDialog.x, passageSaveCandidate.x)
|
||||
&& near(card._openingDialog.y, passageSaveCandidate.y)
|
||||
&& near(card._openingDialog.angle, passageSaveCandidate.angle)
|
||||
&& card._openingDialog.lengthCm === passageSaveCandidate.lengthCm
|
||||
&& !root().querySelector('.opening-preview');
|
||||
card._saveOpening();
|
||||
await settle();
|
||||
const savedPassage = (card._curSpaceCfg.openings || [])
|
||||
.find((item) => !passageIdsBeforeSave.has(item.id));
|
||||
const committedPassage = savedPassage
|
||||
? root().querySelector(`.opening[data-id="${savedPassage.id}"]`)
|
||||
: null;
|
||||
out.passageSaveMatchesCandidate = !!savedPassage && !!passageSaveCandidate
|
||||
&& savedPassage.type === 'passage'
|
||||
&& near(savedPassage.x * 1000, passageSaveCandidate.x)
|
||||
&& near(savedPassage.y * card._spaceH, passageSaveCandidate.y)
|
||||
&& near(savedPassage.angle, passageSaveCandidate.angle)
|
||||
&& near(savedPassage.length * 1000, passageSaveCandidate.renderedLength);
|
||||
out.committedPassageHasNoPreviewSymbol = !!committedPassage
|
||||
&& committedPassage.querySelectorAll(
|
||||
'.passage-preview-cut,.passage-preview-boundary,.op-leaf,.op-arc,.op-glass',
|
||||
).length === 0;
|
||||
await choose('gate');
|
||||
card._cursorPt = wallMid;
|
||||
await settle();
|
||||
const gatePreview = root().querySelector('.opening-preview[data-kind="gate"]');
|
||||
out.gatePreviewGeometry = gatePreview?.querySelectorAll('.op-leaf').length === 2
|
||||
&& gatePreview?.querySelectorAll('.op-arc').length === 0;
|
||||
&& gatePreview?.querySelectorAll('.op-arc').length === 0
|
||||
&& gatePreview?.querySelectorAll('.passage-preview-cut,.passage-preview-boundary').length === 0;
|
||||
await choose('door');
|
||||
const axisCandidate = card._resolveOpeningPlacement(wallMid);
|
||||
const target = axisCandidate?.target;
|
||||
@@ -122,8 +212,6 @@ const res = await page.evaluate(async () => {
|
||||
out.previewAriaHidden = preview?.getAttribute('aria-hidden') === 'true';
|
||||
out.previewHasNoPersistentIdentity = !preview?.hasAttribute('data-id')
|
||||
&& !preview?.hasAttribute('data-hp');
|
||||
out.typeChoicesDoNotMutateGeometry = card._cfgEpoch === epochBeforeMenu
|
||||
&& JSON.stringify(card._curSpaceCfg.openings || []) === geometryBeforeMenu;
|
||||
const wallBody = root().querySelector('.wallbody');
|
||||
out.previewPaintedAfterWall = !wallBody || (!!preview
|
||||
&& !!(wallBody.compareDocumentPosition(preview) & Node.DOCUMENT_POSITION_FOLLOWING));
|
||||
@@ -239,5 +327,8 @@ const res = await page.evaluate(async () => {
|
||||
return out;
|
||||
});
|
||||
|
||||
if (!res.passagePreviewTheme)
|
||||
console.error('passagePreviewTheme diagnostics:', res._passagePreviewThemeDiagnostics);
|
||||
delete res._passagePreviewThemeDiagnostics;
|
||||
checkAll(res);
|
||||
await finish(browser, res);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+25
-3
File diff suppressed because one or more lines are too long
@@ -2,6 +2,10 @@
|
||||
|
||||
## v1.65.0-beta.3 — 2026-08-19
|
||||
|
||||
- Open-passage placement now previews the exact future wall cut before the
|
||||
click: a translucent wall-coloured segment shows its real length and depth,
|
||||
with two orange boundary marks at its ends. The saved passage remains a
|
||||
symbol-free physical opening ([#193](https://github.com/Matysh/houseplan-card/issues/193)).
|
||||
- The House Plan color picker now draws the full spectrum directly on the Hue
|
||||
track, so the target colour family is visible before moving the slider. Mouse,
|
||||
touch and keyboard values remain unchanged, and a contrasting ring keeps the
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
## v1.65.0-beta.3 — 2026-08-19
|
||||
|
||||
- Перед размещением открытого проёма теперь виден точный будущий разрыв стены:
|
||||
полупрозрачный сегмент цвета стены показывает реальную длину и глубину, а две
|
||||
оранжевые засечки отмечают его границы. После сохранения проём по-прежнему
|
||||
остаётся физическим разрывом без отдельного символа
|
||||
([#193](https://github.com/Matysh/houseplan-card/issues/193)).
|
||||
- Палитра House Plan теперь показывает полный спектр прямо на шкале «Оттенок»,
|
||||
поэтому нужную группу цветов видно ещё до движения ползунка. Значения и
|
||||
управление мышью, касанием и клавиатурой не изменились, а контрастный контур
|
||||
|
||||
@@ -1103,6 +1103,13 @@ separately promised workflows:
|
||||
absent far from walls, on virtual spans, over an existing opening, before
|
||||
a type is selected, or in other tools [auto: smoke_opening_preview;
|
||||
golden: opening-placement-door-thick-wall-dark]
|
||||
- [ ] Open-passage placement preview: hover paints one wall-coloured cut segment
|
||||
at effective opacity 0.35 plus exactly two orange boundary marks. Its
|
||||
length and depth come from the resolved candidate, ruler labels remain
|
||||
visible, and save leaves no preview-only symbol in the committed passage
|
||||
[unit: opening-placement, open-passage-contract; auto:
|
||||
smoke_opening_preview; golden:
|
||||
opening-placement-passage-thick-wall-dark/light]
|
||||
- [ ] Split polyline + cursors + Esc (v1.32.0): Merge shows a pointer cursor,
|
||||
Split shows pointer until a room is picked then crosshair; the cut can be
|
||||
a polyline — start on a wall, intermediate clicks inside the room, finish
|
||||
|
||||
+6
-2
@@ -288,8 +288,12 @@ still contain manually assigned devices and explicit measurement sources.
|
||||
|
||||
## 9. Doors, windows, gates and locks
|
||||
|
||||
Choose Opening, select door/window/gate, and click a wall. Defaults are 90 cm,
|
||||
120 cm and 300 cm. The complete opening must fit on the wall.
|
||||
Choose Opening, select door/window/open passage/gate, and click a wall. Defaults
|
||||
are 90 cm, 120 cm, 90 cm and 300 cm. The complete opening must fit on the wall.
|
||||
Before the click, door, window and gate show their translucent architectural
|
||||
symbol. An open passage instead shows the exact future wall cut as a translucent
|
||||
wall-coloured segment with an orange boundary mark at each end. Its depth follows
|
||||
the real wall thickness; after saving it has no standalone symbol.
|
||||
|
||||
An opening may bind a contact; doors and gates may also bind a lock. View paints
|
||||
the moving leaf and state. A lock badge is green when locked and orange when
|
||||
|
||||
@@ -434,9 +434,11 @@ Glow и солнечные лучи, но при Resize комнаты оста
|
||||
|
||||
В **Редакторе плана** кнопка **Проём** сама по себе ничего не размещает: она
|
||||
открывает компактное подменю **Окно / Дверь / Открытый проём / Ворота**. После выбора типа
|
||||
наведите указатель на физическую стену. Полупрозрачный символ показывает точный
|
||||
будущий вид, положение и сторону открывания; он виден поверх тела толстой стены.
|
||||
Клик открывает свойства, а **Сохранить** создаёт объект. После сохранения или
|
||||
наведите указатель на физическую стену. Для окна, двери и ворот полупрозрачный
|
||||
символ показывает точный будущий вид, положение и сторону открывания. Для
|
||||
открытого проёма полупрозрачный сегмент цвета стены показывает точную длину и
|
||||
глубину будущего разрыва, а две оранжевые засечки отмечают его границы. Клик
|
||||
открывает свойства, а **Сохранить** создаёт объект. После сохранения или
|
||||
отмены выбранный тип остаётся активным для серии одинаковых проёмов. `Esc`,
|
||||
выбор другого инструмента, редактора или пространства завершает эту серию.
|
||||
|
||||
|
||||
+14
-2
@@ -249,7 +249,8 @@ import {
|
||||
type OpeningFaceOffset, type OpeningVisibleSpec,
|
||||
} from './render/opening-symbol';
|
||||
import {
|
||||
openingDefaultLengthCm, openingPlacementPreset, resolveOpeningPlacement, sameOpeningPlacementInput,
|
||||
openingDefaultLengthCm, openingPlacementPreset, passagePlacementPreviewGeometry,
|
||||
resolveOpeningPlacement, sameOpeningPlacementInput,
|
||||
type OpeningPlacementCore, type OpeningPlacementPreset, type OpeningPlacementType,
|
||||
} from './opening-placement';
|
||||
import { safeStoredColor } from './color';
|
||||
@@ -17363,10 +17364,21 @@ class HouseplanCard extends LitElement {
|
||||
gridPitch: this._gridPitch,
|
||||
face: candidate.face,
|
||||
};
|
||||
const passageGeometry = candidate.type === 'passage'
|
||||
? passagePlacementPreviewGeometry(candidate, this._gridPitch)
|
||||
: null;
|
||||
return svg`<g class="opening-preview" data-kind=${candidate.type}
|
||||
aria-hidden="true" pointer-events="none"
|
||||
transform="translate(${candidate.x} ${candidate.y}) rotate(${candidate.angle})">
|
||||
${renderOpeningVisibleGeometry(visibleSpec)}
|
||||
${passageGeometry ? svg`
|
||||
<rect class="passage-preview-cut" pointer-events="none"
|
||||
x=${passageGeometry.rect.x} y=${passageGeometry.rect.y}
|
||||
width=${passageGeometry.rect.width} height=${passageGeometry.rect.height}></rect>
|
||||
${passageGeometry.boundaries.map((boundary) => svg`
|
||||
<line class="passage-preview-boundary" pointer-events="none"
|
||||
x1=${boundary.x1} y1=${boundary.y1}
|
||||
x2=${boundary.x2} y2=${boundary.y2}></line>`)}
|
||||
` : renderOpeningVisibleGeometry(visibleSpec)}
|
||||
</g>
|
||||
<circle class="opening-preview-dot opghost-dot" aria-hidden="true" pointer-events="none"
|
||||
cx=${candidate.x} cy=${candidate.y} r=${this._gridPitch * 0.18}></circle>` as unknown as TemplateResult;
|
||||
|
||||
@@ -47,6 +47,14 @@ export interface OpeningPlacementCore {
|
||||
measure: OpeningPlacementMeasureGeometry;
|
||||
}
|
||||
|
||||
export interface PassagePlacementPreviewGeometry {
|
||||
rect: { x: number; y: number; width: number; height: number };
|
||||
boundaries: readonly [
|
||||
{ x1: number; y1: number; x2: number; y2: number },
|
||||
{ x1: number; y1: number; x2: number; y2: number },
|
||||
];
|
||||
}
|
||||
|
||||
export interface ResolveOpeningPlacementInput {
|
||||
pointer: readonly [number, number];
|
||||
preset: OpeningPlacementPreset;
|
||||
@@ -77,6 +85,29 @@ export function openingPlacementPreset(
|
||||
return { type, lengthCm: openingDefaultLengthCm(type), flipH: false, flipV: false, revision };
|
||||
}
|
||||
|
||||
/** Local SVG geometry for the passage-only placement overlay. The candidate
|
||||
* remains the authority for both the future wall cut and the preview. */
|
||||
export function passagePlacementPreviewGeometry(
|
||||
candidate: Pick<OpeningPlacementCore, 'renderedLength' | 'target'>,
|
||||
gridPitch: number,
|
||||
): PassagePlacementPreviewGeometry {
|
||||
const halfLength = Math.max(0, candidate.renderedLength) / 2;
|
||||
const halfDepth = Math.max(0, candidate.target.physicalHalfWidth);
|
||||
const boundaryHalfLength = halfDepth + Math.max(0, gridPitch) * 0.18;
|
||||
const boundary = (x: number) => ({
|
||||
x1: x, y1: -boundaryHalfLength, x2: x, y2: boundaryHalfLength,
|
||||
});
|
||||
return {
|
||||
rect: {
|
||||
x: -halfLength,
|
||||
y: halfDepth ? -halfDepth : 0,
|
||||
width: halfLength * 2,
|
||||
height: halfDepth * 2,
|
||||
},
|
||||
boundaries: [boundary(-halfLength), boundary(halfLength)],
|
||||
};
|
||||
}
|
||||
|
||||
function pointOrder(a: readonly number[], b: readonly number[]): number {
|
||||
return a[0] - b[0] || a[1] - b[1];
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,20 @@ export const cardStyles = css`
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.opening-preview[data-kind="passage"] {
|
||||
opacity: 1;
|
||||
}
|
||||
.passage-preview-cut {
|
||||
fill: var(--wall-fill, #ffffff);
|
||||
fill-opacity: 0.35;
|
||||
stroke: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.passage-preview-boundary {
|
||||
stroke: var(--hp-open, #ff9800);
|
||||
stroke-width: 2.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.opening-preview .op-leaf,
|
||||
.opening-preview .op-arc {
|
||||
transition: none;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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\)/);
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user