diff --git a/dist/index.html b/dist/index.html index 4dba354..e634b55 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,8 +5,8 @@ furniture-app - - + +
diff --git a/src/App.tsx b/src/App.tsx index b45110b..2dcc5dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,29 +13,6 @@ const initialState: LayoutState = { presets: DEFAULT_PRESETS, }; -function syncFurniture(state: LayoutState): LayoutState { - const presets = state.presets || []; - let furniture = [...state.furniture]; - 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)); - } - } - } - return { ...state, furniture }; -} - function makeFurniture(preset: FurniturePreset, index: number): FurnitureConfig { return { id: `${preset.id}_${Date.now()}_${index}`, @@ -53,6 +30,44 @@ function makeFurniture(preset: FurniturePreset, index: number): FurnitureConfig }; } +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)); + } + } + + 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() { const [state, setState] = useState(initialState); const [mode, setMode] = useState<'layout' | 'optimize'>('layout'); diff --git a/src/ai/nn.ts b/src/ai/nn.ts index e967591..c76bc0c 100644 --- a/src/ai/nn.ts +++ b/src/ai/nn.ts @@ -32,7 +32,10 @@ export function optimizeWithNN(state: LayoutState, options: NNOptions = {}): { b 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; + const t = tid + ? candidate.furniture.find((x) => x.id === tid) || + candidate.furniture.find((x) => x.type === tid && x.id !== f.id) + : 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; diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 0ce552b..222755f 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import type { ConditionKind, Entrance, FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types'; +import { useEffect, useState } from 'react'; +import type { ConditionKind, Entrance, FurniturePreset, LayoutState, RoomConfig } from '../types'; import { NumberField } from './NumberField'; interface ControlPanelProps { @@ -34,6 +34,8 @@ const CONDITION_LABELS: Record = { open_circulation: 'Open circulation', }; +const SWATCHES = ['#a1887f', '#d7ccc5', '#90caf9', '#a5d6a7', '#ef9a9a', '#ce93d8', '#ffcc80']; + export function ControlPanel({ state, onStateChange, @@ -46,92 +48,66 @@ export function ControlPanel({ timeLeft, }: ControlPanelProps) { const [openId, setOpenId] = useState(null); + const objects = state.presets || []; - const setRoom = (room: Partial) => { - onStateChange({ ...state, room: { ...state.room, ...room } }); - }; + const selectedTypeId = selectedId + ? state.furniture.find((f) => f.id === selectedId)?.type ?? (selectedId === '__entrance__' ? '__entrance__' : null) + : null; + + useEffect(() => { + if (selectedTypeId) setOpenId(selectedTypeId); + }, [selectedTypeId]); + + const setRoom = (room: Partial) => onStateChange({ ...state, room: { ...state.room, ...room } }); const setEntrance = (entrance: Partial) => { if (!state.entrance) return; onStateChange({ ...state, entrance: { ...state.entrance, ...entrance } }); }; - const placeEntrance = () => { - onStateChange({ ...state, entrance: { x: 20, y: 20, width: 20, height: 20 } }); + const placeEntrance = () => onStateChange({ ...state, entrance: { x: 20, y: 20, width: 20, height: 20 } }); + + const updateObject = (id: string, patch: Partial) => { + const presets = objects.map((p) => (p.id === id ? { ...p, ...patch } : p)); + onStateChange({ ...state, presets }); + }; + + const setAmount = (id: string, amount: number) => updateObject(id, { amount: Math.max(0, amount) }); + + const removeObject = (id: string) => { + onStateChange({ + ...state, + presets: objects.filter((p) => p.id !== id), + furniture: state.furniture.filter((f) => f.type !== id), + }); }; const createObject = () => { const id = 'obj_' + Date.now(); - const newItem: FurnitureConfig = { + const newObject: FurniturePreset = { id, - name: 'Object ' + (state.furniture.length + 1), - type: 'custom', - shape: { type: 'rectangle', x: 100, y: 100, width: 50, height: 50 }, + name: 'Object ' + (objects.filter((o) => !o.isPreset).length + 1), + color: SWATCHES[objects.length % SWATCHES.length], + amount: 1, + isPreset: false, + shape: { type: 'rectangle', width: 50, height: 50 }, conditions: [ { kind: 'walk_to_and_sit', weight: 1 }, { kind: 'accessible', weight: 1 }, ], - color: '#90caf9', - lockedForOptimization: false, - placed: true, }; - onStateChange({ ...state, furniture: [...state.furniture, newItem] }); + onStateChange({ ...state, presets: [...objects, newObject] }); onSelect(id); setOpenId(id); }; - const updatePreset = (id: string, patch: Partial) => { - const presets = (state.presets || []).map((p) => (p.id === id ? { ...p, ...patch } : p)); - onStateChange({ ...state, presets }); - }; - - const setPresetAmount = (id: string, amount: number) => { - const presets = (state.presets || []).map((p) => (p.id === id ? { ...p, amount: Math.max(0, amount) } : p)); - onStateChange({ ...state, presets }); - }; - - const toggleLock = (id: string) => { - const updated = state.furniture.map((f) => - f.id === id ? { ...f, lockedForOptimization: !f.lockedForOptimization } : f, - ); - onStateChange({ ...state, furniture: updated }); - }; - - const setNearObjectTarget = (id: string, targetId: string) => { - const updated = state.furniture.map((f) => { - if (f.id !== id) return f; - return { - ...f, - conditions: f.conditions.map((c) => - c.kind === 'near_object' ? { ...c, params: { ...c.params, targetId } } : c, - ), - }; - }); - onStateChange({ ...state, furniture: updated }); - }; - - const toggleFurnitureCondition = (id: string, k: ConditionKind, enabled: boolean) => { - const updated = state.furniture.map((x) => { - if (x.id !== id) return x; - const has = x.conditions.some((c) => c.kind === k); - if (enabled && !has) { - const params = k === 'near_object' ? { targetId: '' } : {}; - return { ...x, conditions: [...x.conditions, { kind: k, weight: 1, params }] }; - } - if (!enabled && has) { - return { ...x, conditions: x.conditions.filter((c) => c.kind !== k) }; - } - return x; - }); - onStateChange({ ...state, furniture: updated }); - }; - - const togglePresetCondition = (id: string, k: ConditionKind, enabled: boolean) => { - const presets = (state.presets || []).map((p) => { + const toggleObjectCondition = (id: string, k: ConditionKind, enabled: boolean) => { + const presets = objects.map((p) => { if (p.id !== id) return p; const has = p.conditions.some((c) => c.kind === k); if (enabled && !has) { - return { ...p, conditions: [...p.conditions, { kind: k, weight: 1, params: {} }] }; + const params = k === 'near_object' ? { targetId: '' } : {}; + return { ...p, conditions: [...p.conditions, { kind: k, weight: 1, params }] }; } if (!enabled && has) { return { ...p, conditions: p.conditions.filter((c) => c.kind !== k) }; @@ -141,64 +117,44 @@ export function ControlPanel({ onStateChange({ ...state, presets }); }; + const setNearObjectTarget = (id: string, targetId: string) => { + const presets = objects.map((p) => { + if (p.id !== id) return p; + return { + ...p, + conditions: p.conditions.map((c) => + c.kind === 'near_object' ? { ...c, params: { ...c.params, targetId } } : c, + ), + }; + }); + onStateChange({ ...state, presets }); + }; + const toggleOpen = (id: string) => { - onSelect(id); + onSelect(id === '__entrance__' ? '__entrance__' : id); setOpenId((cur) => (cur === id ? null : id)); }; - const conditionsEditor = ( - conditions: FurnitureConfig['conditions'], - onToggle: (k: ConditionKind, enabled: boolean) => void, - nearSelect?: { value: string; onChange: (v: string) => void; options: { id: string; name: string }[] }, - ) => ( -
- {CONDITION_KINDS.map((k) => { - const active = conditions.some((c) => c.kind === k); - return ( -
- - {k === 'near_object' && active && nearSelect && ( - - )} -
- ); - })} -
- ); - - const renderPresetCard = (p: FurniturePreset) => { - const open = openId === 'preset_' + p.id; + const renderObjectCard = (p: FurniturePreset) => { + const open = openId === p.id; return ( -
-
toggleOpen('preset_' + p.id)}> +
+
toggleOpen(p.id)}> ▶ {p.name} e.stopPropagation()}> - setPresetAmount(p.id, v)} - /> + setAmount(p.id, v)} />
{open && (
+ {!p.isPreset && ( + + )}
-
-
- Default Conditions -
- {conditionsEditor(p.conditions, (k, enabled) => togglePresetCondition(p.id, k, enabled))} -
- )} -
- ); - }; - - const renderObjectCard = (f: FurnitureConfig) => { - const open = openId === f.id; - const selected = selectedId === f.id; - return ( -
-
toggleOpen(f.id)}> - ▶ - - {f.name} - {f.lockedForOptimization && locked} -
- {open && ( -
- -
- -
- + + Color + +
+ {SWATCHES.map((c) => ( +
Conditions
- {conditionsEditor( - f.conditions, - (k, enabled) => toggleFurnitureCondition(f.id, k, enabled), - { - value: - (f.conditions.find((c) => c.kind === 'near_object')?.params as { targetId?: string } | undefined) - ?.targetId || '', - onChange: (v) => setNearObjectTarget(f.id, v), - options: state.furniture.filter((o) => o.id !== f.id).map((o) => ({ id: o.id, name: o.name })), - }, +
+ {CONDITION_KINDS.map((k) => { + const active = p.conditions.some((c) => c.kind === k); + const targetId = (p.conditions.find((c) => c.kind === k)?.params as { targetId?: string } | undefined) + ?.targetId; + return ( +
+ + {k === 'near_object' && active && ( + + )} +
+ ); + })} +
+ {!p.isPreset && ( + )}
)} @@ -338,21 +273,11 @@ export function ControlPanel({
@@ -369,17 +294,14 @@ export function ControlPanel({
-
-
Presets
-
{(state.presets || []).map(renderPresetCard)}
-
-
Objects
+
{objects.map(renderObjectCard)}
+ {state.entrance && (
onSelect('__entrance__')}> @@ -391,19 +313,11 @@ export function ControlPanel({
)} - {state.furniture.length === 0 ? ( -
No objects yet — add a preset or create one.
- ) : ( -
{state.furniture.map(renderObjectCard)}
- )}
)} diff --git a/src/furniture/presets.ts b/src/furniture/presets.ts index 450207d..50a31c3 100644 --- a/src/furniture/presets.ts +++ b/src/furniture/presets.ts @@ -6,6 +6,7 @@ export const DEFAULT_PRESETS: FurniturePreset[] = [ name: 'Table', color: '#a1887f', amount: 0, + isPreset: true, shape: { type: 'rectangle', width: 80, height: 60 }, conditions: [ { kind: 'walk_to_and_sit', weight: 1 }, @@ -17,6 +18,7 @@ export const DEFAULT_PRESETS: FurniturePreset[] = [ name: 'Chair', color: '#d7ccc5', amount: 0, + isPreset: true, shape: { type: 'rectangle', width: 40, height: 40 }, conditions: [ { kind: 'walk_to_and_sit', weight: 1 }, diff --git a/src/index.css b/src/index.css index 4729efe..5f22d25 100644 --- a/src/index.css +++ b/src/index.css @@ -387,11 +387,29 @@ label { text-align: center; background: var(--bg-panel); border: 1px solid var(--border); - border-radius: 6px; - padding: 4px 6px; + border-radius: var(--radius-sm); + padding: 5px 6px; font-weight: 600; } +.swatch-row { + display: flex; + gap: 6px; +} + +.swatch-btn { + width: 20px; + height: 20px; + border-radius: 50%; + padding: 0; + border: 2px solid transparent; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3); +} + +.swatch-btn.active { + border-color: var(--accent); +} + .obj-count input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); diff --git a/src/layout/evaluator.ts b/src/layout/evaluator.ts index 1a548c9..e1d25b6 100644 --- a/src/layout/evaluator.ts +++ b/src/layout/evaluator.ts @@ -89,14 +89,19 @@ function scoreCondition( 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 targets = state.furniture.filter((x) => x.id === tid || (x.type === tid && x.id !== f.id)); + if (targets.length === 0) 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); + let best = 0; + for (const t of targets) { + 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); + const s = Math.max(0, 1 - d / 80); + if (s > best) best = s; + } + return best; } case 'open_circulation': { return path && path.length > 2 ? 0.7 : 0.5; diff --git a/src/types/index.ts b/src/types/index.ts index c6f9aa4..7037eae 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -96,6 +96,7 @@ export interface FurniturePreset { conditions: ConditionSpec[]; color?: string; amount?: number; + isPreset?: boolean; } export interface LayoutState {