Skip to content

Commit dc204bf

Browse files
committed
fix(layout): reserve section-aware header/footer height before body layout
1 parent 1bc65ac commit dc204bf

7 files changed

Lines changed: 446 additions & 289 deletions

File tree

packages/layout-engine/layout-bridge/src/incrementalLayout.ts

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import { remeasureParagraph } from './remeasure';
2525
import { computeDirtyRegions } from './diff';
2626
import { MeasureCache } from './cache';
2727
import { layoutHeaderFooterWithCache, HeaderFooterLayoutCache, type HeaderFooterBatch } from './layoutHeaderFooter';
28+
import {
29+
buildSectionAwareHeaderFooterLayoutKey,
30+
buildSectionAwareHeaderFooterMeasurementGroups,
31+
} from './sectionAwareHeaderFooter';
2832
import { FeatureFlags } from './featureFlags';
2933
import { PageTokenLogger, HeaderFooterCacheLogger, globalMetrics } from './instrumentation';
3034
import { HeaderFooterCacheState, invalidateHeaderFooterCache } from './cacheInvalidation';
@@ -886,10 +890,83 @@ export async function incrementalLayout(
886890
* Values are the actual content heights in pixels.
887891
*/
888892
let headerContentHeightsByRId: Map<string, number> | undefined;
893+
let headerContentHeightsBySectionRef: Map<string, number> | undefined;
889894

890895
// Check if we have headers via either headerBlocks (by variant) or headerBlocksByRId (by relationship ID)
891896
const hasHeaderBlocks = headerFooter?.headerBlocks && Object.keys(headerFooter.headerBlocks).length > 0;
892897
const hasHeaderBlocksByRId = headerFooter?.headerBlocksByRId && headerFooter.headerBlocksByRId.size > 0;
898+
const sectionMetadata = options.sectionMetadata ?? [];
899+
900+
const measureHeightsByReference = async (
901+
kind: 'header' | 'footer',
902+
blocksByRId: Map<string, FlowBlock[]> | undefined,
903+
constraints: HeaderFooterConstraints,
904+
measureFn: HeaderFooterMeasureFn,
905+
): Promise<{
906+
heightsByRId?: Map<string, number>;
907+
heightsBySectionRef?: Map<string, number>;
908+
}> => {
909+
if (!blocksByRId || blocksByRId.size === 0) {
910+
return {};
911+
}
912+
913+
const heightsByRId = new Map<string, number>();
914+
const heightsBySectionRef = new Map<string, number>();
915+
const sectionAwareGroups = buildSectionAwareHeaderFooterMeasurementGroups(
916+
kind,
917+
blocksByRId,
918+
sectionMetadata,
919+
constraints,
920+
);
921+
922+
if (sectionAwareGroups.length > 0) {
923+
for (const group of sectionAwareGroups) {
924+
const blocks = blocksByRId.get(group.rId);
925+
if (!blocks || blocks.length === 0) continue;
926+
927+
const measureConstraints = {
928+
maxWidth: group.sectionConstraints.width,
929+
maxHeight: group.sectionConstraints.height,
930+
};
931+
const measures = await Promise.all(blocks.map((block) => measureFn(block, measureConstraints)));
932+
const layout = layoutHeaderFooter(blocks, measures, group.sectionConstraints, kind);
933+
if (!(layout.height > 0)) continue;
934+
935+
const nextHeight = Math.max(0, layout.height);
936+
const currentHeight = heightsByRId.get(group.rId) ?? 0;
937+
if (nextHeight > currentHeight) {
938+
heightsByRId.set(group.rId, nextHeight);
939+
}
940+
941+
for (const sectionIndex of group.sectionIndices) {
942+
heightsBySectionRef.set(buildSectionAwareHeaderFooterLayoutKey(group.rId, sectionIndex), nextHeight);
943+
}
944+
}
945+
946+
return {
947+
heightsByRId: heightsByRId.size > 0 ? heightsByRId : undefined,
948+
heightsBySectionRef: heightsBySectionRef.size > 0 ? heightsBySectionRef : undefined,
949+
};
950+
}
951+
952+
for (const [rId, blocks] of blocksByRId) {
953+
if (!blocks || blocks.length === 0) continue;
954+
955+
const measureConstraints = {
956+
maxWidth: constraints.width,
957+
maxHeight: constraints.height,
958+
};
959+
const measures = await Promise.all(blocks.map((block) => measureFn(block, measureConstraints)));
960+
const layout = layoutHeaderFooter(blocks, measures, constraints, kind);
961+
if (layout.height > 0) {
962+
heightsByRId.set(rId, layout.height);
963+
}
964+
}
965+
966+
return {
967+
heightsByRId: heightsByRId.size > 0 ? heightsByRId : undefined,
968+
};
969+
};
893970

894971
if (headerFooter?.constraints && (hasHeaderBlocks || hasHeaderBlocksByRId)) {
895972
const hfPreStart = performance.now();
@@ -953,22 +1030,14 @@ export async function incrementalLayout(
9531030
// Also extract heights from headerBlocksByRId (for multi-section documents)
9541031
// Store each rId's height separately for per-page margin calculation
9551032
if (hasHeaderBlocksByRId && headerFooter.headerBlocksByRId) {
956-
headerContentHeightsByRId = new Map<string, number>();
957-
for (const [rId, blocks] of headerFooter.headerBlocksByRId) {
958-
if (!blocks || blocks.length === 0) continue;
959-
// Measure blocks to get height
960-
const measureConstraints = {
961-
maxWidth: headerFooter.constraints.width,
962-
maxHeight: headerFooter.constraints.height,
963-
};
964-
const measures = await Promise.all(blocks.map((block) => measureFn(block, measureConstraints)));
965-
// Layout to get actual height — pass full constraints for page-relative normalization
966-
const layout = layoutHeaderFooter(blocks, measures, headerFooter.constraints, 'header');
967-
if (layout.height > 0) {
968-
// Store height by rId for per-page margin calculation
969-
headerContentHeightsByRId.set(rId, layout.height);
970-
}
971-
}
1033+
const measuredHeights = await measureHeightsByReference(
1034+
'header',
1035+
headerFooter.headerBlocksByRId,
1036+
headerFooter.constraints,
1037+
measureFn,
1038+
);
1039+
headerContentHeightsByRId = measuredHeights.heightsByRId;
1040+
headerContentHeightsBySectionRef = measuredHeights.heightsBySectionRef;
9721041
}
9731042

9741043
const hfPreEnd = performance.now();
@@ -993,6 +1062,7 @@ export async function incrementalLayout(
9931062
* Values are the actual content heights in pixels.
9941063
*/
9951064
let footerContentHeightsByRId: Map<string, number> | undefined;
1065+
let footerContentHeightsBySectionRef: Map<string, number> | undefined;
9961066

9971067
// Check if we have footers via either footerBlocks (by variant) or footerBlocksByRId (by relationship ID)
9981068
const hasFooterBlocks = headerFooter?.footerBlocks && Object.keys(headerFooter.footerBlocks).length > 0;
@@ -1064,22 +1134,14 @@ export async function incrementalLayout(
10641134
// Also extract heights from footerBlocksByRId (for multi-section documents)
10651135
// Store each rId's height separately for per-page margin calculation
10661136
if (hasFooterBlocksByRId && headerFooter.footerBlocksByRId) {
1067-
footerContentHeightsByRId = new Map<string, number>();
1068-
for (const [rId, blocks] of headerFooter.footerBlocksByRId) {
1069-
if (!blocks || blocks.length === 0) continue;
1070-
// Measure blocks to get height
1071-
const measureConstraints = {
1072-
maxWidth: headerFooter.constraints.width,
1073-
maxHeight: headerFooter.constraints.height,
1074-
};
1075-
const measures = await Promise.all(blocks.map((block) => measureFn(block, measureConstraints)));
1076-
// Layout to get actual height — pass full constraints for page-relative normalization
1077-
const layout = layoutHeaderFooter(blocks, measures, headerFooter.constraints, 'footer');
1078-
if (layout.height > 0) {
1079-
// Store height by rId for per-page margin calculation
1080-
footerContentHeightsByRId.set(rId, layout.height);
1081-
}
1082-
}
1137+
const measuredHeights = await measureHeightsByReference(
1138+
'footer',
1139+
headerFooter.footerBlocksByRId,
1140+
headerFooter.constraints,
1141+
measureFn,
1142+
);
1143+
footerContentHeightsByRId = measuredHeights.heightsByRId;
1144+
footerContentHeightsBySectionRef = measuredHeights.heightsBySectionRef;
10831145
}
10841146
} catch (error) {
10851147
console.error('[Layout] Footer pre-layout failed:', error);
@@ -1095,7 +1157,9 @@ export async function incrementalLayout(
10951157
...options,
10961158
headerContentHeights, // Pass header heights to prevent overlap (per-variant)
10971159
footerContentHeights, // Pass footer heights to prevent overlap (per-variant)
1160+
headerContentHeightsBySectionRef, // Pass header heights by rId+section for exact page-specific margin calculation
10981161
headerContentHeightsByRId, // Pass header heights by rId for per-page margin calculation
1162+
footerContentHeightsBySectionRef, // Pass footer heights by rId+section for exact page-specific margin calculation
10991163
footerContentHeightsByRId, // Pass footer heights by rId for per-page margin calculation
11001164
remeasureParagraph: (block: FlowBlock, maxWidth: number, firstLineIndent?: number) =>
11011165
remeasureParagraph(block as ParagraphBlock, maxWidth, firstLineIndent),
@@ -1179,7 +1243,9 @@ export async function incrementalLayout(
11791243
...options,
11801244
headerContentHeights, // Pass header heights to prevent overlap (per-variant)
11811245
footerContentHeights, // Pass footer heights to prevent overlap (per-variant)
1246+
headerContentHeightsBySectionRef, // Pass header heights by rId+section for exact page-specific margin calculation
11821247
headerContentHeightsByRId, // Pass header heights by rId for per-page margin calculation
1248+
footerContentHeightsBySectionRef, // Pass footer heights by rId+section for exact page-specific margin calculation
11831249
footerContentHeightsByRId, // Pass footer heights by rId for per-page margin calculation
11841250
remeasureParagraph: (block: FlowBlock, maxWidth: number, firstLineIndent?: number) =>
11851251
remeasureParagraph(block as ParagraphBlock, maxWidth, firstLineIndent),
@@ -1771,6 +1837,10 @@ export async function incrementalLayout(
17711837
footnoteReservedByPageIndex,
17721838
headerContentHeights,
17731839
footerContentHeights,
1840+
headerContentHeightsBySectionRef,
1841+
headerContentHeightsByRId,
1842+
footerContentHeightsBySectionRef,
1843+
footerContentHeightsByRId,
17741844
remeasureParagraph: (block: FlowBlock, maxWidth: number, firstLineIndent?: number) =>
17751845
remeasureParagraph(block as ParagraphBlock, maxWidth, firstLineIndent),
17761846
});

packages/layout-engine/layout-bridge/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ export {
5656
export type { HeaderFooterBatch, DigitBucket } from './layoutHeaderFooter';
5757
export { findWordBoundaries, findParagraphBoundaries } from './text-boundaries';
5858
export type { BoundaryRange } from './text-boundaries';
59+
export {
60+
buildSectionAwareHeaderFooterLayoutKey,
61+
buildSectionContentWidth,
62+
buildEffectiveHeaderFooterRefsBySection,
63+
collectReferencedHeaderFooterRIds,
64+
buildSectionAwareHeaderFooterMeasurementGroups,
65+
} from './sectionAwareHeaderFooter';
66+
export type {
67+
HeaderFooterSectionKind,
68+
HeaderFooterRefs,
69+
SectionAwareHeaderFooterMeasurementGroup,
70+
} from './sectionAwareHeaderFooter';
5971
export { incrementalLayout, measureCache, normalizeMargin } from './incrementalLayout';
6072
export type { HeaderFooterLayoutResult, IncrementalLayoutResult } from './incrementalLayout';
6173
// Re-export computeDisplayPageNumber from layout-engine for section-aware page numbering

0 commit comments

Comments
 (0)