Files
Imhotep/packages/imhotep-topology/src/predicate-queries.ts
T

140 lines
4.6 KiB
TypeScript
Raw Normal View History

// 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' };
}