5.5 KiB
5.5 KiB
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: Phase 1 (UI only)
The ONLY feature right now is drag-and-drop layout:
- Left sidebar (palette) with three source tiles: chair, table, screen.
- Resizable by dragging its right edge (default 264px, min 152px, max 600px). Tiles live in an auto-fill grid: they stretch in width only (fixed 84px height) until another column fits.
- Large rectangular room in the center (fixed 10m x 8m). It is scaled to fill the available space (
pxPerMcomputed viafitScaleand passed down; a ResizeObserver inApp.tsxrecomputes it). A "1 m" scale bar sits under the room. No grid lines. - Click (no drag) a palette tile to open its settings menu. The menu takes over the sidebar (replaces the tiles); close with Escape or the "Back" button:
- Available: how many of that type the venue has. Min = number already placed.
- Tables only: Seats per table (min 1).
- Both are iOS-timer-style scroll wheels (
components/Wheel.tsx): drag 1:1, momentum fling, press to stop mid-roll, spring settle for slow releases, mouse wheel / trackpad, tap a row, arrow keys. Max 999. - Palette tiles show a badge with remaining count (available minus placed). Defaults: 20 chairs, 6 tables, 8 seats per table.
- Drag a tile into the room to create an item. The tile stays in the palette. If remaining is 0, the tile is greyed out and can't be dragged (click still opens the menu).
- Distinguish click from drag with a small movement threshold (5px).
- Screens (2 x 0.2 m default, 2 available by default) are the only resizable type: click a placed screen to select it, then drag its left/right end handles to change its length only (top-down view, so thickness stays 0.2 m; min length 0.5 m, stays inside the room; red + snap back to the old size if it would overlap).
RESIZABLEandresizeRectlive ingeometry.ts. - Drag existing items to move them. Items are clamped inside the room walls.
- No overlap allowed (touching edges is fine). Invalid position = red outline while dragging, green when valid; on drop, snap back to last valid position (or cancel the add if new).
- NO grid snapping: items land exactly where dropped (snapping was removed on request).
Do not build anything beyond this unless asked. 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
type FurnitureType = "chair" | "table" | "screen";
interface FurnitureItem {
id: string;
type: FurnitureType;
x: number; // meters, top-left, room coordinates
y: number;
width: number; // meters
height: number;
rotation: number; // degrees, always 0 for now
locked: boolean; // always false for now, reserved
}
interface Room { width: number; height: number; } // meters
// Per-type settings, edited via the palette menu
interface FurnitureTypeConfig {
type: FurnitureType;
available: number; // inventory count
seatsPerUnit?: number; // tables only: people who can sit at one
}
// remaining = available - items.filter(i => i.type === type).length
Seats per table is a type-level setting for now (applies to all tables). Per-table overrides come later. Chairs and table seats are independent for now.
Default sizes: chair 0.5 x 0.5 m, table 1.8 x 0.8 m, screen 2 x 0.2 m.
Conventions
- Store everything in meters. Convert to pixels only at render time using the
pxPerMscale (dynamic, computed byfitScaleingeometry.ts; there is no fixed px-per-meter constant). - Collision = axis-aligned bounding boxes. 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: clamp, overlap, validity, fitScale, default sizes),src/inventory.ts(pure: placedCount, remaining),src/store.ts(reducer: items, room, configs).src/App.tsx: owns the store, palette drag, room scale.src/components/:Palette(sidebar, tiles, resize),TypeMenu,Wheel,Room(Konva),FurnitureShape,ScaleBar.
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 1.
- After changes, make sure the app builds and type checks.
- Hackathon pace: prefer simple and working over clever.