From 3ba7b0e89fdddd80e84bd74d29776ca433bbd6cd Mon Sep 17 00:00:00 2001 From: plus_one Date: Sun, 18 Oct 2026 08:41:47 -0400 Subject: [PATCH] readme --- _tmp_fix.ts | 0 dist/index.html | 4 +- src/App.tsx | 38 +++-- src/components/ControlPanel.tsx | 234 +++++++++++++++++++++++++++---- src/components/RoomCanvas.tsx | 149 +++++++++++++++++++- src/furniture/Furniture.ts | 3 + src/geometry/bounds.ts | 44 +++++- src/geometry/polygon.ts | 58 ++++++++ src/geometry/polygonContain.ts | 78 +++++++++++ src/index.css | 64 +++++++++ src/layout/evaluator.ts | 113 +++++++++++++-- src/layout/visibility.ts | 77 ++++++++++ src/optimization/prepare.ts | 69 ++++----- src/optimization/randomSearch.ts | 28 +++- src/pathfinding/grid.ts | 14 +- src/types/index.ts | 6 +- 16 files changed, 872 insertions(+), 107 deletions(-) create mode 100644 _tmp_fix.ts create mode 100644 src/geometry/polygon.ts create mode 100644 src/geometry/polygonContain.ts create mode 100644 src/layout/visibility.ts diff --git a/_tmp_fix.ts b/_tmp_fix.ts new file mode 100644 index 0000000..e69de29 diff --git a/dist/index.html b/dist/index.html index 67732ff..6678ebd 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,8 +5,8 @@ furniture-app - - + +
diff --git a/src/App.tsx b/src/App.tsx index 8d345f0..79a01d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,6 @@ import { RoomCanvas } from './components/RoomCanvas'; import { ControlPanel } from './components/ControlPanel'; 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'; @@ -19,7 +18,6 @@ function App() { const [mode, setMode] = useState<'layout' | 'optimize'>('layout'); const [selectedId, setSelectedId] = useState(null); const [optimizing, setOptimizing] = useState(false); - const [timeLeft, setTimeLeft] = useState(15); const [lastScore, setLastScore] = useState(null); useEffect(() => { @@ -37,31 +35,28 @@ function App() { useEffect(() => { if (!optimizing) return; - if (timeLeft <= 0) { - const res = optimizeWithNN(state, { gridSize: 10 }); - setState(res.bestState); - setLastScore(res.bestEval.totalScore); - setOptimizing(false); - setTimeLeft(15); - return; - } - const t = setTimeout(() => setTimeLeft((v) => v - 1), 1000); - const { bestState, bestEval } = runRandomSearch(state, { - iterations: 5, - roomWidth: state.room.width, - roomHeight: state.room.height, - gridSize: state.room.gridSize, - }); - setState(bestState); - setLastScore(bestEval.totalScore); + const tick = () => { + const { bestState, bestEval } = runRandomSearch(state, { + iterations: 40, + roomWidth: state.room.width, + roomHeight: state.room.height, + gridSize: state.room.gridSize, + }); + setState(bestState); + setLastScore(bestEval.totalScore); + }; + const t = setTimeout(tick, 30); return () => clearTimeout(t); - }, [optimizing, timeLeft]); + }, [optimizing, state, state.room.gridSize, state.room.width, state.room.height]); const handleOptimize = () => { + if (optimizing) { + setOptimizing(false); + return; + } setState((s) => prepareForOptimize(s)); setLastScore(null); setOptimizing(true); - setTimeLeft(15); }; return ( @@ -75,7 +70,6 @@ function App() { onSelect={setSelectedId} onOptimize={handleOptimize} optimizing={optimizing} - timeLeft={timeLeft} lastScore={lastScore} />
diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index c031499..7425e9c 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import type { ConditionKind, Entrance, FurniturePreset, LayoutState, RoomConfig } from '../types'; import { NumberField } from './NumberField'; import { clampStateToRoom } from '../geometry/bounds'; +import { rectanglePoints } from '../geometry/polygon'; interface ControlPanelProps { state: LayoutState; @@ -12,7 +13,6 @@ interface ControlPanelProps { onSelect: (id: string | null) => void; onOptimize: () => void; optimizing: boolean; - timeLeft: number; lastScore: number | null; } @@ -24,6 +24,7 @@ const CONDITION_KINDS: ConditionKind[] = [ 'accessible', 'not_blocking_path', 'open_circulation', + 'line_of_sight', ]; const CONDITION_LABELS: Record = { @@ -34,6 +35,7 @@ const CONDITION_LABELS: Record = { accessible: 'Accessible', not_blocking_path: 'Not blocking path', open_circulation: 'Open circulation', + line_of_sight: 'Clear line of sight', }; const SWATCHES = ['#a1887f', '#d7ccc5', '#90caf9', '#a5d6a7', '#ef9a9a', '#ce93d8', '#ffcc80']; @@ -47,7 +49,6 @@ export function ControlPanel({ onSelect, onOptimize, optimizing, - timeLeft, lastScore, }: ControlPanelProps) { const [openId, setOpenId] = useState(null); @@ -57,12 +58,52 @@ export function ControlPanel({ ? state.furniture.find((f) => f.id === selectedId)?.type ?? (selectedId === '__entrance__' ? '__entrance__' : null) : null; + const selectedFurniture = selectedId ? state.furniture.find((f) => f.id === selectedId) ?? null : null; + useEffect(() => { if (selectedTypeId) setOpenId(selectedTypeId); }, [selectedTypeId]); - const setRoom = (room: Partial) => - onStateChange(clampStateToRoom({ ...state, room: { ...state.room, ...room } })); + const setRoom = (room: Partial) => { + const next: RoomConfig = { ...state.room, ...room }; + if (next.points && next.points.length >= 3 && state.room.points && state.room.points.length>=3 && (Math.abs(next.width-state.room.width)>1e-12||Math.abs(next.height-state.room.height)>1e-12)) { + const sx = next.width / (state.room.width || 1); + const sy = next.height / (state.room.height || 1); + next.points = next.points.map((pt) => ({ x: pt.x * sx, y: pt.y * sy })); + } + onStateChange(clampStateToRoom({ ...state, room: next })); + }; + + const setRoomShape = (shape: 'rectangle' | 'polygon') => { + if (shape === 'polygon') { + const points = state.room.points ?? rectanglePoints(state.room.width, state.room.height); + onStateChange(clampStateToRoom({ ...state, room: { ...state.room, points } })); + } else { + const { width, height, gridSize } = state.room; + onStateChange(clampStateToRoom({ ...state, room: { width, height, gridSize } })); + } + }; + + const addRoomVertex = () => { + const pts = state.room.points; + if (!pts || pts.length < 3) return; + let best = -1; + let idx = 0; + for (let i = 0; i < pts.length; i++) { + const a = pts[i]; + const b = pts[(i + 1) % pts.length]; + const len = (b.x - a.x) ** 2 + (b.y - a.y) ** 2; + if (len > best) { + best = len; + idx = i; + } + } + const a = pts[idx]; + const b = pts[(idx + 1) % pts.length]; + const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; + const next = [...pts.slice(0, idx + 1), mid, ...pts.slice(idx + 1)]; + onStateChange(clampStateToRoom({ ...state, room: { ...state.room, points: next } })); + }; const setEntrance = (entrance: Partial) => { if (!state.entrance) return; @@ -72,13 +113,53 @@ export function ControlPanel({ const placeEntrance = () => onStateChange(clampStateToRoom({ ...state, entrance: { x: 20, y: 20, width: 20, height: 20 } })); + const commitPresets = (presets: FurniturePreset[]) => { + const byId = new Map(presets.map((p) => [p.id, p])); + const furniture = state.furniture.map((f) => { + const p = f.type ? byId.get(f.type) : undefined; + if (!p) return f; + return { + ...f, + name: p.name, + color: p.color, + shape: { + ...f.shape, + type: p.shape.type, + width: p.shape.width, + height: p.shape.height, + scale: p.shape.scale ?? 1, + }, + conditions: p.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })), + }; + }); + onStateChange(clampStateToRoom({ ...state, presets, furniture })); + }; + const updateObject = (id: string, patch: Partial) => { const presets = objects.map((p) => (p.id === id ? { ...p, ...patch } : p)); - onStateChange({ ...state, presets }); + commitPresets(presets); }; const setAmount = (id: string, amount: number) => updateObject(id, { amount: Math.max(0, amount) }); + const setFurnitureRotation = (id: string, rotation: number) => { + const furniture = state.furniture.map((f) => + f.id === id ? { ...f, shape: { ...f.shape, rotation } } : f, + ); + onStateChange(clampStateToRoom({ ...state, furniture })); + }; + + const unlockAll = () => { + onStateChange({ + ...state, + furniture: state.furniture.map((f) => ({ ...f, lockedForOptimization: false })), + }); + }; + + const removeAllFurniture = () => { + onStateChange({ ...state, furniture: [] }); + }; + const removeObject = (id: string) => { onStateChange({ ...state, @@ -112,14 +193,14 @@ export function ControlPanel({ const has = p.conditions.some((c) => c.kind === k); if (enabled && !has) { const params = k === 'near_object' ? { targetId: '' } : {}; - return { ...p, conditions: [...p.conditions, { kind: k, weight: 1, params }] }; + return { ...p, conditions: [...p.conditions, { kind: k, weight: 1, leniency: 0.5, params }] }; } if (!enabled && has) { return { ...p, conditions: p.conditions.filter((c) => c.kind !== k) }; } return p; }); - onStateChange({ ...state, presets }); + commitPresets(presets); }; const setNearObjectTarget = (id: string, targetId: string) => { @@ -132,7 +213,18 @@ export function ControlPanel({ ), }; }); - onStateChange({ ...state, presets }); + commitPresets(presets); + }; + + const setConditionLeniency = (id: string, k: ConditionKind, leniency: number) => { + const presets = objects.map((p) => { + if (p.id !== id) return p; + return { + ...p, + conditions: p.conditions.map((c) => (c.kind === k ? { ...c, leniency } : c)), + }; + }); + commitPresets(presets); }; const toggleOpen = (id: string) => { @@ -172,7 +264,7 @@ export function ControlPanel({ )}
@@ -234,8 +328,9 @@ export function ControlPanel({
{CONDITION_KINDS.map((k) => { const active = p.conditions.some((c) => c.kind === k); - const targetId = (p.conditions.find((c) => c.kind === k)?.params as { targetId?: string } | undefined) - ?.targetId; + const cond = p.conditions.find((c) => c.kind === k); + const targetId = (cond?.params as { targetId?: string } | undefined)?.targetId; + const leniency = cond?.leniency ?? 0; return (
+ {active && ( +
+ + Leniency + {Math.round(leniency * 100)}% + + e.stopPropagation()} + onChange={(e) => setConditionLeniency(p.id, k, Number(e.target.value) / 100)} + /> +
+ )} {k === 'near_object' && active && ( setRoomShape(e.target.value as 'rectangle' | 'polygon')} + > + + + +
+ {state.room.points && ( + <> +
+ Drag the blue corners on the canvas to reshape the room. +
+
+ +
+ + )}
@@ -324,17 +467,54 @@ export function ControlPanel({
+ {selectedFurniture && ( +
+
Selected Object
+
+ {selectedFurniture.name} + {selectedFurniture.lockedForOptimization ? ' · locked' : ''} +
+ + setFurnitureRotation(selectedFurniture.id, Number(e.target.value))} + style={{ width: '100%', marginTop: 8, accentColor: '#4f8cff' }} + /> +
+ + + +
+
+ )} +
Entrance
{state.entrance ? ( <>