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
This commit is contained in:
John Dvorak
2026-05-21 13:52:28 -07:00
parent a75c3be9e0
commit c4a3d304ef
3 changed files with 18 additions and 2 deletions
+4 -1
View File
@@ -156,6 +156,7 @@ export async function extractTopology(
function isClippingElement(el) { function isClippingElement(el) {
const style = window.getComputedStyle(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' || return style.overflowX === 'hidden' || style.overflowX === 'scroll' || style.overflowX === 'auto' ||
style.overflowY === 'hidden' || style.overflowY === 'scroll' || style.overflowY === 'auto' || style.overflowY === 'hidden' || style.overflowY === 'scroll' || style.overflowY === 'auto' ||
style.clipPath !== 'none' style.clipPath !== 'none'
@@ -252,10 +253,12 @@ export async function extractTopology(
if (isClippingElement(el)) { if (isClippingElement(el)) {
const r = rectFor(el) const r = rectFor(el)
const style = window.getComputedStyle(el)
const isContainPaint = style.contain && style.contain.includes('paint')
results.clipping.push({ results.clipping.push({
clipNodeId: results.clipping.length, clipNodeId: results.clipping.length,
subjectId, subjectId,
clipKind: 1, clipKind: isContainPaint ? 5 : 1,
clipLeft: r.left, clipLeft: r.left,
clipTop: r.top, clipTop: r.top,
clipRight: r.right, clipRight: r.right,
+1
View File
@@ -330,6 +330,7 @@ export const enum ClipKind {
ClipPath = 2, ClipPath = 2,
Mask = 3, Mask = 3,
SvgClip = 4, SvgClip = 4,
Contain = 5,
} }
export interface Clipping { export interface Clipping {
+13 -1
View File
@@ -687,7 +687,19 @@ export const clippedByPredicate: PredicateEvaluator = {
// Simplified: check if subject's clipping root is the reference // Simplified: check if subject's clipping root is the reference
const clipRoot = world.topology.clippingRootOf[subjectId - 1] ?? 0; const clipRoot = world.topology.clippingRootOf[subjectId - 1] ?? 0;
const pass = clipRoot === referenceId; 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<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]);
}, },
}; };