solver-core-compute
Version:
Shared solver library for floorplan optimization - compatible with Node.js and Deno
393 lines (323 loc) • 9.98 kB
text/typescript
/**
* Constraint Evaluation Utilities
* Pure TypeScript implementation compatible with Deno
*/
import type { Position, Constraint, ConstraintResult } from './types/index.js';
/**
* Evaluate all constraints against positions
*/
export function evaluateConstraints(
positions: Position[],
constraints: Constraint[]
): ConstraintResult {
const violations: ConstraintResult['violations'] = [];
for (let i = 0; i < constraints.length; i++) {
const constraint = constraints[i];
const violation = evaluateConstraint(constraint, positions);
if (violation) {
violations.push(violation);
}
}
return {
satisfied: violations.length === 0,
violations
};
}
/**
* Evaluate a single constraint
*/
function evaluateConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
switch (constraint.type) {
case 'overlap':
return evaluateOverlapConstraint(constraint, positions);
case 'boundary':
return evaluateBoundaryConstraint(constraint, positions);
case 'aspect_ratio':
return evaluateAspectRatioConstraint(constraint, positions);
case 'proximity':
return evaluateProximityConstraint(constraint, positions);
case 'alignment':
return evaluateAlignmentConstraint(constraint, positions);
case 'min_width':
return evaluateMinWidthConstraint(constraint, positions);
case 'min_height':
return evaluateMinHeightConstraint(constraint, positions);
case 'min_clearance':
return evaluateMinClearanceConstraint(constraint, positions);
case 'work_triangle':
return evaluateWorkTriangleConstraint(constraint, positions);
default:
return null;
}
}
/**
* Check for overlap between positions
*/
function evaluateOverlapConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const minGap = constraint.params.minGap || 0;
for (let i = 0; i < positions.length; i++) {
for (let j = i + 1; j < positions.length; j++) {
const p1 = positions[i];
const p2 = positions[j];
const gapX = Math.max(
0,
Math.max(p1.x, p2.x) - Math.min(p1.x + p1.width, p2.x + p2.width)
);
const gapY = Math.max(
0,
Math.max(p1.y, p2.y) - Math.min(p1.y + p1.height, p2.y + p2.height)
);
if (gapX < minGap && gapY < minGap) {
const overlapArea = (p1.width - gapX) * (p1.height - gapY);
return {
constraint,
positions: [p1, p2],
penalty: overlapArea
};
}
}
}
return null;
}
/**
* Check boundary constraints
*/
function evaluateBoundaryConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const boundary = constraint.params.boundary;
// Convert string values to numbers if needed
const x = typeof boundary.x === 'string' ? parseFloat(boundary.x) : boundary.x;
const y = typeof boundary.y === 'string' ? parseFloat(boundary.y) : boundary.y;
const width = typeof boundary.width === 'string' ? parseFloat(boundary.width) : boundary.width;
const height = typeof boundary.height === 'string' ? parseFloat(boundary.height) : boundary.height;
for (const position of positions) {
// Calculate effective dimensions based on rotation
const rotation = position.rotation || 0;
const effectiveWidth = rotation % 180 === 0 ? position.width : position.height;
const effectiveHeight = rotation % 180 === 0 ? position.height : position.width;
if (
position.x < x ||
position.y < y ||
position.x + effectiveWidth > x + width ||
position.y + effectiveHeight > y + height
) {
return {
constraint,
positions: [position],
penalty: 1
};
}
}
return null;
}
/**
* Check aspect ratio constraints
*/
function evaluateAspectRatioConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const { minRatio = 0, maxRatio = Infinity } = constraint.params;
for (const position of positions) {
const ratio = position.width / position.height;
if (ratio < minRatio || ratio > maxRatio) {
return {
constraint,
positions: [position],
penalty: Math.abs(ratio - (minRatio + maxRatio) / 2)
};
}
}
return null;
}
/**
* Check proximity constraints
*/
function evaluateProximityConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const { targetDistance, maxDistance } = constraint.params;
const indices = constraint.params.positions || [];
for (let k = 0; k < indices.length - 1; k++) {
const i: number = indices[k];
const j: number = indices[k + 1];
const p1 = positions[i];
const p2 = positions[j];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > maxDistance) {
return {
constraint,
positions: [p1, p2],
penalty: distance - targetDistance
};
}
}
return null;
}
/**
* Check alignment constraints
*/
function evaluateAlignmentConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const { axis, tolerance = 1 } = constraint.params;
const indices: number[] = constraint.params.positions || [];
if (indices.length < 2) return null;
const referencePos = positions[indices[0]];
const referenceCoord = axis === 'x' ? referencePos.x : referencePos.y;
for (let k = 1; k < indices.length; k++) {
const pos = positions[indices[k]];
const coord = axis === 'x' ? pos.x : pos.y;
if (Math.abs(coord - referenceCoord) > tolerance) {
return {
constraint,
positions: indices.map(i => positions[i]),
penalty: Math.abs(coord - referenceCoord)
};
}
}
return null;
}
/**
* Check minimum width constraint
*/
function evaluateMinWidthConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const minWidth = constraint.params.minWidth;
const itemId = constraint.params.itemId;
const position = positions.find(p => p.id === itemId);
if (!position) {
return null;
}
if (position.width < minWidth) {
return {
constraint,
positions: [position],
penalty: minWidth - position.width
};
}
return null;
}
/**
* Check minimum height constraint
*/
function evaluateMinHeightConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const minHeight = constraint.params.minHeight;
const itemId = constraint.params.itemId;
const position = positions.find(p => p.id === itemId);
if (!position) {
return null;
}
if (position.height < minHeight) {
return {
constraint,
positions: [position],
penalty: minHeight - position.height
};
}
return null;
}
/**
* Check minimum clearance constraint
*/
function evaluateMinClearanceConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const minDistance = constraint.params.minDistance;
const itemA = constraint.params.itemA;
const itemB = constraint.params.itemB;
const posA = positions.find(p => p.id === itemA || p === itemA);
const posB = positions.find(p => p.id === itemB || p === itemB);
if (!posA || !posB) {
return null;
}
const centerAX = posA.x + posA.width / 2;
const centerAY = posA.y + posA.height / 2;
const centerBX = posB.x + posB.width / 2;
const centerBY = posB.y + posB.height / 2;
const distance = Math.sqrt(
Math.pow(centerBX - centerAX, 2) + Math.pow(centerBY - centerAY, 2)
);
if (distance < minDistance) {
return {
constraint,
positions: [posA, posB],
penalty: minDistance - distance
};
}
return null;
}
/**
* Check kitchen work triangle constraint
*/
function evaluateWorkTriangleConstraint(
constraint: Constraint,
positions: Position[]
): ConstraintResult['violations'][0] | null {
const sinkId = constraint.params.sinkId;
const stoveId = constraint.params.stoveId;
const fridgeId = constraint.params.fridgeId;
const minPerimeter = constraint.params.minPerimeter;
const maxPerimeter = constraint.params.maxPerimeter;
const sink = positions.find(p => p.id === sinkId);
const stove = positions.find(p => p.id === stoveId);
const fridge = positions.find(p => p.id === fridgeId);
if (!sink || !stove || !fridge) {
return null;
}
const sinkCenter = { x: sink.x + sink.width / 2, y: sink.y + sink.height / 2 };
const stoveCenter = { x: stove.x + stove.width / 2, y: stove.y + stove.height / 2 };
const fridgeCenter = { x: fridge.x + fridge.width / 2, y: fridge.y + fridge.height / 2 };
const dist = (a: any, b: any) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
const sinkToStove = dist(sinkCenter, stoveCenter);
const stoveToFridge = dist(stoveCenter, fridgeCenter);
const fridgeToSink = dist(fridgeCenter, sinkCenter);
const perimeter = sinkToStove + stoveToFridge + fridgeToSink;
if (perimeter < minPerimeter || perimeter > maxPerimeter) {
return {
constraint,
positions: [sink, stove, fridge],
penalty: Math.abs(perimeter - (minPerimeter + maxPerimeter) / 2)
};
}
return null;
}
/**
* Calculate cost score for a set of positions and constraints
* Lower is better
*/
export function calculateCost(
positions: Position[],
constraints: Constraint[],
objective?: (positions: Position[]) => number
): number {
let cost = 0;
for (const constraint of constraints) {
const violation = evaluateConstraint(constraint, positions);
if (violation) {
cost += violation.penalty;
}
}
if (objective) {
cost += objective(positions);
}
return cost;
}