Phase 5: simulated-annealing optimizer in a Web Worker, auto-fill, undo/redo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
A web tool for venues (events, banquets, conferences) to lay out furniture in a room and, eventually, auto-optimize the layout.
|
||||
|
||||
## Current scope: Phases 1-4 (UI + scoring, no optimizer)
|
||||
## Current scope: Phases 1-5 (UI, scoring, optimizer)
|
||||
|
||||
Layout and editing UI. No optimizer yet.
|
||||
|
||||
@@ -29,7 +29,7 @@ Layout and editing UI. No optimizer yet.
|
||||
- Keep-clear zones: the door swing square and the 0.6 m strip inside the opening, the 0.3 m strip under a wall screen, and obstacles. `getBlockedRects(room)` returns them all and `isValidPlacement` reads it, so furniture can't overlap them (red outline, drop blocked). Doors/screens can't sit on furniture or obstacles or share wall length; obstacles can't overlap furniture, each other, or door/screen zones.
|
||||
- Export downloads `room.json` (width, height, doors, screens, obstacles; no furniture). Import validates with `parseRoom` (`roomSchema.ts`) and shows a clear error; if furniture is placed it asks for confirmation first, and if the placed furniture doesn't fit the imported room it clears all furniture and warns.
|
||||
|
||||
**Rules and scoring (Phase 4)** - measures the layout only; there is NO optimizer and nothing moves automatically.
|
||||
**Rules and scoring (Phase 4)** - measures the layout.
|
||||
- All scoring is pure TS in `src/scoring/` (no React/Konva): `scoreLayout(room, items, configs, rules, weights, {debug})` returns the five sub-scores (0..1 or `null` = n/a), the 0-100 overall (`overallScore`, weighted average of the non-null scores by the 0-10 priority weights), issues (text + item ids), rule violations and optional overlay data. Roughly 12 ms for a 10x8 room, 85 ms for 60x60 with 120 items.
|
||||
- `seats.ts` `getSeatPoints`: N points evenly around a table's perimeter (seatsOverride, else type default); standalone seating = its center, or `seats` points along its long side; a single chair within 0.8 m of a table edge is that table's seat and isn't counted again.
|
||||
- `occupancy.ts`: internal 0.1 m grid (never drawn, nothing snaps to it) from `getBlockedRects` + the true rotated furniture, with a chamfer clearance transform and a Dial's-algorithm `reachFrom`. Cached per variant and rebuilt only when the room or items arrays change. Door keep-clear zones are walkable for pathfinding.
|
||||
@@ -38,7 +38,14 @@ Layout and editing UI. No optimizer yet.
|
||||
- Score panel (top of the sidebar in Layout mode, collapsible): overall number, five labeled bars with priority wheels, required violations, top issues (click to highlight items), and overlay toggles (loading paths, sight lines, table clearance; off by default). Weights and rules live in the store. During a drag the score recomputes at most ~10x/s.
|
||||
- Tests: `npm test` (vitest) covers seat points, empty room, reachable vs walled-off table, blocked line of sight, comfort, rules and the weighted overall.
|
||||
|
||||
Out of scope: the optimizer, auto-moving anything, non-rectangular rooms, floor plan image import, phone scanning, placement rules, scoring, optimization. Do not design in a way that blocks the roadmap below.
|
||||
**Optimizer (Phase 5)**
|
||||
- Simulated annealing in a Web Worker (`src/optimizer/worker.ts`); pure core in `anneal.ts` (no React/Konva). State = x, y, rotation of every unlocked item; locked items never move or rotate. Objective = the weighted overall score. Moves: nudge (shrinking with temperature), relocate, rotate 90 (rotatable types only), swap two items of one type, shift a table with the chairs within 0.8 m. Hard constraints (`constraints.ts`): inside the room, no overlap with furniture, none with `getBlockedRects`, no Required-rule violations. If the start breaks Required rules, a repair phase (up to 20% of the budget) minimizes the violation count first and the result reports what remains. Geometric cooling with 3 restarts from the best state; seeded RNG (`rng.ts`, mulberry32) so the same seed + iteration budget gives the same result.
|
||||
- Speed: search scores at a 0.2 m occupancy grid (`ScoreOpts.res`), the final before/after at 0.1 m. About 5,000 steps/s on a 10x8 room (idle machine); no incremental rescoring, every step rescoring everything.
|
||||
- Auto-fill (`autofill.ts`): with "Seats needed" > 0, tables (most seats first) then loose chairs are placed from remaining inventory at random valid spots until seats >= needed; if inventory/space runs out it says so. "Only place remaining inventory" makes only those new items movable.
|
||||
- Optimize tab: duration 5-60 s (default 15), seats needed, seed (blank = random), Optimize / Cancel (terminates the worker and discards). While running: progress bar, best score, steps; the canvas shows the best layout (posted every ~300 ms), not draggable. On finish: before/after for the overall and the five sub-scores, locked-items-skipped count, auto-fill message, unresolved Required violations, Apply / Revert. Leaving the tab cancels or drops a pending result.
|
||||
- Undo/redo: snapshot history (`past[]`, `future[]` of items + room) in the store; Apply is one undo step. Add/move/update/remove/room changes are undoable too (rapid repeats on the same item coalesce for 700 ms). Cmd/Ctrl+Z, Shift+Cmd/Ctrl+Z (or Ctrl+Y), and Undo/Redo buttons in the Optimize tab.
|
||||
|
||||
Out of scope: phone scanning, templates, export to image, non-rectangular rooms, neural networks, incremental rescoring, floor plan image import, phone scanning, placement rules, scoring, optimization. Do not design in a way that blocks the roadmap below.
|
||||
|
||||
## Tech stack
|
||||
|
||||
@@ -98,9 +105,10 @@ Store (`store.ts`; `setRoom` replaces the room, optionally clearing furniture):
|
||||
## File layout
|
||||
|
||||
- `src/types.ts`; `src/geometry.ts` (pure: `boundsOf`, `clampToRoom`, `overlaps`, `isValidPlacement`, `tryUpdate`, `resizeLength`, `fitScale`, plus room rules: `getBlockedRects`, `isValidWallItem`, `isValidObstacle`, `isLayoutValid`, `minRoomSize`, `tryResizeRoom`, `findWallSpot`, `findObstacleSpot`); `src/roomSchema.ts` (pure: `parseRoom`, `exportRoom`); `src/inventory.ts` (pure: `placedCount`, `remaining`, `effectiveSeats`); `src/store.ts` (reducer + built-in configs).
|
||||
- `src/optimizer/`: `anneal.ts`, `autofill.ts`, `constraints.ts`, `rng.ts`, `worker.ts`, `messages.ts`, `useOptimizer.ts` (React hook that drives the worker), `anneal.test.ts`.
|
||||
- `src/scoring/`: `index.ts` (`scoreLayout`, `overallScore`), `seats.ts`, `occupancy.ts`, `loading.ts`, `screen.ts`, `access.ts`, `comfort.ts`, `rules.ts`, `geom.ts`, `types.ts`, `scoring.test.ts`.
|
||||
- `src/App.tsx`: owns the store, selection, sidebar mode, palette drag, room scale, Delete key.
|
||||
- `src/components/`: `Palette`, `TypeMenu`, `ItemPanel`, `CustomForm`, `ModeToggle`, `RoomPanels`, `ScorePanel`, `RulesPanel`, `Wheel`, `Room` (Konva), `FurnitureShape`, `Icons`, `ScaleBar`, `panelStyles`.
|
||||
- `src/components/`: `Palette`, `TypeMenu`, `ItemPanel`, `CustomForm`, `ModeToggle`, `RoomPanels`, `ScorePanel`, `RulesPanel`, `OptimizePanel`, `Wheel`, `Room` (Konva), `FurnitureShape`, `Icons`, `ScaleBar`, `panelStyles`.
|
||||
|
||||
## Roadmap (not built yet, don't block)
|
||||
|
||||
@@ -127,6 +135,6 @@ npx tsc --noEmit # type check
|
||||
|
||||
## Working agreement
|
||||
|
||||
- Ask before expanding scope past Phase 4. Commit and push after each finished change (a teammate works in the same repo): fetch first, rebase if needed. Don't commit `dist/` or `node_modules/` changes.
|
||||
- Ask before expanding scope past Phase 5. Commit and push after each finished change (a teammate works in the same repo): fetch first, rebase if needed. Don't commit `dist/` or `node_modules/` changes.
|
||||
- After changes, make sure the app builds and type checks.
|
||||
- Hackathon pace: prefer simple and working over clever.
|
||||
Reference in New Issue
Block a user