feat(editor): replace flat layout cache with tree in turbo renderer (#11319)

### TL;DR

Refactored the BlockSuite turbo renderer to use a hierarchical tree structure for layouts instead of a flat list, improving rendering accuracy and performance.

### What changed?

- Redesigned the layout system to use a tree structure (`ViewportLayoutTree`) that better represents the document hierarchy
- Added `blockId` to all layout objects for better tracking and debugging
- Updated the layout query mechanism to work with models directly instead of components
- Enhanced error handling with more descriptive warnings and error messages
- Improved the painting process to traverse the layout tree recursively
- Fixed viewport coordinate calculations for more accurate rendering
- Updated the worker communication to support the new tree-based layout structure

### Why make this change?

The previous flat layout structure didn't properly represent the hierarchical nature of documents, leading to rendering issues with nested blocks. This tree-based approach:

1. Better represents the actual document structure
2. Improves rendering accuracy for nested elements
3. Makes debugging easier with more consistent block identification
4. Provides a more robust foundation for future rendering optimizations
5. Reduces the likelihood of rendering artifacts when scrolling or zooming
This commit is contained in:
doodlewind
2025-04-10 08:49:23 +00:00
parent b8e93ed714
commit f85b35227b
10 changed files with 244 additions and 158 deletions

View File

@@ -6,8 +6,9 @@ import {
segmentSentences,
} from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord } from '@blocksuite/std/gfx';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ListLayout } from './list-painter.worker';
@@ -21,24 +22,27 @@ export class ListLayoutHandlerExtension extends BlockLayoutHandlerExtension<List
);
}
queryLayout(component: GfxBlockComponent): ListLayout | null {
// Select all list items within this list block
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): ListLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
if (!component) return null;
// Find the list items within this specific list component
const listItemSelector =
'.affine-list-block-container .affine-list-rich-text-wrapper [data-v-text="true"]';
const listItemNodes = component.querySelectorAll(listItemSelector);
if (listItemNodes.length === 0) return null;
const viewportRecord = component.gfx.viewport.deserializeRecord(
component.dataset.viewportState
);
if (!viewportRecord) return null;
const { zoom, viewScale } = viewportRecord;
const list: ListLayout = {
type: 'affine:list',
items: [],
blockId: model.id,
rect: { x: 0, y: 0, w: 0, h: 0 },
};
listItemNodes.forEach(listItemNode => {

View File

@@ -77,10 +77,15 @@ class ListLayoutPainter implements BlockLayoutPainter {
return;
}
if (!isListLayout(layout)) return;
if (!isListLayout(layout)) {
console.warn(
'Expected list layout but received different format:',
layout
);
return;
}
const renderedPositions = new Set<string>();
layout.items.forEach(item => {
const fontSize = item.fontSize;
const baselineY = getBaseline(fontSize);

View File

@@ -6,8 +6,9 @@ import {
segmentSentences,
} from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord } from '@blocksuite/std/gfx';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ParagraphLayout } from './paragraph-painter.worker';
@@ -21,58 +22,54 @@ export class ParagraphLayoutHandlerExtension extends BlockLayoutHandlerExtension
);
}
queryLayout(component: GfxBlockComponent): ParagraphLayout | null {
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): ParagraphLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
const paragraphSelector =
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]';
const paragraphNodes = component.querySelectorAll(paragraphSelector);
if (paragraphNodes.length === 0) return null;
const viewportRecord = component.gfx.viewport.deserializeRecord(
component.dataset.viewportState
);
if (!viewportRecord) return null;
const paragraphNode = component.querySelector(paragraphSelector);
if (!paragraphNode) return null;
const { zoom, viewScale } = viewportRecord;
const paragraph: ParagraphLayout = {
type: 'affine:paragraph',
sentences: [],
blockId: model.id,
rect: { x: 0, y: 0, w: 0, h: 0 },
};
paragraphNodes.forEach(paragraphNode => {
const computedStyle = window.getComputedStyle(paragraphNode);
const fontSizeStr = computedStyle.fontSize;
const fontSize = parseInt(fontSizeStr);
const computedStyle = window.getComputedStyle(paragraphNode);
const fontSizeStr = computedStyle.fontSize;
const fontSize = parseInt(fontSizeStr);
const sentences = segmentSentences(paragraphNode.textContent || '');
const sentenceLayouts = sentences.map(sentence => {
const sentenceRects = getSentenceRects(paragraphNode, sentence);
const rects = sentenceRects.map(({ text, rect }) => {
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
rect.x,
rect.y,
]);
return {
text,
rect: {
x: modelX,
y: modelY,
w: rect.w / zoom / viewScale,
h: rect.h / zoom / viewScale,
},
};
});
const sentences = segmentSentences(paragraphNode.textContent || '');
const sentenceLayouts = sentences.map(sentence => {
const sentenceRects = getSentenceRects(paragraphNode, sentence);
const rects = sentenceRects.map(({ text, rect }) => {
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
rect.x,
rect.y,
]);
return {
text: sentence,
rects,
fontSize,
text,
rect: {
x: modelX,
y: modelY,
w: rect.w / zoom / viewScale,
h: rect.h / zoom / viewScale,
},
};
});
paragraph.sentences.push(...sentenceLayouts);
return {
text: sentence,
rects,
fontSize,
};
});
paragraph.sentences.push(...sentenceLayouts);
return paragraph;
}

View File

@@ -73,10 +73,15 @@ class ParagraphLayoutPainter implements BlockLayoutPainter {
return;
}
if (!isParagraphLayout(layout)) return; // cast to ParagraphLayout
if (!isParagraphLayout(layout)) {
console.warn(
'Expected paragraph layout but received different format:',
layout
);
return;
}
const renderedPositions = new Set<string>();
layout.sentences.forEach(sentence => {
const fontSize = sentence.fontSize;
const baselineY = getBaseline(fontSize);