import type { FlooringPlan, FlooringSettings, SkirtingPlan } from '../src/lib/types'; /** Evaluate public plan output. Keep these definitions fixed when comparing search changes. */ export function flooringMetrics(plan: FlooringPlan, s: FlooringSettings) { let shapingCuts = 0; let ripCuts = 0; let patternPenalty = 0; let repeats = 0; let nearH = 0; let similarSteps = 0; let staircases = 0; for (const piece of plan.pieces) { ripCuts += piece.ripEdge === 'both' ? 2 : Number(!!piece.ripEdge); if (!piece.shaped) continue; const close = (a: number, b: number) => Math.abs(a - b) <= 0.01; shapingCuts += piece.template.filter((p, i, path) => { const q = path[(i + 1) % path.length]; return !( (close(p.x, q.x) && (close(p.x, 0) || close(p.x, piece.length))) || (close(p.y, q.y) && (close(p.y, 0) || close(p.y, piece.width))) ); }).length; } const history = new Map(); const step = (from: number, to: number) => { const value = (((to - from + s.length / 2) % s.length) + s.length) % s.length; return ((Math.round(value * 1000) / 1000) % s.length) - s.length / 2; }; const similar = Math.max(s.kerf * 2, s.length * 0.03); for (const row of s.pattern === 'random' ? plan.rows : []) { const recent = history.get(row.roomId) ?? []; const phase = row.offset; if (recent.length >= 2) { const movement = step(recent.at(-1)!, phase); const previous = step(recent.at(-2)!, recent.at(-1)!); // Near-aligned joints with one intervening row still read as an H at the scale of the boards. if (Math.abs(step(recent.at(-2)!, phase)) < Math.min(s.width, s.length / 6) - 0.01) nearH++; patternPenalty += Number(Math.abs(Math.abs(movement) - s.length / 2) <= 0.01); for (let distance = 2; distance <= recent.length; distance++) { if (Math.abs(step(recent.at(-distance)!, phase)) <= similar) { patternPenalty += 5 - distance; if (distance === 2) repeats++; } } if (movement * previous > 0) { patternPenalty++; if (Math.abs(movement - previous) <= similar) { patternPenalty += 3; similarSteps++; } if (recent.length >= 3 && previous * step(recent.at(-3)!, recent.at(-2)!) > 0) { patternPenalty += 2; staircases++; } } } history.set(row.roomId, [...recent, phase].slice(-4)); } return { cuts: plan.cuts, shapingCuts, ripCuts, patternPenalty, repeats, nearH, similarSteps, staircases, boards: plan.boardCount, waste: plan.wasteArea, errors: plan.issues.filter((issue) => issue.severity === 'error').length, }; } export function skirtingMetrics(plan: SkirtingPlan) { return { cuts: plan.cuts, pieces: plan.pieces.length, boards: plan.stockCount, waste: Math.round(plan.waste * 1000) / 1000, errors: plan.issues.filter((issue) => issue.severity === 'error').length, }; } export function compareFlooring( a: Omit, 'nearH'>, b: typeof a, random = false, ) { return ( a.errors - b.errors || (random ? a.patternPenalty - b.patternPenalty : 0) || a.cuts - b.cuts || a.shapingCuts - b.shapingCuts || a.ripCuts - b.ripCuts || a.patternPenalty - b.patternPenalty || a.boards - b.boards ); } /** Current appearance score adds broad H alignments; the original metric stays comparable. */ export function compareCurrentFlooring( a: ReturnType, b: typeof a, random = false, ) { return compareFlooring( { ...a, patternPenalty: a.patternPenalty + 12 * a.nearH }, { ...b, patternPenalty: b.patternPenalty + 12 * b.nearH }, random, ); } export function compareSkirting(a: ReturnType, b: typeof a) { return a.errors - b.errors || a.cuts - b.cuts || a.waste - b.waste; }