// Topology spatial predicates callable from the logic engine (V1.1) // These operate over a materialized geometry world and return explicit truth // values with indeterminate handling — no silent defaults for missing facts. import { GeometryWorld } from 'imhotep-geometry'; // --------------------------------------------------------------------------- // Predicate truth model // --------------------------------------------------------------------------- export type PredicateTruth = | { kind: 'true' } | { kind: 'false' } | { kind: 'indeterminate'; reason: string }; // --------------------------------------------------------------------------- // Helpers: box lookup // --------------------------------------------------------------------------- function findBoxIndexForSubject(world: GeometryWorld, subjectId: number): number { // Boxes are indexed by subjectId; linear scan is acceptable for the // current data-oriented layout because the solver will cache lookups. for (let i = 0; i < world.boxes.boxId.length; i++) { if (world.boxes.subjectId[i] === subjectId) { return i; } } return -1; } interface BoxCoords { left: number; top: number; right: number; bottom: number; } function getBox(world: GeometryWorld, subjectId: number): BoxCoords | undefined { const idx = findBoxIndexForSubject(world, subjectId); if (idx === -1) return undefined; return { left: world.boxes.contentLeft[idx], top: world.boxes.contentTop[idx], right: world.boxes.contentRight[idx], bottom: world.boxes.contentBottom[idx], }; } // --------------------------------------------------------------------------- // Spatial predicates // --------------------------------------------------------------------------- /** * Returns true if `aId` is spatially above `bId`. * "Above" means a's bottom edge is at or above b's top edge. */ export function above(aId: number, bId: number, world: GeometryWorld): PredicateTruth { const a = getBox(world, aId); const b = getBox(world, bId); if (!a || !b) { return { kind: 'indeterminate', reason: 'missing box geometry' }; } return { kind: a.bottom <= b.top ? 'true' : 'false' }; } /** * Returns true if `aId` is spatially below `bId`. * "Below" means a's top edge is at or below b's bottom edge. */ export function below(aId: number, bId: number, world: GeometryWorld): PredicateTruth { const a = getBox(world, aId); const b = getBox(world, bId); if (!a || !b) { return { kind: 'indeterminate', reason: 'missing box geometry' }; } return { kind: a.top >= b.bottom ? 'true' : 'false' }; } /** * Returns true if `aId` is spatially to the left of `bId`. * "Left of" means a's right edge is at or to the left of b's left edge. */ export function leftOf(aId: number, bId: number, world: GeometryWorld): PredicateTruth { const a = getBox(world, aId); const b = getBox(world, bId); if (!a || !b) { return { kind: 'indeterminate', reason: 'missing box geometry' }; } return { kind: a.right <= b.left ? 'true' : 'false' }; } /** * Returns true if `aId` is spatially to the right of `bId`. * "Right of" means a's left edge is at or to the right of b's right edge. */ export function rightOf(aId: number, bId: number, world: GeometryWorld): PredicateTruth { const a = getBox(world, aId); const b = getBox(world, bId); if (!a || !b) { return { kind: 'indeterminate', reason: 'missing box geometry' }; } return { kind: a.left >= b.right ? 'true' : 'false' }; } /** * Returns true if `aId` is fully inside `bId`. * "Inside" means a's box is completely contained within b's box. */ export function inside(aId: number, bId: number, world: GeometryWorld): PredicateTruth { const a = getBox(world, aId); const b = getBox(world, bId); if (!a || !b) { return { kind: 'indeterminate', reason: 'missing box geometry' }; } const value = a.left >= b.left && a.top >= b.top && a.right <= b.right && a.bottom <= b.bottom; return { kind: value ? 'true' : 'false' }; } /** * Returns true if `aId` spatially overlaps `bId`. * "Overlaps" means the intersection of their boxes has positive area. */ export function overlaps(aId: number, bId: number, world: GeometryWorld): PredicateTruth { const a = getBox(world, aId); const b = getBox(world, bId); if (!a || !b) { return { kind: 'indeterminate', reason: 'missing box geometry' }; } const intersectLeft = Math.max(a.left, b.left); const intersectTop = Math.max(a.top, b.top); const intersectRight = Math.min(a.right, b.right); const intersectBottom = Math.min(a.bottom, b.bottom); const value = intersectLeft < intersectRight && intersectTop < intersectBottom; return { kind: value ? 'true' : 'false' }; }