third commit
This commit is contained in:
@@ -2,26 +2,27 @@
|
||||
|
||||
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)
|
||||
## Current scope: Phase 1 + Phase 2 (UI only)
|
||||
|
||||
The ONLY feature right now is drag-and-drop layout:
|
||||
Layout and editing UI. No optimizer yet.
|
||||
|
||||
- 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 (`pxPerM` computed via `fitScale` and passed down; a ResizeObserver in `App.tsx` recomputes 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). `RESIZABLE` and `resizeRect` live in `geometry.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).
|
||||
**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. Props: `step`, `wrap`, `rows`, `disabled`, `format`. If the parent rejects a change the wheel glides back to the real value.
|
||||
|
||||
Do not build anything beyond this unless asked. Do not design in a way that blocks the roadmap below.
|
||||
**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: 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
|
||||
|
||||
@@ -32,47 +33,49 @@ Do not build anything beyond this unless asked. Do not design in a way that bloc
|
||||
## Data model
|
||||
|
||||
```ts
|
||||
type FurnitureType = "chair" | "table" | "screen";
|
||||
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;
|
||||
x: number; // meters, top-left, room coordinates
|
||||
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, always 0 for now
|
||||
locked: boolean; // always false for now, reserved
|
||||
rotation: number; // degrees, around the rect's center
|
||||
locked: boolean;
|
||||
seatsOverride?: number; // tables: overrides config.seatsPerUnit
|
||||
}
|
||||
|
||||
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
|
||||
id: FurnitureType; name: string; shape: Shape;
|
||||
defaultWidth: number; defaultHeight: number; // meters
|
||||
isTable: boolean;
|
||||
available: number; // inventory count
|
||||
seatsPerUnit?: number; // type-level seats
|
||||
}
|
||||
// remaining = available - items.filter(i => i.type === type).length
|
||||
// remaining = available - items.filter(i => i.type === type).length (derived, never stored)
|
||||
// seats for an item = item.seatsOverride ?? config.seatsPerUnit (effectiveSeats)
|
||||
```
|
||||
|
||||
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.
|
||||
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 `pxPerM` scale (dynamic, computed by `fitScale` in `geometry.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.
|
||||
- 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: 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`.
|
||||
- `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)
|
||||
|
||||
@@ -99,6 +102,6 @@ npx tsc --noEmit # type check
|
||||
|
||||
## Working agreement
|
||||
|
||||
- Ask before expanding scope past Phase 1.
|
||||
- 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.
|
||||
Reference in New Issue
Block a user