Смена пространства выбрасывает слой, а не сверяет его поэлементно

#525 увёл оба списка сцены с `map()` на `repeat(items, (item) => item.id, …)`,
чтобы Lit не переиспользовал узлы по позиции и не проигрывал анимацию двери,
которой не было. Правка верная, но на плане с двумя сотнями маркеров она
оказалась дорогой ровно там, где пользы не приносит: при смене пространства
ключи не пересекаются вовсе, и `repeat` строит две карты ключей и обходит оба
списка, чтобы затем всё равно выбросить всё и создать заново.

Бисект по медиане `switchCycleMs` на `large-house`: 871,2 перед #525 → 953,5
после. На раннере эти 80 мс распадаются на шесть-девять дополнительных длинных
задач, и `longTask.countP95` вышел за порог стабильного гейта.

Теперь оба списка рендерятся как `keyed(space.id, repeat(…))`. Внешний ключ
делает смену пространства: поддерево выбрасывается целиком, дифа нет. Внутренний
остаётся, потому что состав списков едет и внутри пространства — у маркеров от
призраков редактора и живого синка, у проёмов от записи с нерешённым хостом,
которая живёт только в режиме plan, — а переходы на `.device-shell-frame`,
`.op-leaf` и `.op-arc` никуда не делись.

Замер после правки: `switchCycleMs` 889,1 против 936,0 на `main` и 871,2 до
#525. Потолок карточки поднят на одну строку — на `import { keyed }`;
переносить нечего, сам рендер не вырос ни на символ.

Issue: #534
User-Visible: yes
This commit is contained in:
Codex
2026-09-11 22:30:11 +03:00
parent a9a6d37bba
commit 5afdb644c7
55 changed files with 650 additions and 563 deletions
+5 -4
View File
@@ -9,6 +9,7 @@
import { LitElement, html, svg, nothing, noChange, TemplateResult, PropertyValues, type PropertyDeclaration } from 'lit';
import { cache as litCache } from 'lit/directives/cache.js';
import { guard } from 'lit/directives/guard.js';
import { keyed } from 'lit/directives/keyed.js';
import { repeat } from 'lit/directives/repeat.js';
import './hp-dialog';
import type { HpDialog } from './hp-dialog';
@@ -11784,9 +11785,9 @@ export class HouseplanCard extends LitElement {
view,
(point) => this._scenePoint(point),
)}
${repeat(devs, (d) => d.id, (d) => this._renderDevice(
${keyed(space.id, repeat(devs, (d) => d.id, (d) => this._renderDevice(
d, view, showLqi, isoOverlays?.devices.get(d.id),
))}
)))}
${this._renderVacuums(this._renderVacuumDevices, view, space.id)}
${this._renderVacFit(view)}
${this._renderOpeningLocks(view, isoOverlays?.locks)}
@@ -12993,7 +12994,7 @@ export class HouseplanCard extends LitElement {
const walls = this._spaceWalls;
const openCuts = this._openCuts();
const openingWallIndex = this._openingWallIndexFor(space, openCuts).value;
return svg`<g class="openinglayer">${repeat(items, (o) => o.id, (o) => {
return svg`<g class="openinglayer">${keyed(space.id, repeat(items, (o) => o.id, (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')}
@@ -13045,7 +13046,7 @@ export class HouseplanCard extends LitElement {
@pointerup=${(e: PointerEvent) => this._opPointerUp(e, o)}
@pointercancel=${(e: PointerEvent) => this._opPointerUp(e, o)}></rect>
</g>`;
})}</g>`;
}))}</g>`;
}
/** Padlock badges for door-like openings with a lock entity. */
+3
View File
@@ -210,6 +210,9 @@ export const devicesStyles = css`
background: transparent;
box-shadow: var(--device-shell-shadow);
/* no box-shadow here: cqw-sized, restarts on every container resize (#524) */
/* these two are why the marker list keeps a per-item key under the
space-level one (#525, #534): ghosts and live config sync shift
positions inside one space — see plan.styles.ts for the full note */
transition: border-color .15s, opacity .2s;
pointer-events: auto;
/* Normative production fallback: never add a per-marker backdrop blur. */
+11 -1
View File
@@ -539,7 +539,17 @@ export const planStyles = css`
Anything animated that is rendered from a list needs a key. The Glow
spots learned it first (glow-scene.ts, opacity), the openings and the
device markers (.device-shell-frame, box-shadow) with this issue.
Witness: demo/smoke_space_switch_transitions.mjs. */
#534: the shape is now keyed(space.id, repeat(items, (o) => o.id, ...)),
and BOTH halves are load-bearing. The outer key does the space switch:
there the key sets are disjoint, so repeat would build two key maps and
walk both lists only to throw everything away — 80 ms of it on a plan
with two hundred markers. The inner key does everything inside one
space, where the list composition still changes underneath: an opening
with an unresolved host is listed ONLY in plan mode (_openingsR), so
entering and leaving the editor shifts the positions of the rest.
Removing either half brings the phantom animation back through a
different door. Witnesses: demo/smoke_space_switch_transitions.mjs. */
.op-leaf {
transition: transform 0.6s ease;
}