mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
fix(warm): revive the editor draft when write access arrives late (#756)
A same-route remount that cannot edit yet - hass arrives after the element is inserted (the demo's own order), or a non-admin waits for the server's can_write - keeps the editor in _pendingNavMode and enters it later through _resumePendingNavMode -> _setMode. The draft revival was wired only into the immediate warm adoption (_requestMode(..., adopt)): on the pending path the draft was lost, _warmRevivePending stayed up for the instance's life, _warmSnapshot stopped writing dlg, and the next remount brought back the predecessor's stale draft. The adoption tail (draft revival once under a held refit, then the settled stage as the refit baseline) is shared by both paths: the sequencing lives in src/warm-mode-adoption.ts, the refit bookkeeping in the card's _holdWarmRefit/_releaseWarmRefit. The pending mode still enters through _setMode, the transition authority smoke_nav_persist holds it to; resumeWarmMode then settles the revival. _setMode ends the passive boot grace and refits the camera to a header measured before the editor chrome rendered, so a camera the pending window left untouched is put back and held exactly as an immediate adoption holds it; a camera that has already moved on (View refit, the user's pan, another space) is left to the ordinary refit. A mode that did not commit, an explicit mode command in the pending window and a route departure settle the revival too: no outcome leaves _warmRevivePending up. The core file gives back 4 lines. smoke_warm_dialogs gains section H through the UI: the three late-write orders keep mode, draft, dirty baseline and a frame-by-frame identical viewport; the chain carries this instance's draft, not the predecessor's; a space switch in the pending window eats the draft. 14 checks are red on dev. The mutant warm-pending-mode-leaves-revive-waiting is guarded by the smoke; docs/WARM-REMOUNT.md §2 describes the pending editor. Issue: #756 User-Visible: yes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -7,8 +7,11 @@
|
||||
// 2) открытый диалог пережил пересоздание вместе с черновиком;
|
||||
// 3) осознанно закрытый (Esc) — НЕ воскресает;
|
||||
// 4) подтверждение «Выровнять всё» — НЕ воскресает никогда;
|
||||
// 5) воскрешение одноразовое: третий экземпляр диалога уже не видит.
|
||||
// ПАДАЕТ на сборке до DEV-B703-03 (вид рецентрировался, диалоги терялись).
|
||||
// 5) воскрешение одноразовое: третий экземпляр диалога уже не видит;
|
||||
// 6) (#756, раздел H) то же — когда право записи приходит позже вставки:
|
||||
// hass после вставки или через такт, не-админ с `can_write` сервера.
|
||||
// ПАДАЕТ на сборке до DEV-B703-03 (вид рецентрировался, диалоги терялись);
|
||||
// раздел H — на сборке до #756 (черновик терялся, флаг ожидания висел).
|
||||
import { launch, check, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 820, height: 760 });
|
||||
@@ -23,11 +26,15 @@ const res = await page.evaluate(async () => {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.style.cssText = 'position:fixed;left:0;top:0;width:800px;z-index:99;background:#000';
|
||||
document.body.appendChild(wrap);
|
||||
const mk = () => {
|
||||
/** `order` (#756): `before` — hass до вставки; `after` — сразу после
|
||||
* (порядок самого демо); `task` — через такт. */
|
||||
const mk = (order = 'before', mkHass = window.__mkHass) => {
|
||||
const c = document.createElement('houseplan-card');
|
||||
c.setConfig({ type: 'custom:houseplan-card' }); // Lovelace: setConfig ДО вставки
|
||||
c.hass = window.__mkHass();
|
||||
if (order === 'before') c.hass = mkHass();
|
||||
wrap.appendChild(c);
|
||||
if (order === 'after') c.hass = mkHass();
|
||||
if (order === 'task') setTimeout(() => { c.hass = mkHass(); }, 0);
|
||||
return c;
|
||||
};
|
||||
const settle = async (c) => {
|
||||
@@ -249,6 +256,80 @@ const res = await page.evaluate(async () => {
|
||||
out.gWarmReturnKeepsOnlySpace = c._space === returnSpace
|
||||
&& c._mode === 'view' && !c._markerDialog;
|
||||
|
||||
// ================= H. право записи приходит позже вставки (#756) =========
|
||||
// Новый экземпляр не может сразу вернуть редактор: hass ещё нет, или
|
||||
// не-админ ждёт `can_write` сервера. Режим ждёт в `_pendingNavMode`, и
|
||||
// черновик обязан вернуться вместе с ним — бит-в-бит, одноразово. Источник
|
||||
// каждый раз холодный, а вход в редактор, диалог и ввод — через UI.
|
||||
const nonAdmin = (configDelay = 0) => () => {
|
||||
const hass = window.__mkHass();
|
||||
const callWS = hass.callWS;
|
||||
return {
|
||||
...hass, user: { id: 'u', name: 'User', is_admin: false },
|
||||
callWS: async (m) => {
|
||||
if (configDelay && m.type === 'houseplan/config/get') await sleep(configDelay);
|
||||
return callWS(m);
|
||||
},
|
||||
};
|
||||
};
|
||||
const root = (card) => card.shadowRoot || card.renderRoot;
|
||||
const markerName = (card) => root(card).querySelector('hp-dialog[data-kind="marker"] #marker-name');
|
||||
const devicesDraft = async (name) => {
|
||||
c.remove(); await sleep(20);
|
||||
customElements.get('houseplan-card')?._warmBootReset?.();
|
||||
c = mk(); await settle(c);
|
||||
root(c).querySelector('[data-hp="mode-tab"][data-mode="devices"]')?.click();
|
||||
// источник сам выходит из мягкой фазы бута: до неё высота сцены ещё не своя
|
||||
await waitFor(() => c._mode === 'devices' && !c._modeTransitionBusy && !c._bootSoft, 4000);
|
||||
c._applyView(3.4, 430, 380); c.requestUpdate(); await c.updateComplete;
|
||||
await waitForStableView(c);
|
||||
const ref = { zoom: c._zoom, view: rect(c) };
|
||||
root(c).querySelector('[data-hp="device"][data-id]')?.click();
|
||||
await waitFor(() => !!markerName(c));
|
||||
if (markerName(c)) await window.__hpTest.input(markerName(c), name);
|
||||
await waitForStableView(c);
|
||||
return ref;
|
||||
};
|
||||
for (const [key, order, mkHass] of [
|
||||
['hAfterInsert', 'after'], ['hNextTask', 'task'], ['hNonAdminCanWrite', 'before', nonAdmin()],
|
||||
]) {
|
||||
const ref = await devicesDraft(`ЧЕРНОВИК-${key}`);
|
||||
out[`${key}Source`] = c._markerDialog?.name === `ЧЕРНОВИК-${key}` && ref.view[0] > 1 && ref.view[1] > 1;
|
||||
c.remove(); await sleep(20);
|
||||
c = mk(order, mkHass);
|
||||
out[`${key}ViewBitExact`] = await watchView(c, ref.zoom, ref.view, 1500);
|
||||
await waitFor(() => !!c._markerDialog);
|
||||
await c.updateComplete;
|
||||
out[`${key}ModeRestored`] = c._mode === 'devices';
|
||||
out[`${key}DraftSurvived`] = c._markerDialog?.name === `ЧЕРНОВИК-${key}`;
|
||||
out[`${key}DirtyBaselineSurvived`] = dialogSave(c, 'marker')?.disabled === false;
|
||||
out[`${key}ReviveSettled`] = c._warmRevivePending === false;
|
||||
if (key !== 'hAfterInsert') continue;
|
||||
// цепочка: черновик, набранный уже в этом экземпляре, — следующий ре-маунт
|
||||
// вернёт его, а не черновик предшественника
|
||||
if (markerName(c)) await window.__hpTest.input(markerName(c), 'ЧЕРНОВИК-ЦЕПОЧКА');
|
||||
await sleep(80);
|
||||
c.remove(); await sleep(20);
|
||||
c = mk();
|
||||
await waitFor(() => c._mode === 'devices' && !!c._markerDialog);
|
||||
out.hChainCarriesOwnDraft = c._markerDialog?.name === 'ЧЕРНОВИК-ЦЕПОЧКА';
|
||||
}
|
||||
// явная навигация в окне ожидания: другое пространство — черновик съеден,
|
||||
// не открыт, и флаг ожидания не висит. Сервер отвечает через 400 мс, чтобы
|
||||
// окно ожидания было, а не гонка.
|
||||
await devicesDraft('ЧЕРНОВИК-ДРУГОЕ-ПРОСТРАНСТВО');
|
||||
const draftSpace = c._space;
|
||||
c.remove(); await sleep(20);
|
||||
c = mk('before', nonAdmin(400));
|
||||
await c.updateComplete; await sleep(50);
|
||||
const otherSpace = c._model.find((sp) => sp.id !== draftSpace)?.id;
|
||||
out.hOtherSpaceFixture = !!otherSpace && c._pendingNavMode === 'devices';
|
||||
root(c).querySelector(`[data-hp="space-tab"][data-id="${otherSpace}"]`)?.click();
|
||||
await waitFor(() => c._mode === 'devices' && c._space === otherSpace && !c._warmRevivePending, 3000);
|
||||
await sleep(300); await c.updateComplete;
|
||||
out.hSpaceSwitchNotRevived = c._space === otherSpace && !c._markerDialog;
|
||||
out.hSpaceSwitchReviveSettled = c._warmRevivePending === false && c._warmSlot?.dlg === null;
|
||||
|
||||
c.remove(); wrap.remove();
|
||||
return out;
|
||||
});
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
slide. A real colour change on the same floor — a light turning on, a new
|
||||
temperature, hover — still fades smoothly
|
||||
([#742](https://github.com/Matysh/houseplan-card/issues/742)).
|
||||
- An editor dialog with unsaved input now survives a card re-creation when
|
||||
write access is confirmed late
|
||||
([#756](https://github.com/Matysh/houseplan-card/issues/756)).
|
||||
|
||||
## v1.79.0-beta.1 — 2026-09-30
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
сдвигом плана. Настоящая смена цвета на том же этаже — включился свет,
|
||||
сменилась температура, наведение — по-прежнему идёт плавно
|
||||
([#742](https://github.com/Matysh/houseplan-card/issues/742)).
|
||||
- После пересоздания карточки открытый в редакторе диалог с несохранённым
|
||||
вводом больше не пропадает, если права на запись подтверждаются с задержкой
|
||||
([#756](https://github.com/Matysh/houseplan-card/issues/756)).
|
||||
|
||||
## v1.79.0-beta.1 — 2026-09-30
|
||||
|
||||
|
||||
@@ -152,6 +152,19 @@ backend уточняет `can_write`, старый DOM редактора мож
|
||||
видимого крестика, сначала отменяет этот pending: поздний ответ сервера не
|
||||
имеет права снова открыть уже закрытый редактор (#95).
|
||||
|
||||
Отложенный редактор — та же тёплая адопция, только позже (#756). Когда право
|
||||
записи известно, `_resumePendingNavMode` входит в редактор через `_setMode`, а
|
||||
`src/warm-mode-adoption.ts` доводит адопцию тем же хвостом, что и немедленная:
|
||||
черновик из слота воскрешается один раз, refit удерживается до оседания
|
||||
редакторской панели. Вход через `_setMode` снимает мягкую фазу бута и
|
||||
подгоняет вид под шапку, измеренную ещё без панели, поэтому камера, которую
|
||||
окно ожидания не трогало, возвращается бит-в-бит. Камера, уже ушедшая сама
|
||||
(рефит Просмотра после мягкой фазы, пан, другое пространство), остаётся
|
||||
обычному рефиту, а черновик проверяется по §3 п. 4. Явная команда в окне
|
||||
ожидания снимает отложенный редактор вместе с черновиком; режим, который так и
|
||||
не зафиксировался, черновик съедает. Ни в одном исходе `_warmRevivePending` не
|
||||
остаётся поднятым.
|
||||
|
||||
Диплинк `#space=<id>` — явная навигация и по-прежнему сильнее памятки. Он
|
||||
выбирает пространство, но, как и обычный возврат на карточку, не включает
|
||||
редактор.
|
||||
|
||||
@@ -16,8 +16,8 @@ to prove that the Node witness actually kills it.
|
||||
| Paint, cascade and layer composition | 26 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
|
||||
| Pointer geometry and trusted interaction | 45 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. |
|
||||
| Responsive DOM layout | 36 | The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size. |
|
||||
| Custom-element and HA browser lifecycle | 88 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
|
||||
| **Total** | **203 / 200** | Growth above the cap fails `mutation-gate --check`. |
|
||||
| Custom-element and HA browser lifecycle | 89 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
|
||||
| **Total** | **204 / 200** | Growth above the cap fails `mutation-gate --check`. |
|
||||
|
||||
## Measured effect
|
||||
|
||||
@@ -274,6 +274,7 @@ The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy
|
||||
- `view-current-space-aria-removed`
|
||||
- `wall-face-apply-skips-overlap-guard`
|
||||
- `wallthick-hit-narrowed`
|
||||
- `warm-pending-mode-leaves-revive-waiting`
|
||||
- `writer-history-skips-finished-chain-normalization`
|
||||
- `zigbee-topology-endpoint-cleanup-skipped`
|
||||
- `zigbee-topology-endpoint-elevation-removed`
|
||||
|
||||
@@ -13522,6 +13522,18 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' // mutant: the visible draft revives without its dirty baseline',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'warm-pending-mode-leaves-revive-waiting',
|
||||
guard: 'node demo/smoke_warm_dialogs.mjs',
|
||||
because: '#756 AC1: an editor that waited for hass or the server can_write is entered by '
|
||||
+ '_resumePendingNavMode; if that branch only commits the mode, the revival keeps waiting '
|
||||
+ 'forever, the draft never opens and the stuck flag later hands the predecessor draft on',
|
||||
patches: [{
|
||||
file: 'src/warm-mode-adoption.ts',
|
||||
find: ' commit();\n if (!host._warmRevivePending) return;\n',
|
||||
replace: ' commit();\n return; // mutant: the pending branch leaves the revival waiting\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'marker-discard-without-asking',
|
||||
guard: 'node demo/smoke_device_settings_form.mjs',
|
||||
|
||||
@@ -602,6 +602,16 @@ export const SMOKE_LINKS = [
|
||||
+ 'отрезка решает, где кладка меняет глубину, а смок проверяет кладку, '
|
||||
+ 'а не толщину, и ни одного имени из резолвера не называет',
|
||||
},
|
||||
{
|
||||
// #756: хвост тёплой адопции редактора. Смоки видят его только как
|
||||
// воскрешённый черновик и покадрово неизменный `_view` после ре-маунта.
|
||||
symbols: ['finishWarmModeAdoption', 'resumeWarmMode', '_holdWarmRefit', '_releaseWarmRefit'],
|
||||
smokes: ['smoke_warm_dialogs.mjs', 'smoke_warm_remount.mjs', 'smoke_nav_persist.mjs'],
|
||||
because: '#756: the warm editor tail — draft revival once the mode is committed and the '
|
||||
+ 'refit hold on the adopted camera — is observed only as the revived draft, the '
|
||||
+ 'frame-by-frame viewport after a remount (immediate or after can_write) and the '
|
||||
+ 'pending mode entering through _setMode; no smoke names these helpers',
|
||||
},
|
||||
];
|
||||
|
||||
/** Смоки, связанные с изменёнными символами через реестр. */
|
||||
|
||||
+37
-41
@@ -242,6 +242,7 @@ import {
|
||||
type WarmEntry, type WarmViewport,
|
||||
} from './card-runtime';
|
||||
import { restoreWarmDialogBaseline, warmDialogBaseline } from './editors/dialog-baseline';
|
||||
import { finishWarmModeAdoption, resumeWarmMode, type WarmModeHost } from './warm-mode-adoption';
|
||||
|
||||
// Chromium records a FAILED module in the page module map permanently — a
|
||||
// retry of the same URL resolves from that map without touching the network.
|
||||
@@ -731,44 +732,37 @@ export class HouseplanCard extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** `adopt`: `true` — a warm remount that may edit at once; `'resume'` — a
|
||||
* pending warm editor once write access is known (#756, src/warm-mode-adoption.ts). */
|
||||
public async _requestMode(
|
||||
mode: 'view' | 'plan' | 'devices' | 'decor',
|
||||
animate = true,
|
||||
adopt = false,
|
||||
adopt: boolean | 'resume' = false,
|
||||
): Promise<void> {
|
||||
const request = ++this._editorModeRequest;
|
||||
if (adopt) {
|
||||
this._warmModeRequest = request;
|
||||
if (this._refitRaf) { cancelAnimationFrame(this._refitRaf); this._refitRaf = 0; }
|
||||
this._pendingRefitSize = null;
|
||||
}
|
||||
if (mode !== 'view' && !(await this._ensureEditorRuntime())) {
|
||||
if (adopt === true) this._holdWarmRefit(request);
|
||||
const ready = mode === 'view' || await this._ensureEditorRuntime();
|
||||
const current = ready && request === this._editorModeRequest && this.isConnected;
|
||||
if (adopt === 'resume') {
|
||||
resumeWarmMode(this as unknown as WarmModeHost, mode, () => { if (current) this._setMode(mode, animate); });
|
||||
} else if (!current) {
|
||||
if (this._warmModeRequest === request) this._warmModeRequest = 0;
|
||||
return;
|
||||
}
|
||||
if (request !== this._editorModeRequest || !this.isConnected) {
|
||||
if (this._warmModeRequest === request) this._warmModeRequest = 0;
|
||||
return;
|
||||
}
|
||||
if (adopt) {
|
||||
} else if (adopt) {
|
||||
this._adoptMode(mode);
|
||||
if (this._warmRevivePending) {
|
||||
clearTimeout(this._warmReviveTimer);
|
||||
this._warmReviveTimer = undefined;
|
||||
this._warmReviveDialog();
|
||||
}
|
||||
this.requestUpdate();
|
||||
void this.updateComplete.then(() => requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
if (this._warmModeRequest !== request || request !== this._editorModeRequest) return;
|
||||
const stage = this._stageEl;
|
||||
this._lastValidStageSize = stage && stage.clientWidth > 0 && stage.clientHeight > 0
|
||||
? [stage.clientWidth, stage.clientHeight] : null;
|
||||
this._pendingRefitSize = null;
|
||||
this._warmModeRequest = 0;
|
||||
})));
|
||||
return;
|
||||
}
|
||||
this._setMode(mode, animate);
|
||||
finishWarmModeAdoption(this as unknown as WarmModeHost, request);
|
||||
} else this._setMode(mode, animate);
|
||||
}
|
||||
/** A warm request owns the stage refit until its restored mode has settled (#756). */
|
||||
public _holdWarmRefit(request: number): void {
|
||||
this._warmModeRequest = request; this._pendingRefitSize = null;
|
||||
if (this._refitRaf) { cancelAnimationFrame(this._refitRaf); this._refitRaf = 0; }
|
||||
}
|
||||
/** Then the settled stage becomes the refit baseline; the camera stays. */
|
||||
public _releaseWarmRefit(request: number): void {
|
||||
if (this._warmModeRequest !== request || request !== this._editorModeRequest) return;
|
||||
const stage = this._stageEl; this._pendingRefitSize = null; this._warmModeRequest = 0;
|
||||
this._lastValidStageSize = stage && stage.clientWidth > 0 && stage.clientHeight > 0
|
||||
? [stage.clientWidth, stage.clientHeight] : null;
|
||||
}
|
||||
public hass?: any;
|
||||
public panelHost = false; public layout: string | null = null; public narrow: boolean | null = null;
|
||||
@@ -3471,21 +3465,21 @@ export class HouseplanCard extends LitElement {
|
||||
* dialog must not be stolen. One task later the previous instance has
|
||||
* detached (`freed`) and the snapshot is ours to consume — exactly once.
|
||||
*/
|
||||
private _warmReviveDialog(): void {
|
||||
private _warmReviveDialog(settle = false): void { // settle (#756): the editor is not coming — no more waiting
|
||||
const e = this._warmSlot; // AUD-159B1-01: OUR slot, never a neighbour's
|
||||
this._warmReviveTimer = undefined;
|
||||
clearTimeout(this._warmReviveTimer); this._warmReviveTimer = undefined;
|
||||
if (!e || !e.dlg) {
|
||||
this._warmRevivePending = false;
|
||||
return;
|
||||
}
|
||||
const d = e.dlg;
|
||||
const freed = e.freed;
|
||||
if (d.mode !== this._mode && d.mode !== 'view' && this._warmVp?.mode === d.mode
|
||||
if (!settle && d.mode !== this._mode && d.mode !== 'view' && this._warmVp?.mode === d.mode
|
||||
&& !this._editorRuntime) {
|
||||
// The dialog belongs to the editor viewport that is currently waiting
|
||||
// for its lazy runtime. Do not consume it against the temporary View;
|
||||
// `_requestMode(..., adopt=true)` revives it immediately after the mode
|
||||
// can be committed safely.
|
||||
// The dialog belongs to the editor viewport that is waiting for its lazy
|
||||
// runtime or for write access. Do not consume it against the temporary
|
||||
// View; the warm tail (src/warm-mode-adoption.ts) revives it right after
|
||||
// the mode is committed — at once or from `_resumePendingNavMode`.
|
||||
this._warmRevivePending = true;
|
||||
return;
|
||||
}
|
||||
@@ -4165,12 +4159,13 @@ export class HouseplanCard extends LitElement {
|
||||
/** Resume only a same-route warm editor intent after permissions arrive.
|
||||
* Always enter through _setMode: it owns transition state, contextual tray
|
||||
* cleanup and navigation persistence. Direct assignment leaves those
|
||||
* surfaces in mutually inconsistent modes. */
|
||||
* surfaces in mutually inconsistent modes. The waiting draft follows (#756). */
|
||||
private _resumePendingNavMode(): boolean {
|
||||
if (!this._pendingNavMode || !this._canEdit || this._config?.kiosk) return false;
|
||||
const pendingMode = this._pendingNavMode;
|
||||
this._pendingNavMode = null;
|
||||
this._setMode(pendingMode, false);
|
||||
if (!this._editorRuntime) void this._requestMode(pendingMode, false, 'resume');
|
||||
else resumeWarmMode(this as unknown as WarmModeHost, pendingMode, () => this._setMode(pendingMode, false));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7370,7 +7365,7 @@ export class HouseplanCard extends LitElement {
|
||||
// disconnect us before its first measured frame. Commit View atomically so
|
||||
// the warm tombstone never records an editor camera under `mode: view`.
|
||||
if (this._mode !== 'view') this._setMode('view', false);
|
||||
this._pendingNavMode = null;
|
||||
this._pendingNavMode = null; this._warmRevivePending = false; // the slot is sealed below
|
||||
this._geometryHistory.clear();
|
||||
this._activeWallChainId = null;
|
||||
this._wallChainSegmentCms = [];
|
||||
@@ -7441,6 +7436,7 @@ export class HouseplanCard extends LitElement {
|
||||
void this._requestMode(mode, animate);
|
||||
return;
|
||||
}
|
||||
if (this._pendingNavMode && this._warmRevivePending) this._warmReviveDialog(true); // #95: the draft goes with the editor
|
||||
this._editorRuntime.stairs.beforeModeChange(mode); this._editorRuntime._setMode(mode, animate);
|
||||
this._editorRuntime.stairs.afterModeChange(); this._stairsView.clearGesture();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* The tail of a warm editor restore (docs/WARM-REMOUNT.md §2, §3).
|
||||
*
|
||||
* A same-route re-mount brings back the editor in one of two ways: at once,
|
||||
* when the new instance may already edit (`_requestMode(..., adopt=true)`),
|
||||
* or later, when the editor waited in `_pendingNavMode` for `hass` or for the
|
||||
* server's `can_write` (`_resumePendingNavMode`). Both end the same way: the
|
||||
* draft the dead instance left in the slot is revived exactly once, and the
|
||||
* adopted camera is not refitted by the editor chrome that lands with the
|
||||
* mode (#756).
|
||||
*/
|
||||
import type { HouseplanMode } from './mode-transition';
|
||||
|
||||
type ViewRect = { x: number; y: number; w: number; h: number };
|
||||
|
||||
export interface WarmModeHost {
|
||||
readonly updateComplete: Promise<unknown>;
|
||||
readonly _mode: HouseplanMode;
|
||||
readonly _warmVp: { view: ViewRect | null } | null;
|
||||
readonly _warmRevivePending: boolean;
|
||||
_view: ViewRect | null;
|
||||
_editorModeRequest: number;
|
||||
_holdWarmRefit(request: number): void;
|
||||
_releaseWarmRefit(request: number): void;
|
||||
_warmReviveDialog(settle?: boolean): void;
|
||||
_liveVp(): void;
|
||||
requestUpdate(): void;
|
||||
}
|
||||
|
||||
/** The mode is committed under a held refit: revive the waiting draft once,
|
||||
* then release the refit two frames after the restored mode has rendered. */
|
||||
export function finishWarmModeAdoption(host: WarmModeHost, request: number): void {
|
||||
if (host._warmRevivePending) host._warmReviveDialog();
|
||||
host.requestUpdate();
|
||||
void host.updateComplete.then(() => requestAnimationFrame(() => requestAnimationFrame(
|
||||
() => host._releaseWarmRefit(request),
|
||||
)));
|
||||
}
|
||||
|
||||
const sameView = (a: ViewRect | null, b: ViewRect | null | undefined): boolean => !!a && !!b
|
||||
&& a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
|
||||
|
||||
/**
|
||||
* Commit a pending warm mode through `commit` (`_setMode`, the transition
|
||||
* authority — smoke_nav_persist), then settle the waiting draft revival.
|
||||
*
|
||||
* Entering the editor this way ends the passive boot grace and refits the
|
||||
* camera to the header measured BEFORE the editor chrome rendered. While the
|
||||
* pending window still shows the adopted memo camera, that camera is put
|
||||
* back and held exactly as an immediate adoption holds it. A camera that has
|
||||
* already moved on (a View refit, the user's own pan, another space) is left
|
||||
* to the ordinary refit. A mode that did not commit eats the draft (§3 p. 4).
|
||||
*/
|
||||
export function resumeWarmMode(host: WarmModeHost, mode: HouseplanMode, commit: () => void): void {
|
||||
const view = host._view;
|
||||
const kept = sameView(view, host._warmVp?.view);
|
||||
commit();
|
||||
if (!host._warmRevivePending) return;
|
||||
if (host._mode !== mode || !kept) { host._warmReviveDialog(true); return; }
|
||||
const request = ++host._editorModeRequest;
|
||||
host._holdWarmRefit(request);
|
||||
host._view = view; host._liveVp();
|
||||
finishWarmModeAdoption(host, request);
|
||||
}
|
||||
Reference in New Issue
Block a user