140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
|
|
// 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' };
|
||
|
|
}
|