v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// 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' };
|
||||
}
|
||||
Reference in New Issue
Block a user