95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
import type { AccessibilityResult, FurnitureConfig, LayoutEvaluation, LayoutState, Point, Rect } from '../types';
|
|
import { buildGrid } from '../pathfinding/grid';
|
|
import { aStar } from '../pathfinding/aStar';
|
|
|
|
export function evaluateLayout(state: LayoutState, gridSize: number = 10): LayoutEvaluation {
|
|
const obstacles: Rect[] = state.furniture
|
|
.map((f) => ({
|
|
x: f.shape.x,
|
|
y: f.shape.y,
|
|
width: f.shape.width,
|
|
height: f.shape.height,
|
|
}))
|
|
.filter((r) => r.width > 0 && r.height > 0);
|
|
|
|
const grid = buildGrid(state.room, obstacles, gridSize);
|
|
const accessibility: AccessibilityResult[] = [];
|
|
let penalty = 0;
|
|
let totalConditionScore = 0;
|
|
const conditionScores: Record<string, number> = {};
|
|
|
|
const entrancePoint = state.entrance ? { x: state.entrance.x, y: state.entrance.y } : { x: 0, y: 0 };
|
|
|
|
for (const f of state.furniture) {
|
|
const cx = f.shape.x + f.shape.width / 2;
|
|
const cy = f.shape.y + f.shape.height / 2;
|
|
const target: Point = { x: cx, y: cy };
|
|
const path = aStar(entrancePoint, target, grid, gridSize);
|
|
if (path === null) {
|
|
accessibility.push({ furnitureId: f.id, distanceFromEntrance: null, reachable: false, pathLength: null });
|
|
penalty += 100;
|
|
} else {
|
|
let len = 0;
|
|
for (let i = 1; i < path.length; i++) {
|
|
const p0 = path[i - 1];
|
|
const p1 = path[i];
|
|
len += Math.sqrt((p0.x - p1.x) ** 2 + (p0.y - p1.y) ** 2);
|
|
}
|
|
accessibility.push({ furnitureId: f.id, distanceFromEntrance: len, reachable: true, pathLength: len });
|
|
}
|
|
|
|
let score = 0;
|
|
for (const c of f.conditions) {
|
|
const s = scoreCondition(c.kind, f, state, accessibility.find((a) => a.furnitureId === f.id) || null, path);
|
|
score += (s * (c.weight || 1)) / (f.conditions.length || 1);
|
|
}
|
|
conditionScores[f.id] = score;
|
|
totalConditionScore += score;
|
|
}
|
|
|
|
const totalScore = totalConditionScore - penalty;
|
|
return { totalScore, conditionScores, accessibility, penalty };
|
|
}
|
|
|
|
function scoreCondition(
|
|
kind: string,
|
|
f: FurnitureConfig,
|
|
state: LayoutState,
|
|
acc: AccessibilityResult | null,
|
|
path: Point[] | null,
|
|
): number {
|
|
switch (kind) {
|
|
case 'walk_to_and_sit': {
|
|
if (acc?.reachable) return Math.min(1, 1 / ((acc.pathLength || 1) / 50 + 1e-6));
|
|
return 0;
|
|
}
|
|
case 'around_open_area': {
|
|
const center = { x: state.room.width / 2, y: state.room.height / 2 };
|
|
const cx = f.shape.x + f.shape.width / 2;
|
|
const cy = f.shape.y + f.shape.height / 2;
|
|
const distCenter = Math.sqrt((cx - center.x) ** 2 + (cy - center.y) ** 2);
|
|
return Math.max(0, 1 - distCenter / Math.max(state.room.width, state.room.height));
|
|
}
|
|
case 'near_entrance': {
|
|
if (!state.entrance) return 0;
|
|
const cx = f.shape.x + f.shape.width / 2;
|
|
const cy = f.shape.y + f.shape.height / 2;
|
|
const ex = state.entrance.x;
|
|
const ey = state.entrance.y;
|
|
const d = Math.sqrt((cx - ex) ** 2 + (cy - ey) ** 2);
|
|
return Math.max(0, 1 - d / 200);
|
|
}
|
|
case 'not_blocking_path': {
|
|
return acc?.reachable ? 0.8 : 0.2;
|
|
}
|
|
case 'accessible': {
|
|
return acc?.reachable ? 1.0 : 0.0;
|
|
}
|
|
case 'open_circulation': {
|
|
return path && path.length > 2 ? 0.7 : 0.5;
|
|
}
|
|
default:
|
|
return 0.5;
|
|
}
|
|
}
|