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
+14 -2
View File
@@ -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;
+31
View File
@@ -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];
}
+14
View File
@@ -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;