Unify presets and objects into one Objects list; every object card has an amount field
This commit is contained in:
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
<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-xKTFqHjg.js"></script>
|
<script type="module" crossorigin src="/assets/index-r1bklXRq.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DfR9GACM.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Gl7EQEpA.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+38
-23
@@ -13,29 +13,6 @@ const initialState: LayoutState = {
|
|||||||
presets: DEFAULT_PRESETS,
|
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 {
|
function makeFurniture(preset: FurniturePreset, index: number): FurnitureConfig {
|
||||||
return {
|
return {
|
||||||
id: `${preset.id}_${Date.now()}_${index}`,
|
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() {
|
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');
|
||||||
|
|||||||
+4
-1
@@ -32,7 +32,10 @@ export function optimizeWithNN(state: LayoutState, options: NNOptions = {}): { b
|
|||||||
targetY = entrance.y + (Math.random() - 0.5) * 40;
|
targetY = entrance.y + (Math.random() - 0.5) * 40;
|
||||||
} else if (needsNearObj) {
|
} else if (needsNearObj) {
|
||||||
const tid = (needsNearObj.params as any)?.targetId;
|
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) {
|
if (t) {
|
||||||
targetX = t.shape.x + t.shape.width / 2 - f.shape.width / 2 + (Math.random() - 0.5) * 30;
|
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;
|
targetY = t.shape.y + t.shape.height / 2 - f.shape.height / 2 + (Math.random() - 0.5) * 30;
|
||||||
|
|||||||
+135
-226
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { ConditionKind, Entrance, FurnitureConfig, FurniturePreset, LayoutState, RoomConfig } from '../types';
|
import type { ConditionKind, Entrance, FurniturePreset, LayoutState, RoomConfig } from '../types';
|
||||||
import { NumberField } from './NumberField';
|
import { NumberField } from './NumberField';
|
||||||
|
|
||||||
interface ControlPanelProps {
|
interface ControlPanelProps {
|
||||||
@@ -34,6 +34,8 @@ const CONDITION_LABELS: Record<ConditionKind, string> = {
|
|||||||
open_circulation: 'Open circulation',
|
open_circulation: 'Open circulation',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SWATCHES = ['#a1887f', '#d7ccc5', '#90caf9', '#a5d6a7', '#ef9a9a', '#ce93d8', '#ffcc80'];
|
||||||
|
|
||||||
export function ControlPanel({
|
export function ControlPanel({
|
||||||
state,
|
state,
|
||||||
onStateChange,
|
onStateChange,
|
||||||
@@ -46,92 +48,66 @@ export function ControlPanel({
|
|||||||
timeLeft,
|
timeLeft,
|
||||||
}: ControlPanelProps) {
|
}: ControlPanelProps) {
|
||||||
const [openId, setOpenId] = useState<string | null>(null);
|
const [openId, setOpenId] = useState<string | null>(null);
|
||||||
|
const objects = state.presets || [];
|
||||||
|
|
||||||
const setRoom = (room: Partial<RoomConfig>) => {
|
const selectedTypeId = selectedId
|
||||||
onStateChange({ ...state, room: { ...state.room, ...room } });
|
? state.furniture.find((f) => f.id === selectedId)?.type ?? (selectedId === '__entrance__' ? '__entrance__' : null)
|
||||||
};
|
: null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTypeId) setOpenId(selectedTypeId);
|
||||||
|
}, [selectedTypeId]);
|
||||||
|
|
||||||
|
const setRoom = (room: Partial<RoomConfig>) => onStateChange({ ...state, room: { ...state.room, ...room } });
|
||||||
|
|
||||||
const setEntrance = (entrance: Partial<Entrance>) => {
|
const setEntrance = (entrance: Partial<Entrance>) => {
|
||||||
if (!state.entrance) return;
|
if (!state.entrance) return;
|
||||||
onStateChange({ ...state, entrance: { ...state.entrance, ...entrance } });
|
onStateChange({ ...state, entrance: { ...state.entrance, ...entrance } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const placeEntrance = () => {
|
const placeEntrance = () => onStateChange({ ...state, entrance: { x: 20, y: 20, width: 20, height: 20 } });
|
||||||
onStateChange({ ...state, entrance: { x: 20, y: 20, width: 20, height: 20 } });
|
|
||||||
|
const updateObject = (id: string, patch: Partial<FurniturePreset>) => {
|
||||||
|
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 createObject = () => {
|
||||||
const id = 'obj_' + Date.now();
|
const id = 'obj_' + Date.now();
|
||||||
const newItem: FurnitureConfig = {
|
const newObject: FurniturePreset = {
|
||||||
id,
|
id,
|
||||||
name: 'Object ' + (state.furniture.length + 1),
|
name: 'Object ' + (objects.filter((o) => !o.isPreset).length + 1),
|
||||||
type: 'custom',
|
color: SWATCHES[objects.length % SWATCHES.length],
|
||||||
shape: { type: 'rectangle', x: 100, y: 100, width: 50, height: 50 },
|
amount: 1,
|
||||||
|
isPreset: false,
|
||||||
|
shape: { type: 'rectangle', width: 50, height: 50 },
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: 'walk_to_and_sit', weight: 1 },
|
{ kind: 'walk_to_and_sit', weight: 1 },
|
||||||
{ kind: 'accessible', 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);
|
onSelect(id);
|
||||||
setOpenId(id);
|
setOpenId(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePreset = (id: string, patch: Partial<FurniturePreset>) => {
|
const toggleObjectCondition = (id: string, k: ConditionKind, enabled: boolean) => {
|
||||||
const presets = (state.presets || []).map((p) => (p.id === id ? { ...p, ...patch } : p));
|
const presets = objects.map((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) => {
|
|
||||||
if (p.id !== id) return p;
|
if (p.id !== id) return p;
|
||||||
const has = p.conditions.some((c) => c.kind === k);
|
const has = p.conditions.some((c) => c.kind === k);
|
||||||
if (enabled && !has) {
|
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) {
|
if (!enabled && has) {
|
||||||
return { ...p, conditions: p.conditions.filter((c) => c.kind !== k) };
|
return { ...p, conditions: p.conditions.filter((c) => c.kind !== k) };
|
||||||
@@ -141,64 +117,44 @@ export function ControlPanel({
|
|||||||
onStateChange({ ...state, presets });
|
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) => {
|
const toggleOpen = (id: string) => {
|
||||||
onSelect(id);
|
onSelect(id === '__entrance__' ? '__entrance__' : id);
|
||||||
setOpenId((cur) => (cur === id ? null : id));
|
setOpenId((cur) => (cur === id ? null : id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const conditionsEditor = (
|
const renderObjectCard = (p: FurniturePreset) => {
|
||||||
conditions: FurnitureConfig['conditions'],
|
const open = openId === p.id;
|
||||||
onToggle: (k: ConditionKind, enabled: boolean) => void,
|
|
||||||
nearSelect?: { value: string; onChange: (v: string) => void; options: { id: string; name: string }[] },
|
|
||||||
) => (
|
|
||||||
<div className="cond-list">
|
|
||||||
{CONDITION_KINDS.map((k) => {
|
|
||||||
const active = conditions.some((c) => c.kind === k);
|
|
||||||
return (
|
|
||||||
<div className="cond" key={k}>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" checked={active} onChange={(e) => onToggle(k, e.target.checked)} />
|
|
||||||
{CONDITION_LABELS[k]}
|
|
||||||
</label>
|
|
||||||
{k === 'near_object' && active && nearSelect && (
|
|
||||||
<select
|
|
||||||
value={nearSelect.value}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onChange={(e) => nearSelect.onChange(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">Select object…</option>
|
|
||||||
{nearSelect.options.map((o) => (
|
|
||||||
<option key={o.id} value={o.id}>
|
|
||||||
{o.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderPresetCard = (p: FurniturePreset) => {
|
|
||||||
const open = openId === 'preset_' + p.id;
|
|
||||||
return (
|
return (
|
||||||
<div className={`obj-card ${open ? 'open' : ''}`} key={p.id}>
|
<div key={p.id} className={`obj-card ${open ? 'open' : ''}`}>
|
||||||
<div className="obj-head" onClick={() => toggleOpen('preset_' + p.id)}>
|
<div className="obj-head" 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>
|
||||||
<span className="obj-count" onClick={(e) => e.stopPropagation()}>
|
<span className="obj-count" onClick={(e) => e.stopPropagation()}>
|
||||||
<NumberField
|
<NumberField value={p.amount ?? 0} min={0} width={44} onChange={(v) => setAmount(p.id, v)} />
|
||||||
value={p.amount ?? 0}
|
|
||||||
min={0}
|
|
||||||
width={44}
|
|
||||||
onChange={(v) => setPresetAmount(p.id, v)}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="obj-body">
|
<div className="obj-body">
|
||||||
|
{!p.isPreset && (
|
||||||
|
<label className="field" style={{ marginBottom: 10 }}>
|
||||||
|
<span>Name</span>
|
||||||
|
<input value={p.name} onChange={(e) => updateObject(p.id, { name: e.target.value })} />
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>Width</span>
|
<span>Width</span>
|
||||||
@@ -206,7 +162,7 @@ export function ControlPanel({
|
|||||||
value={p.shape.width}
|
value={p.shape.width}
|
||||||
min={10}
|
min={10}
|
||||||
width="100%"
|
width="100%"
|
||||||
onChange={(v) => updatePreset(p.id, { shape: { ...p.shape, width: v } })}
|
onChange={(v) => updateObject(p.id, { shape: { ...p.shape, width: v } })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
@@ -215,97 +171,76 @@ export function ControlPanel({
|
|||||||
value={p.shape.height}
|
value={p.shape.height}
|
||||||
min={10}
|
min={10}
|
||||||
width="100%"
|
width="100%"
|
||||||
onChange={(v) => updatePreset(p.id, { shape: { ...p.shape, height: v } })}
|
onChange={(v) => updateObject(p.id, { shape: { ...p.shape, height: v } })}
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="section-title" style={{ marginTop: 12 }}>
|
|
||||||
Default Conditions
|
|
||||||
</div>
|
|
||||||
{conditionsEditor(p.conditions, (k, enabled) => togglePresetCondition(p.id, k, enabled))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderObjectCard = (f: FurnitureConfig) => {
|
|
||||||
const open = openId === f.id;
|
|
||||||
const selected = selectedId === f.id;
|
|
||||||
return (
|
|
||||||
<div key={f.id} className={`obj-card ${selected ? 'selected' : ''}`}>
|
|
||||||
<div className="obj-head" onClick={() => toggleOpen(f.id)}>
|
|
||||||
<span className={`obj-caret ${open ? 'open' : ''}`}>▶</span>
|
|
||||||
<span className="obj-swatch" style={{ background: f.color || '#9aa0a6' }} />
|
|
||||||
<span className="obj-name">{f.name}</span>
|
|
||||||
{f.lockedForOptimization && <span className="obj-badge locked">locked</span>}
|
|
||||||
</div>
|
|
||||||
{open && (
|
|
||||||
<div className="obj-body">
|
|
||||||
<label className="field">
|
|
||||||
<span>Name</span>
|
|
||||||
<input
|
|
||||||
value={f.name}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onChange={(e) => {
|
|
||||||
const updated = state.furniture.map((x) => (x.id === f.id ? { ...x, name: e.target.value } : x));
|
|
||||||
onStateChange({ ...state, furniture: updated });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="grid-2" style={{ marginTop: 8 }}>
|
|
||||||
<label className="field">
|
|
||||||
<span>Width</span>
|
|
||||||
<NumberField
|
|
||||||
value={f.shape.width}
|
|
||||||
min={10}
|
|
||||||
width="100%"
|
|
||||||
onChange={(v) => {
|
|
||||||
const updated = state.furniture.map((x) =>
|
|
||||||
x.id === f.id ? { ...x, shape: { ...x.shape, width: v } } : x,
|
|
||||||
);
|
|
||||||
onStateChange({ ...state, furniture: updated });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="field">
|
|
||||||
<span>Height</span>
|
|
||||||
<NumberField
|
|
||||||
value={f.shape.height}
|
|
||||||
min={10}
|
|
||||||
width="100%"
|
|
||||||
onChange={(v) => {
|
|
||||||
const updated = state.furniture.map((x) =>
|
|
||||||
x.id === f.id ? { ...x, shape: { ...x.shape, height: v } } : x,
|
|
||||||
);
|
|
||||||
onStateChange({ ...state, furniture: updated });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="row" style={{ marginTop: 10 }}>
|
<div className="row" style={{ marginTop: 10 }}>
|
||||||
<label>
|
<span className="hint" style={{ margin: 0 }}>
|
||||||
<input
|
Color
|
||||||
type="checkbox"
|
</span>
|
||||||
checked={f.lockedForOptimization}
|
<div className="swatch-row">
|
||||||
onChange={() => toggleLock(f.id)}
|
{SWATCHES.map((c) => (
|
||||||
/>
|
<button
|
||||||
Lock position
|
key={c}
|
||||||
</label>
|
className={`swatch-btn ${p.color === c ? 'active' : ''}`}
|
||||||
|
style={{ background: c }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
updateObject(p.id, { color: c });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-title" style={{ marginTop: 12 }}>
|
<div className="section-title" style={{ marginTop: 12 }}>
|
||||||
Conditions
|
Conditions
|
||||||
</div>
|
</div>
|
||||||
{conditionsEditor(
|
<div className="cond-list">
|
||||||
f.conditions,
|
{CONDITION_KINDS.map((k) => {
|
||||||
(k, enabled) => toggleFurnitureCondition(f.id, k, enabled),
|
const active = p.conditions.some((c) => c.kind === k);
|
||||||
{
|
const targetId = (p.conditions.find((c) => c.kind === k)?.params as { targetId?: string } | undefined)
|
||||||
value:
|
?.targetId;
|
||||||
(f.conditions.find((c) => c.kind === 'near_object')?.params as { targetId?: string } | undefined)
|
return (
|
||||||
?.targetId || '',
|
<div className="cond" key={k}>
|
||||||
onChange: (v) => setNearObjectTarget(f.id, v),
|
<label>
|
||||||
options: state.furniture.filter((o) => o.id !== f.id).map((o) => ({ id: o.id, name: o.name })),
|
<input
|
||||||
},
|
type="checkbox"
|
||||||
|
checked={active}
|
||||||
|
onChange={(e) => toggleObjectCondition(p.id, k, e.target.checked)}
|
||||||
|
/>
|
||||||
|
{CONDITION_LABELS[k]}
|
||||||
|
</label>
|
||||||
|
{k === 'near_object' && active && (
|
||||||
|
<select
|
||||||
|
value={targetId || ''}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setNearObjectTarget(p.id, e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select object…</option>
|
||||||
|
{objects
|
||||||
|
.filter((o) => o.id !== p.id)
|
||||||
|
.map((o) => (
|
||||||
|
<option key={o.id} value={o.id}>
|
||||||
|
{o.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{!p.isPreset && (
|
||||||
|
<button
|
||||||
|
style={{ marginTop: 12, width: '100%' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeObject(p.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete Object
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -338,21 +273,11 @@ export function ControlPanel({
|
|||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>Width</span>
|
<span>Width</span>
|
||||||
<NumberField
|
<NumberField value={state.room.width} min={100} width="100%" onChange={(v) => setRoom({ width: v })} />
|
||||||
value={state.room.width}
|
|
||||||
min={100}
|
|
||||||
width="100%"
|
|
||||||
onChange={(v) => setRoom({ width: v })}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>Height</span>
|
<span>Height</span>
|
||||||
<NumberField
|
<NumberField value={state.room.height} min={100} width="100%" onChange={(v) => setRoom({ height: v })} />
|
||||||
value={state.room.height}
|
|
||||||
min={100}
|
|
||||||
width="100%"
|
|
||||||
onChange={(v) => setRoom({ height: v })}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,17 +294,14 @@ export function ControlPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
|
||||||
<div className="section-title">Presets</div>
|
|
||||||
<div className="obj-list">{(state.presets || []).map(renderPresetCard)}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<div className="section-title">Objects</div>
|
<div className="section-title">Objects</div>
|
||||||
|
<div className="obj-list">{objects.map(renderObjectCard)}</div>
|
||||||
|
|
||||||
{state.entrance && (
|
{state.entrance && (
|
||||||
<div
|
<div
|
||||||
className={`obj-card ${selectedId === '__entrance__' ? 'selected' : ''}`}
|
className={`obj-card ${selectedId === '__entrance__' ? 'selected' : ''}`}
|
||||||
style={{ marginBottom: 6 }}
|
style={{ marginTop: 6 }}
|
||||||
>
|
>
|
||||||
<div className="obj-head" onClick={() => onSelect('__entrance__')}>
|
<div className="obj-head" onClick={() => onSelect('__entrance__')}>
|
||||||
<span className="obj-swatch" style={{ background: '#3fbf7f' }} />
|
<span className="obj-swatch" style={{ background: '#3fbf7f' }} />
|
||||||
@@ -391,19 +313,11 @@ export function ControlPanel({
|
|||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>X</span>
|
<span>X</span>
|
||||||
<NumberField
|
<NumberField value={state.entrance.x} width="100%" onChange={(v) => setEntrance({ x: v })} />
|
||||||
value={state.entrance.x}
|
|
||||||
width="100%"
|
|
||||||
onChange={(v) => setEntrance({ x: v })}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>Y</span>
|
<span>Y</span>
|
||||||
<NumberField
|
<NumberField value={state.entrance.y} width="100%" onChange={(v) => setEntrance({ y: v })} />
|
||||||
value={state.entrance.y}
|
|
||||||
width="100%"
|
|
||||||
onChange={(v) => setEntrance({ y: v })}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>Width</span>
|
<span>Width</span>
|
||||||
@@ -428,11 +342,6 @@ export function ControlPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.furniture.length === 0 ? (
|
|
||||||
<div className="empty">No objects yet — add a preset or create one.</div>
|
|
||||||
) : (
|
|
||||||
<div className="obj-list">{state.furniture.map(renderObjectCard)}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const DEFAULT_PRESETS: FurniturePreset[] = [
|
|||||||
name: 'Table',
|
name: 'Table',
|
||||||
color: '#a1887f',
|
color: '#a1887f',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
|
isPreset: true,
|
||||||
shape: { type: 'rectangle', width: 80, height: 60 },
|
shape: { type: 'rectangle', width: 80, height: 60 },
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: 'walk_to_and_sit', weight: 1 },
|
{ kind: 'walk_to_and_sit', weight: 1 },
|
||||||
@@ -17,6 +18,7 @@ export const DEFAULT_PRESETS: FurniturePreset[] = [
|
|||||||
name: 'Chair',
|
name: 'Chair',
|
||||||
color: '#d7ccc5',
|
color: '#d7ccc5',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
|
isPreset: true,
|
||||||
shape: { type: 'rectangle', width: 40, height: 40 },
|
shape: { type: 'rectangle', width: 40, height: 40 },
|
||||||
conditions: [
|
conditions: [
|
||||||
{ kind: 'walk_to_and_sit', weight: 1 },
|
{ kind: 'walk_to_and_sit', weight: 1 },
|
||||||
|
|||||||
+20
-2
@@ -387,11 +387,29 @@ label {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-sm);
|
||||||
padding: 4px 6px;
|
padding: 5px 6px;
|
||||||
font-weight: 600;
|
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 {
|
.obj-count input:focus {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||||
|
|||||||
+11
-6
@@ -89,14 +89,19 @@ function scoreCondition(
|
|||||||
case 'near_object': {
|
case 'near_object': {
|
||||||
const tid = (c.params as any)?.targetId;
|
const tid = (c.params as any)?.targetId;
|
||||||
if (!tid) return 0.3;
|
if (!tid) return 0.3;
|
||||||
const t = state.furniture.find((x) => x.id === tid);
|
const targets = state.furniture.filter((x) => x.id === tid || (x.type === tid && x.id !== f.id));
|
||||||
if (!t) return 0.3;
|
if (targets.length === 0) return 0.3;
|
||||||
const cx = f.shape.x + f.shape.width / 2;
|
const cx = f.shape.x + f.shape.width / 2;
|
||||||
const cy = f.shape.y + f.shape.height / 2;
|
const cy = f.shape.y + f.shape.height / 2;
|
||||||
const tx = t.shape.x + t.shape.width / 2;
|
let best = 0;
|
||||||
const ty = t.shape.y + t.shape.height / 2;
|
for (const t of targets) {
|
||||||
const d = Math.sqrt((cx - tx) ** 2 + (cy - ty) ** 2);
|
const tx = t.shape.x + t.shape.width / 2;
|
||||||
return Math.max(0, 1 - d / 80);
|
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': {
|
case 'open_circulation': {
|
||||||
return path && path.length > 2 ? 0.7 : 0.5;
|
return path && path.length > 2 ? 0.7 : 0.5;
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export interface FurniturePreset {
|
|||||||
conditions: ConditionSpec[];
|
conditions: ConditionSpec[];
|
||||||
color?: string;
|
color?: string;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
|
isPreset?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LayoutState {
|
export interface LayoutState {
|
||||||
|
|||||||
Reference in New Issue
Block a user