Add venue planner: OOP furniture, shapes, conditions, room/birds-eye UI, layout+optimize modes, pathfinding, random search + NN stub

This commit is contained in:
2026-10-17 19:41:09 -04:00
parent 767d9502d6
commit a708a0cfe5
14 changed files with 985 additions and 118 deletions
+209
View File
@@ -0,0 +1,209 @@
import type { ConditionKind, Entrance, FurnitureConfig, LayoutState, RoomConfig } from '../types';
interface ControlPanelProps {
state: LayoutState;
onStateChange: (state: LayoutState) => void;
mode: 'layout' | 'optimize';
onModeChange: (mode: 'layout' | 'optimize') => void;
selectedId: string | null;
onSelect: (id: string | null) => void;
onOptimize: () => void;
optimizing: boolean;
timeLeft: number;
}
export function ControlPanel({
state,
onStateChange,
mode,
onModeChange,
selectedId,
onSelect,
onOptimize,
optimizing,
timeLeft,
}: ControlPanelProps) {
const addFurniture = () => {
const id = 'f_' + Date.now();
const newItem: FurnitureConfig = {
id,
name: 'Table ' + state.furniture.length,
shape: { type: 'rectangle', x: 50, y: 50, width: 60, height: 40 },
conditions: [
{ kind: 'walk_to_and_sit', weight: 1 },
{ kind: 'accessible', weight: 1 },
],
color: '#a1887f',
lockedForOptimization: false,
placed: true,
};
onStateChange({ ...state, furniture: [...state.furniture, newItem] });
onSelect(id);
};
const setRoom = (room: Partial<RoomConfig>) => {
onStateChange({ ...state, room: { ...state.room, ...room } });
};
const setEntrance = (entrance: Partial<Entrance>) => {
if (!state.entrance) return;
onStateChange({ ...state, entrance: { ...state.entrance, ...entrance } });
};
const placeEntrance = () => {
onStateChange({ ...state, entrance: { x: 20, y: 20, width: 8, height: 8 } });
};
const toggleLock = () => {
if (!selectedId) return;
const updated = state.furniture.map((f) =>
f.id === selectedId ? { ...f, lockedForOptimization: !f.lockedForOptimization } : f,
);
onStateChange({ ...state, furniture: updated });
};
const updateSelectedCondition = (kind: ConditionKind, enabled: boolean) => {
if (!selectedId) return;
const updated = state.furniture.map((f) => {
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 }] };
}
if (!enabled && has) {
return { ...f, conditions: f.conditions.filter((c) => c.kind !== kind) };
}
return f;
});
onStateChange({ ...state, furniture: updated });
};
const selected = state.furniture.find((f) => f.id === selectedId);
return (
<div className="panel" style={{ padding: 12, borderRight: '1px solid #ccc', minWidth: 260 }}>
<div style={{ marginBottom: 12 }}>
<button onClick={() => onModeChange('layout')} disabled={mode === 'layout'}>
Layout
</button>
<button onClick={() => onModeChange('optimize')} disabled={mode === 'optimize'} style={{ marginLeft: 8 }}>
Optimize
</button>
</div>
{mode === 'layout' && (
<div>
<h3>Room</h3>
<label>
Width:
<input
type="number"
value={state.room.width}
onChange={(e) => setRoom({ width: Number(e.target.value) })}
style={{ width: 80, marginLeft: 4 }}
/>
</label>
<label style={{ marginLeft: 8 }}>
Height:
<input
type="number"
value={state.room.height}
onChange={(e) => setRoom({ height: Number(e.target.value) })}
style={{ width: 80, marginLeft: 4 }}
/>
</label>
<div style={{ marginTop: 8 }}>
<button onClick={addFurniture}>Add Furniture</button>
<button onClick={placeEntrance} style={{ marginLeft: 8 }}>
Place Entrance
</button>
</div>
{state.entrance && (
<div style={{ marginTop: 8 }}>
<label>
Ex:
<input
type="number"
value={state.entrance.x}
onChange={(e) => setEntrance({ x: Number(e.target.value) })}
style={{ width: 60 }}
/>
</label>
<label style={{ marginLeft: 4 }}>
Ey:
<input
type="number"
value={state.entrance.y}
onChange={(e) => setEntrance({ y: Number(e.target.value) })}
style={{ width: 60 }}
/>
</label>
</div>
)}
<h3 style={{ marginTop: 12 }}>Objects</h3>
<div>
{state.furniture.map((f) => (
<div
key={f.id}
onClick={() => onSelect(f.id)}
style={{
padding: 4,
background: selectedId === f.id ? '#eee' : 'transparent',
cursor: 'pointer',
}}
>
{f.name} {f.lockedForOptimization ? '(locked)' : ''}
</div>
))}
</div>
{selected && (
<div style={{ marginTop: 8 }}>
<div>
<label>
Name:
<input
value={selected.name}
onChange={(e) => {
const updated = state.furniture.map((f) => (f.id === selectedId ? { ...f, name: e.target.value } : f));
onStateChange({ ...state, furniture: updated });
}}
style={{ width: 120, marginLeft: 4 }}
/>
</label>
</div>
<div style={{ marginTop: 4 }}>
<button onClick={toggleLock}>{selected.lockedForOptimization ? 'Unlock' : 'Lock'} for opt</button>
</div>
<div style={{ marginTop: 8 }}>
<div>Conditions:</div>
{(['walk_to_and_sit', 'around_open_area', 'near_entrance', 'accessible', 'not_blocking_path', 'open_circulation'] as ConditionKind[]).map(
(k) => (
<label key={k} style={{ display: 'block' }}>
<input
type="checkbox"
checked={selected.conditions.some((c) => c.kind === k)}
onChange={(e) => updateSelectedCondition(k, e.target.checked)}
/>
{k}
</label>
),
)}
</div>
</div>
)}
</div>
)}
{mode === 'optimize' && (
<div>
<h3>Optimize</h3>
<p>Runs random search (15s) then NN-informed placement heuristic.</p>
<button onClick={onOptimize} disabled={optimizing}>
{optimizing ? `Optimizing... ${timeLeft}s` : 'Optimize'}
</button>
{optimizing && <div style={{ marginTop: 8 }}>Randomizing layouts, keeping best...</div>}
</div>
)}
</div>
);
}