Skip to content

Commit 9265da6

Browse files
committed
fix(docs): stabilize modern worker layout rendering
1 parent 663bcdc commit 9265da6

4 files changed

Lines changed: 137 additions & 7 deletions

File tree

packages/engine-render/src/__tests__/worker-layout.spec.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,99 @@ describe('worker document layout session', () => {
643643
localeService.dispose();
644644
});
645645

646+
it('keeps modern explicit-page drawing geometry stable across block publications', () => {
647+
vi.stubGlobal('document', undefined);
648+
vi.stubGlobal('OffscreenCanvas', class {
649+
getContext() {
650+
return {
651+
font: '',
652+
textBaseline: 'alphabetic',
653+
measureText(content: string) {
654+
return {
655+
width: content.length * 7,
656+
fontBoundingBoxAscent: 9,
657+
fontBoundingBoxDescent: 3,
658+
actualBoundingBoxAscent: 8,
659+
actualBoundingBoxDescent: 2,
660+
};
661+
},
662+
};
663+
}
664+
});
665+
666+
const T = DataStreamTreeTokenType;
667+
const firstDrawingId = 'modern-first-page-drawing';
668+
const secondDrawingId = 'modern-second-page-drawing';
669+
const content = [
670+
`First ${T.CUSTOM_BLOCK} page${T.PARAGRAPH}${T.PAGE_BREAK}`,
671+
`Second ${T.CUSTOM_BLOCK} page${T.PARAGRAPH}${T.PAGE_BREAK}`,
672+
...Array.from({ length: 20 }, (_, index) => `Trailing paragraph ${index}${T.PARAGRAPH}`),
673+
].join('');
674+
const sourceModel = createDocumentModelWithStyle(content, {});
675+
const snapshot = sourceModel.getSnapshot();
676+
if (snapshot.body == null) {
677+
throw new Error('Expected the modern drawing test document body.');
678+
}
679+
const firstDrawingStart = content.indexOf(T.CUSTOM_BLOCK);
680+
const secondDrawingStart = content.indexOf(T.CUSTOM_BLOCK, firstDrawingStart + 1);
681+
snapshot.body.customBlocks = [
682+
{ startIndex: firstDrawingStart, blockId: firstDrawingId },
683+
{ startIndex: secondDrawingStart, blockId: secondDrawingId },
684+
];
685+
snapshot.drawings = Object.fromEntries([firstDrawingId, secondDrawingId].map((drawingId) => [drawingId, {
686+
drawingId,
687+
drawingType: DrawingTypeEnum.DRAWING_BLOCK,
688+
unitId: snapshot.id,
689+
subUnitId: snapshot.id,
690+
layoutType: PositionedObjectLayoutType.WRAP_NONE,
691+
docTransform: {
692+
angle: 0,
693+
positionH: { relativeFrom: ObjectRelativeFromH.PAGE, posOffset: 0 },
694+
positionV: { relativeFrom: ObjectRelativeFromV.PARAGRAPH, posOffset: 0 },
695+
size: { width: 40, height: 20 },
696+
},
697+
}]));
698+
const dataModel = new DocumentDataModel(snapshot);
699+
dataModel.updateDocumentStyle({ documentFlavor: DocumentFlavor.MODERN });
700+
const session = new DocumentLayoutSession(dataModel, new LocaleService());
701+
const targetModel = new DocumentDataModel(structuredClone(dataModel.getSnapshot()));
702+
const targetSkeleton = DocumentSkeleton.create(new DocumentViewModel(targetModel), new LocaleService());
703+
const synchronousModel = new DocumentDataModel(structuredClone(dataModel.getSnapshot()));
704+
const synchronousSkeleton = DocumentSkeleton.create(new DocumentViewModel(synchronousModel), new LocaleService());
705+
synchronousSkeleton.calculate();
706+
const generation = session.start({ reason: 'initial' });
707+
const secondDrawingTops: number[] = [];
708+
let result = session.step(generation, 0);
709+
for (let step = 0; step < 100; step++) {
710+
if (result.publication?.kind === 'block') {
711+
targetSkeleton.applyLayoutPublication(structuredClone(result.publication), result.progress);
712+
const drawing = result.publication.block.skeDrawings.find(([drawingId]) => drawingId === secondDrawingId)?.[1];
713+
if (drawing != null && !result.progress.complete) {
714+
secondDrawingTops.push(drawing.aTop);
715+
}
716+
}
717+
if (result.progress.complete) {
718+
break;
719+
}
720+
result = session.step(generation, 0);
721+
}
722+
723+
expect(result.progress).toMatchObject({ complete: true, mode: 'continuous', pageCount: 1 });
724+
expect(secondDrawingTops.length).toBeGreaterThan(1);
725+
expect(new Set(secondDrawingTops).size).toBe(1);
726+
expect(normalizeSkeleton(targetSkeleton.getSkeletonData())).toEqual(
727+
normalizeSkeleton(synchronousSkeleton.getSkeletonData())
728+
);
729+
730+
synchronousSkeleton.dispose();
731+
synchronousModel.dispose();
732+
targetSkeleton.dispose();
733+
targetModel.dispose();
734+
session.dispose();
735+
dataModel.dispose();
736+
sourceModel.dispose();
737+
});
738+
646739
it('preserves tables, column groups, drawings, lists, headers and footers across Worker transport', () => {
647740
vi.stubGlobal('document', undefined);
648741
vi.stubGlobal('OffscreenCanvas', class {

packages/engine-render/src/components/docs/__tests__/document.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,9 +2043,9 @@ describe('documents render', () => {
20432043
documents.dispose();
20442044
});
20452045

2046-
it('draws only viewport-adjacent lines for an infinite-height modern page', () => {
2046+
it('draws only viewport-adjacent lines when imported modern geometry keeps a finite page height', () => {
20472047
const bodyPage = createPage(DocumentSkeletonPageType.BODY, '');
2048-
bodyPage.pageHeight = Number.POSITIVE_INFINITY;
2048+
bodyPage.pageHeight = 120;
20492049
bodyPage.height = 1020;
20502050
bodyPage.skeTables.clear();
20512051

@@ -2063,7 +2063,12 @@ describe('documents render', () => {
20632063
};
20642064
bodyPage.parent = skeletonData;
20652065

2066-
const documents = new Documents('docs-modern-viewport', { getSkeletonData: () => skeletonData } as any, {
2066+
const documents = new Documents('docs-modern-viewport', {
2067+
getSkeletonData: () => skeletonData,
2068+
getViewModel: () => ({
2069+
getDataModel: () => ({ documentStyle: { documentFlavor: DocumentFlavor.MODERN } }),
2070+
}),
2071+
} as any, {
20672072
pageLayoutType: PageLayoutType.VERTICAL,
20682073
pageMarginLeft: 0,
20692074
pageMarginTop: 0,

packages/engine-render/src/components/docs/document.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
import type { DocumentFlavor, ICustomRange, IDocumentRenderConfig, IScale, ITableCell, ITableCellBorder, Nullable } from '@univerjs/core';
1817
import type {
1918
IDocumentSkeletonColumnGroup,
2019
IDocumentSkeletonColumnGroupColumn,
@@ -33,7 +32,7 @@ import type { ComponentExtension, IDrawInfo, IExtensionConfig } from '../extensi
3332
import type { IDocumentsConfig, IPageMarginLayout } from './doc-component';
3433
import type { DocumentSkeleton } from './layout/doc-skeleton';
3534
import type { IDocsTableRenderViewport } from './table-render-viewport';
36-
import { CellValueType, ColumnSeparatorType, DashStyleType, HorizontalAlign, VerticalAlign, WrapStrategy } from '@univerjs/core';
35+
import { CellValueType, ColumnSeparatorType, DashStyleType, DocumentFlavor, HorizontalAlign, type ICustomRange, type IDocumentRenderConfig, type IScale, type ITableCell, type ITableCellBorder, type Nullable, VerticalAlign, WrapStrategy } from '@univerjs/core';
3736
import { Subject } from 'rxjs';
3837
import { BORDER_TYPE as BORDER_LTRB, drawLineByBorderType } from '../../basics';
3938
import { calculateRectRotate, getRotateOffsetAndFarthestHypotenuse } from '../../basics/draw';
@@ -240,6 +239,7 @@ export class Documents extends DocComponent {
240239
this._drawLiquid.reset();
241240

242241
const { pages, skeHeaders, skeFooters } = skeletonData;
242+
const isContinuousLayout = this.getSkeleton()?.getViewModel?.().getDataModel().documentStyle.documentFlavor === DocumentFlavor.MODERN;
243243
const parentScale = this.getParentScale();
244244
// const scale = getScale(parentScale);
245245
const extensions = this.getExtensionsByOrder();
@@ -304,7 +304,7 @@ export class Documents extends DocComponent {
304304
const vertexAngle = degToRad(vertexAngleDeg);
305305
const finalAngle = vertexAngle - centerAngle;
306306

307-
if (this.isSkipByDiffBounds(page, pageTop, pageLeft, bounds)) {
307+
if (!isContinuousLayout && this.isSkipByDiffBounds(page, pageTop, pageLeft, bounds)) {
308308
const { x, y } = this._drawLiquid.translatePage(
309309
page,
310310
this.pageLayoutType,
@@ -456,7 +456,7 @@ export class Documents extends DocComponent {
456456
const { divides, asc = 0, type, lineHeight = 0 } = line;
457457

458458
if (
459-
page.pageHeight === Number.POSITIVE_INFINITY &&
459+
(isContinuousLayout || page.pageHeight === Number.POSITIVE_INFINITY) &&
460460
bounds != null
461461
) {
462462
const lineTop = pageTop + pagePaddingTop + section.top + line.top +

packages/engine-render/src/components/docs/layout/doc-skeleton.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,21 @@ function clonePageFlowForPublish(source: IDocumentSkeletonPage): IDocumentSkelet
379379
const page: IDocumentSkeletonPage = {
380380
...source,
381381
sections: [],
382+
// Continuous-page merging translates drawing coordinates. Keep partial
383+
// publications isolated from the active layout graph so publishing the
384+
// same page fragment again cannot accumulate that translation.
385+
skeDrawings: new Map([...source.skeDrawings].map(([drawingId, drawing]) => [
386+
drawingId,
387+
{ ...drawing },
388+
])),
389+
skeTables: new Map([...source.skeTables].map(([tableId, table]) => [
390+
tableId,
391+
{ ...table },
392+
])),
393+
skeColumnGroups: new Map([...source.skeColumnGroups].map(([groupId, group]) => [
394+
groupId,
395+
{ ...group },
396+
])),
382397
parent: undefined,
383398
};
384399

@@ -634,6 +649,12 @@ function mergeContinuousDuplicatePages(pages: IDocumentSkeletonPage[], mergeAll
634649
previousPage.skeTables.set(tableId, table);
635650
});
636651

652+
page.skeColumnGroups?.forEach((columnGroup, columnGroupId) => {
653+
columnGroup.top += topOffset;
654+
columnGroup.parent = previousPage;
655+
previousPage.skeColumnGroups.set(columnGroupId, columnGroup);
656+
});
657+
637658
previousPage.height += page.height;
638659
previousPage.width = Math.max(previousPage.width, page.width);
639660
previousPage.ed = Math.max(previousPage.ed, page.ed);
@@ -4175,6 +4196,17 @@ export class DocumentSkeleton extends Skeleton {
41754196
private _finishIncrementalLayout(state: IIncrementalLayoutState): void {
41764197
const { ctx } = state;
41774198
const { skeleton } = ctx;
4199+
if (state.mode === 'continuous') {
4200+
const fragmentGeometry = skeleton.pages.map(clonePageFlowForPublish);
4201+
updateBlockIndex(fragmentGeometry, -1, ctx.docsConfig.documentCompatibilityPolicy);
4202+
fragmentGeometry.forEach((geometry, index) => {
4203+
const page = skeleton.pages[index];
4204+
if (page != null) {
4205+
page.height = geometry.height;
4206+
page.width = geometry.width;
4207+
}
4208+
});
4209+
}
41784210
removeDupPages(ctx);
41794211
mergeContinuousDuplicatePages(skeleton.pages, state.mode === 'continuous');
41804212
if (state.mode === 'continuous') {

0 commit comments

Comments
 (0)