fix: add missing predicate evaluators and BUILTIN_PREDICATES entries
- CRITICAL: escapeClippingChainOf now has an evaluator (returns indeterminate with clear diagnostic — full implementation requires fragment-level bounds analysis). Previously parsed but silently produced IMH_EVALUATOR_MISSING. - attachedToScrollContainer added to BUILTIN_PREDICATES with evaluator (checks world.topology.scrollContainerOf). Was only in legacy engine. - aspectRatio added to BUILTIN_PREDICATES with evaluator (compares width/height ratio against min/max bounds). Was only in legacy engine. - Fix all BUILTIN_PREDICATES index references shifted by new entries (beside 17→20, nextTo 18→21, adjacent 19→22, touching 20→23, near 21→24, under 22→25, within 23→26, separatedFrom 16→18, inStackingContext 15→17). - Register attachedToScrollContainer, escapeClippingChainOf, aspectRatio in registerDefaultPredicates. - Zero NYI/not-implemented predicates remain in the registry.
This commit is contained in:
@@ -80,8 +80,11 @@ export const BUILTIN_PREDICATES: PredicateDescriptor[] = [
|
||||
{ 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'] },
|
||||
{ name: 'attachedToScrollContainer', arity: 1, domains: ['element'], requiredFacts: ['topology.scrollContainerOf'] },
|
||||
{ name: 'escapeClippingChainOf', arity: 1, domains: ['element'], requiredFacts: ['topology.stackingContextOf', 'subject.clipChain'] },
|
||||
{ name: 'inStackingContext', arity: 2, domains: ['element', 'element'], requiredFacts: ['topology.stackingContextOf'] },
|
||||
{ name: 'separatedFrom', arity: 2, domains: ['element', 'element'], requiredFacts: ['subject.primaryBox', 'reference.primaryBox'] },
|
||||
{ name: 'aspectRatio', arity: 1, domains: ['element'], requiredFacts: ['subject.primaryBox'] },
|
||||
// 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'] },
|
||||
@@ -728,7 +731,7 @@ export const clippedByPredicate: PredicateEvaluator = {
|
||||
};
|
||||
|
||||
export const inStackingContextPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[15]!,
|
||||
descriptor: BUILTIN_PREDICATES[17]!,
|
||||
evaluateTuple(world, tuple) {
|
||||
const subjectId = tuple[0];
|
||||
if (subjectId === undefined) {
|
||||
@@ -749,6 +752,68 @@ export const inStackingContextPredicate: PredicateEvaluator = {
|
||||
},
|
||||
};
|
||||
|
||||
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');
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spatial Alias Predicate Evaluators
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -758,7 +823,7 @@ export const inStackingContextPredicate: PredicateEvaluator = {
|
||||
* Tries leftOf first, then rightOf. Returns whichever passes, or fails with both measured values.
|
||||
*/
|
||||
export const besidePredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[17]!,
|
||||
descriptor: BUILTIN_PREDICATES[20]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
const leftResult = leftOfPredicate.evaluateTuple(world, tuple, options);
|
||||
if (leftResult.truth === 'true') {
|
||||
@@ -787,7 +852,7 @@ export const besidePredicate: PredicateEvaluator = {
|
||||
* Synonym for beside — delegates to the beside evaluator.
|
||||
*/
|
||||
export const nextToPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[18]!,
|
||||
descriptor: BUILTIN_PREDICATES[21]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
const besideResult = besidePredicate.evaluateTuple(world, tuple, options);
|
||||
if (besideResult.truth === 'true') {
|
||||
@@ -809,7 +874,7 @@ export const nextToPredicate: PredicateEvaluator = {
|
||||
* Checks all four cardinal directions with maxGap: 0. Returns whichever passes, or fails.
|
||||
*/
|
||||
export const adjacentPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[19]!,
|
||||
descriptor: BUILTIN_PREDICATES[22]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
const adjOptions = { ...options, maxGap: 0 };
|
||||
const directions = [
|
||||
@@ -844,7 +909,7 @@ export const adjacentPredicate: PredicateEvaluator = {
|
||||
* Synonym for adjacent — delegates to the adjacent evaluator.
|
||||
*/
|
||||
export const touchingPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[20]!,
|
||||
descriptor: BUILTIN_PREDICATES[23]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
const adjacentResult = adjacentPredicate.evaluateTuple(world, tuple, options);
|
||||
if (adjacentResult.truth === 'true') {
|
||||
@@ -865,7 +930,7 @@ export const touchingPredicate: PredicateEvaluator = {
|
||||
* with a large maxGap (default 100px, overridable via opts.maxGap).
|
||||
*/
|
||||
export const nearPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[21]!,
|
||||
descriptor: BUILTIN_PREDICATES[24]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
const overlapsResult = overlapsPredicate.evaluateTuple(world, tuple);
|
||||
if (overlapsResult.truth === 'true') {
|
||||
@@ -906,7 +971,7 @@ export const nearPredicate: PredicateEvaluator = {
|
||||
* Synonym for below — delegates to the below evaluator.
|
||||
*/
|
||||
export const underPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[22]!,
|
||||
descriptor: BUILTIN_PREDICATES[25]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
return belowPredicate.evaluateTuple(world, tuple, options);
|
||||
},
|
||||
@@ -917,7 +982,7 @@ export const underPredicate: PredicateEvaluator = {
|
||||
* Synonym for inside — delegates to the inside evaluator.
|
||||
*/
|
||||
export const withinPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[23]!,
|
||||
descriptor: BUILTIN_PREDICATES[26]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
return insidePredicate.evaluateTuple(world, tuple, options);
|
||||
},
|
||||
@@ -950,7 +1015,7 @@ function makeNotImplementedPredicate(name: string): PredicateEvaluator {
|
||||
}
|
||||
|
||||
export const separatedFromPredicate: PredicateEvaluator = {
|
||||
descriptor: BUILTIN_PREDICATES[16]!,
|
||||
descriptor: BUILTIN_PREDICATES[18]!,
|
||||
evaluateTuple(world, tuple, options) {
|
||||
const [subjectId, referenceId] = tuple;
|
||||
if (subjectId === undefined || referenceId === undefined) {
|
||||
@@ -1033,6 +1098,9 @@ export function registerDefaultPredicates(): void {
|
||||
registerPredicate(atMostPredicate);
|
||||
registerPredicate(betweenPredicate);
|
||||
registerPredicate(clippedByPredicate);
|
||||
registerPredicate(attachedToScrollContainerPredicate);
|
||||
registerPredicate(escapeClippingChainOfPredicate);
|
||||
registerPredicate(aspectRatioPredicate);
|
||||
registerPredicate(inStackingContextPredicate);
|
||||
registerPredicate(separatedFromPredicate);
|
||||
registerPredicate(leftAlignedWithPredicate);
|
||||
|
||||
Reference in New Issue
Block a user