# Furniture Optimizer (hackathon project) 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-3 (UI only) Layout and editing UI. No optimizer yet. **Layout (Phase 1)** - Left sidebar (palette): one tile per furniture type (built-in chair, table, screen + any custom) and a dashed "Custom" tile. Resizable by dragging its right edge (default 264px, min 152, max 600); tiles live in an auto-fill grid and stretch in width only (84px tall). The sidebar scrolls vertically. - Room in the center (fixed 10m x 8m), scaled to fill the space (`pxPerM` from `fitScale`, recomputed by a ResizeObserver in `App.tsx`), with a "1 m" scale bar. No grid, NO snapping: items land exactly where dropped. - Drag a tile into the room to create an item (tile stays). Click (<5px movement) a tile to open its type menu (takes over the sidebar; Escape / Back closes): "Available" wheel (min = placed) and, if the type has seats, a seats wheel. Badge = available - placed; at 0 the tile greys out and can't be dragged. - Items are free-placed, clamped to the room, no overlap (touching is fine). Red outline while the position is invalid, green when valid; invalid drop snaps back (or cancels a new add). - Wheels (`components/Wheel.tsx`) are iOS-timer style: 1:1 drag, momentum fling, press to stop, spring settle, mouse wheel/trackpad, tap a row, arrow keys, and hover + type a number (Enter, leaving, or 1.5 s idle commits; Escape cancels). Props: `step`, `wrap`, `rows`, `disabled`, `format`. If the parent rejects a change the wheel glides back to the real value. **Editing (Phase 2)** - Click an item to select it (blue outline); click empty floor to deselect (Escape/Done too). Dragging still moves. Selecting opens the item panel in the sidebar (replaces tiles / type menu). - Item panel (chairs have no rotation controls; every other type does): Rotate 90 button + free angle wheel (0-359, wraps); Lock/Unlock; Delete (also Delete/Backspace key, ignored while typing in an input); "Seats at this table" wheel for tables (per-item override, "Reset to default" link when overridden); Size wheels (0.3-6 m, 0.1 steps; screens show only Length). - Rotation turns around the item's center and collision/wall clamping use the rotated bounding box (`boundsOf`). A rotate/size change is pushed back inside the walls, then rejected if it would overlap (`tryUpdate`). - Locked items can't be dragged or rotated and show a padlock. (Size, seats and delete are still allowed on locked items.) - Delete frees inventory automatically (remaining is derived from `items`). - Screens: while selected, unlocked and unrotated, left/right end handles change their length (`resizeLength`, min 0.5 m). Thickness is only changed by size wheels, never handles (top-down view). - Custom furniture: "Custom" tile opens a form (name, Seating/Table, width, length, seats, available). Saving adds a config + palette tile. Custom seating draws as a chair, custom tables as a table. **Room features (Phase 3)** - A Layout / Room toggle sits at the top of the sidebar. Switching closes menus and deselects. In Room mode furniture is visible but can't be moved or selected. - Room panel: width and length wheels (3-60 m, step 0.1; the room grows/shrinks from its top-left). It can't shrink past the bounding box of placed furniture, obstacles and wall items (`minRoomSize`; anything else that would clash is rejected via `tryResizeRoom` and the wheel glides back). Add door / Add screen (max 2) / Add obstacle, Export room, Import room. - Walls are drawn inside the canvas (`WALL_PX`); a door is a gap in the wall plus a quarter-circle swing arc. Doors and wall screens slide along their own wall only (no corner-hopping to adjacent walls). Door panel: width 0.7-4 m, "Loading door" toggle, delete (the last door can't be deleted). Wall-screen panel: width 1-12 m, delete. Obstacles (default 0.6 x 0.6 m, 0.2-10 m) drag anywhere; width/length wheels, delete. Delete/Backspace also deletes the selected room element. - 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. Out of scope: non-rectangular rooms, floor plan image import, phone scanning, placement rules, scoring, optimization. Do not design in a way that blocks the roadmap below. ## Tech stack - React + TypeScript, Vite - react-konva (Konva) for the canvas - State: single top-level store (useState/useReducer). Keep it centralized so an optimizer can read and write it later. ## Data model ```ts type FurnitureType = string; // config id: "chair" | "table" | "screen" | "custom-1" ... type Shape = "chair" | "table" | "screen"; // how a type is drawn interface FurnitureItem { id: string; type: FurnitureType; // config id x: number; // meters, top-left of the UNROTATED rect, room coordinates y: number; width: number; // meters height: number; rotation: number; // degrees, around the rect's center locked: boolean; seatsOverride?: number; // tables: overrides config.seatsPerUnit } type Wall = "top" | "bottom" | "left" | "right"; // offset = meters from the wall's start (top/bottom start at the left, left/right at the top) to the near edge interface Door { id: string; wall: Wall; offset: number; width: number; isLoading: boolean; } interface RoomScreen { id: string; wall: Wall; offset: number; width: number; } // spec calls it "Screen"; renamed to avoid clashing with the screen furniture type / DOM Screen interface Obstacle { id: string; x: number; y: number; width: number; height: number; } interface Room { width: number; height: number; doors: Door[]; screens: RoomScreen[]; obstacles: Obstacle[]; } // meters interface FurnitureTypeConfig { id: FurnitureType; name: string; shape: Shape; defaultWidth: number; defaultHeight: number; // meters isTable: boolean; rotatable: boolean; // chairs: false (no rotation controls) available: number; // inventory count seatsPerUnit?: number; // type-level seats } // remaining = available - items.filter(i => i.type === type).length (derived, never stored) // seats for an item = item.seatsOverride ?? config.seatsPerUnit (effectiveSeats) ``` Store (`store.ts`; `setRoom` replaces the room, optionally clearing furniture): `{ room, items, configs: Record, typeOrder: id[] }`. Defaults live in the configs (chair 0.5x0.5, table 1.8x0.8 w/ 8 seats, screen 2x0.2). Chairs and table seats are independent. ## Conventions - Store everything in meters. Convert to pixels only at render time using the dynamic `pxPerM` scale (`fitScale` in `geometry.ts`). - Collision = axis-aligned bounding boxes of the rotated rect (`boundsOf`). Keep collision and clamping logic in pure functions (no React or Konva imports) so the optimizer can reuse them. - Keep geometry/rules logic separate from rendering components. - Small, typed, readable code. No new dependencies without a good reason. - Keep the UI minimal: light grey app background, room centered in the remaining space. ## 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/App.tsx`: owns the store, selection, sidebar mode, palette drag, room scale, Delete key. - `src/components/`: `Palette`, `TypeMenu`, `ItemPanel`, `CustomForm`, `ModeToggle`, `RoomPanels`, `Wheel`, `Room` (Konva), `FurnitureShape`, `Icons`, `ScaleBar`, `panelStyles`. ## Roadmap (not built yet, don't block) Venue-focused features planned for later phases: - Furniture inventory: set X amount of each furniture type - Semi-custom furniture: couches, chairs, tables of any size and seating count - Room import - Furniture placement rules - Locking furniture in place - Custom seating limits (per table or global) - Optimizers for: room loading/unloading flow, screen visibility, max comfort, max table access - Optimization approach idea: user-set rule priorities, possibly a neural net with weights and biases to score "optimal placements" (undecided; start with hard constraints like no overlap, then weighted scoring) - Input ideas: phone scanning of a room, manual entry ## Commands ``` npm install npm run dev # start dev server npm run build # production build npx tsc --noEmit # type check ``` ## Working agreement - Ask before expanding scope past Phase 3. - After changes, make sure the app builds and type checks. - Hackathon pace: prefer simple and working over clever.