@@ -35,10 +35,10 @@ import {
3535 buildLayoutSourceIdentityForFragment ,
3636 normalizeColumnLayout ,
3737 getFragmentZIndex ,
38+ resolveAnchoredGraphicY ,
3839 resolveEffectiveHeaderFooterRef ,
3940 selectHeaderFooterVariantForPage ,
4041} from '@superdoc/contracts' ;
41- import { buildLayoutSourceIdentityForFragment , normalizeColumnLayout , getFragmentZIndex , resolveAnchoredGraphicY } from '@superdoc/contracts' ;
4242import { createFloatingObjectManager , computeAnchorX } from './floating-objects.js' ;
4343import { computeNextSectionPropsAtBreak } from './section-props' ;
4444import {
@@ -2526,10 +2526,6 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
25262526 }
25272527 }
25282528
2529- // Paragraph start Y (OOXML: anchor for vertAnchor="text"). Captured before layout so
2530- // paragraph-anchored tables use it as base; offsetV (tblpY) positions below start to avoid overlap.
2531- const paragraphStartY = paginator . ensurePage ( ) . cursorY ;
2532-
25332529 layoutParagraphBlock (
25342530 {
25352531 block,
@@ -2569,16 +2565,26 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
25692565 if ( tablesForPara ) {
25702566 const state = paginator . ensurePage ( ) ;
25712567 const columnWidthForTable = getCurrentColumnWidth ( ) ;
2568+
2569+ // Paragraph top after layout (first fragment on this page). Pre-layout cursorY can still
2570+ // sit on the previous page when the anchor paragraph breaks across pages.
2571+ let anchorParagraphTopY = state . cursorY ;
2572+ for ( const fragment of state . page . fragments ) {
2573+ if ( fragment . kind === 'para' && fragment . blockId === block . id ) {
2574+ anchorParagraphTopY = Math . min ( anchorParagraphTopY , fragment . y ) ;
2575+ }
2576+ }
2577+
25722578 let tableBottomY = state . cursorY ;
2579+ let nextStackY = state . cursorY ;
25732580 for ( const { block : tableBlock , measure : tableMeasure } of tablesForPara ) {
25742581 if ( placedAnchoredTableIds . has ( tableBlock . id ) ) continue ;
25752582 const totalWidth = tableMeasure . totalWidth ?? 0 ;
25762583 if ( columnWidthForTable > 0 && totalWidth >= columnWidthForTable * ANCHORED_TABLE_FULL_WIDTH_RATIO ) continue ;
25772584
2578- // OOXML anchor base is paragraph-relative. Clamp to paragraph bottom so the table never overlaps
2579- // paragraph text, then apply offsetV from that resolved anchor position.
2585+ // OOXML anchor base is paragraph-relative. Clamp below laid-out paragraph text, then offsetV.
25802586 const offsetV = tableBlock . anchor ?. offsetV ?? 0 ;
2581- const anchorBaseY = Math . max ( paragraphStartY , state . cursorY ) ;
2587+ const anchorBaseY = Math . max ( anchorParagraphTopY , nextStackY ) ;
25822588 const anchorY = anchorBaseY + offsetV ;
25832589 floatManager . registerTable ( tableBlock , tableMeasure , anchorY , state . columnIndex , state . page . number ) ;
25842590
@@ -2593,7 +2599,9 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
25932599 const wrapType = tableBlock . wrap ?. type ?? 'None' ;
25942600 if ( wrapType !== 'None' ) {
25952601 const bottom = anchorY + ( tableMeasure . totalHeight ?? 0 ) ;
2602+ const distBottom = tableBlock . wrap ?. distBottom ?? 0 ;
25962603 if ( bottom > tableBottomY ) tableBottomY = bottom ;
2604+ nextStackY = bottom + distBottom ;
25972605 }
25982606 }
25992607 state . cursorY = tableBottomY ;
0 commit comments