UNPKG

solver-core-compute

Version:

Shared solver library for floorplan optimization - compatible with Node.js and Deno

393 lines (323 loc) 9.98 kB
/** * 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; }