162 lines
4.1 KiB
TypeScript
162 lines
4.1 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string, number>,
|
||
|
|
witness?: Record<string, number | undefined>,
|
||
|
|
): 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 },
|
||
|
|
);
|
||
|
|
}
|