From c4a3d304efe285e3c64f0a15c1db837076bce74a Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Thu, 21 May 2026 13:52:28 -0700 Subject: [PATCH] feat: solver consumes CSS contain:paint for clippedBy diagnostics - Add Contain=5 to geometry world ClipKind enum (avoids collision with topology engine's CONTAIN=3 vs Mask=3) - Fix CDP isClippingElement() to detect contain:paint (style.contain) - CDP topology builder sets clipKind=5 for contain:paint, 1 for overflow - Enrich clippedByPredicate with clipKind metric: 1=contain:paint, 2=overflow (hidden/scroll/auto) - Reads world.clipping.clipKind of the reference's clip node --- packages/imhotep-cdp/src/topology.ts | 5 ++++- packages/imhotep-geometry/src/world.ts | 1 + packages/imhotep-solver/src/predicates.ts | 14 +++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/imhotep-cdp/src/topology.ts b/packages/imhotep-cdp/src/topology.ts index 3923fc9..e8d8341 100644 --- a/packages/imhotep-cdp/src/topology.ts +++ b/packages/imhotep-cdp/src/topology.ts @@ -156,6 +156,7 @@ export async function extractTopology( function isClippingElement(el) { const style = window.getComputedStyle(el) + if (style.contain && style.contain.includes('paint')) return true return style.overflowX === 'hidden' || style.overflowX === 'scroll' || style.overflowX === 'auto' || style.overflowY === 'hidden' || style.overflowY === 'scroll' || style.overflowY === 'auto' || style.clipPath !== 'none' @@ -252,10 +253,12 @@ export async function extractTopology( if (isClippingElement(el)) { const r = rectFor(el) + const style = window.getComputedStyle(el) + const isContainPaint = style.contain && style.contain.includes('paint') results.clipping.push({ clipNodeId: results.clipping.length, subjectId, - clipKind: 1, + clipKind: isContainPaint ? 5 : 1, clipLeft: r.left, clipTop: r.top, clipRight: r.right, diff --git a/packages/imhotep-geometry/src/world.ts b/packages/imhotep-geometry/src/world.ts index 4e15220..e3ba1eb 100644 --- a/packages/imhotep-geometry/src/world.ts +++ b/packages/imhotep-geometry/src/world.ts @@ -330,6 +330,7 @@ export const enum ClipKind { ClipPath = 2, Mask = 3, SvgClip = 4, + Contain = 5, } export interface Clipping { diff --git a/packages/imhotep-solver/src/predicates.ts b/packages/imhotep-solver/src/predicates.ts index 0128266..f7486e2 100644 --- a/packages/imhotep-solver/src/predicates.ts +++ b/packages/imhotep-solver/src/predicates.ts @@ -687,7 +687,19 @@ export const clippedByPredicate: PredicateEvaluator = { // Simplified: check if subject's clipping root is the reference const clipRoot = world.topology.clippingRootOf[subjectId - 1] ?? 0; const pass = clipRoot === referenceId; - return makePredicateResult(pass ? 'true' : 'false', {}, [subjectId, referenceId]); + // Determine clip kind from the clipping table entry for the reference. + // Encoding: 1=contain:paint, 2=overflow:hidden/scroll/auto + const metrics: Record = {}; + 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]); }, };