Skip to content
Merged
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
261 changes: 261 additions & 0 deletions packages/layout-engine/layout-engine/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3256,6 +3256,196 @@ describe('requirePageBoundary edge cases', () => {
expect(fragment.x).toBeGreaterThanOrEqual(DEFAULT_OPTIONS.margins!.left + 5);
expect(fragment.y).toBeGreaterThanOrEqual(DEFAULT_OPTIONS.margins!.top + 3);
});

it('creates fragment for page-relative anchored drawing (SD-1838)', () => {
const paragraphBlock: FlowBlock = {
kind: 'paragraph',
id: 'para-anchor',
runs: [],
};
const drawingBlock: FlowBlock = {
kind: 'drawing',
id: 'drawing-page-relative',
drawingKind: 'vectorShape',
geometry: { width: 100, height: 60, rotation: 0 },
anchor: {
isAnchored: true,
hRelativeFrom: 'column',
vRelativeFrom: 'page',
alignH: 'left',
offsetH: 10,
offsetV: 80,
},
wrap: {
type: 'None',
},
};
const paragraphMeasure = makeMeasure([20]);
const drawingMeasure: DrawingMeasure = {
kind: 'drawing',
drawingKind: 'vectorShape',
width: 100,
height: 60,
scale: 1,
naturalWidth: 100,
naturalHeight: 60,
geometry: { width: 100, height: 60, rotation: 0, flipH: false, flipV: false },
};
const layout = layoutDocument(
[paragraphBlock, drawingBlock],
[paragraphMeasure, drawingMeasure],
DEFAULT_OPTIONS,
);
const fragment = layout.pages[0].fragments.find(
(frag) => frag.blockId === 'drawing-page-relative',
) as DrawingFragment;
expect(fragment).toBeTruthy();
expect(fragment.kind).toBe('drawing');
expect(fragment.isAnchored).toBe(true);
expect(fragment.y).toBe(80); // offsetV from page top
expect(fragment.width).toBe(100);
expect(fragment.height).toBe(60);
});

it('emits pre-registered page-relative drawings on their stored page after pagination advances', () => {
const firstPageParagraph: FlowBlock = {
kind: 'paragraph',
id: 'para-page-1',
runs: [],
};
const forcedBreak: FlowBlock = {
kind: 'pageBreak',
id: 'pb-before-drawing',
};
const secondPageParagraph: FlowBlock = {
kind: 'paragraph',
id: 'para-page-2',
runs: [],
};
const drawingBlock: FlowBlock = {
kind: 'drawing',
id: 'drawing-pre-reg-page',
drawingKind: 'vectorShape',
geometry: { width: 120, height: 120, rotation: 0 },
anchor: {
isAnchored: true,
hRelativeFrom: 'column',
vRelativeFrom: 'page',
alignH: 'left',
alignV: 'top',
offsetH: 0,
offsetV: 0,
},
wrap: {
type: 'Square',
wrapText: 'right',
distLeft: 0,
distRight: 10,
},
};
const paragraphMeasure = makeMeasure([20]);
const drawingMeasure: DrawingMeasure = {
kind: 'drawing',
drawingKind: 'vectorShape',
width: 120,
height: 120,
scale: 1,
naturalWidth: 120,
naturalHeight: 120,
geometry: { width: 120, height: 120, rotation: 0, flipH: false, flipV: false },
};

const remeasureParagraph: NonNullable<LayoutOptions['remeasureParagraph']> = (_block, _maxWidth) => {
return makeMeasure([20]);
};

const layout = layoutDocument(
[firstPageParagraph, forcedBreak, drawingBlock, secondPageParagraph],
[paragraphMeasure, { kind: 'pageBreak' }, drawingMeasure, paragraphMeasure],
{
...DEFAULT_OPTIONS,
remeasureParagraph,
},
);

expect(layout.pages).toHaveLength(2);

const page1 = layout.pages[0];
const page2 = layout.pages[1];

const wrappedPara = page1.fragments.find(
(fragment) => fragment.kind === 'para' && fragment.blockId === 'para-page-1',
) as ParaFragment;
expect(wrappedPara).toBeTruthy();
expect(wrappedPara.x).toBeGreaterThan(DEFAULT_OPTIONS.margins!.left);

const page2Para = page2.fragments.find(
(fragment) => fragment.kind === 'para' && fragment.blockId === 'para-page-2',
) as ParaFragment;
expect(page2Para).toBeTruthy();

const drawingOnPage1 = page1.fragments.find(
(fragment) => fragment.kind === 'drawing' && fragment.blockId === 'drawing-pre-reg-page',
);
const drawingOnPage2 = page2.fragments.find(
(fragment) => fragment.kind === 'drawing' && fragment.blockId === 'drawing-pre-reg-page',
);

expect(drawingOnPage1).toBeTruthy();
expect(drawingOnPage2).toBeUndefined();
});

it('creates fragment for margin-relative anchored drawing with wrapNone', () => {
const paragraphBlock: FlowBlock = {
kind: 'paragraph',
id: 'para-anchor-2',
runs: [],
};
const drawingBlock: FlowBlock = {
kind: 'drawing',
id: 'drawing-margin-relative',
drawingKind: 'vectorShape',
geometry: { width: 80, height: 40, rotation: 0 },
anchor: {
isAnchored: true,
hRelativeFrom: 'margin',
vRelativeFrom: 'margin',
alignH: 'left',
alignV: 'top',
offsetH: 0,
offsetV: 15,
},
wrap: {
type: 'None',
},
};
const paragraphMeasure = makeMeasure([20]);
const drawingMeasure: DrawingMeasure = {
kind: 'drawing',
drawingKind: 'vectorShape',
width: 80,
height: 40,
scale: 1,
naturalWidth: 80,
naturalHeight: 40,
geometry: { width: 80, height: 40, rotation: 0, flipH: false, flipV: false },
};
const layout = layoutDocument(
[paragraphBlock, drawingBlock],
[paragraphMeasure, drawingMeasure],
DEFAULT_OPTIONS,
);
const fragment = layout.pages[0].fragments.find(
(frag) => frag.blockId === 'drawing-margin-relative',
) as DrawingFragment;
expect(fragment).toBeTruthy();
expect(fragment.kind).toBe('drawing');
expect(fragment.isAnchored).toBe(true);
// margin-relative, alignV='top', offsetV=15: contentTop + 15
expect(fragment.y).toBe(DEFAULT_OPTIONS.margins!.top + 15);
expect(fragment.width).toBe(80);
expect(fragment.height).toBe(40);
});
});

describe('anchored images bounds and zIndex', () => {
Expand Down Expand Up @@ -3285,6 +3475,77 @@ describe('requirePageBoundary edge cases', () => {
// behindDoc → zIndex 0
expect(img.zIndex).toBe(0);
});

it('emits pre-registered page-relative images on their stored page after pagination advances', () => {
const firstPageParagraph: FlowBlock = {
kind: 'paragraph',
id: 'para-page-1',
runs: [],
};
const forcedBreak: FlowBlock = {
kind: 'pageBreak',
id: 'pb-before-image',
};
const secondPageParagraph: FlowBlock = {
kind: 'paragraph',
id: 'para-page-2',
runs: [],
};
const imageBlock: ImageBlock = {
kind: 'image',
id: 'img-pre-reg-page',
src: 'data:image/png;base64,xxx',
anchor: {
isAnchored: true,
hRelativeFrom: 'column',
vRelativeFrom: 'page',
alignH: 'left',
alignV: 'top',
offsetH: 0,
offsetV: 0,
},
wrap: {
type: 'Square',
wrapText: 'right',
distLeft: 0,
distRight: 10,
},
};
const paragraphMeasure = makeMeasure([20]);
const imageMeasure: ImageMeasure = {
kind: 'image',
width: 120,
height: 120,
};

const remeasureParagraph: NonNullable<LayoutOptions['remeasureParagraph']> = (_block, _maxWidth) => {
return makeMeasure([20]);
};

const layout = layoutDocument(
[firstPageParagraph, forcedBreak, imageBlock, secondPageParagraph],
[paragraphMeasure, { kind: 'pageBreak' }, imageMeasure, paragraphMeasure],
{
...DEFAULT_OPTIONS,
remeasureParagraph,
},
);

expect(layout.pages).toHaveLength(2);

const page1 = layout.pages[0];
const page2 = layout.pages[1];

const imageOnPage1 = page1.fragments.find(
(fragment) => fragment.kind === 'image' && fragment.blockId === 'img-pre-reg-page',
);
const imageOnPage2 = page2.fragments.find(
(fragment) => fragment.kind === 'image' && fragment.blockId === 'img-pre-reg-page',
);

expect(imageOnPage1).toBeTruthy();
expect(imageOnPage2).toBeUndefined();
});
});

describe('tables in columns/pages', () => {
Expand Down
41 changes: 40 additions & 1 deletion packages/layout-engine/layout-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1937,7 +1937,7 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
Number.isFinite(preRegPos.pageNumber)
) {
// Use pre-computed position for page-relative anchors
const state = paginator.ensurePage();
const state = paginator.getPageByNumber(preRegPos.pageNumber);
const imgBlock = block as ImageBlock;
const imgMeasure = measure as ImageMeasure;

Expand Down Expand Up @@ -2003,6 +2003,45 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
if (measure.kind !== 'drawing') {
throw new Error(`layoutDocument: expected drawing measure for block ${block.id}`);
}

// Check if this is a pre-registered page-relative anchor
const preRegPos = preRegisteredPositions.get(block.id);
if (
preRegPos &&
Number.isFinite(preRegPos.anchorX) &&
Number.isFinite(preRegPos.anchorY) &&
Number.isFinite(preRegPos.pageNumber)
) {
// Use pre-computed position for page-relative anchored drawings
const state = paginator.getPageByNumber(preRegPos.pageNumber);
const drawBlock = block as DrawingBlock;
const drawMeasure = measure as DrawingMeasure;

const fragment: DrawingFragment = {
kind: 'drawing',
blockId: drawBlock.id,
drawingKind: drawBlock.drawingKind,
x: preRegPos.anchorX,
y: preRegPos.anchorY,
width: drawMeasure.width,
height: drawMeasure.height,
geometry: drawMeasure.geometry,
scale: drawMeasure.scale,
isAnchored: true,
behindDoc: drawBlock.anchor?.behindDoc === true,
zIndex: getFragmentZIndex(drawBlock),
drawingContentId: drawBlock.drawingContentId,
};

const attrs = drawBlock.attrs as Record<string, unknown> | undefined;
if (attrs?.pmStart != null) fragment.pmStart = attrs.pmStart as number;
if (attrs?.pmEnd != null) fragment.pmEnd = attrs.pmEnd as number;

state.page.fragments.push(fragment);
placedAnchoredIds.add(drawBlock.id);
continue;
}

layoutDrawingBlock({
block: block as DrawingBlock,
measure: measure as DrawingMeasure,
Expand Down
5 changes: 5 additions & 0 deletions packages/layout-engine/layout-engine/src/paginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ export function createPaginator(opts: PaginatorOptions) {
return startNewPage();
};

const getPageByNumber = (pageNumber: number): PageState => {
return states.find((s) => s.page.number === pageNumber) ?? ensurePage();
};

return {
pages,
states,
Expand All @@ -125,5 +129,6 @@ export function createPaginator(opts: PaginatorOptions) {
advanceColumn,
columnX,
getActiveColumnsForState,
getPageByNumber,
} as const;
}
Loading
Loading