fix(plan): key room shapes by space and id so a floor switch never repaints a stranger (#742)

The flat room list was a bare map(), so Lit reused room nodes by position.
On a floor switch the previous floor's room node became the new floor's room
and `.room { transition: 0.12s }` drew its fill and stroke in from the old
computed values: one paper-white frame, then two or three frames darker than
the final fill (alpha rises while fill-opacity falls), then the fill. Between
two filled floors the fill bled in from the other floor's colour.

The list is now keyed(space.id, repeat(rooms, (r, i) => r.id || i, ...)), the
shape #534 settled on for openings and markers. The outer key handles the
floor switch, including room ids repeated on two floors (ids are unique only
within a space); the inner key keeps a node bound to its room inside a space,
where inserts, merges, splits and the editor filter shift positions. An
id-less room keys by its numeric index, which never equals a string id. The
transition itself stays: it smooths hover and a real fill change on the same
floor.

Witness: a new section of smoke_space_switch_transitions, before physicalize
(after it the first room changes template branch and the node is recreated
anyway). Red on dev: five transitions on g1, node r1 reused for g1, computed
fill rgba(0, 0, 0, 0) / 1; room nodes swapped by an insert; same-id case
reuses r1. A real custom_fill change still runs a fill transition (catches
`transition: none`). Two mutants: inner key removed, outer key removed.

Issue: #742
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:
Claude
2026-10-01 10:13:50 +00:00
committed by claude[bot]
parent 88985d2e4c
commit a0f72280ee
7 changed files with 185 additions and 9 deletions
+29
View File
@@ -9902,6 +9902,35 @@ const MUTANT_DEFINITIONS = [
replace: ' ${keyed(space.id, devs.map((d) => this._renderDevice(',
}],
},
{
id: 'rooms-rendered-without-keys',
guard: 'node demo/smoke_space_switch_transitions.mjs',
because: 'inside one space the room list still shifts (a room pushed first, merge, split, '
+ 'the editor filter): a positional map() hands each room node to its neighbour, so the '
+ 'node no longer belongs to its data-id and any fill difference replays the 0.12s .room '
+ 'transition from the neighbour\'s value (#742 AC2)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' return keyed(space.id, repeat(shownRooms, (r, index) => r.id || index, (r) => {',
replace: ' return keyed(space.id, shownRooms.map((r) => {',
}],
},
{
id: 'rooms-rendered-without-space-key',
guard: 'node demo/smoke_space_switch_transitions.mjs',
because: 'room ids are unique only inside a space: without the outer space key repeat matches '
+ 'r1 of the previous floor to r1 of the next one, keeps the node, and its fill draws in '
+ 'from the previous floor\'s computed value — the white frame of #742 (AC1, same-id case)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' return keyed(space.id, repeat(shownRooms, (r, index) => r.id || index, (r) => {',
replace: ' return repeat(shownRooms, (r, index) => r.id || index, (r) => {',
}, {
file: 'src/houseplan-card.ts',
find: ' return svg`${shape}${outline}`;\n }));\n })()}',
replace: ' return svg`${shape}${outline}`;\n });\n })()}',
}],
},
{
id: 'live-editor-devices-drops-align-guides',
guard: 'node demo/smoke_align_guides.mjs',