85 lines
3.4 KiB
TypeScript
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 };
|
|
}
|