Files
furniture/src/ai/nn.ts
T

85 lines
3.4 KiB
TypeScript

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 };
}