import type { LayoutEvaluation, LayoutState } from '../types'; import { evaluateLayout } from '../layout/evaluator'; import { entranceCollider, findFreeSpot, furnitureCollider, overlapsAny, resolveMove } from '../geometry/bounds'; import type { Collider } from '../geometry/collision'; export interface NNOptions { gridSize?: number; iterations?: number; } export function optimizeWithNN(state: LayoutState, options: NNOptions = {}): { bestState: LayoutState; bestEval: LayoutEvaluation } { const candidate: LayoutState = { room: { ...state.room }, entrance: state.entrance ? { ...state.entrance } : null, furniture: state.furniture.map((f) => ({ ...f, shape: { ...f.shape, points: f.shape.points?.map((p) => ({ ...p })) }, })), }; const entrance = candidate.entrance; const centerX = candidate.room.width / 2; const centerY = candidate.room.height / 2; for (const f of candidate.furniture) { if (f.lockedForOptimization) continue; let targetX = f.shape.x; let targetY = f.shape.y; const needsNearEntrance = f.conditions.some((c) => c.kind === 'near_entrance'); const needsCenter = f.conditions.some((c) => c.kind === 'around_open_area'); const needsNearObj = f.conditions.find((c) => c.kind === 'near_object'); if (entrance && needsNearEntrance) { targetX = entrance.x + (Math.random() - 0.5) * 40; 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) || 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; } else if (needsCenter) { targetX = centerX - f.shape.width / 2 + (Math.random() - 0.5) * 60; targetY = centerY - f.shape.height / 2 + (Math.random() - 0.5) * 60; } else { targetX = Math.random() * (candidate.room.width - f.shape.width); targetY = Math.random() * (candidate.room.height - f.shape.height); } } else if (needsCenter) { targetX = centerX - f.shape.width / 2 + (Math.random() - 0.5) * 60; targetY = centerY - f.shape.height / 2 + (Math.random() - 0.5) * 60; } else { targetX = Math.random() * (candidate.room.width - f.shape.width); targetY = Math.random() * (candidate.room.height - f.shape.height); } if (targetX < 0) targetX = 0; if (targetY < 0) targetY = 0; const room = candidate.room; const others: Collider[] = candidate.furniture .filter((o) => o.id !== f.id) .map(furnitureCollider); const ent = entranceCollider(candidate); if (ent) others.push(ent); const moving: Collider = { x: targetX, y: targetY, width: f.shape.width, height: f.shape.height, rotation: f.shape.rotation ?? 0, }; const b = overlapsAny(moving, others) ? findFreeSpot(room, { width: f.shape.width, height: f.shape.height }, others) : resolveMove(moving, others, room); f.shape.x = b.x; f.shape.y = b.y; f.shape.width = b.width; f.shape.height = b.height; } const bestEval = evaluateLayout(candidate, options.gridSize || 10); return { bestState: candidate, bestEval }; }