Add presets (table/chair), object count, near_object condition, optimization locks placed objects and randomizes remaining
This commit is contained in:
Vendored
+1
-1
@@ -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
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,9 +245,10 @@ 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' }}>
|
||||||
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selected.conditions.some((c) => c.kind === k)}
|
checked={selected.conditions.some((c) => c.kind === k)}
|
||||||
@@ -186,6 +256,23 @@ export function ControlPanel({
|
|||||||
/>
|
/>
|
||||||
{k}
|
{k}
|
||||||
</label>
|
</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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user