diff --git a/dist/index.html b/dist/index.html index bec119b..bb04329 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,7 +5,7 @@ furniture-app - + diff --git a/src/App.tsx b/src/App.tsx index a538cef..be86321 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,38 @@ const initialState: LayoutState = { objectCount: 0, }; +function makePreset(type: string, index: number): FurnitureConfig { + if (type === 'table') { + return { + id: `${type}_${Date.now()}_${index}`, + name: `Table ${index + 1}`, + type: 'table', + shape: { type: 'rectangle', x: 80 + index * 20, y: 80, width: 80, height: 60 }, + conditions: [ + { kind: 'walk_to_and_sit', weight: 1 }, + { kind: 'accessible', weight: 1 }, + ], + color: '#a1887f', + lockedForOptimization: false, + placed: false, + }; + } + return { + id: `${type}_${Date.now()}_${index}`, + name: `Chair ${index + 1}`, + type: 'chair', + shape: { type: 'rectangle', x: 120 + index * 20, y: 160, width: 40, height: 40 }, + conditions: [ + { kind: 'walk_to_and_sit', weight: 1 }, + { kind: 'near_object', weight: 1, params: { targetId: '' } }, + { kind: 'accessible', weight: 1 }, + ], + color: '#d7ccc5', + lockedForOptimization: false, + placed: false, + }; +} + function App() { const [state, setState] = useState(initialState); const [mode, setMode] = useState<'layout' | 'optimize'>('layout'); @@ -20,6 +52,32 @@ function App() { const [timeLeft, setTimeLeft] = useState(15); + useEffect(() => { + const amounts = state.objectAmounts || { table: 0, chair: 0 }; + const types = ['table', 'chair']; + let furniture = [...state.furniture]; + for (const type of types) { + const desired = amounts[type] ?? 0; + let existing = furniture.filter((f) => f.type === type); + if (existing.length > desired) { + // remove extras (unplaced first) + const toRemove = new Set(); + const sorted = [...existing].sort((a, b) => Number(a.placed) - Number(b.placed)); + for (let i = 0; i < existing.length - desired; i++) { + toRemove.add(sorted[i].id); + } + furniture = furniture.filter((f) => !toRemove.has(f.id)); + } else if (existing.length < desired) { + for (let i = existing.length; i < desired; i++) { + furniture.push(makePreset(type, i)); + } + } + } + if (furniture.length !== state.furniture.length) { + setState({ ...state, furniture }); + } + }, [state.objectAmounts]); + useEffect(() => { if (!optimizing) return; if (timeLeft <= 0) { @@ -41,37 +99,11 @@ function App() { }, [optimizing, timeLeft]); const handleOptimize = () => { - // lock all objects that are on map; generate remaining up to objectCount with random positions - const existing = state.furniture.map((f) => ({ ...f, lockedForOptimization: true, placed: true })); - const target = state.objectCount ?? existing.length; - const need = Math.max(0, target - existing.length); - const generated: FurnitureConfig[] = []; - for (let i = 0; i < need; i++) { - const id = 'g_' + Date.now() + '_' + i; - generated.push({ - id, - name: 'Obj ' + (existing.length + i + 1), - shape: { - type: 'rectangle', - x: Math.random() * Math.max(50, state.room.width - 60), - y: Math.random() * Math.max(50, state.room.height - 60), - width: 40 + Math.random() * 40, - height: 30 + Math.random() * 30, - }, - conditions: [ - { kind: 'walk_to_and_sit', weight: 1 }, - { kind: 'accessible', weight: 1 }, - ], - color: '#90caf9', - lockedForOptimization: false, - placed: false, - }); - } - const lockedState: LayoutState = { - ...state, - furniture: [...existing, ...generated], - }; - setState(lockedState); + const existing = state.furniture.map((f) => ({ + ...f, + lockedForOptimization: f.placed ? true : false, + })); + setState({ ...state, furniture: existing }); setOptimizing(true); setTimeLeft(15); }; diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index a7470b0..11d05e6 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -1,4 +1,4 @@ -import type { ConditionKind, Entrance, FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types'; +import type { ConditionKind, Entrance, LayoutState, RoomConfig } from '../types'; interface ControlPanelProps { state: LayoutState; @@ -23,61 +23,7 @@ export function ControlPanel({ optimizing, timeLeft, }: ControlPanelProps) { - const addPreset = (p: FurniturePreset) => { - const id = 'f_' + Date.now(); - const newItem: FurnitureConfig = { - id, - name: p.name, - shape: { ...p.shape, x: 50, y: 50 }, - conditions: p.conditions.map((c) => ({ ...c })), - color: p.color, - lockedForOptimization: false, - placed: true, - }; - onStateChange({ ...state, furniture: [...state.furniture, newItem] }); - onSelect(id); - }; - const addFurniture = () => { - const id = 'f_' + Date.now(); - const newItem: FurnitureConfig = { - id, - name: 'Table ' + state.furniture.length, - shape: { type: 'rectangle', x: 50, y: 50, width: 60, height: 40 }, - conditions: [ - { kind: 'walk_to_and_sit', weight: 1 }, - { kind: 'accessible', weight: 1 }, - ], - color: '#a1887f', - lockedForOptimization: false, - placed: true, - }; - onStateChange({ ...state, furniture: [...state.furniture, newItem] }); - onSelect(id); - }; - - const tablePreset: FurniturePreset = { - id: 'table', - name: 'Table', - shape: { type: 'rectangle', width: 80, height: 60 }, - conditions: [ - { kind: 'walk_to_and_sit', weight: 1 }, - { kind: 'accessible', weight: 1 }, - ], - color: '#a1887f', - }; - - const chairPreset: FurniturePreset = { - id: 'chair', - name: 'Chair', - shape: { type: 'rectangle', width: 40, height: 40 }, - conditions: [ - { kind: 'walk_to_and_sit', weight: 1 }, - { kind: 'near_object', weight: 1, params: {} }, - { kind: 'accessible', weight: 1 }, - ], - color: '#d7ccc5', - }; const setRoom = (room: Partial) => { onStateChange({ ...state, room: { ...state.room, ...room } }); }; @@ -88,7 +34,7 @@ export function ControlPanel({ }; const placeEntrance = () => { - onStateChange({ ...state, entrance: { x: 20, y: 20, width: 8, height: 8 } }); + onStateChange({ ...state, entrance: { x: 20, y: 20, width: 20, height: 20 } }); }; const toggleLock = () => { @@ -128,6 +74,16 @@ export function ControlPanel({ onStateChange({ ...state, furniture: updated }); }; + const updateSelectedSize = (width: number, height: number) => { + if (!selectedId) return; + const updated = state.furniture.map((f) => + f.id === selectedId + ? { ...f, shape: { ...f.shape, width: Math.max(10, width), height: Math.max(10, height) } } + : f, + ); + onStateChange({ ...state, furniture: updated }); + }; + const selected = state.furniture.find((f) => f.id === selectedId); return ( @@ -162,29 +118,10 @@ export function ControlPanel({ style={{ width: 80, marginLeft: 4 }} /> +
- -
-
- -
{state.entrance && ( @@ -209,8 +146,53 @@ export function ControlPanel({ )} +

Premade Objects

+
+
+ Tables: + + onStateChange({ + ...state, + objectAmounts: { ...(state.objectAmounts || {}), table: Math.max(0, Number(e.target.value)) }, + }) + } + style={{ width: 60 }} + /> +
+
+ Chairs: + + onStateChange({ + ...state, + objectAmounts: { ...(state.objectAmounts || {}), chair: Math.max(0, Number(e.target.value)) }, + }) + } + style={{ width: 60 }} + /> +
+

Objects

+ {state.entrance && ( +
onSelect('__entrance__')} + style={{ + padding: 4, + background: selectedId === '__entrance__' ? '#eee' : 'transparent', + cursor: 'pointer', + }} + > + Entrance +
+ )} {state.furniture.map((f) => (
))}
+ {selectedId === '__entrance__' && state.entrance && ( +
+
Entrance Settings
+ + +
+ + +
+
+ )} {selected && (
@@ -243,6 +270,29 @@ export function ControlPanel({
+
+
Size:
+ + +
Conditions:
{(['walk_to_and_sit', 'around_open_area', 'near_entrance', 'near_object', 'accessible', 'not_blocking_path', 'open_circulation'] as ConditionKind[]).map( diff --git a/src/components/RoomCanvas.tsx b/src/components/RoomCanvas.tsx index 0447e31..9192a1c 100644 --- a/src/components/RoomCanvas.tsx +++ b/src/components/RoomCanvas.tsx @@ -14,11 +14,8 @@ interface DragState { offsetX: number; offsetY: number; resizing?: boolean; - edge?: 'se' | 'e' | 's' | 'nw' | 'ne' | 'sw' | 'n' | 'w'; } -const MIN_SIZE = 10; - export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect }: RoomCanvasProps) { const canvasRef = useRef(null); const [drag, setDrag] = useState(null); @@ -42,9 +39,20 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect }: ctx.strokeRect(0, 0, state.room.width, state.room.height); if (state.entrance) { + const e = state.entrance; + const w = e.width ?? 20; + const h = e.height ?? 20; ctx.fillStyle = '#4caf50'; - ctx.fillRect(state.entrance.x - 4, state.entrance.y - 4, 8, 8); - ctx.fillText('ENTRANCE', state.entrance.x + 6, state.entrance.y + 4); + ctx.fillRect(e.x, e.y, w, h); + ctx.strokeStyle = selectedId === '__entrance__' ? '#ff5722' : '#333'; + ctx.lineWidth = selectedId === '__entrance__' ? 3 : 1; + ctx.strokeRect(e.x, e.y, w, h); + ctx.fillStyle = '#000'; + ctx.font = '10px sans-serif'; + ctx.fillText('ENTRANCE', e.x + 2, e.y + 12); + // resize handle (SE) + ctx.fillStyle = selectedId === '__entrance__' ? '#ff5722' : '#666'; + ctx.fillRect(e.x + w - 4, e.y + h - 4, 8, 8); } for (const f of state.furniture) { @@ -75,15 +83,21 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect }: const handleMouseDown = (e: React.MouseEvent) => { const pos = getMousePos(e); - if (selectedId) { - const f = state.furniture.find((x) => x.id === selectedId); - if (f) { - const seX = f.shape.x + f.shape.width; - const seY = f.shape.y + f.shape.height; - if (Math.abs(pos.x - seX) < 8 && Math.abs(pos.y - seY) < 8) { - setDrag({ id: selectedId, offsetX: 0, offsetY: 0, resizing: true, edge: 'se' }); - return; - } + if (state.entrance) { + const en = state.entrance; + const w = en.width ?? 20; + const h = en.height ?? 20; + const handleX = en.x + w - 4; + const handleY = en.y + h - 4; + if (Math.abs(pos.x - handleX) <= 6 && Math.abs(pos.y - handleY) <= 6) { + onSelect('__entrance__'); + setDrag({ id: '__entrance__', offsetX: 0, offsetY: 0, resizing: true }); + return; + } + if (pointInRect(pos, { x: en.x, y: en.y, w, h })) { + onSelect('__entrance__'); + setDrag({ id: '__entrance__', offsetX: pos.x - en.x, offsetY: pos.y - en.y }); + return; } } for (let i = state.furniture.length - 1; i >= 0; i--) { @@ -101,18 +115,22 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect }: const handleMouseMove = (e: React.MouseEvent) => { if (!drag) return; const pos = getMousePos(e); + if (drag.id === '__entrance__' && state.entrance) { + const en = state.entrance; + if (drag.resizing) { + const newW = Math.max(10, pos.x - en.x); + const newH = Math.max(10, pos.y - en.y); + onStateChange({ ...state, entrance: { ...en, width: newW, height: newH } }); + } else { + onStateChange({ + ...state, + entrance: { ...en, x: pos.x - drag.offsetX, y: pos.y - drag.offsetY }, + }); + } + return; + } const updated = state.furniture.map((f) => { if (f.id !== drag.id) return f; - if (drag.resizing && drag.edge === 'se') { - return { - ...f, - shape: { - ...f.shape, - width: Math.max(MIN_SIZE, pos.x - f.shape.x), - height: Math.max(MIN_SIZE, pos.y - f.shape.y), - }, - }; - } return { ...f, shape: { @@ -126,6 +144,10 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect }: }; const handleMouseUp = () => { + if (drag && drag.id !== '__entrance__') { + const updated = state.furniture.map((f) => (f.id === drag.id ? { ...f, placed: true } : f)); + onStateChange({ ...state, furniture: updated }); + } setDrag(null); }; diff --git a/src/types/index.ts b/src/types/index.ts index c7d600f..63d1a06 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -40,6 +40,7 @@ export interface ConditionSpec { export interface FurnitureConfig { id: string; name: string; + type?: string; shape: ShapeData; conditions: ConditionSpec[]; color?: string; @@ -102,4 +103,5 @@ export interface LayoutState { furniture: FurnitureConfig[]; presets?: FurniturePreset[]; objectCount?: number; + objectAmounts?: Record; }