Files
AFFiNE-Mirror/blocksuite/affine/gfx/turbo-renderer/src/painter/painter.worker.ts
T
doodlewind f85b35227b 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
2025-04-10 08:49:23 +00:00

130 lines
3.3 KiB
TypeScript

import {
Container,
createIdentifier,
type ServiceProvider,
} from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import type {
BlockLayoutPainter,
BlockLayoutTreeNode,
HostToWorkerMessage,
ViewportLayoutTree,
WorkerToHostMessage,
} from '../types';
export const BlockPainterProvider = createIdentifier<BlockLayoutPainter>(
'block-painter-provider'
);
export const BlockLayoutPainterExtension = (
type: string,
painter: new () => BlockLayoutPainter
): ExtensionType => {
return {
setup: di => {
di.addImpl(BlockPainterProvider(type), painter);
},
};
};
export class ViewportLayoutPainter {
private readonly canvas: OffscreenCanvas = new OffscreenCanvas(0, 0);
private ctx: OffscreenCanvasRenderingContext2D | null = null;
private zoom = 1;
public provider: ServiceProvider;
getPainter(type: string): BlockLayoutPainter | null {
return this.provider.getOptional(BlockPainterProvider(type));
}
constructor(extensions: ExtensionType[]) {
const container = new Container();
extensions.forEach(extension => {
extension.setup(container);
});
this.provider = container.provider();
self.onmessage = this.handler;
}
setSize(layoutRectW: number, layoutRectH: number, dpr: number, zoom: number) {
const width = layoutRectW * dpr * zoom;
const height = layoutRectH * dpr * zoom;
this.canvas.width = width;
this.canvas.height = height;
this.ctx = this.canvas.getContext('2d')!;
this.ctx.scale(dpr, dpr);
this.zoom = zoom;
this.clearBackground();
}
private clearBackground() {
if (!this.canvas || !this.ctx) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
paint(layout: ViewportLayoutTree, version: number) {
const { canvas, ctx } = this;
if (!canvas || !ctx) return;
this.paintTree(layout, version);
}
paintTree(layout: ViewportLayoutTree, version: number) {
const { canvas, ctx } = this;
const { overallRect } = layout;
if (!canvas || !ctx) return;
this.clearBackground();
ctx.scale(this.zoom, this.zoom);
const paintNode = (node: BlockLayoutTreeNode) => {
const painter = this.getPainter(node.type);
painter?.paint(ctx, node.layout, overallRect.x, overallRect.y);
node.children.forEach(paintNode);
};
layout.roots.forEach(root => paintNode(root));
const bitmap = canvas.transferToImageBitmap();
const message: WorkerToHostMessage = {
type: 'bitmapPainted',
bitmap,
version,
};
self.postMessage(message, { transfer: [bitmap] });
}
handler = async (e: MessageEvent<HostToWorkerMessage>) => {
const { type, data } = e.data;
switch (type) {
case 'paintLayout': {
const { layout, width, height, dpr, zoom, version } = data;
this.setSize(width, height, dpr, zoom);
this.paint(layout, version);
break;
}
}
};
}
const meta = {
emSize: 2048,
hHeadAscent: 1984,
hHeadDescent: -494,
};
export function getBaseline(fontSize: number) {
const lineHeight = 1.2 * fontSize;
const A = fontSize * (meta.hHeadAscent / meta.emSize); // ascent
const D = fontSize * (meta.hHeadDescent / meta.emSize); // descent
const AD = A + Math.abs(D); // ascent + descent
const L = lineHeight - AD; // leading
const y = A + L / 2;
return y;
}