mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
fix: reconcile lightweight interaction frames
Issue: #451 User-Visible: no
This commit is contained in:
+49
-54
@@ -1094,7 +1094,7 @@ export class HouseplanCard extends LitElement {
|
||||
state.presented = presented;
|
||||
this._zoom = presented.zoom;
|
||||
this._view = { ...presented.viewBox };
|
||||
this._scheduleLiveViewport();
|
||||
this._liveVp(true);
|
||||
}
|
||||
|
||||
private _settleCameraTransition(state: CameraTransitionState): void {
|
||||
@@ -4513,7 +4513,7 @@ export class HouseplanCard extends LitElement {
|
||||
if (this.isConnected) this.requestUpdate();
|
||||
}
|
||||
|
||||
private _scheduleLiveViewport(): void { this._liveRt ? this._liveRt.viewport() : this.requestUpdate(); }
|
||||
private _liveVp(now = false): void { this._liveRt ? this._liveRt.viewport(now) : this.requestUpdate(); }
|
||||
private _syncLiveHover(): void { this._liveRt ? this._liveRt.hover() : this.requestUpdate(); }
|
||||
/** Bypass live routing so a gesture terminal publishes the last HA frame. */
|
||||
private _flushHa(): void { if (this._liveRt?.take()) super.requestUpdate(); }
|
||||
@@ -6424,7 +6424,7 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
this._zoom = z;
|
||||
this._view = next;
|
||||
this._scheduleLiveViewport();
|
||||
this._liveVp();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6619,7 +6619,7 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
this._zoom = result.target.zoom;
|
||||
this._view = { ...result.target.viewBox };
|
||||
this._scheduleLiveViewport();
|
||||
this._liveVp();
|
||||
}
|
||||
|
||||
private _onWheel(ev: WheelEvent): void {
|
||||
@@ -6908,7 +6908,7 @@ export class HouseplanCard extends LitElement {
|
||||
fit,
|
||||
);
|
||||
this._viewportGestureDirty = true;
|
||||
this._scheduleLiveViewport();
|
||||
this._liveVp();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6992,12 +6992,7 @@ export class HouseplanCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
if (this._decorMove?.pid === ev.pointerId) {
|
||||
this._editorRuntimeOrThrow()._flushPointerMove('decor-move');
|
||||
if (this._decorMove.moved) {
|
||||
this._recordGeometry(this._t('history.decor_move'), this._decorMove.before);
|
||||
this._saveConfig();
|
||||
}
|
||||
this._decorMove = null;
|
||||
this._editorRuntimeOrThrow()._dmUp();
|
||||
return;
|
||||
}
|
||||
if (this._editorRuntime?._furnPointerUp(ev)) return;
|
||||
@@ -11414,9 +11409,6 @@ export class HouseplanCard extends LitElement {
|
||||
const stageBg = this._editing ? '' : this._stageBg(disp);
|
||||
// opening rulers: the drag of an existing one OR the placement preview
|
||||
const opMeasure = this._opMeasureView;
|
||||
const decorMeasure = this._decorMeasure;
|
||||
const bdLive = this._bdLive;
|
||||
const furnLive = this._editorRuntime?._furnLive() ?? null;
|
||||
const editorChromeMode = this._mode === 'view' ? this._editorChromeMode : this._mode;
|
||||
const roomHover = this._roomHoverPaths(space);
|
||||
const backdropHref = space.bg ? this._display(space.bg.href) : '';
|
||||
@@ -11803,7 +11795,7 @@ export class HouseplanCard extends LitElement {
|
||||
below the snap overlay so snap highlights keep priority. */}
|
||||
${this._markup ? svg`<g class="hp-editor-only-layer"
|
||||
opacity="${modeVisual?.editorWeight ?? 1}">${this._renderActiveChainInk()}</g>` : nothing}
|
||||
${this._markup ? svg`<g class="hp-editor-only-layer"
|
||||
${this._markup ? svg`<g class="hp-editor-only-layer hp-plan-snap-layer"
|
||||
opacity="${modeVisual?.editorWeight ?? 1}">${this._renderPlanSnapOverlay()}</g>` : nothing}
|
||||
${disp.hideOpenings && !this._markup
|
||||
? nothing
|
||||
@@ -11851,46 +11843,10 @@ export class HouseplanCard extends LitElement {
|
||||
: nothing}
|
||||
${this._markup ? space.rooms.map((r) => this._renderRoomGear(r, space, view)) : nothing}
|
||||
</div>
|
||||
${this._measureAnchor
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${this._renderMeasureLabel(view)}</div>`
|
||||
: nothing}
|
||||
${this._resize?.liveLabels
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${this._resize?.liveLabels?.map((l) => html`<div
|
||||
class="measurelabel ${l.kind === 'area' ? 'rszarea' : 'rszlength'}"
|
||||
data-hp=${l.kind === 'area' ? 'resize-area-label' : 'resize-length-label'}
|
||||
data-room=${l.kind === 'area' ? l.roomId : nothing}
|
||||
data-side=${l.kind === 'area' ? l.placement.side : nothing}
|
||||
style="left:${(((l.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((l.y - view.y) / view.h) * 100).toFixed(2)}%;${l.kind === 'area'
|
||||
? `--rsz-label-x:${l.placement.offsetXPx.toFixed(2)}px;--rsz-label-y:${l.placement.offsetYPx.toFixed(2)}px;--rsz-label-tangent:${l.placement.tangentOffsetPx.toFixed(2)}px`
|
||||
: ''}">${l.text}</div>`)}</div>`
|
||||
: nothing}
|
||||
${opMeasure
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${opMeasure.labels.map((l) => html`<div
|
||||
class="measurelabel opshoulder ${l.dimension ? 'opdimension' : ''}"
|
||||
data-dimension-source=${l.dimension?.source || nothing}
|
||||
data-dimension-room=${l.dimension?.roomId || nothing}
|
||||
style="left:${(((l.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((l.y - view.y) / view.h) * 100).toFixed(2)}%;${l.dimension
|
||||
? `--op-label-shift-x:${(l.dimension.labelNormal[0] * 12).toFixed(2)}px;--op-label-shift-y:${(l.dimension.labelNormal[1] * 12).toFixed(2)}px`
|
||||
: ''}">${l.text}</div>`)}</div>`
|
||||
: nothing}
|
||||
<div data-hp-live-editor-html></div>
|
||||
${this._wallDialog
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${this._renderWallThickDialog()}</div>`
|
||||
: nothing}
|
||||
${decorMeasure
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera"><div
|
||||
class="measurelabel dmeasure ${decorMeasure.on45 ? 'on45' : ''}"
|
||||
style="left:${(((decorMeasure.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((decorMeasure.y - view.y) / view.h) * 100).toFixed(2)}%">${decorMeasure.text}</div></div>`
|
||||
: nothing}
|
||||
${furnLive
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${furnLive.map((l) => html`<div
|
||||
class="measurelabel furnmeasure"
|
||||
style="left:${(((l.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((l.y - view.y) / view.h) * 100).toFixed(2)}%">${l.text}</div>`)}</div>`
|
||||
: nothing}
|
||||
${bdLive
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera"><div
|
||||
class="measurelabel bdmeasure"
|
||||
style="left:${(((bdLive.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((bdLive.y - view.y) / view.h) * 100).toFixed(2)}%">${bdLive.text}</div></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="zoombadge" data-hp-live-zoom hidden><span data-hp-live-zoom-value></span></div>
|
||||
${this._renderFarHint()}
|
||||
@@ -12834,6 +12790,45 @@ export class HouseplanCard extends LitElement {
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
/** Pointer-owned HTML painted by the live child renderer, outside the host Lit tree. */
|
||||
public _renderLiveEditorMeasurements(value: unknown): TemplateResult | typeof nothing {
|
||||
const view = value as { x: number; y: number; w: number; h: number };
|
||||
const opMeasure = this._opMeasureView, decorMeasure = this._decorMeasure, bdLive = this._bdLive,
|
||||
furnLive = this._editorRuntime?._furnLive() ?? null;
|
||||
return html`
|
||||
${this._measureAnchor
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${this._renderMeasureLabel(view)}</div>`
|
||||
: nothing}
|
||||
${this._resize?.liveLabels
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${this._resize.liveLabels.map((l) => html`<div
|
||||
class="measurelabel ${l.kind === 'area' ? 'rszarea' : 'rszlength'}"
|
||||
data-hp=${l.kind === 'area' ? 'resize-area-label' : 'resize-length-label'} data-room=${l.kind === 'area' ? l.roomId : nothing} data-side=${l.kind === 'area' ? l.placement.side : nothing}
|
||||
style="left:${(((l.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((l.y - view.y) / view.h) * 100).toFixed(2)}%;${l.kind === 'area'
|
||||
? `--rsz-label-x:${l.placement.offsetXPx.toFixed(2)}px;--rsz-label-y:${l.placement.offsetYPx.toFixed(2)}px;--rsz-label-tangent:${l.placement.tangentOffsetPx.toFixed(2)}px`
|
||||
: ''}">${l.text}</div>`)}</div>`
|
||||
: nothing}
|
||||
${opMeasure
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${opMeasure.labels.map((l) => html`<div
|
||||
class="measurelabel opshoulder ${l.dimension ? 'opdimension' : ''}"
|
||||
data-dimension-source=${l.dimension?.source || nothing} data-dimension-room=${l.dimension?.roomId || nothing}
|
||||
style="left:${(((l.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((l.y - view.y) / view.h) * 100).toFixed(2)}%;${l.dimension
|
||||
? `--op-label-shift-x:${(l.dimension.labelNormal[0] * 12).toFixed(2)}px;--op-label-shift-y:${(l.dimension.labelNormal[1] * 12).toFixed(2)}px`
|
||||
: ''}">${l.text}</div>`)}</div>`
|
||||
: nothing}
|
||||
${decorMeasure
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera"><div class="measurelabel dmeasure ${decorMeasure.on45 ? 'on45' : ''}"
|
||||
style="left:${(((decorMeasure.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((decorMeasure.y - view.y) / view.h) * 100).toFixed(2)}%">${decorMeasure.text}</div></div>`
|
||||
: nothing}
|
||||
${furnLive
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera">${furnLive.map((l) => html`<div class="measurelabel furnmeasure"
|
||||
style="left:${(((l.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((l.y - view.y) / view.h) * 100).toFixed(2)}%">${l.text}</div>`)}</div>`
|
||||
: nothing}
|
||||
${bdLive
|
||||
? html`<div class="measurelayer" data-hp-live-layer="camera"><div class="measurelabel bdmeasure"
|
||||
style="left:${(((bdLive.x - view.x) / view.w) * 100).toFixed(2)}%;top:${(((bdLive.y - view.y) / view.h) * 100).toFixed(2)}%">${bdLive.text}</div></div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
/** Where the live measurement starts: the last outline point, or the first split point. */
|
||||
private get _measureAnchor(): number[] | null {
|
||||
@@ -13020,7 +13015,7 @@ export class HouseplanCard extends LitElement {
|
||||
const walls = this._spaceWalls;
|
||||
const openCuts = this._openCuts();
|
||||
const openingWallIndex = this._openingWallIndexFor(space, openCuts).value;
|
||||
return svg`${items.map((o) => {
|
||||
return svg`<g class="openinglayer">${items.map((o) => {
|
||||
if (o.orphanReason) return svg`<g class="opening orphan" data-hp="opening-orphan"
|
||||
data-id=${o.id} role="button" tabindex="0"
|
||||
aria-label=${this._t('opening.partition_orphan')}
|
||||
@@ -13072,7 +13067,7 @@ export class HouseplanCard extends LitElement {
|
||||
@pointerup=${(e: PointerEvent) => this._opPointerUp(e, o)}
|
||||
@pointercancel=${(e: PointerEvent) => this._opPointerUp(e, o)}></rect>
|
||||
</g>`;
|
||||
})}`;
|
||||
})}</g>`;
|
||||
}
|
||||
|
||||
/** Padlock badges for door-like openings with a lock entity. */
|
||||
|
||||
@@ -4095,7 +4095,7 @@ public _renderResizeLayer(view: { x: number; y: number; w: number; h: number }):
|
||||
parts.push(svg`<g class="rszicon ${disabled ? 'disabled' : ''}" transform="translate(${mx} ${my}) rotate(${ang})"><path class="rszhalo" d="${iconD}"></path><path class="rszink" d="${iconD}"></path></g>`);
|
||||
}
|
||||
}
|
||||
return svg`${parts}`;
|
||||
return svg`<g class="resize-layer">${parts}</g>`;
|
||||
}
|
||||
|
||||
public _partitionOpeningCuts(
|
||||
@@ -4356,9 +4356,7 @@ public _decorShapeDown(ev: PointerEvent, shape: DecorShape): void {
|
||||
}
|
||||
|
||||
public _decorMoveUpdate(ev: PointerEvent): void {
|
||||
const m = this.host._decorMove!;
|
||||
// Furniture is dragged by its CENTRE and has a magnet of its own
|
||||
// (docs/FURNITURE.md §5) — a different rule, not a different gesture.
|
||||
const m = this.host._decorMove; if (!m) return;
|
||||
if (m.orig?.kind === 'furniture') { this._furnMoveUpdate(ev); return; }
|
||||
const p = this._svgPoint(ev);
|
||||
const o0: any = m.orig;
|
||||
@@ -4396,6 +4394,11 @@ public _decorMoveUpdate(ev: PointerEvent): void {
|
||||
});
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
public _dmUp(): void {
|
||||
flushHouseplanPointerMove(this.host, 'decor-move'); const move = this.host._decorMove;
|
||||
if (move?.moved) { this._recordGeometry(this.host._t('history.decor_move'), move.before); this._saveConfig(); }
|
||||
this.host._decorMove = null; this.host.requestUpdate();
|
||||
}
|
||||
|
||||
public _decorShapeDbl(ev: MouseEvent, shape: DecorShape): void {
|
||||
if (this.host._mode !== 'decor' || this.host._decorTool !== 'select') return;
|
||||
@@ -5058,7 +5061,7 @@ public _furnPointerUp(ev: PointerEvent): boolean {
|
||||
}
|
||||
|
||||
public _furnMoveUpdate(ev: PointerEvent): void {
|
||||
const m = this.host._decorMove!;
|
||||
const m = this.host._decorMove; if (!m) return;
|
||||
if (m.orig.kind !== 'furniture') return;
|
||||
const o: any = m.orig;
|
||||
const sp = this.host._curSpaceCfg;
|
||||
@@ -6560,8 +6563,8 @@ public _opPointerUp(ev: PointerEvent, o: OpeningCfg): void {
|
||||
this._commitPhysicalGeometry(this.host._t('history.move_opening'), drag.before);
|
||||
}
|
||||
// keep the flag until the click event that follows pointerup, then let it go
|
||||
if (moved) window.setTimeout(() => (this.host._opDrag = null), 0);
|
||||
else this.host._opDrag = null;
|
||||
const finish = () => { this.host._opDrag = null; this.host.requestUpdate(); };
|
||||
if (moved) window.setTimeout(finish, 0); else finish();
|
||||
}
|
||||
|
||||
public _opClick(ev: MouseEvent, o: RenderOpening): void {
|
||||
@@ -6893,13 +6896,11 @@ public _markupMove(ev: MouseEvent): void {
|
||||
conflicts: resolved.conflicts,
|
||||
};
|
||||
this._syncPlanSnapConflictMarkers(resolved.conflicts);
|
||||
this._syncPlanSnapActiveMarker(resolved.candidate);
|
||||
// Before the first click there is no rubber-band to repaint. Updating the
|
||||
// dedicated marker avoids walking the complete large-house Lit tree for
|
||||
// every mouse move while click still resolves from the event coordinate.
|
||||
if (!this.host._path.length) {
|
||||
this._syncPlanSnapActiveMarker(resolved.candidate);
|
||||
return;
|
||||
}
|
||||
if (!this.host._path.length) return;
|
||||
this.host._cursorPt = resolved.point;
|
||||
return;
|
||||
}
|
||||
|
||||
+37
-6
@@ -4,6 +4,7 @@ import { cancelHouseplanPointerMove } from './pointer-move-queue';
|
||||
interface LiveEditorState {
|
||||
raf: number;
|
||||
hidden: HTMLElement[];
|
||||
transparent: HTMLElement[];
|
||||
}
|
||||
|
||||
interface LiveEditorHost {
|
||||
@@ -11,6 +12,7 @@ interface LiveEditorHost {
|
||||
renderRoot: ParentNode;
|
||||
requestUpdate: () => void;
|
||||
_mode: 'view' | 'plan' | 'devices' | 'decor';
|
||||
_tool: string;
|
||||
_deviceDrag: { id: string } | null;
|
||||
_physicalDrag?: unknown;
|
||||
_physicalRotate?: unknown;
|
||||
@@ -44,6 +46,7 @@ interface LiveEditorHost {
|
||||
_renderDecorLayer: () => 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[];
|
||||
}
|
||||
@@ -62,7 +65,7 @@ const gestureProperties = new Set<PropertyKey>([
|
||||
const stateOf = (host: object): LiveEditorState => {
|
||||
let state = states.get(host);
|
||||
if (!state) {
|
||||
state = { raf: 0, hidden: [] };
|
||||
state = { raf: 0, hidden: [], transparent: [] };
|
||||
states.set(host, state);
|
||||
}
|
||||
return state;
|
||||
@@ -114,9 +117,21 @@ const hide = (state: LiveEditorState, root: ParentNode, selector: string): void
|
||||
}
|
||||
};
|
||||
|
||||
/** Keep the captured source hittable while only its live copy stays visible. */
|
||||
const makeTransparent = (state: LiveEditorState, root: ParentNode, selector: string): void => {
|
||||
for (const element of root.querySelectorAll<HTMLElement>(selector)) {
|
||||
if (element.closest('[data-hp-live-editor]')) continue;
|
||||
if (element.style.opacity === '0') continue;
|
||||
element.style.opacity = '0';
|
||||
state.transparent.push(element);
|
||||
}
|
||||
};
|
||||
|
||||
const restore = (state: LiveEditorState): void => {
|
||||
for (const element of state.hidden) element.style.removeProperty('visibility');
|
||||
state.hidden.length = 0;
|
||||
for (const element of state.transparent) element.style.removeProperty('opacity');
|
||||
state.transparent.length = 0;
|
||||
};
|
||||
|
||||
const planTemplate = (host: LiveEditorHost): TemplateResult => {
|
||||
@@ -132,15 +147,18 @@ const planTemplate = (host: LiveEditorHost): TemplateResult => {
|
||||
${host._renderResizeLayer(view)}
|
||||
</g>`;
|
||||
}
|
||||
const display = host._spaceDisplayForRender();
|
||||
return svg`<g class="hp-live-plan" aria-hidden="true" pointer-events="none">
|
||||
${host._opDrag ? host._renderWallBodies(display) : nothing}
|
||||
${host._renderMarkupLayer(vb)}
|
||||
${host._renderHiddenWallDiagnosticOverlay()}
|
||||
${host._renderOpeningPlacementPreview()}
|
||||
${measure ? host._renderOpeningDimensionGuides(measure) : nothing}
|
||||
${measure?.guide ? host._renderOpeningCenterTick(measure.guide) : nothing}
|
||||
${host._renderActiveChainInk()}
|
||||
${host._renderPlanSnapOverlay()}
|
||||
${host._tool === 'draw' ? nothing : host._renderPlanSnapOverlay()}
|
||||
${host._renderWallThickUi()}
|
||||
${host._opDrag ? host._renderOpenings(display) : nothing}
|
||||
</g>`;
|
||||
};
|
||||
|
||||
@@ -182,13 +200,20 @@ export function paintHouseplanEditor(value: object): void {
|
||||
const target = root.querySelector<SVGElement>('[data-hp-live-editor]');
|
||||
if (!target) return;
|
||||
if (host._mode === 'plan') {
|
||||
hide(state, root, '.hp-editor-only-layer');
|
||||
if (host._resize?.dragging) hide(state, root, '.wallbodies');
|
||||
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) makeTransparent(state, root, '.openinglayer');
|
||||
} else if (host._mode === 'decor') {
|
||||
hide(state, root, '.decorlayer');
|
||||
hide(state, root, '.dtframe, .backdropframe');
|
||||
makeTransparent(state, root, '.decorlayer');
|
||||
makeTransparent(state, root, '.dtframe, .backdropframe');
|
||||
}
|
||||
render(editorTemplate(host), target);
|
||||
const htmlTarget = root.querySelector<HTMLElement>('[data-hp-live-editor-html]');
|
||||
if (htmlTarget) {
|
||||
const view = host._viewOr(host._baseVb());
|
||||
render(host._renderLiveEditorMeasurements(view), htmlTarget);
|
||||
}
|
||||
host._liveEditorPaintCount = (host._liveEditorPaintCount || 0) + 1;
|
||||
}
|
||||
|
||||
@@ -238,6 +263,12 @@ export function commitHouseplanEditor(value: object): void {
|
||||
const target = (host.renderRoot as ParentNode | undefined)
|
||||
?.querySelector<SVGElement>('[data-hp-live-editor]');
|
||||
if (target) render(nothing, target);
|
||||
const htmlTarget = (host.renderRoot as ParentNode | undefined)
|
||||
?.querySelector<HTMLElement>('[data-hp-live-editor-html]');
|
||||
if (htmlTarget) {
|
||||
const view = host._viewOr(host._baseVb());
|
||||
render(host._renderLiveEditorMeasurements(view), htmlTarget);
|
||||
}
|
||||
}
|
||||
|
||||
export function disposeHouseplanEditor(host: object): void {
|
||||
|
||||
@@ -35,8 +35,15 @@ export class LiveRuntime {
|
||||
const change = classifyHassRenderChange(before, after, dependencies);
|
||||
const defer = change === 'state' && this.active();
|
||||
if (defer) this.deferredHass = true;
|
||||
(this.host as LiveRuntimeHost)._renderLife.observe(before, after, dependencies, intake);
|
||||
return change !== 'none' && !defer;
|
||||
const render = change !== 'none' && !defer;
|
||||
// Preserve ReactiveElement's ordering for snapshots which are about to
|
||||
// render: willUpdate owns their intake. Tests, config editors and HA can
|
||||
// legitimately finish other same-turn mutations after assigning `hass`.
|
||||
// Only a deliberately skipped update needs operational intake here.
|
||||
if (!render) {
|
||||
(this.host as LiveRuntimeHost)._renderLife.observe(before, after, dependencies, intake);
|
||||
}
|
||||
return render;
|
||||
}
|
||||
public clear(): void { this.deferredHass = false; }
|
||||
public take(): boolean {
|
||||
@@ -44,7 +51,7 @@ export class LiveRuntime {
|
||||
this.deferredHass = false;
|
||||
return pending;
|
||||
}
|
||||
public viewport(): void { scheduleHouseplanViewport(this.host); }
|
||||
public viewport(now = false): void { scheduleHouseplanViewport(this.host, now); }
|
||||
public hover(): void { syncHouseplanHover(this.host); }
|
||||
public active(): boolean {
|
||||
const host = this.host as LiveRuntimeHost;
|
||||
|
||||
+10
-2
@@ -118,10 +118,18 @@ const frameOf = (host: LiveViewportHost): LiveViewportFrame => {
|
||||
};
|
||||
|
||||
/** Coalesce camera input into one lightweight paint per animation frame. */
|
||||
export function scheduleHouseplanViewport(value: object): void {
|
||||
export function scheduleHouseplanViewport(value: object, now = false): void {
|
||||
const host = value as LiveViewportHost;
|
||||
const state = stateOf(value);
|
||||
state.pending = frameOf(host);
|
||||
const next = frameOf(host);
|
||||
if (now) {
|
||||
if (state.raf && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(state.raf);
|
||||
state.raf = 0; state.pending = null;
|
||||
if (!state.painted) state.painted = next;
|
||||
paintLiveViewport(host.renderRoot, state.painted, next);
|
||||
return;
|
||||
}
|
||||
state.pending = next;
|
||||
if (state.raf || typeof requestAnimationFrame !== 'function') return;
|
||||
state.raf = requestAnimationFrame(() => {
|
||||
state.raf = 0;
|
||||
|
||||
+13
-14
@@ -1,8 +1,15 @@
|
||||
interface QueuedMove { raf: number; run: () => void }
|
||||
interface QueuedMove { run: () => void }
|
||||
|
||||
const moveQueues = new WeakMap<object, Map<string, QueuedMove>>();
|
||||
|
||||
/** Keep only the latest raw move of each active gesture between two frames. */
|
||||
/**
|
||||
* Keep only the latest raw move from one browser event turn.
|
||||
*
|
||||
* Gesture state must be current before a following pointerup/cancel is handled:
|
||||
* postponing the input itself until requestAnimationFrame lets the terminal
|
||||
* event clear that state first. The live renderer still coalesces the paint
|
||||
* to one animation frame; this queue only coalesces the state calculation.
|
||||
*/
|
||||
export function queueHouseplanPointerMove(host: object, key: string, run: () => void): void {
|
||||
let queues = moveQueues.get(host);
|
||||
if (!queues) {
|
||||
@@ -14,14 +21,10 @@ export function queueHouseplanPointerMove(host: object, key: string, run: () =>
|
||||
queued.run = run;
|
||||
return;
|
||||
}
|
||||
const entry: QueuedMove = { raf: 0, run };
|
||||
const entry: QueuedMove = { run };
|
||||
queues.set(key, entry);
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
queues.delete(key);
|
||||
run();
|
||||
return;
|
||||
}
|
||||
entry.raf = requestAnimationFrame(() => {
|
||||
queueMicrotask(() => {
|
||||
if (queues!.get(key) !== entry) return;
|
||||
queues!.delete(key);
|
||||
entry.run();
|
||||
});
|
||||
@@ -31,7 +34,6 @@ export function flushHouseplanPointerMove(host: object, key: string): void {
|
||||
const queues = moveQueues.get(host);
|
||||
const queued = queues?.get(key);
|
||||
if (!queued) return;
|
||||
if (queued.raf && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(queued.raf);
|
||||
queues!.delete(key);
|
||||
queued.run();
|
||||
}
|
||||
@@ -40,9 +42,6 @@ export function cancelHouseplanPointerMove(host: object, key?: string): void {
|
||||
const queues = moveQueues.get(host);
|
||||
if (!queues) return;
|
||||
const entries = key ? [...queues].filter(([name]) => name === key) : [...queues];
|
||||
for (const [name, queued] of entries) {
|
||||
if (queued.raf && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(queued.raf);
|
||||
queues.delete(name);
|
||||
}
|
||||
for (const [name] of entries) queues.delete(name);
|
||||
if (!queues.size) moveQueues.delete(host);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user