diff --git a/packages/layout-engine/painters/dom/src/index.test.ts b/packages/layout-engine/painters/dom/src/index.test.ts index 3a1e8f57bf..35f24eb71d 100644 --- a/packages/layout-engine/painters/dom/src/index.test.ts +++ b/packages/layout-engine/painters/dom/src/index.test.ts @@ -3029,7 +3029,7 @@ describe('DomPainter', () => { expect(appliedWordSpacing).toBeCloseTo(expectedWordSpacing, 5); }); - it('reuses fragment DOM nodes when layout geometry changes', () => { + it('rebuilds fragment DOM nodes when layout geometry changes to keep line epochs in sync', () => { const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); @@ -3051,9 +3051,12 @@ describe('DomPainter', () => { painter.paint(movedLayout, mount); const fragmentAfter = mount.querySelector('.superdoc-fragment') as HTMLElement; + const lineAfter = fragmentAfter.querySelector('.superdoc-line') as HTMLElement; - expect(fragmentAfter).toBe(fragmentBefore); + expect(fragmentAfter).not.toBe(fragmentBefore); expect(fragmentAfter.style.left).toBe('60px'); + expect(fragmentAfter.dataset.layoutEpoch).toBeTruthy(); + expect(lineAfter.dataset.layoutEpoch).toBe(fragmentAfter.dataset.layoutEpoch); }); it('rebuilds fragment DOM when block content changes via setData', () => { @@ -5016,10 +5019,13 @@ describe('DomPainter', () => { painter.paint(updatedLayout, mount); const updatedWrapper = mount.querySelector('.superdoc-fragment-list-item') as HTMLElement; - expect(updatedWrapper).toBe(initialWrapper); + const updatedLine = updatedWrapper.querySelector('.superdoc-line') as HTMLElement; + expect(updatedWrapper).not.toBe(initialWrapper); expect(updatedWrapper.style.left).toBe('90px'); expect(updatedWrapper.style.top).toBe('55px'); expect(updatedWrapper.style.width).toBe('310px'); + expect(updatedWrapper.dataset.layoutEpoch).toBeTruthy(); + expect(updatedLine.dataset.layoutEpoch).toBe(updatedWrapper.dataset.layoutEpoch); }); it('applies resolved zIndex only to anchored media fragments', () => { diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 350110f057..f3d2acf7a8 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -2714,6 +2714,7 @@ export class DomPainter { if (current) { existing.delete(key); + const geometryChanged = hasFragmentGeometryChanged(current.fragment, fragment); const sdtBoundaryMismatch = shouldRebuildForSdtBoundary(current.element, sdtBoundary); // Detect mismatch in any between-border property const betweenBorderMismatch = @@ -2730,6 +2731,7 @@ export class DomPainter { current.element.dataset.pmStart != null && this.currentMapping.map(Number(current.element.dataset.pmStart)) !== newPmStart; const needsRebuild = + geometryChanged || this.changedBlocks.has(fragment.blockId) || current.signature !== fragmentSignature(fragment, this.blockLookup) || sdtBoundaryMismatch || @@ -7288,6 +7290,16 @@ const fragmentSignature = (fragment: Fragment, lookup: BlockLookup): string => { return base; }; +const hasFragmentGeometryChanged = (previous: Fragment, next: Fragment): boolean => + previous.x !== next.x || + previous.y !== next.y || + previous.width !== next.width || + ('height' in previous && + 'height' in next && + typeof previous.height === 'number' && + typeof next.height === 'number' && + previous.height !== next.height); + const getSdtMetadataId = (metadata: SdtMetadata | null | undefined): string => { if (!metadata) return ''; if ('id' in metadata && metadata.id != null) {