@@ -29,7 +29,12 @@ import type {
2929 FlowMode ,
3030 NormalizedColumnLayout ,
3131} from '@superdoc/contracts' ;
32- import { buildLayoutSourceIdentityForFragment , normalizeColumnLayout , getFragmentZIndex , resolveAnchoredGraphicY } from '@superdoc/contracts' ;
32+ import {
33+ buildLayoutSourceIdentityForFragment ,
34+ normalizeColumnLayout ,
35+ getFragmentZIndex ,
36+ resolveAnchoredGraphicY ,
37+ } from '@superdoc/contracts' ;
3338import { createFloatingObjectManager , computeAnchorX } from './floating-objects.js' ;
3439import { computeNextSectionPropsAtBreak } from './section-props' ;
3540import {
@@ -2544,10 +2549,6 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
25442549 }
25452550 }
25462551
2547- // Paragraph start Y (OOXML: anchor for vertAnchor="text"). Captured before layout so
2548- // paragraph-anchored tables use it as base; offsetV (tblpY) positions below start to avoid overlap.
2549- const paragraphStartY = paginator . ensurePage ( ) . cursorY ;
2550-
25512552 layoutParagraphBlock (
25522553 {
25532554 block,
@@ -2587,16 +2588,26 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
25872588 if ( tablesForPara ) {
25882589 const state = paginator . ensurePage ( ) ;
25892590 const columnWidthForTable = getCurrentColumnWidth ( ) ;
2591+
2592+ // Paragraph top after layout (first fragment on this page). Pre-layout cursorY can still
2593+ // sit on the previous page when the anchor paragraph breaks across pages.
2594+ let anchorParagraphTopY = state . cursorY ;
2595+ for ( const fragment of state . page . fragments ) {
2596+ if ( fragment . kind === 'para' && fragment . blockId === block . id ) {
2597+ anchorParagraphTopY = Math . min ( anchorParagraphTopY , fragment . y ) ;
2598+ }
2599+ }
2600+
25902601 let tableBottomY = state . cursorY ;
2602+ let nextStackY = state . cursorY ;
25912603 for ( const { block : tableBlock , measure : tableMeasure } of tablesForPara ) {
25922604 if ( placedAnchoredTableIds . has ( tableBlock . id ) ) continue ;
25932605 const totalWidth = tableMeasure . totalWidth ?? 0 ;
25942606 if ( columnWidthForTable > 0 && totalWidth >= columnWidthForTable * ANCHORED_TABLE_FULL_WIDTH_RATIO ) continue ;
25952607
2596- // OOXML anchor base is paragraph-relative. Clamp to paragraph bottom so the table never overlaps
2597- // paragraph text, then apply offsetV from that resolved anchor position.
2608+ // OOXML anchor base is paragraph-relative. Clamp below laid-out paragraph text, then offsetV.
25982609 const offsetV = tableBlock . anchor ?. offsetV ?? 0 ;
2599- const anchorBaseY = Math . max ( paragraphStartY , state . cursorY ) ;
2610+ const anchorBaseY = Math . max ( anchorParagraphTopY , nextStackY ) ;
26002611 const anchorY = anchorBaseY + offsetV ;
26012612 floatManager . registerTable ( tableBlock , tableMeasure , anchorY , state . columnIndex , state . page . number ) ;
26022613
@@ -2611,7 +2622,9 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
26112622 const wrapType = tableBlock . wrap ?. type ?? 'None' ;
26122623 if ( wrapType !== 'None' ) {
26132624 const bottom = anchorY + ( tableMeasure . totalHeight ?? 0 ) ;
2625+ const distBottom = tableBlock . wrap ?. distBottom ?? 0 ;
26142626 if ( bottom > tableBottomY ) tableBottomY = bottom ;
2627+ nextStackY = bottom + distBottom ;
26152628 }
26162629 }
26172630 state . cursorY = tableBottomY ;
0 commit comments