fix: no default-fit flash when opening with a saved zoom (HP-1551)

The cached config (LS_CFG) painted its first frames with _zoom=1 because
the saved per-space zoom was applied only by _restoreZoom()'s rAF after
the server config round-trip - the plan visibly jumped to the saved scale.

- setConfig now arms _zoom from LS_ZOOM for the resolved space before the
  first view computation (view mode, no established view only);
- _restoreZoom applies the view synchronously when the stage is already
  measured (space tabs, kiosk carousel, editor exit included); the rAF
  path remains only for an unmeasured stage, where updated() already fits
  with the correct zoom before the first paint.

New smoke_zoom_flash.mjs samples every rAF frame from the first possible
moment with a primed config cache, saved zoom 1.8 and a 350 ms server
delay: a visible stage with a default-scale viewBox fails the run
(22 such frames before the fix, 0 after).
This commit is contained in:
Matysh
2026-08-02 10:08:04 +03:00
parent 8b439ea0f3
commit 9bcfebe8f4
5 changed files with 179 additions and 48 deletions
+21
View File
@@ -684,6 +684,13 @@ class HouseplanCard extends LitElement {
} catch {
/* ignore */
}
// HP-1551: the saved per-space zoom used to be applied only by
// _restoreZoom()'s rAF after the server round-trip, so the cached config
// painted its first frames at the default fit and the plan visibly
// jumped to the saved zoom. The zoom store is already in hand here —
// arm it BEFORE the first view computation, so the very first paint is
// already at the user's zoom.
if (this._mode === 'view' && !this._view) this._zoom = this._zoomBySpace[this._space] || 1;
}
/** Save a snapshot of the config+layout to localStorage for an instant start. */
@@ -1643,6 +1650,20 @@ class HouseplanCard extends LitElement {
private _restoreZoom(): void {
const z = this._zoomBySpace[this._space] || 1;
this._zoom = z;
const stage = this._stageEl;
if (stage && stage.clientHeight) {
// HP-1551: the stage is already measured — apply the view NOW, before
// the next paint. The unconditional rAF here used to let one frame
// (or a whole server round-trip worth of frames, via the updated()
// refit running with the stale zoom before this method was called at
// all) paint at the default fit — the visible "flash" on opening.
const vb = this._baseVb();
this._applyView(z, vb[0] + vb[2] / 2, vb[1] + vb[3] / 2);
this.requestUpdate();
return;
}
// stage not measurable yet: let updated() fit it with the (already
// correct) _zoom on the first layout, then center on the plan
this._view = null;
requestAnimationFrame(() => {
if (!this._stageEl) return;