Skip to content

Commit 9c6ccb0

Browse files
authored
[10/16] refactor(painter): deliver resolved items to decoration provider callback (#2827)
* refactor(painter): deliver resolved items to decoration provider callback * fix: normalize header/footer resolved item coordinates before paint
1 parent 5f95114 commit 9c6ccb0

16 files changed

Lines changed: 383 additions & 22 deletions

packages/layout-engine/painters/dom/src/renderer.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ type OptionalBlockMeasurePair = {
275275

276276
type PageDecorationPayload = {
277277
fragments: Fragment[];
278+
/** Resolved items aligned 1:1 with `fragments`. Same length, same order.
279+
* Absent when provider has no resolved data (painter falls back to blockLookup). */
280+
items?: ResolvedPaintItem[];
278281
height: number;
279282
/** Optional measured content height to aid bottom alignment in footers. */
280283
contentHeight?: number;
@@ -2451,16 +2454,13 @@ export class DomPainter {
24512454
* Used to determine special Y positioning for page-relative anchored media
24522455
* in header/footer decoration sections.
24532456
*/
2454-
private isPageRelativeAnchoredFragment(fragment: Fragment): boolean {
2457+
private isPageRelativeAnchoredFragment(fragment: Fragment, resolvedItem?: ResolvedPaintItem): boolean {
24552458
if (fragment.kind !== 'image' && fragment.kind !== 'drawing') {
24562459
return false;
24572460
}
2458-
const lookup = this.blockLookup.get(fragment.blockId);
2459-
if (!lookup) {
2460-
return false;
2461-
}
2462-
const block = lookup.block;
2463-
if (block.kind !== 'image' && block.kind !== 'drawing') {
2461+
const resolvedBlock = resolvedItem && 'block' in resolvedItem ? resolvedItem.block : undefined;
2462+
const block = resolvedBlock ?? this.blockLookup.get(fragment.blockId)?.block;
2463+
if (!block || (block.kind !== 'image' && block.kind !== 'drawing')) {
24642464
return false;
24652465
}
24662466
return block.anchor?.vRelativeFrom === 'page';
@@ -2580,9 +2580,10 @@ export class DomPainter {
25802580
const contentHeight =
25812581
typeof data.contentHeight === 'number'
25822582
? data.contentHeight
2583-
: data.fragments.reduce((max, f) => {
2583+
: data.fragments.reduce((max, f, fi) => {
2584+
const resolvedItem = data.items?.[fi];
25842585
const fragHeight =
2585-
'height' in f && typeof f.height === 'number' ? f.height : this.estimateFragmentHeight(f);
2586+
'height' in f && typeof f.height === 'number' ? f.height : this.estimateFragmentHeight(f, resolvedItem);
25862587
return Math.max(max, f.y + Math.max(0, fragHeight));
25872588
}, 0);
25882589
// Offset to push content to bottom of container
@@ -2599,7 +2600,7 @@ export class DomPainter {
25992600
};
26002601

26012602
// Compute between-border flags for header/footer paragraph fragments
2602-
const betweenBorderFlags = computeBetweenBorderFlags(data.fragments, this.blockLookup);
2603+
const betweenBorderFlags = computeBetweenBorderFlags(data.fragments, this.blockLookup, data.items);
26032604

26042605
// Separate behindDoc fragments from normal fragments.
26052606
// Prefer explicit fragment.behindDoc when present. Keep zIndex===0 as a
@@ -2636,8 +2637,15 @@ export class DomPainter {
26362637
// By inserting at the beginning and using z-index: 0, they render below body content
26372638
// which also has z-index values but comes later in DOM order.
26382639
behindDocFragments.forEach(({ fragment, originalIndex }) => {
2639-
const fragEl = this.renderFragment(fragment, context, undefined, betweenBorderFlags.get(originalIndex));
2640-
const isPageRelative = this.isPageRelativeAnchoredFragment(fragment);
2640+
const resolvedItem = data.items?.[originalIndex];
2641+
const fragEl = this.renderFragment(
2642+
fragment,
2643+
context,
2644+
undefined,
2645+
betweenBorderFlags.get(originalIndex),
2646+
resolvedItem,
2647+
);
2648+
const isPageRelative = this.isPageRelativeAnchoredFragment(fragment, resolvedItem);
26412649

26422650
let pageY: number;
26432651
if (isPageRelative && kind === 'footer') {
@@ -2660,8 +2668,15 @@ export class DomPainter {
26602668

26612669
// Render normal fragments in the header/footer container
26622670
normalFragments.forEach(({ fragment, originalIndex }) => {
2663-
const fragEl = this.renderFragment(fragment, context, undefined, betweenBorderFlags.get(originalIndex));
2664-
const isPageRelative = this.isPageRelativeAnchoredFragment(fragment);
2671+
const resolvedItem = data.items?.[originalIndex];
2672+
const fragEl = this.renderFragment(
2673+
fragment,
2674+
context,
2675+
undefined,
2676+
betweenBorderFlags.get(originalIndex),
2677+
resolvedItem,
2678+
);
2679+
const isPageRelative = this.isPageRelativeAnchoredFragment(fragment, resolvedItem);
26652680

26662681
if (isPageRelative && kind === 'footer') {
26672682
// Footer page-relative: fragment.y is normalized to band-local coords
@@ -7043,7 +7058,10 @@ export class DomPainter {
70437058
* @param fragment - The fragment to estimate height for
70447059
* @returns Estimated height in pixels, or 0 if height cannot be determined
70457060
*/
7046-
private estimateFragmentHeight(fragment: Fragment): number {
7061+
private estimateFragmentHeight(fragment: Fragment, resolvedItem?: ResolvedPaintItem): number {
7062+
if (resolvedItem && 'height' in resolvedItem && typeof resolvedItem.height === 'number') {
7063+
return resolvedItem.height;
7064+
}
70477065
const lookup = this.blockLookup.get(fragment.blockId);
70487066
const measure = lookup?.measure;
70497067

packages/super-editor/src/editors/v1/core/presentation-editor/header-footer/HeaderFooterSessionManager.ts

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@
1111
* @module presentation-editor/header-footer/HeaderFooterSessionManager
1212
*/
1313

14-
import type { Layout, FlowBlock, Measure, Page, SectionMetadata, Fragment } from '@superdoc/contracts';
14+
import type {
15+
Layout,
16+
FlowBlock,
17+
Measure,
18+
Page,
19+
SectionMetadata,
20+
Fragment,
21+
ResolvedHeaderFooterLayout,
22+
ResolvedPaintItem,
23+
} from '@superdoc/contracts';
1524
import type { PageDecorationProvider } from '@superdoc/painter-dom';
25+
import { resolveHeaderFooterLayout } from '@superdoc/layout-resolved';
1626
import type { HeaderFooterPartStoryLocator } from '@superdoc/document-api';
1727
import { DOM_CLASS_NAMES } from '@superdoc/dom-contract';
1828

@@ -339,6 +349,55 @@ type HeaderFooterActivationOptions = {
339349
initialSelection?: 'end' | 'defer';
340350
};
341351

352+
// =============================================================================
353+
// Helpers
354+
// =============================================================================
355+
356+
/**
357+
* Resolve a `HeaderFooterLayoutResult` into a `ResolvedHeaderFooterLayout`.
358+
* Paired with the originals so the decoration provider can deliver aligned
359+
* `items` alongside `fragments`.
360+
*/
361+
function resolveResult(result: HeaderFooterLayoutResult): ResolvedHeaderFooterLayout {
362+
return resolveHeaderFooterLayout(result.layout, result.blocks, result.measures);
363+
}
364+
365+
function shiftResolvedPaintItemY(item: ResolvedPaintItem, yOffset: number): ResolvedPaintItem {
366+
if (item.kind === 'group') {
367+
return {
368+
...item,
369+
y: item.y + yOffset,
370+
children: item.children.map((child) => shiftResolvedPaintItemY(child, yOffset)),
371+
};
372+
}
373+
374+
return {
375+
...item,
376+
y: item.y + yOffset,
377+
};
378+
}
379+
380+
function normalizeDecorationFragments(fragments: Fragment[], layoutMinY: number): Fragment[] {
381+
if (layoutMinY >= 0) {
382+
return fragments;
383+
}
384+
385+
const yOffset = -layoutMinY;
386+
return fragments.map((fragment) => ({ ...fragment, y: fragment.y + yOffset }));
387+
}
388+
389+
function normalizeDecorationItems(
390+
items: ResolvedPaintItem[] | undefined,
391+
layoutMinY: number,
392+
): ResolvedPaintItem[] | undefined {
393+
if (!items || layoutMinY >= 0) {
394+
return items;
395+
}
396+
397+
const yOffset = -layoutMinY;
398+
return items.map((item) => shiftResolvedPaintItemY(item, yOffset));
399+
}
400+
342401
// =============================================================================
343402
// HeaderFooterSessionManager
344403
// =============================================================================
@@ -365,6 +424,12 @@ export class HeaderFooterSessionManager {
365424
#headerLayoutsByRId: Map<string, HeaderFooterLayoutResult> = new Map();
366425
#footerLayoutsByRId: Map<string, HeaderFooterLayoutResult> = new Map();
367426

427+
// Resolved layouts (aligned 1:1 with the results above)
428+
#resolvedHeaderLayouts: ResolvedHeaderFooterLayout[] | null = null;
429+
#resolvedFooterLayouts: ResolvedHeaderFooterLayout[] | null = null;
430+
#resolvedHeaderByRId: Map<string, ResolvedHeaderFooterLayout> = new Map();
431+
#resolvedFooterByRId: Map<string, ResolvedHeaderFooterLayout> = new Map();
432+
368433
// Decoration providers
369434
#headerDecorationProvider: PageDecorationProvider | undefined;
370435
#footerDecorationProvider: PageDecorationProvider | undefined;
@@ -492,6 +557,7 @@ export class HeaderFooterSessionManager {
492557
/** Set header layout results */
493558
set headerLayoutResults(results: HeaderFooterLayoutResult[] | null) {
494559
this.#headerLayoutResults = results;
560+
this.#resolvedHeaderLayouts = results ? results.map(resolveResult) : null;
495561
}
496562

497563
/** Footer layout results */
@@ -502,6 +568,7 @@ export class HeaderFooterSessionManager {
502568
/** Set footer layout results */
503569
set footerLayoutResults(results: HeaderFooterLayoutResult[] | null) {
504570
this.#footerLayoutResults = results;
571+
this.#resolvedFooterLayouts = results ? results.map(resolveResult) : null;
505572
}
506573

507574
/** Header layouts by rId */
@@ -612,6 +679,8 @@ export class HeaderFooterSessionManager {
612679
): void {
613680
this.#headerLayoutResults = headerResults;
614681
this.#footerLayoutResults = footerResults;
682+
this.#resolvedHeaderLayouts = headerResults ? headerResults.map(resolveResult) : null;
683+
this.#resolvedFooterLayouts = footerResults ? footerResults.map(resolveResult) : null;
615684
}
616685

617686
/**
@@ -1449,10 +1518,20 @@ export class HeaderFooterSessionManager {
14491518
layout: Layout,
14501519
sectionMetadata: SectionMetadata[],
14511520
): Promise<void> {
1452-
return await layoutPerRIdHeaderFooters(headerFooterInput, layout, sectionMetadata, {
1521+
await layoutPerRIdHeaderFooters(headerFooterInput, layout, sectionMetadata, {
14531522
headerLayoutsByRId: this.#headerLayoutsByRId,
14541523
footerLayoutsByRId: this.#footerLayoutsByRId,
14551524
});
1525+
1526+
// Rebuild resolved maps aligned 1:1 with the raw rId maps.
1527+
this.#resolvedHeaderByRId.clear();
1528+
for (const [key, result] of this.#headerLayoutsByRId) {
1529+
this.#resolvedHeaderByRId.set(key, resolveResult(result));
1530+
}
1531+
this.#resolvedFooterByRId.clear();
1532+
for (const [key, result] of this.#footerLayoutsByRId) {
1533+
this.#resolvedFooterByRId.set(key, resolveResult(result));
1534+
}
14561535
}
14571536

14581537
#computeMetrics(
@@ -2092,6 +2171,8 @@ export class HeaderFooterSessionManager {
20922171
createDecorationProvider(kind: 'header' | 'footer', layout: Layout): PageDecorationProvider | undefined {
20932172
const results = kind === 'header' ? this.#headerLayoutResults : this.#footerLayoutResults;
20942173
const layoutsByRId = kind === 'header' ? this.#headerLayoutsByRId : this.#footerLayoutsByRId;
2174+
const resolvedResults = kind === 'header' ? this.#resolvedHeaderLayouts : this.#resolvedFooterLayouts;
2175+
const resolvedByRId = kind === 'header' ? this.#resolvedHeaderByRId : this.#resolvedFooterByRId;
20952176

20962177
if ((!results || results.length === 0) && (!layoutsByRId || layoutsByRId.size === 0)) {
20972178
return undefined;
@@ -2166,6 +2247,15 @@ export class HeaderFooterSessionManager {
21662247
const slotPage = this.#findPageForNumber(rIdLayout.layout.pages, pageNumber);
21672248
if (slotPage) {
21682249
const fragments = slotPage.fragments ?? [];
2250+
const resolvedLayout = resolvedByRId.get(rIdLayoutKey);
2251+
const resolvedSlotPage = resolvedLayout?.pages.find((p) => p.number === slotPage.number);
2252+
const resolvedItems = resolvedSlotPage?.items;
2253+
if (resolvedItems && resolvedItems.length !== fragments.length) {
2254+
console.warn(
2255+
`[HeaderFooterSessionManager] Resolved items length (${resolvedItems.length}) does not match fragments length (${fragments.length}) for rId '${rIdLayoutKey}' page ${pageNumber}. Dropping items.`,
2256+
);
2257+
}
2258+
const alignedItems = resolvedItems && resolvedItems.length === fragments.length ? resolvedItems : undefined;
21692259
const pageHeight = page?.size?.h ?? layout.pageSize?.h ?? layoutOptions.pageSize?.h ?? defaultPageSize.h;
21702260
const margins = pageMargins ?? layout.pages[0]?.margins ?? layoutOptions.margins ?? defaultMargins;
21712261
const decorationMargins =
@@ -2180,11 +2270,12 @@ export class HeaderFooterSessionManager {
21802270
const metrics = this.#computeMetrics(kind, rawLayoutHeight, box, pageHeight, margins?.footer ?? 0);
21812271

21822272
const layoutMinY = rIdLayout.layout.minY ?? 0;
2183-
const normalizedFragments =
2184-
layoutMinY < 0 ? fragments.map((f) => ({ ...f, y: f.y - layoutMinY })) : fragments;
2273+
const normalizedFragments = normalizeDecorationFragments(fragments, layoutMinY);
2274+
const normalizedItems = normalizeDecorationItems(alignedItems, layoutMinY);
21852275

21862276
return {
21872277
fragments: normalizedFragments,
2278+
items: normalizedItems,
21882279
height: metrics.containerHeight,
21892280
contentHeight: metrics.layoutHeight > 0 ? metrics.layoutHeight : metrics.containerHeight,
21902281
offset: metrics.offset,
@@ -2205,7 +2296,8 @@ export class HeaderFooterSessionManager {
22052296
return null;
22062297
}
22072298

2208-
const variant = results.find((entry) => entry.type === headerFooterType);
2299+
const variantIndex = results.findIndex((entry) => entry.type === headerFooterType);
2300+
const variant = variantIndex >= 0 ? results[variantIndex] : undefined;
22092301
if (!variant || !variant.layout?.pages?.length) {
22102302
return null;
22112303
}
@@ -2216,6 +2308,17 @@ export class HeaderFooterSessionManager {
22162308
}
22172309
const fragments = slotPage.fragments ?? [];
22182310

2311+
const resolvedVariant = resolvedResults?.[variantIndex];
2312+
const resolvedVariantPage = resolvedVariant?.pages.find((p) => p.number === slotPage.number);
2313+
const resolvedVariantItems = resolvedVariantPage?.items;
2314+
if (resolvedVariantItems && resolvedVariantItems.length !== fragments.length) {
2315+
console.warn(
2316+
`[HeaderFooterSessionManager] Resolved items length (${resolvedVariantItems.length}) does not match fragments length (${fragments.length}) for variant '${headerFooterType}' page ${pageNumber}. Dropping items.`,
2317+
);
2318+
}
2319+
const alignedVariantItems =
2320+
resolvedVariantItems && resolvedVariantItems.length === fragments.length ? resolvedVariantItems : undefined;
2321+
22192322
const pageHeight = page?.size?.h ?? layout.pageSize?.h ?? layoutOptions.pageSize?.h ?? defaultPageSize.h;
22202323
const margins = pageMargins ?? layout.pages[0]?.margins ?? layoutOptions.margins ?? defaultMargins;
22212324
const decorationMargins =
@@ -2228,10 +2331,12 @@ export class HeaderFooterSessionManager {
22282331
const finalHeaderId = sectionRId ?? fallbackId ?? undefined;
22292332

22302333
const layoutMinY = variant.layout.minY ?? 0;
2231-
const normalizedFragments = layoutMinY < 0 ? fragments.map((f) => ({ ...f, y: f.y - layoutMinY })) : fragments;
2334+
const normalizedFragments = normalizeDecorationFragments(fragments, layoutMinY);
2335+
const normalizedItems = normalizeDecorationItems(alignedVariantItems, layoutMinY);
22322336

22332337
return {
22342338
fragments: normalizedFragments,
2339+
items: normalizedItems,
22352340
height: metrics.containerHeight,
22362341
contentHeight: metrics.layoutHeight > 0 ? metrics.layoutHeight : metrics.containerHeight,
22372342
offset: metrics.offset,
@@ -2312,6 +2417,10 @@ export class HeaderFooterSessionManager {
23122417
this.#footerLayoutResults = null;
23132418
this.#headerLayoutsByRId.clear();
23142419
this.#footerLayoutsByRId.clear();
2420+
this.#resolvedHeaderLayouts = null;
2421+
this.#resolvedFooterLayouts = null;
2422+
this.#resolvedHeaderByRId.clear();
2423+
this.#resolvedFooterByRId.clear();
23152424

23162425
// Clear decoration providers
23172426
this.#headerDecorationProvider = undefined;

0 commit comments

Comments
 (0)