diff --git a/CLAUDE.md b/CLAUDE.md index eeacba5..7f5dd52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ Layout and editing UI. No optimizer yet. **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). +- Item panel (chairs have no rotation controls; every other type does): Rotate 90 button + a circular angle dial (`AngleDial`: drag the knob around the ring, Shift snaps to 15°, arrows/scroll nudge 1°); 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`). diff --git a/src/components/AngleDial.tsx b/src/components/AngleDial.tsx new file mode 100644 index 0000000..6aa31a6 --- /dev/null +++ b/src/components/AngleDial.tsx @@ -0,0 +1,83 @@ +import { useRef } from "react"; + +interface Props { + value: number; // degrees, 0 = knob at the top, clockwise + disabled?: boolean; + onChange: (deg: number) => void; +} + +const SIZE = 120; +const R = 46; // knob orbit radius + +/** Circular dial: drag the knob around the ring. Shift snaps to 15°. Arrow keys / scroll nudge by 1°. */ +export function AngleDial({ value, disabled = false, onChange }: Props) { + const box = useRef(null); + const dragging = useRef(false); + + const norm = (d: number) => ((Math.round(d) % 360) + 360) % 360; + + const angleAt = (e: React.PointerEvent) => { + const r = box.current!.getBoundingClientRect(); + const dx = e.clientX - (r.left + r.width / 2); + const dy = e.clientY - (r.top + r.height / 2); + let deg = (Math.atan2(dx, -dy) * 180) / Math.PI; // 0 at top, clockwise + if (e.shiftKey) deg = Math.round(deg / 15) * 15; + return norm(deg); + }; + + const down = (e: React.PointerEvent) => { + if (disabled) return; + e.currentTarget.setPointerCapture(e.pointerId); + dragging.current = true; + onChange(angleAt(e)); + }; + const move = (e: React.PointerEvent) => { + if (dragging.current) onChange(angleAt(e)); + }; + const up = () => { + dragging.current = false; + }; + + const rad = (value * Math.PI) / 180; + const c = SIZE / 2; + const kx = c + R * Math.sin(rad); + const ky = c - R * Math.cos(rad); + + return ( + !disabled && onChange(norm(value + Math.sign(e.deltaY)))} + onKeyDown={(e) => { + if (disabled) return; + if (e.key === "ArrowRight" || e.key === "ArrowUp") onChange(norm(value + 1)); + else if (e.key === "ArrowLeft" || e.key === "ArrowDown") onChange(norm(value - 1)); + else return; + e.preventDefault(); + }} + > + + + {[0, 90, 180, 270].map((t) => { + const a = (t * Math.PI) / 180; + return ; + })} + + + + {value}° + + + ); +} diff --git a/src/components/ItemPanel.tsx b/src/components/ItemPanel.tsx index 7705f11..3294cef 100644 --- a/src/components/ItemPanel.tsx +++ b/src/components/ItemPanel.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import type { FurnitureItem, FurnitureTypeConfig } from "../types"; import { effectiveSeats } from "../inventory"; +import { AngleDial } from "./AngleDial"; import { Wheel } from "./Wheel"; import { backBtn, btn, hint, label, panelRoot, title, twoCol } from "./panelStyles"; @@ -49,16 +50,7 @@ export function ItemPanel({ item, config, onPatch, onDelete, onClose }: Props) { {config.rotatable && (
Rotation
- `${v}°`} - onChange={(v) => patch({ rotation: v })} - /> + patch({ rotation: v })} />