Treat object count as optimizer spec; drag cards onto room to pin/lock, optimizer fills the rest
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-Be3LQNz7.js"></script>
|
<script type="module" crossorigin src="/assets/index-CCxBSCEa.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Gl7EQEpA.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Gl7EQEpA.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+14
-72
@@ -1,11 +1,11 @@
|
|||||||
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 { FurnitureConfig, FurniturePreset, LayoutState } from './types';
|
import type { LayoutState } from './types';
|
||||||
import { runRandomSearch } from './optimization/randomSearch';
|
import { runRandomSearch } from './optimization/randomSearch';
|
||||||
import { optimizeWithNN } from './ai/nn';
|
import { optimizeWithNN } from './ai/nn';
|
||||||
|
import { prepareForOptimize } from './optimization/prepare';
|
||||||
import { DEFAULT_PRESETS } from './furniture/presets';
|
import { DEFAULT_PRESETS } from './furniture/presets';
|
||||||
import { clampRectToRoom, clampStateToRoom } from './geometry/bounds';
|
|
||||||
|
|
||||||
const initialState: LayoutState = {
|
const initialState: LayoutState = {
|
||||||
room: { width: 600, height: 400, gridSize: 10 },
|
room: { width: 600, height: 400, gridSize: 10 },
|
||||||
@@ -14,70 +14,6 @@ const initialState: LayoutState = {
|
|||||||
presets: DEFAULT_PRESETS,
|
presets: DEFAULT_PRESETS,
|
||||||
};
|
};
|
||||||
|
|
||||||
function makeFurniture(preset: FurniturePreset, index: number, room: LayoutState['room']): FurnitureConfig {
|
|
||||||
const spawn = clampRectToRoom(
|
|
||||||
{
|
|
||||||
x: 80 + (index % 5) * 60,
|
|
||||||
y: 80 + Math.floor(index / 5) * 60,
|
|
||||||
width: preset.shape.width,
|
|
||||||
height: preset.shape.height,
|
|
||||||
},
|
|
||||||
room,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
id: `${preset.id}_${Date.now()}_${index}`,
|
|
||||||
name: `${preset.name} ${index + 1}`,
|
|
||||||
type: preset.id,
|
|
||||||
shape: {
|
|
||||||
...preset.shape,
|
|
||||||
x: spawn.x,
|
|
||||||
y: spawn.y,
|
|
||||||
width: spawn.width,
|
|
||||||
height: spawn.height,
|
|
||||||
},
|
|
||||||
conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })),
|
|
||||||
color: preset.color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncFurniture(state: LayoutState): LayoutState {
|
|
||||||
const presets = state.presets || [];
|
|
||||||
const validIds = new Set(presets.map((p) => p.id));
|
|
||||||
let furniture = state.furniture.filter((f) => !f.type || f.type === 'custom' || validIds.has(f.type));
|
|
||||||
|
|
||||||
for (const preset of presets) {
|
|
||||||
const desired = preset.amount ?? 0;
|
|
||||||
const existing = furniture.filter((f) => f.type === preset.id);
|
|
||||||
|
|
||||||
if (existing.length > desired) {
|
|
||||||
const removeIds = new Set(
|
|
||||||
[...existing]
|
|
||||||
.sort((a, b) => Number(a.placed) - Number(b.placed))
|
|
||||||
.slice(0, existing.length - desired)
|
|
||||||
.map((f) => f.id),
|
|
||||||
);
|
|
||||||
furniture = furniture.filter((f) => !removeIds.has(f.id));
|
|
||||||
} else if (existing.length < desired) {
|
|
||||||
for (let i = existing.length; i < desired; i++) {
|
|
||||||
furniture.push(makeFurniture(preset, i, state.room));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
furniture = furniture.map((f) =>
|
|
||||||
f.type === preset.id
|
|
||||||
? {
|
|
||||||
...f,
|
|
||||||
shape: { ...f.shape, width: preset.shape.width, height: preset.shape.height },
|
|
||||||
color: preset.color,
|
|
||||||
conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })),
|
|
||||||
}
|
|
||||||
: f,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...state, furniture };
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [state, setState] = useState<LayoutState>(initialState);
|
const [state, setState] = useState<LayoutState>(initialState);
|
||||||
const [mode, setMode] = useState<'layout' | 'optimize'>('layout');
|
const [mode, setMode] = useState<'layout' | 'optimize'>('layout');
|
||||||
@@ -86,8 +22,17 @@ function App() {
|
|||||||
const [timeLeft, setTimeLeft] = useState(15);
|
const [timeLeft, setTimeLeft] = useState(15);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState((s) => clampStateToRoom(syncFurniture(s)));
|
const onKey = (e: KeyboardEvent) => {
|
||||||
}, [state.presets]);
|
if (e.key !== 'Delete' && e.key !== 'Backspace') return;
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return;
|
||||||
|
if (!selectedId || selectedId === '__entrance__') return;
|
||||||
|
setState((s) => ({ ...s, furniture: s.furniture.filter((f) => f.id !== selectedId) }));
|
||||||
|
setSelectedId(null);
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [selectedId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!optimizing) return;
|
if (!optimizing) return;
|
||||||
@@ -110,10 +55,7 @@ function App() {
|
|||||||
}, [optimizing, timeLeft]);
|
}, [optimizing, timeLeft]);
|
||||||
|
|
||||||
const handleOptimize = () => {
|
const handleOptimize = () => {
|
||||||
setState((s) => ({
|
setState((s) => prepareForOptimize(s));
|
||||||
...s,
|
|
||||||
furniture: s.furniture.map((f) => ({ ...f, lockedForOptimization: f.placed ? true : false })),
|
|
||||||
}));
|
|
||||||
setOptimizing(true);
|
setOptimizing(true);
|
||||||
setTimeLeft(15);
|
setTimeLeft(15);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -140,12 +140,22 @@ export function ControlPanel({
|
|||||||
|
|
||||||
const renderObjectCard = (p: FurniturePreset) => {
|
const renderObjectCard = (p: FurniturePreset) => {
|
||||||
const open = openId === p.id;
|
const open = openId === p.id;
|
||||||
|
const placedCount = state.furniture.filter((f) => f.type === p.id && f.placed).length;
|
||||||
return (
|
return (
|
||||||
<div key={p.id} className={`obj-card ${open ? 'open' : ''}`}>
|
<div key={p.id} className={`obj-card ${open ? 'open' : ''}`}>
|
||||||
<div className="obj-head" onClick={() => toggleOpen(p.id)}>
|
<div
|
||||||
|
className="obj-head"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => {
|
||||||
|
e.dataTransfer.setData('application/x-preset-id', p.id);
|
||||||
|
e.dataTransfer.effectAllowed = 'copy';
|
||||||
|
}}
|
||||||
|
onClick={() => toggleOpen(p.id)}
|
||||||
|
>
|
||||||
<span className={`obj-caret ${open ? 'open' : ''}`}>▶</span>
|
<span className={`obj-caret ${open ? 'open' : ''}`}>▶</span>
|
||||||
<span className="obj-swatch" style={{ background: p.color }} />
|
<span className="obj-swatch" style={{ background: p.color }} />
|
||||||
<span className="obj-name">{p.name}</span>
|
<span className="obj-name">{p.name}</span>
|
||||||
|
{placedCount > 0 && <span className="obj-badge">{placedCount} placed</span>}
|
||||||
<span className="obj-count" onClick={(e) => e.stopPropagation()}>
|
<span className="obj-count" onClick={(e) => e.stopPropagation()}>
|
||||||
<NumberField value={p.amount ?? 0} min={0} width={44} onChange={(v) => setAmount(p.id, v)} />
|
<NumberField value={p.amount ?? 0} min={0} width={44} onChange={(v) => setAmount(p.id, v)} />
|
||||||
</span>
|
</span>
|
||||||
@@ -340,6 +350,10 @@ export function ControlPanel({
|
|||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<div className="section-title">Objects</div>
|
<div className="section-title">Objects</div>
|
||||||
|
<div className="hint">
|
||||||
|
Count tells the optimizer how many are needed. Drag a card onto the room to pin one in place
|
||||||
|
(locked); the optimizer arranges the rest.
|
||||||
|
</div>
|
||||||
<div className="obj-list">{objects.map(renderObjectCard)}</div>
|
<div className="obj-list">{objects.map(renderObjectCard)}</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import type { LayoutState, Point } from '../types';
|
import type { LayoutState, Point } from '../types';
|
||||||
import { computePaths } from '../pathfinding/paths';
|
import { computePaths } from '../pathfinding/paths';
|
||||||
import { clampRectToRoom } from '../geometry/bounds';
|
import { clampRectToRoom } from '../geometry/bounds';
|
||||||
|
import { makePlaced } from '../optimization/prepare';
|
||||||
|
|
||||||
interface RoomCanvasProps {
|
interface RoomCanvasProps {
|
||||||
state: LayoutState;
|
state: LayoutState;
|
||||||
@@ -156,7 +157,7 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMousePos = (e: React.MouseEvent<HTMLCanvasElement>): Point => {
|
const getMousePos = (e: { clientX: number; clientY: number }): Point => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return { x: 0, y: 0 };
|
if (!canvas) return { x: 0, y: 0 };
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@@ -250,6 +251,23 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s
|
|||||||
setDrag(null);
|
setDrag(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLCanvasElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLCanvasElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const presetId = e.dataTransfer.getData('application/x-preset-id');
|
||||||
|
if (!presetId) return;
|
||||||
|
const preset = (state.presets || []).find((p) => p.id === presetId);
|
||||||
|
if (!preset) return;
|
||||||
|
const pos = getMousePos(e);
|
||||||
|
const f = makePlaced(preset, pos.x - preset.shape.width / 2, pos.y - preset.shape.height / 2, state.room);
|
||||||
|
onStateChange({ ...state, furniture: [...state.furniture, f] });
|
||||||
|
onSelect(f.id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
@@ -259,6 +277,8 @@ export function RoomCanvas({ state, onStateChange, mode, selectedId, onSelect, s
|
|||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type { FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types';
|
||||||
|
import { clampRectToRoom, clampStateToRoom } from '../geometry/bounds';
|
||||||
|
|
||||||
|
export function makeGenerated(preset: FurniturePreset, index: number, room: RoomConfig): FurnitureConfig {
|
||||||
|
const b = clampRectToRoom(
|
||||||
|
{
|
||||||
|
x: Math.random() * Math.max(0, room.width - preset.shape.width),
|
||||||
|
y: Math.random() * Math.max(0, room.height - preset.shape.height),
|
||||||
|
width: preset.shape.width,
|
||||||
|
height: preset.shape.height,
|
||||||
|
},
|
||||||
|
room,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: `${preset.id}_gen_${Date.now()}_${index}`,
|
||||||
|
name: preset.name,
|
||||||
|
type: preset.id,
|
||||||
|
shape: { ...preset.shape, ...b },
|
||||||
|
conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })),
|
||||||
|
color: preset.color,
|
||||||
|
placed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makePlaced(preset: FurniturePreset, x: number, y: number, room: RoomConfig): FurnitureConfig {
|
||||||
|
const b = clampRectToRoom(
|
||||||
|
{ x, y, width: preset.shape.width, height: preset.shape.height },
|
||||||
|
room,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: `${preset.id}_placed_${Date.now()}_${Math.round(Math.random() * 1e6)}`,
|
||||||
|
name: preset.name,
|
||||||
|
type: preset.id,
|
||||||
|
shape: { ...preset.shape, ...b },
|
||||||
|
conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })),
|
||||||
|
color: preset.color,
|
||||||
|
placed: true,
|
||||||
|
lockedForOptimization: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareForOptimize(state: LayoutState): LayoutState {
|
||||||
|
const presets = state.presets || [];
|
||||||
|
let furniture = [...state.furniture];
|
||||||
|
|
||||||
|
for (const preset of presets) {
|
||||||
|
const wanted = preset.amount ?? 0;
|
||||||
|
const ofType = furniture.filter((f) => f.type === preset.id);
|
||||||
|
const placed = ofType.filter((f) => f.placed);
|
||||||
|
let generated = ofType.filter((f) => !f.placed);
|
||||||
|
|
||||||
|
const need = Math.max(0, wanted - placed.length);
|
||||||
|
if (generated.length > need) {
|
||||||
|
generated = generated.slice(0, need);
|
||||||
|
}
|
||||||
|
while (generated.length < need) {
|
||||||
|
generated.push(makeGenerated(preset, generated.length, state.room));
|
||||||
|
}
|
||||||
|
|
||||||
|
const synced = [...placed, ...generated].map((f) => ({
|
||||||
|
...f,
|
||||||
|
name: preset.name,
|
||||||
|
color: preset.color,
|
||||||
|
shape: { ...f.shape, width: preset.shape.width, height: preset.shape.height },
|
||||||
|
conditions: preset.conditions.map((c) => ({ ...c, params: { ...(c.params || {}) } })),
|
||||||
|
lockedForOptimization: !!f.placed,
|
||||||
|
}));
|
||||||
|
|
||||||
|
furniture = furniture.filter((f) => f.type !== preset.id);
|
||||||
|
furniture = [...furniture, ...synced];
|
||||||
|
}
|
||||||
|
|
||||||
|
return clampStateToRoom({ ...state, furniture });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user