fix: address render performance review

Issue: #451
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-04 23:45:57 +03:00
parent f8b3445dc3
commit cb68492c99
41 changed files with 6985 additions and 6600 deletions
+138 -14
View File
@@ -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);