d98ce8ddb7
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6.7 KiB
6.7 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 + Phase 2 (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 (
pxPerMfromfitScale, recomputed by a ResizeObserver inApp.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.
Out of scope: room features (doors, room screen), room import, placement rules, optimization, scanning. 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 = 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
}
interface Room { width: number; height: number; } // 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): { room, items, configs: Record<id, config>, 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
pxPerMscale (fitScaleingeometry.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);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,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 2.
- After changes, make sure the app builds and type checks.
- Hackathon pace: prefer simple and working over clever.