From 8f8211c9b72d793e6c943e03b7985199a6d8615d Mon Sep 17 00:00:00 2001 From: plus_one Date: Sat, 17 Oct 2026 21:50:00 -0400 Subject: [PATCH] Treat object count as optimizer spec; drag cards onto room to pin/lock, optimizer fills the rest --- dist/index.html | 2 +- src/App.tsx | 86 ++++++--------------------------- src/components/ControlPanel.tsx | 16 +++++- src/components/RoomCanvas.tsx | 22 ++++++++- src/optimization/prepare.ts | 74 ++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 75 deletions(-) create mode 100644 src/optimization/prepare.ts diff --git a/dist/index.html b/dist/index.html index f8101ee..c6b1040 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 e8f20f2..be77a09 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; import { RoomCanvas } from './components/RoomCanvas'; import { ControlPanel } from './components/ControlPanel'; -import type { FurnitureConfig, FurniturePreset, LayoutState } from './types'; +import type { LayoutState } from './types'; import { runRandomSearch } from './optimization/randomSearch'; import { optimizeWithNN } from './ai/nn'; +import { prepareForOptimize } from './optimization/prepare'; import { DEFAULT_PRESETS } from './furniture/presets'; -import { clampRectToRoom, clampStateToRoom } from './geometry/bounds'; const initialState: LayoutState = { room: { width: 600, height: 400, gridSize: 10 }, @@ -14,70 +14,6 @@ const initialState: LayoutState = { presets: DEFAULT_PRESETS, }; -function makeFurniture(preset: FurniturePreset, index: number, room: LayoutState['room']): FurnitureConfig { - const spawn = clampRectToRoom( - { - x: 80 + (index % 5) * 60, - y: 80 + Math.floor(index / 5) * 60, - width: preset.shape.width, - height: preset.shape.height, - }, - room, - ); - return { - id: `${preset.id}_${Date.now()}_${index}`, - name: `${preset.name} ${index + 1}`, - type: preset.id, - shape: { - ...preset.shape, - x: spawn.x, - y: spawn.y, - width: spawn.width, - height: spawn.height, - }, - conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), - color: preset.color, - }; -} - -function syncFurniture(state: LayoutState): LayoutState { - const presets = state.presets || []; - const validIds = new Set(presets.map((p) => p.id)); - let furniture = state.furniture.filter((f) => !f.type || f.type === 'custom' || validIds.has(f.type)); - - for (const preset of presets) { - const desired = preset.amount ?? 0; - const existing = furniture.filter((f) => f.type === preset.id); - - if (existing.length > desired) { - const removeIds = new Set( - [...existing] - .sort((a, b) => Number(a.placed) - Number(b.placed)) - .slice(0, existing.length - desired) - .map((f) => f.id), - ); - furniture = furniture.filter((f) => !removeIds.has(f.id)); - } else if (existing.length < desired) { - for (let i = existing.length; i < desired; i++) { - furniture.push(makeFurniture(preset, i, state.room)); - } - } - - furniture = furniture.map((f) => - f.type === preset.id - ? { - ...f, - shape: { ...f.shape, width: preset.shape.width, height: preset.shape.height }, - color: preset.color, - conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), - } - : f, - ); - } - - return { ...state, furniture }; -} - function App() { const [state, setState] = useState(initialState); const [mode, setMode] = useState<'layout' | 'optimize'>('layout'); @@ -86,8 +22,17 @@ function App() { const [timeLeft, setTimeLeft] = useState(15); useEffect(() => { - setState((s) => clampStateToRoom(syncFurniture(s))); - }, [state.presets]); + const onKey = (e: KeyboardEvent) => { + if (e.key !== 'Delete' && e.key !== 'Backspace') return; + const target = e.target as HTMLElement; + if (target && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return; + if (!selectedId || selectedId === '__entrance__') return; + setState((s) => ({ ...s, furniture: s.furniture.filter((f) => f.id !== selectedId) })); + setSelectedId(null); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [selectedId]); useEffect(() => { if (!optimizing) return; @@ -110,10 +55,7 @@ function App() { }, [optimizing, timeLeft]); const handleOptimize = () => { - setState((s) => ({ - ...s, - furniture: s.furniture.map((f) => ({ ...f, lockedForOptimization: f.placed ? true : false })), - })); + setState((s) => prepareForOptimize(s)); setOptimizing(true); setTimeLeft(15); }; diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 06cfbae..601d412 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -140,12 +140,22 @@ export function ControlPanel({ const renderObjectCard = (p: FurniturePreset) => { const open = openId === p.id; + const placedCount = state.furniture.filter((f) => f.type === p.id && f.placed).length; return (
-
toggleOpen(p.id)}> +
{ + e.dataTransfer.setData('application/x-preset-id', p.id); + e.dataTransfer.effectAllowed = 'copy'; + }} + onClick={() => toggleOpen(p.id)} + > ▶ {p.name} + {placedCount > 0 && {placedCount} placed} e.stopPropagation()}> setAmount(p.id, v)} /> @@ -340,6 +350,10 @@ export function ControlPanel({
Objects
+
+ Count tells the optimizer how many are needed. Drag a card onto the room to pin one in place + (locked); the optimizer arranges the rest. +
{objects.map(renderObjectCard)}
diff --git a/src/components/RoomCanvas.tsx b/src/components/RoomCanvas.tsx index fac571a..dffbd02 100644 --- a/src/components/RoomCanvas.tsx +++ b/src/components/RoomCanvas.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import type { LayoutState, Point } from '../types'; import { computePaths } from '../pathfinding/paths'; import { clampRectToRoom } from '../geometry/bounds'; +import { makePlaced } from '../optimization/prepare'; interface RoomCanvasProps { state: LayoutState; @@ -156,7 +157,7 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s ctx.restore(); }; - const getMousePos = (e: React.MouseEvent): Point => { + const getMousePos = (e: { clientX: number; clientY: number }): Point => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); @@ -250,6 +251,23 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s setDrag(null); }; + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + const presetId = e.dataTransfer.getData('application/x-preset-id'); + if (!presetId) return; + const preset = (state.presets || []).find((p) => p.id === presetId); + if (!preset) return; + const pos = getMousePos(e); + const f = makePlaced(preset, pos.x - preset.shape.width / 2, pos.y - preset.shape.height / 2, state.room); + onStateChange({ ...state, furniture: [...state.furniture, f] }); + onSelect(f.id); + }; + return ( ); } diff --git a/src/optimization/prepare.ts b/src/optimization/prepare.ts new file mode 100644 index 0000000..9cccf42 --- /dev/null +++ b/src/optimization/prepare.ts @@ -0,0 +1,74 @@ +import type { FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types'; +import { clampRectToRoom, clampStateToRoom } from '../geometry/bounds'; + +export function makeGenerated(preset: FurniturePreset, index: number, room: RoomConfig): FurnitureConfig { + const b = clampRectToRoom( + { + x: Math.random() * Math.max(0, room.width - preset.shape.width), + y: Math.random() * Math.max(0, room.height - preset.shape.height), + width: preset.shape.width, + height: preset.shape.height, + }, + room, + ); + return { + id: `${preset.id}_gen_${Date.now()}_${index}`, + name: preset.name, + type: preset.id, + shape: { ...preset.shape, ...b }, + conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), + color: preset.color, + placed: false, + }; +} + +export function makePlaced(preset: FurniturePreset, x: number, y: number, room: RoomConfig): FurnitureConfig { + const b = clampRectToRoom( + { x, y, width: preset.shape.width, height: preset.shape.height }, + room, + ); + return { + id: `${preset.id}_placed_${Date.now()}_${Math.round(Math.random() * 1e6)}`, + name: preset.name, + type: preset.id, + shape: { ...preset.shape, ...b }, + conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), + color: preset.color, + placed: true, + lockedForOptimization: true, + }; +} + +export function prepareForOptimize(state: LayoutState): LayoutState { + const presets = state.presets || []; + let furniture = [...state.furniture]; + + for (const preset of presets) { + const wanted = preset.amount ?? 0; + const ofType = furniture.filter((f) => f.type === preset.id); + const placed = ofType.filter((f) => f.placed); + let generated = ofType.filter((f) => !f.placed); + + const need = Math.max(0, wanted - placed.length); + if (generated.length > need) { + generated = generated.slice(0, need); + } + while (generated.length < need) { + generated.push(makeGenerated(preset, generated.length, state.room)); + } + + const synced = [...placed, ...generated].map((f) => ({ + ...f, + name: preset.name, + color: preset.color, + shape: { ...f.shape, width: preset.shape.width, height: preset.shape.height }, + conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), + lockedForOptimization: !!f.placed, + })); + + furniture = furniture.filter((f) => f.type !== preset.id); + furniture = [...furniture, ...synced]; + } + + return clampStateToRoom({ ...state, furniture }); +}