7.7 KiB
Open spans + wall-centric Delete — design
Superseded on 2026-08-06 by the accepted UX-02 decision: opening a boundary, closing a boundary, merging rooms, and deleting a room are separate tools. This file remains only as the historical rationale for the original open-span implementation; current work is tracked in GitHub Issues + Project v2.
Status: approved in chat (2026-08-05). Implementation plan follows after spec review.
Goal
Replace whole-boundary open-wall toggle with partial virtual stretches (anchor → second point on a shared wall), and make Delete operate on walls (close virtual → merge / delete room) with confirmation.
Current behaviour (baseline)
- Tool «Открытая граница»: one click toggles
room.open_tofor a room pair; allsharedBoundarystretches between the pair become virtual. - Tool «Удалить»: click inside a room → confirm → delete room.
- No geometric open-span storage; light zones use
open_toonly.
Decisions (from brainstorming)
| Topic | Decision |
|---|---|
| Data model | Approach 1: space.open_spans + keep open_to as connectivity index |
| Open target | Shared wall only; outer wall → toast |
| Gesture | Click1 = anchor P1; click2 = P2 on same edge; P2 clamped to nearest corners either side of P1 |
| Cursor (openwall) | crosshair (same as Draw), not pointer |
| Snap | Corner / existing open joint first; else grid projected onto the wall |
| Close virtual | Both openwall (click on span) and Delete (first hit on span) |
| Thickness on open | Clear walls[] cm on covered keys immediately |
| Thickness on close | Inherit cm from remaining solid collinear part of same wall; if none → DRAW_WALL_DEFAULT_CM (15 cm / imperial) |
| Delete + virtual | First click closes span (no confirm) |
| Delete + shared solid | Confirm → merge the two rooms along the entire shared boundary |
| Delete + outer wall | Confirm → delete room |
| Delete + inside room | Same as outer: confirm → delete room |
| Merge / delete room | Always confirm() |
| Openings on virtual | Forbidden; remove any openings on a span when it becomes virtual; cannot place new ones there |
Legacy open_to |
On read, treat full sharedBoundary of the pair as open spans; persist open_spans on first save/edit |
§1 Data model
Storage (per space)
open_spans: Array<{ key, … }>— geometric virtual stretches. Keying aligns withwalls[](quantised midpoint + direction mod 180°). Store enough to recover segment endpoints after rekey (explicita/bnormalised coords and/or length along the edge — implementation detail in the plan).rooms[].open_to: string[]— symmetric room-id links foropenZoneOf/ glow (unchanged consumer API).
Invariants
- A span exists only on a shared boundary of exactly two rooms.
- While a pair has ≥1 span → both rooms list each other in
open_to. Removing the last span clears the link. - An open span and a positive
walls[]thickness on the same covered keys do not coexist: opening clears thickness; closing restores per rules above. - Legacy:
open_topresent,open_spansabsent/empty → read path expands each linked pair to its fullsharedBoundarysegment list as virtual spans; write them intoopen_spanson the next config save that touches the space (or any openwall/delete edit). - Geometry lifecycle: resize / undo / scale / split / merge rekey spans like walls; unmatched keys degrade silently; merge of a pair drops spans between those two rooms.
§2 Gestures
Tool: Open boundary (openwall)
- Hover on a shared solid wall: highlight eligible wall; cursor crosshair. Outer wall: no “hot” affordance beyond default crosshair; click → toast (shared only).
- Click1: anchor P1 on the wall (snap: corner/joint → else grid on edge). Remember the edge and nearest corners L, R along that edge.
- Preview: segment P1→cursor, clamped to [L, R] on the same edge.
- Click2: P2 in [L, R]. If length below minimum → toast, clear anchor.
Else write span, clear thickness on covered keys, remove openings on the
span, refresh
open_to. - Click on existing virtual span: close that span; restore thickness (neighbour solid / default 15); no confirm.
- Esc, click away, tool change, leave markup: clear in-progress anchor.
Tool: Delete (delroom — wall-centric)
Hit priority: virtual span → shared solid wall → outer wall → point inside room.
| Hit | Action |
|---|---|
| Virtual span | Close span (no confirm), same as openwall |
| Shared solid wall | Confirm → merge both rooms (entire shared boundary) |
| Outer wall | Confirm → delete the room that owns that outer edge |
| Inside room | Confirm → delete that room |
| Miss | No-op (optional light toast) |
After closing a virtual span, a later Delete click on that now-solid shared wall runs the merge path (with confirm).
§3 Thickness, openings, errors, tests
Thickness
- Opening a span: strip
walls[]entries (or null cm) for keys covered by the span (atomic collinear pieces under the segment). - Closing a span: if a collinear solid remainder of the same wall still has cm,
copy that cm onto the newly solid stretch; else apply
DRAW_WALL_DEFAULT_CM. - Wall-thickness tool continues to refuse open stretches (toast).
Openings (doors / windows)
- Not allowed on virtual spans.
- When a stretch becomes virtual: delete any openings whose centre lies on that stretch (same association rules as wall bodies / angle-aware hit).
- Opening tool: refuse placement on a virtual span (toast).
Errors / cancel
- Outer / not-a-wall / too short / click2 off the anchored edge → toast and clear the in-progress anchor.
- Merge and room delete always go through
confirm(); cancel → no-op. - Saves stay optimistic like other plan edits.
Testing
Unit
- Clamp P2 to nearest corners; min length.
- Span write ↔
open_toadd/remove. - Thickness clear on open; restore from neighbour / default 15 on close.
- Legacy
open_to→ virtual full sharedBoundary spans. - Rekey after edge move / scale; degrade orphans.
- Opening removal when span opens; placement blocked on virtual.
Smoke
- Anchor + P2 → dashed span; glow crosses only when span links rooms.
- Close via openwall and via Delete.
- Delete merge / delete-room confirms; outer open attempt toasts.
- Crosshair cursor in openwall tool.
Docs / i18n
- Update
docs/ARCHITECTURE.md(open boundaries),WALL-THICKNESS.md(open refuses thickness + clear on open),UX-MODES/ TESTING hints, CHANGELOG. - New toasts: shared-only, short segment, openings removed, delete/merge
confirms copy; refresh
title.markup_openwall/delroomhints.
Out of scope
- Partial merge along only the former virtual segment (explicitly rejected: merge is whole shared boundary).
- Outer-wall virtual openings (“to outdoors”).
- Restoring thickness from pre-open snapshot (neighbour / default only).
- Changing glow math beyond “any span ⇒ rooms linked”.
Implementation sketch (not a plan)
- Pure helpers: expand legacy, clamp segment, span CRUD, thickness clear/restore, opening purge on span, rekey/degrade.
- Card: openwall two-click state machine; Delete hit priority + confirms.
- Render: dashed strokes from
open_spans(fallback legacy expand); trim outlines/cuts from span list. - Smokes + unit tests; docs/i18n; version bump when shipping.
Open points for the implementation plan only
- Exact JSON shape of one
open_spansentry (keyonly vskey+a+b). - Minimum segment length (e.g.
gridPitchor fixed cm). - Whether Delete merge reuses the existing merge-tool code path verbatim.