diff --git a/packages/imhotep-cdp/src/extractor.ts b/packages/imhotep-cdp/src/extractor.ts index be5abcf..addfcfb 100644 --- a/packages/imhotep-cdp/src/extractor.ts +++ b/packages/imhotep-cdp/src/extractor.ts @@ -546,6 +546,7 @@ export class CDPExtractor { scroll: [], clipping: [], topology: { + subjectIds: [], containingBlockOf: [], nearestPositionedAncestorOf: [], scrollContainerOf: [], @@ -557,6 +558,9 @@ export class CDPExtractor { }, } + // ... etc until the next empty topology at line ~964 + + if (request.requiredFacts.topology !== false && backendNodeIds.length > 0) { try { const { result, errors } = await extractTopology(session, backendNodeIds, subjectIds) @@ -956,6 +960,7 @@ function createEmptySnapshot(request: ExtractorRequest): GeometryWorldSnapshot { fontWeight: [], }, topology: { + subjectIds: [], containingBlockOf: [], nearestPositionedAncestorOf: [], scrollContainerOf: [], diff --git a/packages/imhotep-cdp/src/topology.ts b/packages/imhotep-cdp/src/topology.ts index dbe2d79..949e424 100644 --- a/packages/imhotep-cdp/src/topology.ts +++ b/packages/imhotep-cdp/src/topology.ts @@ -42,6 +42,7 @@ export interface ClippingRecord { * Topology relation record. */ export interface TopologyRecord { + subjectIds: number[] containingBlockOf: number[] nearestPositionedAncestorOf: number[] scrollContainerOf: number[] @@ -83,6 +84,7 @@ export async function extractTopology( scroll: [], clipping: [], topology: { + subjectIds: [], containingBlockOf: [], nearestPositionedAncestorOf: [], scrollContainerOf: [], @@ -138,6 +140,7 @@ export async function extractTopology( scroll: [], clipping: [], topology: { + subjectIds: [], containingBlockOf: [], nearestPositionedAncestorOf: [], scrollContainerOf: [], @@ -269,6 +272,7 @@ export async function extractTopology( }) } + results.topology.subjectIds.push(subjectId) results.topology.containingBlockOf.push(getId(getContainingBlock(el))) results.topology.nearestPositionedAncestorOf.push(getId(getNearestPositionedAncestor(el))) results.topology.scrollContainerOf.push(getId(getScrollContainer(el))) @@ -301,6 +305,7 @@ export async function extractTopology( scroll: [], clipping: [], topology: { + subjectIds: [], containingBlockOf: [], nearestPositionedAncestorOf: [], scrollContainerOf: [], diff --git a/packages/imhotep-fixtures/src/pages/hard-fixture.html b/packages/imhotep-fixtures/src/pages/hard-fixture.html new file mode 100644 index 0000000..3514ccb --- /dev/null +++ b/packages/imhotep-fixtures/src/pages/hard-fixture.html @@ -0,0 +1,167 @@ + + + + +Imhotep Hard Geometric Fixture + + + + +
+ Imhotep Hard Geometric Fixture
+ Open DevTools Console to see bounding rects of all data-testid elements.
+ All elements are absolutely positioned for deterministic layout. +
+ + +
1. TRANSFORM (rotate 15deg + scale 1.5)
+
+
A
+
B
+
+ + +
2. overflown-childChild 50x250 in 200x200 container
+
+
+
+
+ + +
3. NESTED (relative > absolute > fixed, contain:paint)
+
+
+
fixed
+
+
+ + +
4. OVERLAPPING Z-INDEX (z:1,2,3)
+
z=1
+
z=2
+
z=3
+ + +
5. ZERO-SIZE / HIDDEN
+
+
none
+
hidden
+
opacity:0
+ + +
6. FLEXBOX GAP (gap:20px)
+
+
1
+
2
+
3
+
+ + +
7. BORDER (12px) + PADDING (20px) border-box
+
border-box
+ + +
8. EDGE ALIGNMENT
+
ref
+
L
+
R
+
T
+
B
+ + +
9. DIAGONAL SEPARATION
+
TL
+
BR
+ + +
10. ADJACENT pair (edge touch)
+
A
+
B
+
CENTERED child
+
+
+
+ + diff --git a/packages/imhotep-playwright/src/extraction.ts b/packages/imhotep-playwright/src/extraction.ts index 17a200e..67d6301 100644 --- a/packages/imhotep-playwright/src/extraction.ts +++ b/packages/imhotep-playwright/src/extraction.ts @@ -744,7 +744,28 @@ function remapTopologyIds(world: GeometryWorld): void { backendToSolver.set(world.subjects.domNodeId[i], world.subjects.ids[i]) } - const remap = (src: ArrayLike): number[] => { + const rawSubjectIds = (world as any)._topologySubjectIds as number[] | undefined + + function reorderAndRemap(rawValues: ArrayLike, targetLength: number): number[] { + if (!rawSubjectIds || rawSubjectIds.length === 0) { + return remapSimple(rawValues) + } + + const rawToValue = new Map() + for (let j = 0; j < rawSubjectIds.length; j++) { + rawToValue.set(rawSubjectIds[j], rawValues[j] ?? 0) + } + + const out = new Array(targetLength) + for (let i = 0; i < targetLength; i++) { + const backendId = world.subjects.domNodeId[i] + const rawVal = rawToValue.get(backendId) ?? 0 + out[i] = rawVal > 0 ? (backendToSolver.get(rawVal) ?? 0) : 0 + } + return out + } + + function remapSimple(src: ArrayLike): number[] { const out = new Array(src.length) for (let i = 0; i < src.length; i++) { const backendId = src[i] @@ -754,19 +775,21 @@ function remapTopologyIds(world: GeometryWorld): void { } const t = world.topology - t.containingBlockOf = remap(t.containingBlockOf) - t.nearestPositionedAncestorOf = remap(t.nearestPositionedAncestorOf) - t.scrollContainerOf = remap(t.scrollContainerOf) - t.stackingContextOf = remap(t.stackingContextOf) - t.formattingContextOf = remap(t.formattingContextOf) - t.clippingRootOf = remap(t.clippingRootOf) + t.containingBlockOf = reorderAndRemap(t.containingBlockOf, nSubjects) + t.nearestPositionedAncestorOf = reorderAndRemap(t.nearestPositionedAncestorOf, nSubjects) + t.scrollContainerOf = reorderAndRemap(t.scrollContainerOf, nSubjects) + t.stackingContextOf = reorderAndRemap(t.stackingContextOf, nSubjects) + t.formattingContextOf = reorderAndRemap(t.formattingContextOf, nSubjects) + t.clippingRootOf = reorderAndRemap(t.clippingRootOf, nSubjects) if (world.clipping) { - world.clipping.subjectId = remap(world.clipping.subjectId) + world.clipping.subjectId = remapSimple(world.clipping.subjectId) } if (world.scroll) { - world.scroll.containerId = remap(world.scroll.containerId) + world.scroll.containerId = remapSimple(world.scroll.containerId) } + + delete (world as any)._topologySubjectIds } export async function extractWorldCdp( @@ -865,6 +888,8 @@ export async function extractWorldCdp( fontWeight: Array.from(canonical.styles.fontWeight), } + ;(world as any)._topologySubjectIds = (canonical as any).topology?.subjectIds + remapTopologyIds(world) const selectorToIds = new Map() @@ -1166,6 +1191,7 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto const unaryPredicate = clause.relation === 'atLeast' || clause.relation === 'atMost' || clause.relation === 'aspectRatio' + || (clause.relation === 'inStackingContext' && !clause.reference) body = { type: 'FormulaNode', @@ -1213,6 +1239,7 @@ export function compileCanonicalClauseToFormula(clause: CanonicalClauseDescripto || clause.relation === 'atMost' || clause.relation === 'between' || clause.relation === 'aspectRatio' + || (clause.relation === 'inStackingContext' && !clause.reference) if (isUnary) { return {