mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-11 14:08:55 +08:00
f85b35227b
### 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
130 lines
3.3 KiB
TypeScript
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;
|
|
}
|