2025-08-15 10:00:00 -07:00
/**
* Predicate registry and evaluator contracts for the Imhotep logic engine.
*
* Predicates are the atomic propositions over which first-order logic
* formulas are built. Each predicate declares its name, arity, domain
* signature, and required geometry facts.
*
* Invariant: a predicate evaluator is a pure function from a geometry
* world and a tuple of bound subject ids to a PredicateTruth result.
*/
import type { DiagnosticCode } from 'imhotep-core'
import type {
GeometryWorld ,
ClauseResult ,
ClauseStatus ,
TruthValue ,
} from './registry.js' ;
// ---------------------------------------------------------------------------
// Predicate Truth
// ---------------------------------------------------------------------------
export type PredicateTruth = 'true' | 'false' | 'indeterminate' ;
export interface PredicateResult {
truth : PredicateTruth ;
metrics? : Record < string , number > ;
witness ? : {
subjectIds : number [ ] ;
} ;
diagnostics? : Array < {
code : DiagnosticCode ;
severity : 'error' | 'warning' | 'info' ;
message : string ;
} > ;
}
// ---------------------------------------------------------------------------
// Predicate Descriptor
// ---------------------------------------------------------------------------
export interface PredicateDescriptor {
name : string ;
arity : number ;
domains : string [ ] ;
requiredFacts : string [ ] ;
}
// ---------------------------------------------------------------------------
// Predicate Evaluator
// ---------------------------------------------------------------------------
export interface PredicateEvaluator {
descriptor : PredicateDescriptor ;
evaluateTuple (
world : GeometryWorld ,
tuple : number [ ] ,
options? : Record < string , unknown > ,
) : PredicateResult ;
}
// ---------------------------------------------------------------------------
// Built-in Predicates
// ---------------------------------------------------------------------------
export const BUILTIN_PREDICATES : PredicateDescriptor [ ] = [
{ name : 'width' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'subject.primaryBox' ] } ,
{ name : 'height' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'subject.primaryBox' ] } ,
{ name : 'above' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'below' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'leftOf' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'rightOf' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'inside' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'contains' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'overlaps' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'alignedWith' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'centeredWithin' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'atLeast' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'subject.primaryBox' ] } ,
{ name : 'atMost' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'subject.primaryBox' ] } ,
{ name : 'between' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'subject.primaryBox' ] } ,
{ name : 'clippedBy' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.clipChain' , 'reference.clipChain' ] } ,
2026-05-21 20:10:17 -07:00
{ name : 'attachedToScrollContainer' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'topology.scrollContainerOf' ] } ,
{ name : 'escapeClippingChainOf' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'topology.stackingContextOf' , 'subject.clipChain' ] } ,
2026-05-21 17:10:38 -07:00
{ name : 'inStackingContext' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'topology.stackingContextOf' ] } ,
2025-08-15 10:00:00 -07:00
{ name : 'separatedFrom' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
2026-05-21 20:10:17 -07:00
{ name : 'aspectRatio' , arity : 1 , domains : [ 'element' ] , requiredFacts : [ 'subject.primaryBox' ] } ,
2025-08-15 10:00:00 -07:00
// Spatial alias predicates
{ name : 'beside' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'nextTo' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'adjacent' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'touching' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'near' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'under' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
{ name : 'within' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
] ;
// ---------------------------------------------------------------------------
// Predicate Registry
// ---------------------------------------------------------------------------
export class PredicateRegistry {
private registry = new Map < string , PredicateEvaluator > ( ) ;
register ( evaluator : PredicateEvaluator ) : void {
this . registry . set ( evaluator . descriptor . name , evaluator ) ;
}
get ( name : string ) : PredicateEvaluator | undefined {
return this . registry . get ( name ) ;
}
list ( ) : string [ ] {
return Array . from ( this . registry . keys ( ) ) ;
}
clear ( ) : void {
this . registry . clear ( ) ;
}
getDescriptor ( name : string ) : PredicateDescriptor | undefined {
return this . registry . get ( name ) ? . descriptor ;
}
getRequiredFacts ( name : string ) : string [ ] {
return this . getDescriptor ( name ) ? . requiredFacts ? ? [ ] ;
}
}
// Global default instance for backward compatibility.
export const globalPredicateRegistry = new PredicateRegistry ( ) ;
export function registerPredicate ( evaluator : PredicateEvaluator ) : void {
globalPredicateRegistry . register ( evaluator ) ;
}
export function getPredicateEvaluator ( name : string ) : PredicateEvaluator | undefined {
return globalPredicateRegistry . get ( name ) ;
}
export function listRegisteredPredicates ( ) : string [ ] {
return globalPredicateRegistry . list ( ) ;
}
export function clearPredicateRegistry ( ) : void {
globalPredicateRegistry . clear ( ) ;
defaultPredicatesRegistered = false ;
}
export function getPredicateDescriptor ( name : string ) : PredicateDescriptor | undefined {
return globalPredicateRegistry . getDescriptor ( name ) ;
}
export function getRequiredFactsForPredicate ( name : string ) : string [ ] {
return globalPredicateRegistry . getRequiredFacts ( name ) ;
}
// ---------------------------------------------------------------------------
// Built-in Predicate Evaluators
// ---------------------------------------------------------------------------
function getBorderRect ( world : GeometryWorld , subjectId : number ) {
const { boxes } = world ;
// Build a subjectId -> boxIndex map on first use for O(1) lookups.
// This avoids O(n) linear scans on every predicate evaluation.
let index = ( world as any ) . __boxIndex as Map < number , number > | undefined ;
if ( ! index ) {
index = new Map < number , number > ( ) ;
for ( let i = 0 ; i < boxes . subjectId . length ; i ++ ) {
index . set ( boxes . subjectId [ i ] , i ) ;
}
( world as any ) . __boxIndex = index ;
}
const i = index . get ( subjectId ) ;
if ( i === undefined ) {
return null ;
}
return {
left : boxes.borderLeft [ i ] ,
top : boxes.borderTop [ i ] ,
right : boxes.borderRight [ i ] ,
bottom : boxes.borderBottom [ i ] ,
} ;
}
function makePredicateResult (
truth : PredicateTruth ,
metrics? : Record < string , number > ,
witness? : number [ ] ,
diagnostics? : PredicateResult [ 'diagnostics' ] ,
) : PredicateResult {
return {
truth ,
metrics ,
witness : witness ? { subjectIds : witness } : undefined ,
diagnostics ,
} ;
}
/**
* Build a diagnostic for a failing predicate that includes observed values.
*/
function makePredicateDiagnostic (
predicate : string ,
metrics : Record < string , number > ,
subjectIds : number [ ] ,
) : PredicateResult [ 'diagnostics' ] {
const entries = Object . entries ( metrics )
. map ( ( [ k , v ] ) = > ` ${ k } = ${ typeof v === 'number' ? v . toFixed ( 2 ) : String ( v ) } ` )
. join ( ', ' ) ;
return [
{
code : 'IMH_PREDICATE_FAILED' ,
severity : 'error' ,
message : ` Predicate " ${ predicate } " failed for subjects [ ${ subjectIds . join ( ', ' ) } ]. Observed: { ${ entries } } ` ,
} ,
] ;
}
function parseLengthOption ( raw : unknown ) : { value : number ; unit : string } | null {
if ( typeof raw === 'number' ) {
return { value : raw , unit : 'px' } ;
}
if ( typeof raw === 'string' ) {
const trimmed = raw . trim ( ) ;
const m = trimmed . match ( /^(-?\d*\.?\d+)\s*([a-z%]+)?$/i ) ;
if ( ! m ) return null ;
const value = Number . parseFloat ( m [ 1 ] ? ? '' ) ;
if ( Number . isNaN ( value ) ) return null ;
return { value , unit : ( m [ 2 ] ? ? 'px' ) . toLowerCase ( ) } ;
}
if ( raw && typeof raw === 'object' ) {
const obj = raw as Record < string , unknown > ;
if ( typeof obj . value === 'number' ) {
return {
value : obj.value ,
unit : typeof obj . unit === 'string' ? obj . unit . toLowerCase ( ) : 'px' ,
} ;
}
}
return null ;
}
function getSubjectFontSizePx ( world : GeometryWorld , subjectId : number ) : number {
const styles = ( world as any ) . styles as { fontSize? : ArrayLike < number > } | undefined ;
const fontSize = styles ? . fontSize ;
if ( ! fontSize ) return 16 ;
const ids = world . subjects ? . ids ;
if ( ! ids ) return 16 ;
const idx = ids . indexOf ( subjectId ) ;
if ( idx < 0 ) return 16 ;
const fs = Number ( fontSize [ idx ] ? ? 16 ) ;
return Number . isFinite ( fs ) && fs > 0 ? fs : 16 ;
}
function getRootFontSizePx ( world : GeometryWorld ) : number {
const styles = ( world as any ) . styles as { fontSize? : ArrayLike < number > } | undefined ;
const fontSize = styles ? . fontSize ;
if ( ! fontSize || fontSize . length === 0 ) return 16 ;
const fs = Number ( fontSize [ 0 ] ? ? 16 ) ;
return Number . isFinite ( fs ) && fs > 0 ? fs : 16 ;
}
function getSubjectChWidthPx ( world : GeometryWorld , subjectId : number ) : number {
const styles = ( world as any ) . styles as {
subjectId? : ArrayLike < number >
chWidth? : ArrayLike < number >
} | undefined ;
const chWidth = styles ? . chWidth ;
if ( ! chWidth || chWidth . length === 0 ) {
return getSubjectFontSizePx ( world , subjectId ) * 0.5 ;
}
const styleSubjectIds = styles ? . subjectId ;
if ( styleSubjectIds && styleSubjectIds . length > 0 ) {
for ( let i = 0 ; i < styleSubjectIds . length ; i ++ ) {
if ( Number ( styleSubjectIds [ i ] ) !== subjectId ) continue ;
const w = Number ( chWidth [ i ] ) ;
if ( Number . isFinite ( w ) && w > 0 ) return w ;
break ;
}
}
const ids = world . subjects ? . ids ;
if ( ids ) {
const idx = ids . indexOf ( subjectId ) ;
if ( idx >= 0 ) {
const w = Number ( chWidth [ idx ] ) ;
if ( Number . isFinite ( w ) && w > 0 ) return w ;
}
}
return getSubjectFontSizePx ( world , subjectId ) * 0.5 ;
}
function resolveLengthToPx (
world : GeometryWorld ,
subjectId : number ,
raw : unknown ,
dimension : 'width' | 'height' ,
) : number | undefined {
const parsed = parseLengthOption ( raw ) ;
if ( ! parsed ) return undefined ;
const { value , unit } = parsed ;
switch ( unit ) {
case 'px' :
return value ;
case 'vw' :
return value * ( world . env . viewportWidth / 100 ) ;
case 'vh' :
return value * ( world . env . viewportHeight / 100 ) ;
case '%' :
return value * ( ( dimension === 'height' ? world.env.viewportHeight : world.env.viewportWidth ) / 100 ) ;
case 'rem' :
return value * getRootFontSizePx ( world ) ;
case 'em' :
return value * getSubjectFontSizePx ( world , subjectId ) ;
case 'ch' :
return value * getSubjectChWidthPx ( world , subjectId ) ;
default :
return value ;
}
}
export const widthPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 0 ] ! ,
evaluateTuple ( world , tuple ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const rect = getBorderRect ( world , subjectId ) ;
if ( ! rect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const w = rect . right - rect . left ;
return makePredicateResult ( 'true' , { width : w , left : rect.left , right : rect.right } , [ subjectId ] ) ;
} ,
} ;
export const heightPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 1 ] ! ,
evaluateTuple ( world , tuple ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const rect = getBorderRect ( world , subjectId ) ;
if ( ! rect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const h = rect . bottom - rect . top ;
return makePredicateResult ( 'true' , { height : h } , [ subjectId ] ) ;
} ,
} ;
export const abovePredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 2 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const gap = rRect . top - sRect . bottom ;
const minGap = ( options ? . minGap as number | undefined ) ? ? 0 ;
const maxGap = ( options ? . maxGap as number | undefined ) ? ? Infinity ;
const pass = gap >= minGap && gap <= maxGap ;
const metrics = { gap , observedGap : gap , minGap , maxGap , subjectBottom : sRect.bottom , refTop : rRect.top } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'above' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const belowPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 3 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const gap = sRect . top - rRect . bottom ;
const minGap = ( options ? . minGap as number | undefined ) ? ? 0 ;
const maxGap = ( options ? . maxGap as number | undefined ) ? ? Infinity ;
const pass = gap >= minGap && gap <= maxGap ;
const metrics = { gap , observedGap : gap , minGap , maxGap } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'below' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const leftOfPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 4 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const gap = rRect . left - sRect . right ;
const minGap = ( options ? . minGap as number | undefined ) ? ? 0 ;
const maxGap = ( options ? . maxGap as number | undefined ) ? ? Infinity ;
const pass = gap >= minGap && gap <= maxGap ;
const metrics = { gap , observedGap : gap , minGap , maxGap , subjectRight : sRect.right , refLeft : rRect.left } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'leftOf' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const rightOfPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 5 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const gap = sRect . left - rRect . right ;
const minGap = ( options ? . minGap as number | undefined ) ? ? 0 ;
const maxGap = ( options ? . maxGap as number | undefined ) ? ? Infinity ;
const pass = gap >= minGap && gap <= maxGap ;
const metrics = { gap , observedGap : gap , minGap , maxGap } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'rightOf' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const insidePredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 6 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const tolerance = ( options ? . tolerance as number | undefined ) ? ? 0 ;
const epsilon = ( options ? . epsilon as number | undefined ) ? ? 0.1 ;
const effectiveTolerance = tolerance + epsilon ;
const pass =
sRect . left >= rRect . left - effectiveTolerance &&
sRect . top >= rRect . top - effectiveTolerance &&
sRect . right <= rRect . right + effectiveTolerance &&
sRect . bottom <= rRect . bottom + effectiveTolerance ;
2026-05-21 14:11:47 -07:00
const metrics : Record < string , number > = {
2025-08-15 10:00:00 -07:00
overflowLeft : Math.max ( 0 , rRect . left - sRect . left ) ,
overflowTop : Math.max ( 0 , rRect . top - sRect . top ) ,
overflowRight : Math.max ( 0 , sRect . right - rRect . right ) ,
overflowBottom : Math.max ( 0 , sRect . bottom - rRect . bottom ) ,
tolerance ,
epsilon ,
effectiveTolerance ,
subjectLeft : sRect.left ,
subjectTop : sRect.top ,
subjectRight : sRect.right ,
subjectBottom : sRect.bottom ,
refLeft : rRect.left ,
refTop : rRect.top ,
refRight : rRect.right ,
refBottom : rRect.bottom ,
} ;
2026-05-21 14:11:47 -07:00
const hasOverflow = metrics . overflowLeft > 0 || metrics . overflowTop > 0
|| metrics . overflowRight > 0 || metrics . overflowBottom > 0 ;
if ( hasOverflow && world . clipping ? . subjectId ) {
for ( let i = 0 ; i < world . clipping . subjectId . length ; i ++ ) {
if ( world . clipping . subjectId [ i ] === referenceId ) {
metrics . hasClippedOverflow = 1 ;
break ;
}
}
}
2025-08-15 10:00:00 -07:00
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'inside' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const containsPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 7 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const tolerance = ( options ? . tolerance as number | undefined ) ? ? 0 ;
const pass =
rRect . left >= sRect . left - tolerance &&
rRect . top >= sRect . top - tolerance &&
rRect . right <= sRect . right + tolerance &&
rRect . bottom <= sRect . bottom + tolerance ;
2026-05-21 14:11:47 -07:00
const metrics : Record < string , number > = {
2025-08-15 10:00:00 -07:00
tolerance ,
subjectLeft : sRect.left ,
subjectTop : sRect.top ,
subjectRight : sRect.right ,
subjectBottom : sRect.bottom ,
refLeft : rRect.left ,
refTop : rRect.top ,
refRight : rRect.right ,
refBottom : rRect.bottom ,
} ;
2026-05-21 14:11:47 -07:00
const overflowLeft = Math . max ( 0 , sRect . left - rRect . left ) ;
const overflowTop = Math . max ( 0 , sRect . top - rRect . top ) ;
const overflowRight = Math . max ( 0 , rRect . right - sRect . right ) ;
const overflowBottom = Math . max ( 0 , rRect . bottom - sRect . bottom ) ;
if ( overflowLeft > 0 || overflowTop > 0 || overflowRight > 0 || overflowBottom > 0 ) {
if ( world . clipping ? . subjectId ) {
for ( let i = 0 ; i < world . clipping . subjectId . length ; i ++ ) {
if ( world . clipping . subjectId [ i ] === subjectId ) {
metrics . hasClippedOverflow = 1 ;
break ;
}
}
}
}
2025-08-15 10:00:00 -07:00
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'contains' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const overlapsPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 8 ] ! ,
evaluateTuple ( world , tuple ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const pass =
sRect . left < rRect . right &&
sRect . right > rRect . left &&
sRect . top < rRect . bottom &&
sRect . bottom > rRect . top ;
return makePredicateResult ( pass ? 'true' : 'false' , { } , [ subjectId , referenceId ] ) ;
} ,
} ;
export const alignedWithPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 9 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const axis = ( options ? . axis as string | undefined ) ? ? 'centerY' ;
const tolerance = ( options ? . tolerance as number | undefined ) ? ? 0 ;
let delta = Infinity ;
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 ;
}
const pass = delta <= tolerance ;
const metrics : Record < string , number > = { delta , tolerance } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'alignedWith' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const centeredWithinPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 10 ] ! ,
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const tolerance = ( options ? . tolerance as number | undefined ) ? ? 0 ;
const sCx = ( sRect . left + sRect . right ) / 2 ;
const sCy = ( sRect . top + sRect . bottom ) / 2 ;
const rCx = ( rRect . left + rRect . right ) / 2 ;
const rCy = ( rRect . top + rRect . bottom ) / 2 ;
const deltaX = Math . abs ( sCx - rCx ) ;
const deltaY = Math . abs ( sCy - rCy ) ;
const pass = deltaX <= tolerance && deltaY <= tolerance ;
const metrics = { deltaX , deltaY , tolerance } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'centeredWithin' , metrics , [ subjectId , referenceId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] , diagnostics ) ;
} ,
} ;
export const atLeastPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 11 ] ! ,
evaluateTuple ( world , tuple , options ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const rect = getBorderRect ( world , subjectId ) ;
if ( ! rect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const dimension = options ? . dimension as string | undefined ;
const min = resolveLengthToPx ( world , subjectId , options ? . min , dimension === 'height' ? 'height' : 'width' ) ;
const value = dimension === 'height' ? rect . bottom - rect.top : rect.right - rect . left ;
if ( min === undefined ) {
return makePredicateResult ( 'true' , { value , left : rect.left , right : rect.right , top : rect.top , bottom : rect.bottom } , [ subjectId ] ) ;
}
const pass = value >= min ;
const metrics = { value , min , left : rect.left , right : rect.right , top : rect.top , bottom : rect.bottom } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'atLeast' , metrics , [ subjectId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId ] , diagnostics ) ;
} ,
} ;
export const atMostPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 12 ] ! ,
evaluateTuple ( world , tuple , options ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const rect = getBorderRect ( world , subjectId ) ;
if ( ! rect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const dimension = options ? . dimension as string | undefined ;
const max = resolveLengthToPx ( world , subjectId , options ? . max , dimension === 'height' ? 'height' : 'width' ) ;
const value = dimension === 'height' ? rect . bottom - rect.top : rect.right - rect . left ;
if ( max === undefined ) {
return makePredicateResult ( 'true' , { value , left : rect.left , right : rect.right , top : rect.top , bottom : rect.bottom } , [ subjectId ] ) ;
}
const pass = value <= max ;
const metrics = { value , max , left : rect.left , right : rect.right , top : rect.top , bottom : rect.bottom } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'atMost' , metrics , [ subjectId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId ] , diagnostics ) ;
} ,
} ;
export const betweenPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 13 ] ! ,
evaluateTuple ( world , tuple , options ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const rect = getBorderRect ( world , subjectId ) ;
if ( ! rect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const dimension = options ? . dimension as string | undefined ;
const min = resolveLengthToPx ( world , subjectId , options ? . min , dimension === 'height' ? 'height' : 'width' ) ;
const max = resolveLengthToPx ( world , subjectId , options ? . max , dimension === 'height' ? 'height' : 'width' ) ;
const value = dimension === 'height' ? rect . bottom - rect.top : rect.right - rect . left ;
if ( min === undefined && max === undefined ) {
return makePredicateResult ( 'true' , { value , left : rect.left , right : rect.right , top : rect.top , bottom : rect.bottom } , [ subjectId ] ) ;
}
const pass = ( min === undefined || value >= min ) && ( max === undefined || value <= max ) ;
const metrics = { value , min : min ? ? - Infinity , max : max ? ? Infinity , left : rect.left , right : rect.right , top : rect.top , bottom : rect.bottom } ;
const diagnostics = pass ? undefined : makePredicateDiagnostic ( 'between' , metrics , [ subjectId ] ) ;
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId ] , diagnostics ) ;
} ,
} ;
export const clippedByPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 14 ] ! ,
evaluateTuple ( world , tuple ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
// Simplified: check if subject's clipping root is the reference
const clipRoot = world . topology . clippingRootOf [ subjectId - 1 ] ? ? 0 ;
const pass = clipRoot === referenceId ;
2026-05-21 13:52:28 -07:00
// Determine clip kind from the clipping table entry for the reference.
// Encoding: 1=contain:paint, 2=overflow:hidden/scroll/auto
const metrics : Record < string , number > = { } ;
if ( referenceId !== undefined ) {
const { clipping } = world ;
for ( let i = 0 ; i < clipping . subjectId . length ; i ++ ) {
if ( clipping . subjectId [ i ] === referenceId ) {
metrics . clipKind = clipping . clipKind [ i ] === 5 /* Contain */ ? 1 : 2 ;
break ;
}
}
}
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] ) ;
2025-08-15 10:00:00 -07:00
} ,
} ;
export const inStackingContextPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 17 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sc = world . topology . stackingContextOf [ subjectId - 1 ] ? ? 0 ;
2026-05-21 17:10:38 -07:00
const subjectHasSC = sc > 0 ;
if ( tuple . length >= 2 && tuple [ 1 ] !== undefined && tuple [ 1 ] !== 0 ) {
const referenceId = tuple [ 1 ] ;
const refSC = world . topology . stackingContextOf [ referenceId - 1 ] ? ? 0 ;
const pass = subjectHasSC && refSC > 0 && sc === refSC ;
return makePredicateResult ( pass ? 'true' : 'false' , { stackingContext : sc , referenceStackingContext : refSC } , [ subjectId , referenceId ] ) ;
}
const pass = subjectHasSC ;
2025-08-15 10:00:00 -07:00
return makePredicateResult ( pass ? 'true' : 'false' , { stackingContext : sc } , [ subjectId ] ) ;
} ,
} ;
2026-05-21 20:10:17 -07:00
export const attachedToScrollContainerPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 15 ] ! ,
evaluateTuple ( world , tuple ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const scrollContainer = world . topology . scrollContainerOf [ subjectId - 1 ] ? ? 0 ;
const pass = scrollContainer > 0 ;
return makePredicateResult ( pass ? 'true' : 'false' , { scrollContainer } , [ subjectId ] ) ;
} ,
} ;
export const escapeClippingChainOfPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 16 ] ! ,
evaluateTuple ( _world , tuple ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
return makePredicateResult ( 'indeterminate' , { } , [ subjectId ] , [
{
code : 'IMH_TOPOLOGY_UNSUPPORTED' ,
severity : 'warning' ,
message : ` escapeClippingChainOf is not yet implemented. The predicate parses but the evaluator returns indeterminate — visual clipping chain escape geometry requires fragment-level bounds analysis. Use clippedBy as an alternative for overflow containment checks. ` ,
} ,
] ) ;
} ,
} ;
export const aspectRatioPredicate : PredicateEvaluator = {
descriptor : BUILTIN_PREDICATES [ 19 ] ! ,
evaluateTuple ( world , tuple , options ) {
const subjectId = tuple [ 0 ] ;
if ( subjectId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const boxId = world . subjects . primaryBoxId [ subjectId - 1 ] ;
if ( boxId === undefined || boxId < 0 ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const w = world . boxes . contentRight [ boxId ] - world . boxes . contentLeft [ boxId ] ;
const h = world . boxes . contentBottom [ boxId ] - world . boxes . contentTop [ boxId ] ;
if ( h <= 0 ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const ratio = w / h ;
const min = ( options as any ) ? . min as number | undefined ;
const max = ( options as any ) ? . max as number | undefined ;
if ( min !== undefined && ratio < min ) {
return makePredicateResult ( 'false' , { aspectRatio : ratio , min } , [ subjectId ] ) ;
}
if ( max !== undefined && ratio > max ) {
return makePredicateResult ( 'false' , { aspectRatio : ratio , max } , [ subjectId ] ) ;
}
if ( min !== undefined || max !== undefined ) {
return makePredicateResult ( 'true' , { aspectRatio : ratio , . . . ( min !== undefined ? { min } : { } ) , . . . ( max !== undefined ? { max } : { } ) } , [ subjectId ] ) ;
}
return makePredicateResult ( 'indeterminate' ) ;
} ,
} ;
2025-08-15 10:00:00 -07:00
// ---------------------------------------------------------------------------
// Spatial Alias Predicate Evaluators
// ---------------------------------------------------------------------------
/**
* beside(subject, opts) = leftOf(subject, opts) OR rightOf(subject, opts)
* Tries leftOf first, then rightOf. Returns whichever passes, or fails with both measured values.
*/
export const besidePredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 20 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
const leftResult = leftOfPredicate . evaluateTuple ( world , tuple , options ) ;
if ( leftResult . truth === 'true' ) {
return makePredicateResult ( 'true' , { . . . leftResult . metrics , direction : 'left' } as any , tuple ) ;
}
const rightResult = rightOfPredicate . evaluateTuple ( world , tuple , options ) ;
if ( rightResult . truth === 'true' ) {
return makePredicateResult ( 'true' , { . . . rightResult . metrics , direction : 'right' } as any , tuple ) ;
}
const metrics = {
leftGap : leftResult.metrics?.gap ? ? NaN ,
rightGap : rightResult.metrics?.gap ? ? NaN ,
} ;
return makePredicateResult ( 'false' , metrics , tuple , [
{
code : 'IMH_RELATION_BESIDE_FAILED' ,
severity : 'error' ,
message : ` Predicate "beside" failed: neither leftOf (gap= ${ ( metrics . leftGap as number ) . toFixed ( 2 ) } ) nor rightOf (gap= ${ ( metrics . rightGap as number ) . toFixed ( 2 ) } ) satisfied. ` ,
} ,
] ) ;
} ,
} ;
/**
* nextTo(subject, opts) = beside(subject, opts)
* Synonym for beside — delegates to the beside evaluator.
*/
export const nextToPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 21 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
const besideResult = besidePredicate . evaluateTuple ( world , tuple , options ) ;
if ( besideResult . truth === 'true' ) {
return makePredicateResult ( 'true' , { . . . besideResult . metrics , alias : 'nextTo' } as any , tuple ) ;
}
// Rewrite diagnostic to mention nextTo
const diagnostics = besideResult . diagnostics ? . map ( ( d ) = > ( {
. . . d ,
code : 'IMH_RELATION_BESIDE_FAILED' as const ,
message : d.message.replace ( '"beside"' , '"nextTo"' ) ,
} ) ) ;
return makePredicateResult ( 'false' , besideResult . metrics ? ? { } , tuple , diagnostics ) ;
} ,
} ;
/**
* adjacent(subject, opts) = leftOr(subject, { maxGap: 0 }) OR rightOf(subject, { maxGap: 0 })
* OR above(subject, { maxGap: 0 }) OR below(subject, { maxGap: 0 })
* Checks all four cardinal directions with maxGap: 0. Returns whichever passes, or fails.
*/
export const adjacentPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 22 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
const adjOptions = { . . . options , maxGap : 0 } ;
const directions = [
{ name : 'leftOf' , result : leftOfPredicate.evaluateTuple ( world , tuple , adjOptions ) } ,
{ name : 'rightOf' , result : rightOfPredicate.evaluateTuple ( world , tuple , adjOptions ) } ,
{ name : 'above' , result : abovePredicate.evaluateTuple ( world , tuple , adjOptions ) } ,
{ name : 'below' , result : belowPredicate.evaluateTuple ( world , tuple , adjOptions ) } ,
] ;
for ( const dir of directions ) {
if ( dir . result . truth === 'true' ) {
return makePredicateResult ( 'true' , { . . . dir . result . metrics , direction : dir.name } as any , tuple ) ;
}
}
const metrics = {
leftGap : directions [ 0 ] . result . metrics ? . gap ? ? NaN ,
rightGap : directions [ 1 ] . result . metrics ? . gap ? ? NaN ,
aboveGap : directions [ 2 ] . result . metrics ? . gap ? ? NaN ,
belowGap : directions [ 3 ] . result . metrics ? . gap ? ? NaN ,
} ;
return makePredicateResult ( 'false' , metrics , tuple , [
{
code : 'IMH_RELATION_ADJACENT_FAILED' ,
severity : 'error' ,
message : ` Predicate "adjacent" failed: no direction satisfied with maxGap=0. ` ,
} ,
] ) ;
} ,
} ;
/**
* touching(subject, opts) = adjacent(subject, opts)
* Synonym for adjacent — delegates to the adjacent evaluator.
*/
export const touchingPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 23 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
const adjacentResult = adjacentPredicate . evaluateTuple ( world , tuple , options ) ;
if ( adjacentResult . truth === 'true' ) {
return makePredicateResult ( 'true' , { . . . adjacentResult . metrics , alias : 'touching' } as any , tuple ) ;
}
const diagnostics = adjacentResult . diagnostics ? . map ( ( d ) = > ( {
. . . d ,
code : 'IMH_RELATION_ADJACENT_FAILED' as const ,
message : d.message.replace ( '"adjacent"' , '"touching"' ) ,
} ) ) ;
return makePredicateResult ( 'false' , adjacentResult . metrics ? ? { } , tuple , diagnostics ) ;
} ,
} ;
/**
* near(subject, opts) = overlaps(subject) OR inside a proximity radius
* First checks overlaps. If that fails, checks directional proximity using leftOf/rightOf/above/below
* with a large maxGap (default 100px, overridable via opts.maxGap).
*/
export const nearPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 24 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
const overlapsResult = overlapsPredicate . evaluateTuple ( world , tuple ) ;
if ( overlapsResult . truth === 'true' ) {
return makePredicateResult ( 'true' , { . . . overlapsResult . metrics , mode : 'overlaps' } as any , tuple ) ;
}
const radius = ( options ? . maxGap as number | undefined ) ? ? 100 ;
const proximityOptions = { . . . options , maxGap : radius } ;
const directions = [
{ name : 'leftOf' , result : leftOfPredicate.evaluateTuple ( world , tuple , proximityOptions ) } ,
{ name : 'rightOf' , result : rightOfPredicate.evaluateTuple ( world , tuple , proximityOptions ) } ,
{ name : 'above' , result : abovePredicate.evaluateTuple ( world , tuple , proximityOptions ) } ,
{ name : 'below' , result : belowPredicate.evaluateTuple ( world , tuple , proximityOptions ) } ,
] ;
for ( const dir of directions ) {
if ( dir . result . truth === 'true' ) {
return makePredicateResult ( 'true' , { . . . dir . result . metrics , direction : dir.name , mode : 'proximity' , radius } as any , tuple ) ;
}
}
const metrics = {
radius ,
leftGap : directions [ 0 ] . result . metrics ? . gap ? ? NaN ,
rightGap : directions [ 1 ] . result . metrics ? . gap ? ? NaN ,
aboveGap : directions [ 2 ] . result . metrics ? . gap ? ? NaN ,
belowGap : directions [ 3 ] . result . metrics ? . gap ? ? NaN ,
} ;
return makePredicateResult ( 'false' , metrics , tuple , [
{
code : 'IMH_RELATION_NEAR_FAILED' ,
severity : 'error' ,
message : ` Predicate "near" failed: elements neither overlap nor are within proximity radius ${ radius } . ` ,
} ,
] ) ;
} ,
} ;
/**
* under(subject, opts) = below(subject, opts)
* Synonym for below — delegates to the below evaluator.
*/
export const underPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 25 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
return belowPredicate . evaluateTuple ( world , tuple , options ) ;
} ,
} ;
/**
* within(subject, opts) = inside(subject, opts)
* Synonym for inside — delegates to the inside evaluator.
*/
export const withinPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 26 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
return insidePredicate . evaluateTuple ( world , tuple , options ) ;
} ,
} ;
export const separatedFromPredicate : PredicateEvaluator = {
2026-05-21 20:10:17 -07:00
descriptor : BUILTIN_PREDICATES [ 18 ] ! ,
2025-08-15 10:00:00 -07:00
evaluateTuple ( world , tuple , options ) {
const [ subjectId , referenceId ] = tuple ;
if ( subjectId === undefined || referenceId === undefined ) {
return makePredicateResult ( 'indeterminate' ) ;
}
const sRect = getBorderRect ( world , subjectId ) ;
const rRect = getBorderRect ( world , referenceId ) ;
if ( ! sRect || ! rRect ) {
return makePredicateResult ( 'indeterminate' ) ;
}
// separatedFrom = NOT overlaps
const overlaps =
sRect . left < rRect . right &&
sRect . right > rRect . left &&
sRect . top < rRect . bottom &&
sRect . bottom > rRect . top ;
const pass = ! overlaps ;
const metrics : Record < string , number > = { } ;
if ( ! pass ) {
// Calculate overlap area for diagnostics
const overlapLeft = Math . max ( sRect . left , rRect . left ) ;
const overlapTop = Math . max ( sRect . top , rRect . top ) ;
const overlapRight = Math . min ( sRect . right , rRect . right ) ;
const overlapBottom = Math . min ( sRect . bottom , rRect . bottom ) ;
metrics . overlapWidth = overlapRight - overlapLeft ;
metrics . overlapHeight = overlapBottom - overlapTop ;
}
return makePredicateResult ( pass ? 'true' : 'false' , metrics , [ subjectId , referenceId ] ) ;
} ,
} ;
2026-05-21 14:36:02 -07:00
export const leftAlignedWithPredicate : PredicateEvaluator = {
descriptor : { name : 'leftAlignedWith' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
evaluateTuple ( world , tuple , options ) {
return alignedWithPredicate . evaluateTuple ( world , tuple , { . . . options as Record < string , unknown > , axis : 'left' } ) ;
} ,
} ;
export const rightAlignedWithPredicate : PredicateEvaluator = {
descriptor : { name : 'rightAlignedWith' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
evaluateTuple ( world , tuple , options ) {
return alignedWithPredicate . evaluateTuple ( world , tuple , { . . . options as Record < string , unknown > , axis : 'right' } ) ;
} ,
} ;
export const topAlignedWithPredicate : PredicateEvaluator = {
descriptor : { name : 'topAlignedWith' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
evaluateTuple ( world , tuple , options ) {
return alignedWithPredicate . evaluateTuple ( world , tuple , { . . . options as Record < string , unknown > , axis : 'top' } ) ;
} ,
} ;
export const bottomAlignedWithPredicate : PredicateEvaluator = {
descriptor : { name : 'bottomAlignedWith' , arity : 2 , domains : [ 'element' , 'element' ] , requiredFacts : [ 'subject.primaryBox' , 'reference.primaryBox' ] } ,
evaluateTuple ( world , tuple , options ) {
return alignedWithPredicate . evaluateTuple ( world , tuple , { . . . options as Record < string , unknown > , axis : 'bottom' } ) ;
} ,
} ;
2025-08-15 10:00:00 -07:00
// ---------------------------------------------------------------------------
// Register Defaults
// ---------------------------------------------------------------------------
let defaultPredicatesRegistered = false ;
export function registerDefaultPredicates ( ) : void {
if ( defaultPredicatesRegistered ) return ;
defaultPredicatesRegistered = true ;
registerPredicate ( widthPredicate ) ;
registerPredicate ( heightPredicate ) ;
registerPredicate ( abovePredicate ) ;
registerPredicate ( belowPredicate ) ;
registerPredicate ( leftOfPredicate ) ;
registerPredicate ( rightOfPredicate ) ;
registerPredicate ( insidePredicate ) ;
registerPredicate ( containsPredicate ) ;
registerPredicate ( overlapsPredicate ) ;
registerPredicate ( alignedWithPredicate ) ;
registerPredicate ( centeredWithinPredicate ) ;
registerPredicate ( atLeastPredicate ) ;
registerPredicate ( atMostPredicate ) ;
registerPredicate ( betweenPredicate ) ;
registerPredicate ( clippedByPredicate ) ;
2026-05-21 20:10:17 -07:00
registerPredicate ( attachedToScrollContainerPredicate ) ;
registerPredicate ( escapeClippingChainOfPredicate ) ;
registerPredicate ( aspectRatioPredicate ) ;
2025-08-15 10:00:00 -07:00
registerPredicate ( inStackingContextPredicate ) ;
registerPredicate ( separatedFromPredicate ) ;
registerPredicate ( leftAlignedWithPredicate ) ;
registerPredicate ( rightAlignedWithPredicate ) ;
registerPredicate ( topAlignedWithPredicate ) ;
registerPredicate ( bottomAlignedWithPredicate ) ;
registerPredicate ( besidePredicate ) ;
registerPredicate ( nextToPredicate ) ;
registerPredicate ( adjacentPredicate ) ;
registerPredicate ( touchingPredicate ) ;
registerPredicate ( nearPredicate ) ;
registerPredicate ( underPredicate ) ;
registerPredicate ( withinPredicate ) ;
}