mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 23:07:02 +08:00
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:
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user