Add presets (table/chair), object count, near_object condition, optimization locks placed objects and randomizes remaining

This commit is contained in:
2026-10-17 19:52:52 -04:00
parent a708a0cfe5
commit ced33b672f
6 changed files with 172 additions and 16 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>furniture-app</title> <title>furniture-app</title>
<script type="module" crossorigin src="/assets/index-PcoLP7A2.js"></script> <script type="module" crossorigin src="/assets/index-j9ORS9Fz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B4HUQdL0.css"> <link rel="stylesheet" crossorigin href="/assets/index-B4HUQdL0.css">
</head> </head>
<body> <body>
+34 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { RoomCanvas } from './components/RoomCanvas'; import { RoomCanvas } from './components/RoomCanvas';
import { ControlPanel } from './components/ControlPanel'; import { ControlPanel } from './components/ControlPanel';
import type { LayoutState } from './types'; import type { FurnitureConfig, LayoutState } from './types';
import { runRandomSearch } from './optimization/randomSearch'; import { runRandomSearch } from './optimization/randomSearch';
import { optimizeWithNN } from './ai/nn'; import { optimizeWithNN } from './ai/nn';
@@ -9,6 +9,7 @@ const initialState: LayoutState = {
room: { width: 600, height: 400, gridSize: 10 }, room: { width: 600, height: 400, gridSize: 10 },
entrance: { x: 30, y: 30 }, entrance: { x: 30, y: 30 },
furniture: [], furniture: [],
objectCount: 0,
}; };
function App() { function App() {
@@ -18,10 +19,10 @@ function App() {
const [optimizing, setOptimizing] = useState(false); const [optimizing, setOptimizing] = useState(false);
const [timeLeft, setTimeLeft] = useState(15); const [timeLeft, setTimeLeft] = useState(15);
useEffect(() => { useEffect(() => {
if (!optimizing) return; if (!optimizing) return;
if (timeLeft <= 0) { if (timeLeft <= 0) {
// run NN optimization after random search window conceptually; run final pass
const res = optimizeWithNN(state, { gridSize: 10 }); const res = optimizeWithNN(state, { gridSize: 10 });
setState(res.bestState); setState(res.bestState);
setOptimizing(false); setOptimizing(false);
@@ -29,7 +30,6 @@ function App() {
return; return;
} }
const t = setTimeout(() => setTimeLeft((v) => v - 1), 1000); const t = setTimeout(() => setTimeLeft((v) => v - 1), 1000);
// also do a quick random search step each tick by sampling (lightweight)
const { bestState } = runRandomSearch(state, { const { bestState } = runRandomSearch(state, {
iterations: 5, iterations: 5,
roomWidth: state.room.width, roomWidth: state.room.width,
@@ -41,6 +41,37 @@ function App() {
}, [optimizing, timeLeft]); }, [optimizing, timeLeft]);
const handleOptimize = () => { 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); setOptimizing(true);
setTimeLeft(15); setTimeLeft(15);
}; };
+14
View File
@@ -26,9 +26,23 @@ export function optimizeWithNN(state: LayoutState, options: NNOptions = {}): { b
let targetY = f.shape.y; let targetY = f.shape.y;
const needsNearEntrance = f.conditions.some((c) => c.kind === 'near_entrance'); const needsNearEntrance = f.conditions.some((c) => c.kind === 'near_entrance');
const needsCenter = f.conditions.some((c) => c.kind === 'around_open_area'); const needsCenter = f.conditions.some((c) => c.kind === 'around_open_area');
const needsNearObj = f.conditions.find((c) => c.kind === 'near_object');
if (entrance && needsNearEntrance) { if (entrance && needsNearEntrance) {
targetX = entrance.x + (Math.random() - 0.5) * 40; targetX = entrance.x + (Math.random() - 0.5) * 40;
targetY = entrance.y + (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) { } else if (needsCenter) {
targetX = centerX - f.shape.width / 2 + (Math.random() - 0.5) * 60; targetX = centerX - f.shape.width / 2 + (Math.random() - 0.5) * 60;
targetY = centerY - f.shape.height / 2 + (Math.random() - 0.5) * 60; targetY = centerY - f.shape.height / 2 + (Math.random() - 0.5) * 60;
+98 -11
View File
@@ -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 { interface ControlPanelProps {
state: LayoutState; state: LayoutState;
@@ -23,6 +23,21 @@ export function ControlPanel({
optimizing, optimizing,
timeLeft, timeLeft,
}: ControlPanelProps) { }: 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 addFurniture = () => {
const id = 'f_' + Date.now(); const id = 'f_' + Date.now();
const newItem: FurnitureConfig = { const newItem: FurnitureConfig = {
@@ -41,6 +56,28 @@ export function ControlPanel({
onSelect(id); 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<RoomConfig>) => { const setRoom = (room: Partial<RoomConfig>) => {
onStateChange({ ...state, room: { ...state.room, ...room } }); onStateChange({ ...state, room: { ...state.room, ...room } });
}; };
@@ -68,7 +105,8 @@ export function ControlPanel({
if (f.id !== selectedId) return f; if (f.id !== selectedId) return f;
const has = f.conditions.some((c) => c.kind === kind); const has = f.conditions.some((c) => c.kind === kind);
if (enabled && !has) { 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) { if (!enabled && has) {
return { ...f, conditions: f.conditions.filter((c) => c.kind !== kind) }; return { ...f, conditions: f.conditions.filter((c) => c.kind !== kind) };
@@ -78,6 +116,18 @@ export function ControlPanel({
onStateChange({ ...state, furniture: updated }); 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); const selected = state.furniture.find((f) => f.id === selectedId);
return ( return (
@@ -112,10 +162,29 @@ export function ControlPanel({
style={{ width: 80, marginLeft: 4 }} style={{ width: 80, marginLeft: 4 }}
/> />
</label> </label>
<div style={{ marginTop: 8 }}>
<label>
Object Count:
<input
type="number"
min={0}
value={state.objectCount ?? state.furniture.length}
onChange={(e) =>
onStateChange({ ...state, objectCount: Math.max(0, Number(e.target.value)) })
}
style={{ width: 80, marginLeft: 4 }}
/>
</label>
</div>
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<button onClick={addFurniture}>Add Furniture</button> <button onClick={addFurniture}>Add Furniture</button>
<button onClick={placeEntrance} style={{ marginLeft: 8 }}> <button onClick={placeEntrance} style={{ marginLeft: 8 }}>
Place Entrance Place Entrance
</button> <button onClick={() => addPreset(tablePreset)} style={{ marginLeft: 8 }}>
Add Table
</button>
<button onClick={() => addPreset(chairPreset)} style={{ marginLeft: 8 }}>
Add Chair
</button> </button>
</div> </div>
{state.entrance && ( {state.entrance && (
@@ -176,16 +245,34 @@ export function ControlPanel({
</div> </div>
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<div>Conditions:</div> <div>Conditions:</div>
{(['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) => (
<label key={k} style={{ display: 'block' }}> <div key={k} style={{ display: 'block' }}>
<input <label>
type="checkbox" <input
checked={selected.conditions.some((c) => c.kind === k)} type="checkbox"
onChange={(e) => updateSelectedCondition(k, e.target.checked)} checked={selected.conditions.some((c) => c.kind === k)}
/> onChange={(e) => updateSelectedCondition(k, e.target.checked)}
{k} />
</label> {k}
</label>
{k === 'near_object' && selected.conditions.some((c) => c.kind === 'near_object') && (
<select
style={{ marginLeft: 8 }}
value={(selected.conditions.find((c) => c.kind === 'near_object')?.params as any)?.targetId || ''}
onChange={(e) => setNearObjectTarget(e.target.value)}
>
<option value="">Select object</option>
{state.furniture
.filter((f) => f.id !== selected.id)
.map((f) => (
<option key={f.id} value={f.id}>
{f.name}
</option>
))}
</select>
)}
</div>
), ),
)} )}
</div> </div>
+14 -1
View File
@@ -40,7 +40,7 @@ export function evaluateLayout(state: LayoutState, gridSize: number = 10): Layou
let score = 0; let score = 0;
for (const c of f.conditions) { 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); score += (s * (c.weight || 1)) / (f.conditions.length || 1);
} }
conditionScores[f.id] = score; conditionScores[f.id] = score;
@@ -53,6 +53,7 @@ export function evaluateLayout(state: LayoutState, gridSize: number = 10): Layou
function scoreCondition( function scoreCondition(
kind: string, kind: string,
c: any,
f: FurnitureConfig, f: FurnitureConfig,
state: LayoutState, state: LayoutState,
acc: AccessibilityResult | null, acc: AccessibilityResult | null,
@@ -85,6 +86,18 @@ function scoreCondition(
case 'accessible': { case 'accessible': {
return acc?.reachable ? 1.0 : 0.0; 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': { case 'open_circulation': {
return path && path.length > 2 ? 0.7 : 0.5; return path && path.length > 2 ? 0.7 : 0.5;
} }
+11
View File
@@ -26,6 +26,7 @@ export type ConditionKind =
| 'walk_to_and_sit' | 'walk_to_and_sit'
| 'around_open_area' | 'around_open_area'
| 'near_entrance' | 'near_entrance'
| 'near_object'
| 'not_blocking_path' | 'not_blocking_path'
| 'accessible' | 'accessible'
| 'open_circulation'; | 'open_circulation';
@@ -87,8 +88,18 @@ export interface LayoutEvaluation {
penalty: number; penalty: number;
} }
export interface FurniturePreset {
id: string;
name: string;
shape: Omit<ShapeData, 'x' | 'y'>;
conditions: ConditionSpec[];
color?: string;
}
export interface LayoutState { export interface LayoutState {
room: RoomConfig; room: RoomConfig;
entrance: Entrance | null; entrance: Entrance | null;
furniture: FurnitureConfig[]; furniture: FurnitureConfig[];
presets?: FurniturePreset[];
objectCount?: number;
} }