Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
352 changes: 352 additions & 0 deletions docs/superdoc-feature-reports/sd-2656-implementation-report.md

Large diffs are not rendered by default.

558 changes: 558 additions & 0 deletions docs/superdoc-feature-reports/sd-2656-plan.md

Large diffs are not rendered by default.

60 changes: 54 additions & 6 deletions packages/layout-engine/layout-bridge/src/incrementalLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1815,10 +1815,47 @@ export async function incrementalLayout(
return { columns, idsByColumn };
};

// SD-3049: per-footnote total body height; accounting mirrors `computeFootnoteLayoutPlan`.
let bodyHeightById = new Map<string, number>();
const refreshBodyHeights = (measures: Map<string, Measure>) => {
const map = new Map<string, number>();
footnotesInput.blocksById.forEach((blocks, footnoteId) => {
let total = 0;
for (const block of blocks) {
const measure = measures.get(block.id);
if (!measure) continue;
if (measure.kind === 'paragraph') {
const measureH = (measure as { totalHeight?: number }).totalHeight;
if (typeof measureH === 'number' && Number.isFinite(measureH)) total += measureH;
const spacing = (block as { attrs?: { spacing?: { after?: number; lineSpaceAfter?: number } } }).attrs
?.spacing;
const after = spacing?.after ?? spacing?.lineSpaceAfter;
if (typeof after === 'number' && Number.isFinite(after) && after > 0) total += after;
} else if (measure.kind === 'image' || measure.kind === 'drawing') {
const measureH = (measure as { height?: number }).height;
if (typeof measureH === 'number' && Number.isFinite(measureH)) total += measureH;
} else if (measure.kind === 'table') {
const measureH = (measure as { totalHeight?: number }).totalHeight;
if (typeof measureH === 'number' && Number.isFinite(measureH)) total += measureH;
} else if (measure.kind === 'list' && block.kind === 'list') {
for (const item of block.items) {
const itemMeasure = measure.items.find((entry) => entry.itemId === item.id);
if (!itemMeasure?.paragraph?.lines) continue;
for (const line of itemMeasure.paragraph.lines) total += line.lineHeight ?? 0;
total += getParagraphSpacingAfter(item.paragraph);
}
}
}
if (total > 0) map.set(footnoteId, total);
});
bodyHeightById = map;
};

const relayout = (footnoteReservedByPageIndex: number[]) =>
layoutDocument(currentBlocks, currentMeasures, {
...options,
footnoteReservedByPageIndex,
footnotes: { ...footnotesInput, bodyHeightById },
headerContentHeights,
footerContentHeights,
headerContentHeightsBySectionRef,
Expand All @@ -1829,9 +1866,17 @@ export async function incrementalLayout(
remeasureParagraph(block as ParagraphBlock, maxWidth, firstLineIndent),
});

// Pass 1: assign + reserve from current layout.
// SD-3049: every reachable footnote id, computed once. Used to keep
// `bodyHeightById` complete across convergence iterations even when refs
// migrate between pages — the assigned-by-column subset can drop ids
// mid-loop, which would zero their entries and cause oscillation.
const allFootnoteIds = new Set(footnotesInput.refs.map((ref) => ref.id));

// Pass 1: assign + reserve from current layout. Pre-measure ALL footnote
// bodies (the cache makes the assigned-only subset essentially free).
let { columns: pageColumns, idsByColumn } = resolveFootnoteAssignments(layout);
let { measuresById } = await measureFootnoteBlocks(collectFootnoteIdsByColumn(idsByColumn));
let { measuresById } = await measureFootnoteBlocks(allFootnoteIds);
refreshBodyHeights(measuresById);
let plan = computeFootnoteLayoutPlan(layout, idsByColumn, measuresById, [], pageColumns);
let reserves = plan.reserves;

Expand All @@ -1843,7 +1888,11 @@ export async function incrementalLayout(
for (let pass = 0; pass < MAX_FOOTNOTE_LAYOUT_PASSES; pass += 1) {
layout = relayout(reserves);
({ columns: pageColumns, idsByColumn } = resolveFootnoteAssignments(layout));
({ measuresById } = await measureFootnoteBlocks(collectFootnoteIdsByColumn(idsByColumn)));
// SD-3049: measure the full set each iteration so `bodyHeightById`
// stays complete; refs migrating between pages must not drop their
// measured demand from the per-block lookup.
({ measuresById } = await measureFootnoteBlocks(allFootnoteIds));
refreshBodyHeights(measuresById);
plan = computeFootnoteLayoutPlan(layout, idsByColumn, measuresById, reserves, pageColumns);
const nextReserves = plan.reserves;
const reservesStable =
Expand Down Expand Up @@ -1899,9 +1948,8 @@ export async function incrementalLayout(
layout = relayout(target);
reservesAppliedToLayout = target;
({ columns: finalPageColumns, idsByColumn: finalIdsByColumn } = resolveFootnoteAssignments(layout));
({ blocks: finalBlocks, measuresById: finalMeasuresById } = await measureFootnoteBlocks(
collectFootnoteIdsByColumn(finalIdsByColumn),
));
({ blocks: finalBlocks, measuresById: finalMeasuresById } = await measureFootnoteBlocks(allFootnoteIds));
refreshBodyHeights(finalMeasuresById);
finalPlan = computeFootnoteLayoutPlan(
layout,
finalIdsByColumn,
Expand Down
Loading
Loading