diff --git a/dist/index.html b/dist/index.html index ba48e80..bec119b 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 7826559..a538cef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { RoomCanvas } from './components/RoomCanvas'; import { ControlPanel } from './components/ControlPanel'; -import type { LayoutState } from './types'; +import type { FurnitureConfig, LayoutState } from './types'; import { runRandomSearch } from './optimization/randomSearch'; import { optimizeWithNN } from './ai/nn'; @@ -9,6 +9,7 @@ const initialState: LayoutState = { room: { width: 600, height: 400, gridSize: 10 }, entrance: { x: 30, y: 30 }, furniture: [], + objectCount: 0, }; function App() { @@ -18,10 +19,10 @@ function App() { const [optimizing, setOptimizing] = useState(false); const [timeLeft, setTimeLeft] = useState(15); + useEffect(() => { if (!optimizing) return; if (timeLeft <= 0) { - // run NN optimization after random search window conceptually; run final pass const res = optimizeWithNN(state, { gridSize: 10 }); setState(res.bestState); setOptimizing(false); @@ -29,7 +30,6 @@ function App() { return; } const t = setTimeout(() => setTimeLeft((v) => v - 1), 1000); - // also do a quick random search step each tick by sampling (lightweight) const { bestState } = runRandomSearch(state, { iterations: 5, roomWidth: state.room.width, @@ -41,6 +41,37 @@ 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); setOptimizing(true); setTimeLeft(15); }; diff --git a/src/ai/nn.ts b/src/ai/nn.ts index 00e3e1a..e967591 100644 --- a/src/ai/nn.ts +++ b/src/ai/nn.ts @@ -26,9 +26,23 @@ export function optimizeWithNN(state: LayoutState, options: NNOptions = {}): { b let targetY = f.shape.y; const needsNearEntrance = f.conditions.some((c) => c.kind === 'near_entrance'); const needsCenter = f.conditions.some((c) => c.kind === 'around_open_area'); + const needsNearObj = f.conditions.find((c) => c.kind === 'near_object'); if (entrance && needsNearEntrance) { targetX = entrance.x + (Math.random() - 0.5) * 40; targetY = entrance.y + (Math.random() - 0.5) * 40; + } else if (needsNearObj) { + const tid = (needsNearObj.params as any)?.targetId; + const t = tid ? candidate.furniture.find((x) => x.id === tid) : undefined; + if (t) { + targetX = t.shape.x + t.shape.width / 2 - f.shape.width / 2 + (Math.random() - 0.5) * 30; + targetY = t.shape.y + t.shape.height / 2 - f.shape.height / 2 + (Math.random() - 0.5) * 30; + } else if (needsCenter) { + targetX = centerX - f.shape.width / 2 + (Math.random() - 0.5) * 60; + targetY = centerY - f.shape.height / 2 + (Math.random() - 0.5) * 60; + } else { + targetX = Math.random() * (candidate.room.width - f.shape.width); + targetY = Math.random() * (candidate.room.height - f.shape.height); + } } else if (needsCenter) { targetX = centerX - f.shape.width / 2 + (Math.random() - 0.5) * 60; targetY = centerY - f.shape.height / 2 + (Math.random() - 0.5) * 60; diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 408385b..a7470b0 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -1,4 +1,4 @@ -import type { ConditionKind, Entrance, FurnitureConfig, LayoutState, RoomConfig } from '../types'; +import type { ConditionKind, Entrance, FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types'; interface ControlPanelProps { state: LayoutState; @@ -23,6 +23,21 @@ 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 = { @@ -41,6 +56,28 @@ export function ControlPanel({ 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 } }); }; @@ -68,7 +105,8 @@ export function ControlPanel({ if (f.id !== selectedId) return f; const has = f.conditions.some((c) => c.kind === kind); if (enabled && !has) { - return { ...f, conditions: [...f.conditions, { kind, weight: 1 }] }; + const params = kind === 'near_object' ? { targetId: '' } : {}; + return { ...f, conditions: [...f.conditions, { kind, weight: 1, params }] }; } if (!enabled && has) { return { ...f, conditions: f.conditions.filter((c) => c.kind !== kind) }; @@ -78,6 +116,18 @@ export function ControlPanel({ onStateChange({ ...state, furniture: updated }); }; + const setNearObjectTarget = (targetId: string) => { + if (!selectedId) return; + const updated = state.furniture.map((f) => { + if (f.id !== selectedId) return f; + return { + ...f, + conditions: f.conditions.map((c) => (c.kind === 'near_object' ? { ...c, params: { ...c.params, targetId } } : c)), + }; + }); + onStateChange({ ...state, furniture: updated }); + }; + const selected = state.furniture.find((f) => f.id === selectedId); return ( @@ -112,10 +162,29 @@ export function ControlPanel({ style={{ width: 80, marginLeft: 4 }} /> +
+ +
+
{state.entrance && ( @@ -176,16 +245,34 @@ export function ControlPanel({
Conditions:
- {(['walk_to_and_sit', 'around_open_area', 'near_entrance', 'accessible', 'not_blocking_path', 'open_circulation'] as ConditionKind[]).map( + {(['walk_to_and_sit', 'around_open_area', 'near_entrance', 'near_object', 'accessible', 'not_blocking_path', 'open_circulation'] as ConditionKind[]).map( (k) => ( - +
+ + {k === 'near_object' && selected.conditions.some((c) => c.kind === 'near_object') && ( + + )} +
), )}
diff --git a/src/layout/evaluator.ts b/src/layout/evaluator.ts index c22f096..1a548c9 100644 --- a/src/layout/evaluator.ts +++ b/src/layout/evaluator.ts @@ -40,7 +40,7 @@ export function evaluateLayout(state: LayoutState, gridSize: number = 10): Layou let score = 0; for (const c of f.conditions) { - const s = scoreCondition(c.kind, f, state, accessibility.find((a) => a.furnitureId === f.id) || null, path); + const s = scoreCondition(c.kind, c, f, state, accessibility.find((a) => a.furnitureId === f.id) || null, path); score += (s * (c.weight || 1)) / (f.conditions.length || 1); } conditionScores[f.id] = score; @@ -53,6 +53,7 @@ export function evaluateLayout(state: LayoutState, gridSize: number = 10): Layou function scoreCondition( kind: string, + c: any, f: FurnitureConfig, state: LayoutState, acc: AccessibilityResult | null, @@ -85,6 +86,18 @@ function scoreCondition( case 'accessible': { return acc?.reachable ? 1.0 : 0.0; } + case 'near_object': { + const tid = (c.params as any)?.targetId; + if (!tid) return 0.3; + const t = state.furniture.find((x) => x.id === tid); + if (!t) return 0.3; + const cx = f.shape.x + f.shape.width / 2; + const cy = f.shape.y + f.shape.height / 2; + const tx = t.shape.x + t.shape.width / 2; + const ty = t.shape.y + t.shape.height / 2; + const d = Math.sqrt((cx - tx) ** 2 + (cy - ty) ** 2); + return Math.max(0, 1 - d / 80); + } case 'open_circulation': { return path && path.length > 2 ? 0.7 : 0.5; } diff --git a/src/types/index.ts b/src/types/index.ts index 739d167..c7d600f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -26,6 +26,7 @@ export type ConditionKind = | 'walk_to_and_sit' | 'around_open_area' | 'near_entrance' + | 'near_object' | 'not_blocking_path' | 'accessible' | 'open_circulation'; @@ -87,8 +88,18 @@ export interface LayoutEvaluation { penalty: number; } +export interface FurniturePreset { + id: string; + name: string; + shape: Omit; + conditions: ConditionSpec[]; + color?: string; +} + export interface LayoutState { room: RoomConfig; entrance: Entrance | null; furniture: FurnitureConfig[]; + presets?: FurniturePreset[]; + objectCount?: number; }