mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
HP-1543: editor-exit zoom after cross-floor switch; motion flash restart on rapid retrip
HP-1543-01 (medium): exiting an editor AFTER switching floors inside it left the editor working zoom on screen in view mode — the snapshot guard (snap.space) dropped the restore and nothing else put the viewport back. A space-mismatched exit now falls back to _restoreZoom() of the current floor's saved view zoom (per-space store/LS_ZOOM were never polluted, the view-only _saveZoom guard is untouched). Regression: crossFloor* asserts in demo/smoke_zoom_out.mjs — red on v1.54.3, green now. HP-1543-02 (low): a rapid off->on retrip before the current flash ended did not restart the CSS animation: the senseflash class and animation-name never changed, so the browser kept the old timeline and the second detection played nothing once the first one-shot had finished (base opacity 0). Every witnessed trip now bumps a generation counter; its parity alternates the identical keyframes hp-sense / hp-sense-b via the .sf2 marker — a new animation identity forces a fresh timeline per detection. flashTs and the window timer re-arm as before; prefers-reduced-motion keeps the static ring (retrip only extends the window). Regression: rapid* asserts in demo/smoke_motion_sense.mjs — red on v1.54.3, green now.
This commit is contained in:
+30
-5
@@ -362,7 +362,7 @@ class HouseplanCard extends LitElement {
|
||||
the flash is keyed to the TRANSITION, not to the 'on' state. `timer`
|
||||
is the one setTimeout per entry that repaints the card when the flash
|
||||
window closes (cleared in disconnectedCallback). */
|
||||
private _senseRt = new Map<string, { last: string; flashTs: number; timer: number }>();
|
||||
private _senseRt = new Map<string, { last: string; flashTs: number; timer: number; gen: number }>();
|
||||
/** live-vacuum runtime per marker: RAW robot coords (matrix applied at render) */
|
||||
private _vacRt = new Map<string, { trail: VacPt[]; lastKey: string; lastTs: number;
|
||||
moving: boolean; jump: boolean; endedTs: number; lastPos: VacPt | null }>();
|
||||
@@ -1331,7 +1331,12 @@ class HouseplanCard extends LitElement {
|
||||
// the state has not changed. A new off→on trip re-arms the flash.
|
||||
if (dc === 'motion') {
|
||||
const rt = this._senseRt.get(d.id);
|
||||
return rt && rt.flashTs && Date.now() - rt.flashTs < SENSE_FLASH_MS ? 'senseflash' : '';
|
||||
if (!(rt && rt.flashTs && Date.now() - rt.flashTs < SENSE_FLASH_MS)) return '';
|
||||
// HP-1543-02: alternate the animation identity per trip (see
|
||||
// _senseTick) — odd generations ride the base hp-sense keyframes,
|
||||
// even ones the identical hp-sense-b twin, so a rapid re-trip
|
||||
// restarts the flash instead of silently inheriting the old timeline
|
||||
return rt.gen % 2 === 0 ? 'senseflash sf2' : 'senseflash';
|
||||
}
|
||||
// OCCUPANCY/PRESENCE while 'on': a calm STATIC ring ('sensehold',
|
||||
// no animation in styles.ts) — «комната обитаема» is a state, not
|
||||
@@ -1923,8 +1928,7 @@ class HouseplanCard extends LitElement {
|
||||
if (mode === 'view') {
|
||||
const snap = this._viewModeSnap;
|
||||
this._viewModeSnap = null;
|
||||
// restore only for the space the snapshot was taken in — after a space
|
||||
// switch inside the editor the saved per-space zoom already applies
|
||||
// restore the snapshot only for the space it was taken in
|
||||
if (snap && snap.space === this._space) {
|
||||
this._zoom = snap.zoom;
|
||||
this._view = null;
|
||||
@@ -1934,6 +1938,17 @@ class HouseplanCard extends LitElement {
|
||||
this._saveZoom(); // editor wheel zoom wrote itself to LS_ZOOM — put the view zoom back
|
||||
this.requestUpdate();
|
||||
});
|
||||
} else if (snap) {
|
||||
// HP-1543-01: the floor CHANGED inside the editor — the snapshot
|
||||
// belongs to the floor the editor was entered from, while _zoom still
|
||||
// carries the editor's working zoom for the floor we are exiting on.
|
||||
// Dropping the snapshot alone left that editor zoom (say 500%) on
|
||||
// screen in view mode. The per-space store was never polluted
|
||||
// (_saveZoom is view-only), so the standard centred
|
||||
// _restoreZoom() puts back this floor's saved VIEW viewport. Its rAF
|
||||
// reads _zoomBySpace, not the snapshot, so the same-tick floor-tab
|
||||
// race the view-only guard protects against stays closed.
|
||||
this._restoreZoom();
|
||||
}
|
||||
}
|
||||
this._path = [];
|
||||
@@ -4462,9 +4477,19 @@ class HouseplanCard extends LitElement {
|
||||
const p = this.hass.states[d.primary];
|
||||
if (p?.attributes?.device_class !== 'motion') continue;
|
||||
const rt = this._senseRt.get(d.id);
|
||||
if (!rt) { this._senseRt.set(d.id, { last: p.state, flashTs: 0, timer: 0 }); continue; }
|
||||
if (!rt) { this._senseRt.set(d.id, { last: p.state, flashTs: 0, timer: 0, gen: 0 }); continue; }
|
||||
if (p.state === 'on' && rt.last === 'off') {
|
||||
rt.flashTs = Date.now();
|
||||
// HP-1543-02: a retrip BEFORE the previous flash finished kept the
|
||||
// same 'senseflash' class and the same animation-name on the same
|
||||
// pseudo-element — browsers never restart such a CSS animation, so
|
||||
// the second detection played nothing once the first one-shot had
|
||||
// ended (base opacity 0). Every trip bumps the generation;
|
||||
// _stateClass maps its parity to alternating keyframe names
|
||||
// (hp-sense / hp-sense-b in styles.ts), which is a NEW animation
|
||||
// identity and forces a fresh timeline per detection. No class is
|
||||
// ever removed mid-flash, so a lone first flash still plays whole.
|
||||
rt.gen++;
|
||||
clearTimeout(rt.timer);
|
||||
rt.timer = window.setTimeout(() => this.requestUpdate(), SENSE_FLASH_MS + 60);
|
||||
}
|
||||
|
||||
+16
-1
@@ -907,6 +907,13 @@ export const cardStyles = css`
|
||||
animation: hp-sense 1.1s ease-in-out 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* HP-1543-02: a rapid off→on retrip must RESTART the flash. The card
|
||||
flips the .sf2 marker on every trip (_senseTick generation parity);
|
||||
hp-sense-b duplicates hp-sense exactly — switching the animation-name
|
||||
is what makes the browser abandon the old timeline and start a fresh
|
||||
one on the same pseudo-element (a same-name animation never restarts
|
||||
while the class stays on). */
|
||||
.dev.senseflash.sf2::after { animation-name: hp-sense-b; }
|
||||
/* OCCUPANCY/PRESENCE while 'on': a calm STATIC ring, no animation —
|
||||
«комната обитаема» is a state, not an event, so it must not blink.
|
||||
Brightness matches the reduced-motion variant of the flash. */
|
||||
@@ -924,6 +931,13 @@ export const cardStyles = css`
|
||||
60% { transform: scale(1.12); opacity: 0.12; }
|
||||
100% { transform: scale(1.12); opacity: 0; }
|
||||
}
|
||||
/* identical twin of hp-sense — exists ONLY as the alternate animation
|
||||
identity for the retrip restart (HP-1543-02), keep the two in sync */
|
||||
@keyframes hp-sense-b {
|
||||
0% { transform: scale(0.9); opacity: 0.5; }
|
||||
60% { transform: scale(1.12); opacity: 0.12; }
|
||||
100% { transform: scale(1.12); opacity: 0; }
|
||||
}
|
||||
/* alarms pulse red over everything */
|
||||
.dev.alarm::after {
|
||||
content: '';
|
||||
@@ -943,7 +957,8 @@ export const cardStyles = css`
|
||||
/* the motion flash becomes a static ring for the same ~3.3s class
|
||||
window — consistent with alarm's treatment; sensehold is static
|
||||
by design already */
|
||||
.dev.senseflash::after { animation: none; opacity: 0.4; }
|
||||
.dev.senseflash::after,
|
||||
.dev.senseflash.sf2::after { animation: none; opacity: 0.4; } /* HP-1543-02: retrip re-arms the window, ring stays static */
|
||||
}
|
||||
.dev .newdot {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user