mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
perf(stairs): draw all treads of a stair as one path (#740)
A floor with stairs pays for them on every switch to it: the stair layer is emptied on other floors, so Lit recreates every symbol on each return and the browser lays out and paints it again. With 250 stairs (the large-house fixture, the per-floor limit) that was 2,875 SVG elements and about 40 ms per entry locally; each stair carried 3-7 separate tread lines with four bound coordinates each. The treads of one stair are now a single <path class="hp-stair-tread"> with one `M a L b` subpath per tread, in geometry order and with the numbers the lines carried. Treads of one stair never overlap (straight: parallel, >= 20 cm apart; spiral: inner ends >= 6.7 cm apart at the 3.6 cm stroke), so the path paints the same pixels at any opacity. The outline points and the tread data are built once per cached geometry object (cachedStairMarkup, weak keys), not on every render. The View and plan-editor layers share the strings; outline, hit polygon, trapezoid, arrow, attributes and handlers are unchanged. Floor 1 of the fixture drops from 4,210 to 2,960 elements. Witnesses: the unit test for the path data and its cache, and the smoke_stairs markup checks in View and in the plan editor, are red on dev. The stairs-view-tread-lines mutant restores the View lines. Issue: #740 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
convertStairKind, normalizeStairAngle, snapStairToStairs, stairTargetState,
|
||||
} from './stairs-editor-model';
|
||||
import {
|
||||
cachedStairRenderGeometry, MAX_STAIRS_PER_SPACE, stairList, stairStyleVars,
|
||||
cachedStairMarkup, cachedStairRenderGeometry, MAX_STAIRS_PER_SPACE, stairList, stairStyleVars,
|
||||
stairVisualFields, stairVisualStyle, type Stair, type StairVisualStyle,
|
||||
} from './stairs';
|
||||
import type { SpaceModel } from './types';
|
||||
@@ -502,7 +502,7 @@ export class StairEditorRuntime {
|
||||
|
||||
private renderStair(stair: Stair, spaceIds: ReadonlySet<string>, selected: boolean, draft: boolean): TemplateResult {
|
||||
const geometry = cachedStairRenderGeometry(stair, this.owner._cellCm);
|
||||
const outline = geometry.outline.map((point) => point.join(',')).join(' ');
|
||||
const markup = cachedStairMarkup(geometry);
|
||||
const targetState = stairTargetState(
|
||||
stair, this.owner._space, spaceIds, this.owner._hasFixedFloor,
|
||||
);
|
||||
@@ -526,14 +526,13 @@ export class StairEditorRuntime {
|
||||
event.stopPropagation();
|
||||
if (this.owner._mode === 'plan' && !draft) this.openDialog(stair);
|
||||
}}>
|
||||
<polygon class="hp-stair-outline" points=${outline}></polygon>
|
||||
<polygon class="hp-stair-hit" points=${outline}
|
||||
<polygon class="hp-stair-outline" points=${markup.outline}></polygon>
|
||||
<polygon class="hp-stair-hit" points=${markup.outline}
|
||||
@pointerdown=${(event: PointerEvent) => this.pointerDown(event, stair, 'move')}
|
||||
@click=${select}></polygon>
|
||||
${geometry.trapezoid.map((line) => svg`<line class="hp-stair-trapezoid"
|
||||
x1=${line.a[0]} y1=${line.a[1]} x2=${line.b[0]} y2=${line.b[1]}></line>`)}
|
||||
${geometry.treads.map((line) => svg`<line class="hp-stair-tread"
|
||||
x1=${line.a[0]} y1=${line.a[1]} x2=${line.b[0]} y2=${line.b[1]}></line>`)}
|
||||
${markup.treads ? svg`<path class="hp-stair-tread" d=${markup.treads}></path>` : nothing}
|
||||
<path class="hp-stair-arrow" d=${geometry.arrowPath}></path>
|
||||
</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,6 +1,6 @@
|
||||
import { nothing, svg, type TemplateResult } from 'lit';
|
||||
import {
|
||||
cachedStairRenderGeometry, stairList, stairStyleVars, type Stair,
|
||||
cachedStairMarkup, cachedStairRenderGeometry, stairList, stairStyleVars, type Stair,
|
||||
} from './stairs';
|
||||
import { stairTargetState } from './stairs-editor-model';
|
||||
import type { SpaceModel } from './types';
|
||||
@@ -68,7 +68,7 @@ export class StairViewRuntime {
|
||||
const interactive = this.owner._mode === 'view';
|
||||
const items = this.stairs.map((stair) => {
|
||||
const geometry = cachedStairRenderGeometry(stair, this.owner._cellCm);
|
||||
const outline = geometry.outline.map((point) => point.join(',')).join(' ');
|
||||
const markup = cachedStairMarkup(geometry);
|
||||
const targetState = stairTargetState(
|
||||
stair, this.owner._space, spaceIds, this.owner._hasFixedFloor,
|
||||
);
|
||||
@@ -107,13 +107,12 @@ export class StairViewRuntime {
|
||||
navigate(event);
|
||||
}
|
||||
}}>
|
||||
<polygon class="hp-stair-outline" points=${outline}></polygon>
|
||||
<polygon class="hp-stair-hit" points=${outline}
|
||||
<polygon class="hp-stair-outline" points=${markup.outline}></polygon>
|
||||
<polygon class="hp-stair-hit" points=${markup.outline}
|
||||
@pointerdown=${(event: PointerEvent) => this.pointerDown(event)}></polygon>
|
||||
${geometry.trapezoid.map((line) => svg`<line class="hp-stair-trapezoid"
|
||||
x1=${line.a[0]} y1=${line.a[1]} x2=${line.b[0]} y2=${line.b[1]}></line>`)}
|
||||
${geometry.treads.map((line) => svg`<line class="hp-stair-tread"
|
||||
x1=${line.a[0]} y1=${line.a[1]} x2=${line.b[0]} y2=${line.b[1]}></line>`)}
|
||||
${markup.treads ? svg`<path class="hp-stair-tread" d=${markup.treads}></path>` : nothing}
|
||||
<path class="hp-stair-arrow" d=${geometry.arrowPath}></path>
|
||||
</g>`;
|
||||
});
|
||||
|
||||
@@ -328,6 +328,43 @@ export function cachedStairRenderGeometry(
|
||||
return geometry;
|
||||
}
|
||||
|
||||
/** Attribute strings of one stair symbol, shared by the View and plan-editor layers. */
|
||||
export interface StairMarkup {
|
||||
/** `points` of the outline polygon and of its hit twin. */
|
||||
outline: string;
|
||||
/** `d` of the single tread path; empty when the stair has no treads. */
|
||||
treads: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* #740: all treads of one stair are a single `<path>`, one `M a L b` subpath
|
||||
* per tread in geometry order with the numbers the separate `<line>`s carried.
|
||||
* Treads of one stair never overlap (parallel ≥ 20 cm apart; spiral inner ends
|
||||
* ≥ 6.7 cm apart at the 3.6 cm stroke), so the path paints the same pixels.
|
||||
*/
|
||||
export function stairTreadPath(treads: readonly StairLine[]): string {
|
||||
return treads.map((line) => `M ${line.a[0]} ${line.a[1]} L ${line.b[0]} ${line.b[1]}`).join(' ');
|
||||
}
|
||||
|
||||
const MARKUP_CACHE = new WeakMap<StairRenderGeometry, StairMarkup>();
|
||||
|
||||
/**
|
||||
* The strings are built once per geometry object, not on every render: the
|
||||
* render-geometry cache hands out a new object only when the geometry changes,
|
||||
* and weak keys release the strings together with it.
|
||||
*/
|
||||
export function cachedStairMarkup(geometry: StairRenderGeometry): StairMarkup {
|
||||
let markup = MARKUP_CACHE.get(geometry);
|
||||
if (!markup) {
|
||||
markup = {
|
||||
outline: geometry.outline.map((point) => point.join(',')).join(' '),
|
||||
treads: stairTreadPath(geometry.treads),
|
||||
};
|
||||
MARKUP_CACHE.set(geometry, markup);
|
||||
}
|
||||
return markup;
|
||||
}
|
||||
|
||||
export function stairFootprintGeometry(stair: Stair, scale = NORM_W): Geom {
|
||||
const ring = stairOutline(stair, scale);
|
||||
return (ring.length ? [[[...ring, ring[0]]]] : []) as unknown as Geom;
|
||||
|
||||
Reference in New Issue
Block a user