mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
+138
-14
@@ -1,10 +1,18 @@
|
||||
import { nothing, render, svg, type TemplateResult } from 'lit';
|
||||
import { cancelHouseplanPointerMove } from './pointer-move-queue';
|
||||
import { gridVisualUnits } from './grid-scale';
|
||||
import {
|
||||
wallBodyNeedsSolid, wallCmToUnits, wallEdgePathD,
|
||||
wallHatchNeedsSolid, wallHatchStepUnits,
|
||||
type WallEdgeBody,
|
||||
} from './wall-thickness';
|
||||
import { NORM_W } from './space-geometry';
|
||||
|
||||
interface LiveEditorState {
|
||||
raf: number;
|
||||
hidden: HTMLElement[];
|
||||
transparent: HTMLElement[];
|
||||
dimmed: HTMLElement[];
|
||||
}
|
||||
|
||||
interface LiveEditorHost {
|
||||
@@ -17,11 +25,11 @@ interface LiveEditorHost {
|
||||
_physicalDrag?: unknown;
|
||||
_physicalRotate?: unknown;
|
||||
_decorDraft?: unknown;
|
||||
_decorMove?: unknown;
|
||||
_dtDrag?: unknown;
|
||||
_decorMove?: { id: string } | null;
|
||||
_dtDrag?: { id: string } | null;
|
||||
_bdDrag?: unknown;
|
||||
_opDrag?: unknown;
|
||||
_resize?: { dragging: boolean };
|
||||
_resize?: { dragging: boolean; plan?: { roomIds: string[]; movingOpeningIds: string[] } | null };
|
||||
_layout: unknown;
|
||||
_dtBox: { id: string; x: number; y: number; w: number; h: number } | null;
|
||||
_dtSel: { id: string; kind: string } | null;
|
||||
@@ -33,8 +41,8 @@ interface LiveEditorHost {
|
||||
_spaceDisplayForRender: () => unknown;
|
||||
_renderWallBodies: (display: unknown) => unknown;
|
||||
_renderResizeMeasurements: () => unknown;
|
||||
_renderOpenings: (display: unknown) => unknown;
|
||||
_renderResizeLayer: (view: unknown) => unknown;
|
||||
_renderOpenings: (display: unknown, onlyIds?: readonly string[]) => unknown;
|
||||
_renderResizeLayer: (view: unknown, roomIds?: readonly string[]) => unknown;
|
||||
_renderMarkupLayer: (viewBox: number[]) => unknown;
|
||||
_renderHiddenWallDiagnosticOverlay: () => unknown;
|
||||
_renderOpeningPlacementPreview: () => unknown;
|
||||
@@ -43,12 +51,20 @@ interface LiveEditorHost {
|
||||
_renderActiveChainInk: () => unknown;
|
||||
_renderPlanSnapOverlay: () => unknown;
|
||||
_renderWallThickUi: () => unknown;
|
||||
_renderDecorLayer: () => unknown;
|
||||
_renderDecorLayer: (onlyId?: string | null) => unknown;
|
||||
_renderBackdropFrame: (view: unknown) => unknown;
|
||||
_renderTextFrame: (view: unknown) => unknown;
|
||||
_renderLiveEditorMeasurements: (view: unknown) => unknown;
|
||||
_livePos: (device: { id: string }) => { x: number; y: number };
|
||||
_scenePoint: (point: number[]) => number[];
|
||||
_renderProjection: string;
|
||||
_spaceModel: () => any;
|
||||
_spaceWalls: any[];
|
||||
_cellCm: number;
|
||||
_gridPitch: number;
|
||||
_openingsR: any[];
|
||||
_stageEl: HTMLElement | null;
|
||||
_fillColors: { wall_fill: { c: string; a: number } };
|
||||
}
|
||||
|
||||
const states = new WeakMap<object, LiveEditorState>();
|
||||
@@ -65,7 +81,7 @@ const gestureProperties = new Set<PropertyKey>([
|
||||
const stateOf = (host: object): LiveEditorState => {
|
||||
let state = states.get(host);
|
||||
if (!state) {
|
||||
state = { raf: 0, hidden: [], transparent: [] };
|
||||
state = { raf: 0, hidden: [], transparent: [], dimmed: [] };
|
||||
states.set(host, state);
|
||||
}
|
||||
return state;
|
||||
@@ -132,6 +148,76 @@ const restore = (state: LiveEditorState): void => {
|
||||
state.hidden.length = 0;
|
||||
for (const element of state.transparent) element.style.removeProperty('opacity');
|
||||
state.transparent.length = 0;
|
||||
for (const element of state.dimmed) element.style.removeProperty('opacity');
|
||||
state.dimmed.length = 0;
|
||||
};
|
||||
|
||||
/** Resize moves paint individual wall strips; pointerup still validates the canonical union. */
|
||||
const resizePreviewWalls = (
|
||||
host: LiveEditorHost, display: any, roomIds: readonly string[] = [],
|
||||
): TemplateResult => {
|
||||
if (host._renderProjection === 'iso') return svg``;
|
||||
const space = host._spaceModel();
|
||||
if (!space) return svg``;
|
||||
const affected = new Set(roomIds);
|
||||
const boundary = (space.rooms || []).filter((room: any) => affected.has(room.id))
|
||||
.flatMap((room: any) => (room.poly || []).map((point: number[], index: number) => [
|
||||
point, room.poly[(index + 1) % room.poly.length],
|
||||
]));
|
||||
const onSegment = (point: number[], segment: number[][]): boolean => {
|
||||
const [a, b] = segment;
|
||||
const dx = b[0] - a[0], dy = b[1] - a[1];
|
||||
const length2 = dx * dx + dy * dy;
|
||||
if (!(length2 > 0)) return false;
|
||||
const t = ((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / length2;
|
||||
const clamped = Math.max(0, Math.min(1, t));
|
||||
return Math.hypot(point[0] - a[0] - dx * clamped, point[1] - a[1] - dy * clamped) < 1e-4;
|
||||
};
|
||||
const edge = (a: number[], b: number[], cm: number, key: string): WallEdgeBody | null => {
|
||||
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
if (!(length > 1e-9) || !(cm > 0)) return null;
|
||||
const half = wallCmToUnits(cm, host._cellCm, host._gridPitch) / 2;
|
||||
const nx = -(b[1] - a[1]) / length * half;
|
||||
const ny = (b[0] - a[0]) / length * half;
|
||||
return {
|
||||
key, kind: 'outer', cm, a, b, depthUnits: half * 2,
|
||||
quad: [[a[0] + nx, a[1] + ny], [b[0] + nx, b[1] + ny],
|
||||
[b[0] - nx, b[1] - ny], [a[0] - nx, a[1] - ny]],
|
||||
};
|
||||
};
|
||||
const edges = [
|
||||
...host._spaceWalls.filter((wall) => {
|
||||
const a = (wall.a || []).map((value: number) => value * NORM_W);
|
||||
const b = (wall.b || []).map((value: number) => value * NORM_W);
|
||||
return boundary.some((segment: number[][]) => onSegment(a, segment) && onSegment(b, segment));
|
||||
}).map((wall, index) => edge(
|
||||
(wall.a || []).map((value: number) => value * NORM_W),
|
||||
(wall.b || []).map((value: number) => value * NORM_W),
|
||||
wall.cm, wall.key || `wall-${index}`,
|
||||
)),
|
||||
].filter((item): item is WallEdgeBody => !!item);
|
||||
const openings = host._openingsR.map((opening) => ({
|
||||
x: opening.rx, y: opening.ry, angle: opening.angle, length: opening.rlen,
|
||||
}));
|
||||
const paths = edges.map((edge) => wallEdgePathD(edge, openings)).filter(Boolean);
|
||||
if (!paths.length) return svg``;
|
||||
const depth = Math.max(...edges.map((edge) => edge.depthUnits), 0);
|
||||
const view = host._viewOr(host._baseVb());
|
||||
const px = host._stageEl?.clientWidth && view.w ? host._stageEl.clientWidth / view.w : 1;
|
||||
const solid = wallBodyNeedsSolid(depth, px)
|
||||
|| wallHatchNeedsSolid(wallHatchStepUnits(host._cellCm), px);
|
||||
const stroke = display?.color || '#607d8b';
|
||||
const fill = host._fillColors.wall_fill;
|
||||
const d = paths.join(' ');
|
||||
return svg`<g class="wallbodies hp-resize-preview-walls"
|
||||
style="--room-stroke:${stroke};--wall-fill:${fill.c};--wall-fill-op:${fill.a}">
|
||||
<path class="wallbody-fill" data-component="preview" d=${d}
|
||||
fill=${fill.c} fill-opacity=${fill.a} fill-rule="nonzero" stroke="none"></path>
|
||||
<path class="wallbody ${solid ? 'solid' : ''}" data-hp="wall"
|
||||
data-id="preview" data-kind="preview" d=${d}
|
||||
fill=${solid ? 'none' : 'url(#hp-wall-hatch)'} fill-rule="nonzero"
|
||||
stroke=${stroke} stroke-width=${gridVisualUnits(0.6, host._cellCm)}></path>
|
||||
</g>`;
|
||||
};
|
||||
|
||||
const planTemplate = (host: LiveEditorHost): TemplateResult => {
|
||||
@@ -140,11 +226,12 @@ const planTemplate = (host: LiveEditorHost): TemplateResult => {
|
||||
const measure = host._opMeasureView;
|
||||
if (host._resize?.dragging) {
|
||||
const display = host._spaceDisplayForRender();
|
||||
const plan = host._resize.plan;
|
||||
return svg`<g class="hp-live-resize" aria-hidden="true" pointer-events="none">
|
||||
${host._renderWallBodies(display)}
|
||||
${resizePreviewWalls(host, display, plan?.roomIds)}
|
||||
${host._renderResizeMeasurements()}
|
||||
${host._renderOpenings(display)}
|
||||
${host._renderResizeLayer(view)}
|
||||
${host._renderOpenings(display, plan?.movingOpeningIds)}
|
||||
${host._renderResizeLayer(view, plan?.roomIds)}
|
||||
</g>`;
|
||||
}
|
||||
const display = host._spaceDisplayForRender();
|
||||
@@ -166,8 +253,9 @@ const editorTemplate = (host: LiveEditorHost): TemplateResult | typeof nothing =
|
||||
if (host._mode === 'plan') return planTemplate(host);
|
||||
if (host._mode === 'decor') {
|
||||
const view = host._viewOr(host._baseVb());
|
||||
const activeId = host._dtDrag?.id || host._decorMove?.id || null;
|
||||
return svg`<g class="hp-live-decor" aria-hidden="true" pointer-events="none">
|
||||
${host._renderDecorLayer()}
|
||||
${host._renderDecorLayer(activeId)}
|
||||
${host._renderBackdropFrame(view)}
|
||||
${host._renderTextFrame(view)}
|
||||
</g>`;
|
||||
@@ -189,6 +277,37 @@ const paintDevice = (host: LiveEditorHost, root: ParentNode): void => {
|
||||
element.style.top = `${((point[1] - view.y) / view.h) * 100}%`;
|
||||
};
|
||||
|
||||
const makeDecorShapeTransparent = (
|
||||
state: LiveEditorState, root: ParentNode, id: string,
|
||||
): void => {
|
||||
for (const element of root.querySelectorAll<HTMLElement>('.decorlayer [data-hp="decor"]')) {
|
||||
if (element.closest('[data-hp-live-editor]') || element.dataset.id !== id) continue;
|
||||
if (element.style.opacity === '0') continue;
|
||||
element.style.opacity = '0';
|
||||
state.transparent.push(element);
|
||||
}
|
||||
};
|
||||
|
||||
const makeMovingOpeningsTransparent = (
|
||||
state: LiveEditorState, root: ParentNode, ids: readonly string[],
|
||||
): void => {
|
||||
const moving = new Set(ids);
|
||||
for (const element of root.querySelectorAll<HTMLElement>('.openinglayer [data-hp="opening"]')) {
|
||||
if (!element.closest('[data-hp-live-editor]') && moving.has(element.dataset.id || '')) {
|
||||
element.style.opacity = '0';
|
||||
state.transparent.push(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dim = (state: LiveEditorState, root: ParentNode, selector: string): void => {
|
||||
for (const element of root.querySelectorAll<HTMLElement>(selector)) {
|
||||
if (element.closest('[data-hp-live-editor]')) continue;
|
||||
element.style.opacity = '0.35';
|
||||
state.dimmed.push(element);
|
||||
}
|
||||
};
|
||||
|
||||
/** One editor-only render root per card; the settled scene remains untouched. */
|
||||
export function paintHouseplanEditor(value: object): void {
|
||||
const host = value as LiveEditorHost;
|
||||
@@ -201,11 +320,16 @@ export function paintHouseplanEditor(value: object): void {
|
||||
if (!target) return;
|
||||
if (host._mode === 'plan') {
|
||||
makeTransparent(state, root, '.hp-editor-only-layer:not(.hp-plan-snap-layer)');
|
||||
if (host._resize?.dragging || host._opDrag) hide(state, root, '.wallbodies');
|
||||
if (host._resize?.dragging) makeTransparent(state, root, '.resize-layer');
|
||||
if (host._opDrag) hide(state, root, '.wallbodies');
|
||||
if (host._resize?.dragging) {
|
||||
dim(state, root, '.wallbodies');
|
||||
makeTransparent(state, root, '.resize-layer');
|
||||
makeMovingOpeningsTransparent(state, root, host._resize.plan?.movingOpeningIds || []);
|
||||
}
|
||||
if (host._opDrag) makeTransparent(state, root, '.openinglayer');
|
||||
} else if (host._mode === 'decor') {
|
||||
makeTransparent(state, root, '.decorlayer');
|
||||
const activeId = host._dtDrag?.id || host._decorMove?.id;
|
||||
if (activeId) makeDecorShapeTransparent(state, root, activeId);
|
||||
makeTransparent(state, root, '.dtframe, .backdropframe');
|
||||
}
|
||||
render(editorTemplate(host), target);
|
||||
|
||||
Reference in New Issue
Block a user