/** * Alignment evaluators: * alignedWith, centeredWithin * * Alignment is evaluated against border-box geometry in the shared frame. */ import { type GeometryWorld, type ClauseDescriptor, type ClauseResult, } from './registry.js'; function findBoxIndex(world: GeometryWorld, subjectId: number): number { const { boxes } = world; for (let i = 0; i < boxes.boxId.length; i++) { if (boxes.subjectId[i] === subjectId) { return i; } } return -1; } function getBorderRect(world: GeometryWorld, subjectId: number) { const idx = findBoxIndex(world, subjectId); if (idx === -1) return null; const b = world.boxes; return { left: b.borderLeft[idx], top: b.borderTop[idx], right: b.borderRight[idx], bottom: b.borderBottom[idx], }; } function result( clauseId: string, status: 'pass' | 'fail' | 'error', truth: 'determinate' | 'indeterminate', metrics?: Record, witness?: Record, ): ClauseResult { return { clauseId, status, truth, metrics, witness: witness ? { subjectId: witness.subjectId, referenceId: witness.referenceId, frameId: witness.frameId, } : undefined, }; } export function evaluateAlignedWith( world: GeometryWorld, clause: ClauseDescriptor, ): ClauseResult { const { subjectRef, referenceRef, clauseId, options } = clause; if (subjectRef === undefined || referenceRef === undefined) { return result(clauseId, 'error', 'indeterminate', undefined, { subjectRef, referenceRef, }); } const sRect = getBorderRect(world, subjectRef); const rRect = getBorderRect(world, referenceRef); if (!sRect || !rRect) { return result(clauseId, 'error', 'indeterminate', undefined, { subjectRef, referenceRef, }); } const axis = (options?.axis as string) ?? 'centerY'; const tolerance = (options?.tolerance as number) ?? 0; let delta = 0; switch (axis) { case 'left': delta = Math.abs(sRect.left - rRect.left); break; case 'right': delta = Math.abs(sRect.right - rRect.right); break; case 'top': delta = Math.abs(sRect.top - rRect.top); break; case 'bottom': delta = Math.abs(sRect.bottom - rRect.bottom); break; case 'centerX': delta = Math.abs( (sRect.left + sRect.right) / 2 - (rRect.left + rRect.right) / 2, ); break; case 'centerY': delta = Math.abs( (sRect.top + sRect.bottom) / 2 - (rRect.top + rRect.bottom) / 2, ); break; default: delta = Math.abs( (sRect.top + sRect.bottom) / 2 - (rRect.top + rRect.bottom) / 2, ); } const pass = delta <= tolerance; return result( clauseId, pass ? 'pass' : 'fail', 'determinate', { delta, tolerance, axis: axis === 'centerY' ? 0 : axis === 'centerX' ? 1 : 2 }, { subjectId: subjectRef, referenceId: referenceRef }, ); } export function evaluateCenteredWithin( world: GeometryWorld, clause: ClauseDescriptor, ): ClauseResult { const { subjectRef, referenceRef, clauseId, options } = clause; if (subjectRef === undefined || referenceRef === undefined) { return result(clauseId, 'error', 'indeterminate', undefined, { subjectRef, referenceRef, }); } const sRect = getBorderRect(world, subjectRef); const rRect = getBorderRect(world, referenceRef); if (!sRect || !rRect) { return result(clauseId, 'error', 'indeterminate', undefined, { subjectRef, referenceRef, }); } const tolerance = (options?.tolerance as number) ?? 0; const centerXSubject = (sRect.left + sRect.right) / 2; const centerYSubject = (sRect.top + sRect.bottom) / 2; const centerXRef = (rRect.left + rRect.right) / 2; const centerYRef = (rRect.top + rRect.bottom) / 2; const deltaX = Math.abs(centerXSubject - centerXRef); const deltaY = Math.abs(centerYSubject - centerYRef); const pass = deltaX <= tolerance && deltaY <= tolerance; return result( clauseId, pass ? 'pass' : 'fail', 'determinate', { deltaX, deltaY, tolerance }, { subjectId: subjectRef, referenceId: referenceRef }, ); }