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
+5 -2
View File
@@ -10946,7 +10946,10 @@ export class HouseplanCard extends LitElement {
};
const otherPolys = (rr: any) =>
space.rooms.filter((o) => o !== rr).map(polyOf).filter(Boolean) as number[][][];
return space.rooms.filter((r) => r.area || this._mode === 'view' || this._markup || disp.showBorders).map((r) => {
const shownRooms = space.rooms.filter((r) => r.area || this._mode === 'view' || this._markup || disp.showBorders);
// #742: keyed like openings and markers (plan.styles.ts, #525/#534/#742): a bare map() handed the
// previous floor's room node to the new floor, and `.room`'s transition drew its fill in from white.
return keyed(space.id, repeat(shownRooms, (r, index) => r.id || index, (r) => {
let cls = 'room ' + (space.bg ? 'overlay' : 'yard') + (this._markup ? ' outlined' : '');
if (this._markup && (r.id === this._mergeSel || r.id === this._splitSel?.roomId))
cls += ' picked';
@@ -11060,7 +11063,7 @@ export class HouseplanCard extends LitElement {
style=${this._markup ? nothing : `stroke:${disp.color};stroke-opacity:${disp.showBorders ? disp.opacity : 0}`}></path>`
: nothing;
return svg`${shape}${outline}`;
});
}));
})()}
${this._renderRoomHoverFill(roomHover)}
${this._renderOpeningTunnelFills(space, roomFills)}
+14 -1
View File
@@ -490,7 +490,20 @@ export const planStyles = css`
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. */
different door.
#742: the room shapes share the rule. .room above transitions every
property for 0.12 s, and the flat room list was a bare map(): on a floor
switch the previous floor's room node became the new floor's room, and
its fill drew in from the stranger's value — one paper-white frame, then
darker than the fill. The list is now keyed(space.id, repeat(rooms,
(r, i) => r.id || i, ...)) for the same two reasons: room ids are unique
only inside a space, and inside a space insert, merge, split and the
editor's filter shift positions. An id-less room (the backend accepts
an empty id) keys by its index: a number never equals a string id in
repeat's key map. The transition itself stays — it is hover and a real
fill change on the same floor.
Witnesses: demo/smoke_space_switch_transitions.mjs. */
.op-leaf {
transition: transform 0.6s ease;
}