# Implementation Plan: Room Editor Refactor (Core & Logic) **Objective:** Fix the pixel vs. grid-unit mismatch in stored room data, and robustify the editor for professional room planning. **Background:** The editor (`RoomCanvas.svelte`) already stores and renders in grid units (1 unit = 40 px). However, the demo seed (`demo_seed.sql`) was written with raw pixel values (e.g. `width: 200`), causing demo Room A to render broken (200 grid units = 8000 px). Any room created via the editor since launch is correct; any room predating the editor's grid-unit switch (or the demo room) is broken. A one-time data migration is the first priority. **Note on the `type`/`kind` field:** `backend/src/models.rs:81` already bridges this with `#[serde(rename = "type")] pub kind: String`. The wire format is `type` and `frontend/src/lib/types.ts:33` already uses `type`. **No rename is needed.** If the Rust internal name is ever changed to `type`, a raw identifier (`r#type`) is required since `type` is reserved. **Note on backend validation:** `backend/src/routes/rooms.rs:18–69` already implements `validate_layout` with empty-layout check, unique IDs, allowed types (`seat`, `table`, `gap`, `door`), unique seat labels, and non-negative geometry. Tests at lines 184–322 cover all of it. **Task 2 below replaces the previously planned duplicate work.** **Note on `SeatMap.svelte`:** This plan does **not** touch `SeatMap.svelte`. Its retirement and replacement with a dynamic renderer is handled by the sibling visualization plan. Any `LayoutElement` contract change made here must be cross-checked against `backend/src/routes/checkin.rs:53,194` (which deserialises it) and `frontend/src/lib/types.ts:86` (`CheckinInfo.layout`). --- ## 1. Data Migration & Seed Fix ### Task 1: Pixel → Grid-Unit Migration **Files to Modify:** - `backend/migrations/003_normalize_room_layout_units.sql` *(create)* - `backend/demo/demo_seed.sql` - `backend/src/routes/rooms.rs` (update tests that assert large numeric coordinates) **Changes:** - Write `003_normalize_room_layout_units.sql`. For each row in `rooms`, parse `layout_json`; if any element has `x`, `y`, `width`, or `height` > 50, divide all four by 40 and update the row. This heuristic is safe because grid-unit values are small integers/half-steps (max ~30), while pixel values are large (typically 80–800). - Update `demo_seed.sql:16–41` to use grid units (e.g. `width: 200` → `width: 5`). The 24 elements in demo Room A need to be re-measured in grid units. - Update any integration tests in `rooms.rs` that rely on large pixel-scale layout values. ### Task 2: Backend Validation (Scope Reduction) **Files to Modify:** - `backend/src/routes/rooms.rs` **Changes (additive only — do not duplicate existing logic):** - Add upper-bound validation: `x` and `y` must be < a `MAX_CANVAS` constant (e.g. 100 grid units). Reject elements that fall off the canvas. - Add grid-step validation: `x`, `y`, `width`, `height` must be multiples of 0.5 (i.e. `(value * 2) % 1 == 0`). Apply post-migration so existing data has already been normalised. - Add a test for each new validator. --- ## 2. Editor Core Refactor ### Task 3: RoomCanvas State & Behaviour **Files to Modify:** - `frontend/src/lib/RoomCanvas.svelte` **Current state (188 lines):** - Drag: `draggingId / startX / startY` only (lines 26–28). No resize state or handles exist. - Snap: lines 47–48 snap to 0.25 grid units (`Math.round(.../10)*10/40`). This is partially correct but the increment should be configurable (0.5 default). - Rendering: already multiplies by `GRID_SIZE = 40` (line 85). Unit separation is mostly correct. - Bug: `onmousemove` / `onmouseup` are bound on the SVG only (lines 69–71). Releasing the cursor outside the SVG strands the drag. Move these listeners to `window` for the duration of a drag. **Changes:** - **Build resize from scratch.** Add resize handles (e.g. bottom-right corner hit area) per element. Track `resizingId`, `resizeStartX`, `resizeStartY`, `resizeStartW`, `resizeStartH` as drag state. Snap resize delta to 0.5 increments. - **Fix drag escape.** Bind `mousemove`/`mouseup` to `window` when dragging begins; remove them on drop. - **Snap increment.** Change snap to 0.5 grid units (from 0.25). Accept an optional `snapStep` prop (default `0.5`) for the snap-toggle feature below. ### Task 4: Editor UI Improvements **Files to Modify:** - `frontend/src/routes/admin/rooms/[roomId]/+page.svelte` **What already exists (do not re-add):** - Width/height inputs with `step="0.5"` (lines 90–97) - Label input (line 87) - Add seat/table/door buttons (lines 64–66) - Delete button (line 101) **What to add:** - **X/Y numeric inputs** (with `step="0.5"`) for precise coordinate editing of the selected element, bound to its `x` and `y` fields. - **"+ Gap" button** alongside the existing add buttons. `gap` is accepted by `validate_layout` but is currently unreachable from the UI. - **"Snap to Grid" toggle.** Bind to a boolean state; pass as `snapStep={snapEnabled ? 0.5 : 0}` to `RoomCanvas`. - **"Duplicate element" button.** Copies the selected element with a new UUID and offsets it by 1 grid unit. - **Surface save errors.** `saveLayout` (lines 27–29) currently only `console.error`. Display an inline error message in the UI. --- ## 3. Verification ### Automated Tests: - `backend/src/routes/rooms.rs`: Tests for the new upper-bound and grid-step validators. - `backend/migrations/`: Verify migration 003 runs cleanly on the test DB (use `sqlx migrate run`). - `frontend/tests/rooms.spec.ts` *(new)*: Playwright test — create a room, add table and two seats via the UI, drag a seat (verify snap), save and reload, assert coordinates are preserved. ### Manual Verification: 1. `make seed-demo` — reseed with the fixed `demo_seed.sql`. 2. Open `Admin → Rooms → Room A` in the editor. All elements must appear at sensible grid positions (not far off-screen). 3. Drag an element: verify it snaps to 0.5-unit increments. 4. Resize an element: verify handles appear and snap correctly. 5. Add a Gap element and verify it can be placed and saved. 6. Inspect the SQLite DB directly: `SELECT layout_json FROM rooms LIMIT 1`. All element coordinates must be small numbers (≤ 30), not pixel values (≥ 80). 7. Save and reload: verify coordinates are exactly preserved (no rounding drift).