347 lines
12 KiB
TypeScript
347 lines
12 KiB
TypeScript
import type { ConditionKind, Entrance, 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 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: 20, height: 20 } });
|
|
};
|
|
|
|
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) {
|
|
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) };
|
|
}
|
|
return f;
|
|
});
|
|
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 updateSelectedSize = (width: number, height: number) => {
|
|
if (!selectedId) return;
|
|
const updated = state.furniture.map((f) =>
|
|
f.id === selectedId
|
|
? { ...f, shape: { ...f.shape, width: Math.max(10, width), height: Math.max(10, height) } }
|
|
: 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={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 }}>Premade Objects</h3>
|
|
<div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<span>Tables:</span>
|
|
<input
|
|
type="number"
|
|
min={0}
|
|
value={state.objectAmounts?.table ?? 0}
|
|
onChange={(e) =>
|
|
onStateChange({
|
|
...state,
|
|
objectAmounts: { ...(state.objectAmounts || {}), table: Math.max(0, Number(e.target.value)) },
|
|
})
|
|
}
|
|
style={{ width: 60 }}
|
|
/>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
|
|
<span>Chairs:</span>
|
|
<input
|
|
type="number"
|
|
min={0}
|
|
value={state.objectAmounts?.chair ?? 0}
|
|
onChange={(e) =>
|
|
onStateChange({
|
|
...state,
|
|
objectAmounts: { ...(state.objectAmounts || {}), chair: Math.max(0, Number(e.target.value)) },
|
|
})
|
|
}
|
|
style={{ width: 60 }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<h3 style={{ marginTop: 12 }}>Objects</h3>
|
|
<div>
|
|
{state.entrance && (
|
|
<div
|
|
onClick={() => onSelect('__entrance__')}
|
|
style={{
|
|
padding: 4,
|
|
background: selectedId === '__entrance__' ? '#eee' : 'transparent',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
Entrance
|
|
</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>
|
|
{selectedId === '__entrance__' && state.entrance && (
|
|
<div style={{ marginTop: 8 }}>
|
|
<div>Entrance Settings</div>
|
|
<label>
|
|
X:
|
|
<input
|
|
type="number"
|
|
value={state.entrance.x}
|
|
onChange={(e) => setEntrance({ x: Number(e.target.value) })}
|
|
style={{ width: 60, marginLeft: 4 }}
|
|
/>
|
|
</label>
|
|
<label style={{ marginLeft: 8 }}>
|
|
Y:
|
|
<input
|
|
type="number"
|
|
value={state.entrance.y}
|
|
onChange={(e) => setEntrance({ y: Number(e.target.value) })}
|
|
style={{ width: 60, marginLeft: 4 }}
|
|
/>
|
|
</label>
|
|
<div style={{ marginTop: 4 }}>
|
|
<label>
|
|
W:
|
|
<input
|
|
type="number"
|
|
min={10}
|
|
value={state.entrance.width ?? 20}
|
|
onChange={(e) => setEntrance({ width: Math.max(10, Number(e.target.value)) })}
|
|
style={{ width: 60, marginLeft: 4 }}
|
|
/>
|
|
</label>
|
|
<label style={{ marginLeft: 8 }}>
|
|
H:
|
|
<input
|
|
type="number"
|
|
min={10}
|
|
value={state.entrance.height ?? 20}
|
|
onChange={(e) => setEntrance({ height: Math.max(10, Number(e.target.value)) })}
|
|
style={{ width: 60, marginLeft: 4 }}
|
|
/>
|
|
</label>
|
|
</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>Size:</div>
|
|
<label>
|
|
W:
|
|
<input
|
|
type="number"
|
|
min={10}
|
|
value={selected.shape.width}
|
|
onChange={(e) => updateSelectedSize(Number(e.target.value), selected.shape.height)}
|
|
style={{ width: 60, marginLeft: 4 }}
|
|
/>
|
|
</label>
|
|
<label style={{ marginLeft: 8 }}>
|
|
H:
|
|
<input
|
|
type="number"
|
|
min={10}
|
|
value={selected.shape.height}
|
|
onChange={(e) => updateSelectedSize(selected.shape.width, Number(e.target.value))}
|
|
style={{ width: 60, marginLeft: 4 }}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div style={{ marginTop: 8 }}>
|
|
<div>Conditions:</div>
|
|
{(['walk_to_and_sit', 'around_open_area', 'near_entrance', 'near_object', 'accessible', 'not_blocking_path', 'open_circulation'] as ConditionKind[]).map(
|
|
(k) => (
|
|
<div key={k} style={{ display: 'block' }}>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.conditions.some((c) => c.kind === k)}
|
|
onChange={(e) => updateSelectedCondition(k, e.target.checked)}
|
|
/>
|
|
{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>
|
|
)}
|
|
</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>
|
|
);
|
|
}
|