mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 22:29:05 +00:00
Прямые и винтовые лестницы получили отдельную модель, инструменты редактора, безопасную межэтажную навигацию, вычитание из чистой площади и плоское отображение в 2.5D. Issue: #663 User-Visible: yes
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
import { nothing, svg, type TemplateResult } from 'lit';
|
|
import {
|
|
cachedStairRenderGeometry, stairList, type Stair,
|
|
} from './stairs';
|
|
import { stairTargetState } from './stairs-editor-model';
|
|
import type { SpaceModel } from './types';
|
|
|
|
export interface StairViewHostPort {
|
|
_mode: 'view' | 'plan' | 'devices' | 'decor';
|
|
_curSpaceCfg: unknown;
|
|
_model: SpaceModel[];
|
|
_space: string;
|
|
_hasFixedFloor: boolean;
|
|
_suppressClick: boolean;
|
|
_cellCm: number;
|
|
_tabClick(spaceId: string): void;
|
|
_t(key: 'markup.stairs'): string;
|
|
}
|
|
|
|
type ViewPress = { pid: number; startedAt: number };
|
|
|
|
/** Eager, read-only stair surface. All editing stays in the lazy editor graph. */
|
|
export class StairViewRuntime {
|
|
private press: ViewPress | null = null;
|
|
private suppressClick = false;
|
|
|
|
public constructor(private readonly owner: StairViewHostPort) {}
|
|
|
|
private get stairs(): Stair[] {
|
|
return stairList((this.owner._curSpaceCfg as { stairs?: unknown } | null)?.stairs);
|
|
}
|
|
|
|
public pointerDown(event: PointerEvent): void {
|
|
this.suppressClick = false;
|
|
if (event.pointerType === 'touch') {
|
|
this.press = { pid: event.pointerId, startedAt: performance.now() };
|
|
}
|
|
}
|
|
|
|
public pointerUp(event: PointerEvent): void {
|
|
const press = this.press;
|
|
if (!press || press.pid !== event.pointerId) return;
|
|
this.press = null;
|
|
if (performance.now() - press.startedAt >= 600) this.suppressClick = true;
|
|
}
|
|
|
|
public pointerCancel(event: PointerEvent): void {
|
|
if (this.press?.pid !== event.pointerId) return;
|
|
this.press = null;
|
|
this.suppressClick = true;
|
|
}
|
|
|
|
public clearGesture(): void {
|
|
this.press = null;
|
|
this.suppressClick = false;
|
|
}
|
|
|
|
public renderLayer(): TemplateResult {
|
|
const spaceIds = new Set(this.owner._model.map((item) => item.id));
|
|
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 targetState = stairTargetState(
|
|
stair, this.owner._space, spaceIds, this.owner._hasFixedFloor,
|
|
);
|
|
const active = interactive && targetState === 'active';
|
|
const navigate = (event: Event): void => {
|
|
event.stopPropagation();
|
|
if (event.type === 'keydown') this.suppressClick = false;
|
|
if (!active || this.owner._suppressClick || this.suppressClick
|
|
|| !stair.target_space_id) return;
|
|
this.owner._tabClick(stair.target_space_id);
|
|
};
|
|
return svg`<g class="hp-stair ${active ? 'navigable' : ''} ${interactive ? 'input-enabled' : ''}"
|
|
data-hp="stair" data-id=${stair.id} data-kind=${stair.kind}
|
|
data-target-state=${targetState}
|
|
role=${active ? 'link' : 'img'} tabindex=${active ? '0' : nothing}
|
|
aria-label=${this.owner._t('markup.stairs')}
|
|
@click=${navigate}
|
|
@keydown=${(event: KeyboardEvent) => {
|
|
if (active && (event.key === 'Enter' || event.key === ' ')) {
|
|
event.preventDefault();
|
|
navigate(event);
|
|
}
|
|
}}>
|
|
<polygon class="hp-stair-outline" points=${outline}></polygon>
|
|
<polygon class="hp-stair-hit" points=${outline}
|
|
@pointerdown=${(event: PointerEvent) => this.pointerDown(event)}></polygon>
|
|
${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>`)}
|
|
<path class="hp-stair-arrow" d=${geometry.arrowPath}></path>
|
|
</g>`;
|
|
});
|
|
return svg`<g class="hp-stairs-layer">${items}</g>` as unknown as TemplateResult;
|
|
}
|
|
}
|