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
@@ -6,8 +6,9 @@ import {
segmentSentences, segmentSentences,
} from '@blocksuite/affine-gfx-turbo-renderer'; } from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di'; import type { Container } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std'; import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord } from '@blocksuite/std/gfx'; import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ListLayout } from './list-painter.worker'; import type { ListLayout } from './list-painter.worker';
@@ -21,24 +22,27 @@ export class ListLayoutHandlerExtension extends BlockLayoutHandlerExtension<List
); );
} }
queryLayout(component: GfxBlockComponent): ListLayout | null { override queryLayout(
// Select all list items within this list block 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 = const listItemSelector =
'.affine-list-block-container .affine-list-rich-text-wrapper [data-v-text="true"]'; '.affine-list-block-container .affine-list-rich-text-wrapper [data-v-text="true"]';
const listItemNodes = component.querySelectorAll(listItemSelector); const listItemNodes = component.querySelectorAll(listItemSelector);
if (listItemNodes.length === 0) return null; if (listItemNodes.length === 0) return null;
const viewportRecord = component.gfx.viewport.deserializeRecord(
component.dataset.viewportState
);
if (!viewportRecord) return null;
const { zoom, viewScale } = viewportRecord; const { zoom, viewScale } = viewportRecord;
const list: ListLayout = { const list: ListLayout = {
type: 'affine:list', type: 'affine:list',
items: [], items: [],
blockId: model.id,
rect: { x: 0, y: 0, w: 0, h: 0 },
}; };
listItemNodes.forEach(listItemNode => { listItemNodes.forEach(listItemNode => {
@@ -77,10 +77,15 @@ class ListLayoutPainter implements BlockLayoutPainter {
return; return;
} }
if (!isListLayout(layout)) return; if (!isListLayout(layout)) {
console.warn(
'Expected list layout but received different format:',
layout
);
return;
}
const renderedPositions = new Set<string>(); const renderedPositions = new Set<string>();
layout.items.forEach(item => { layout.items.forEach(item => {
const fontSize = item.fontSize; const fontSize = item.fontSize;
const baselineY = getBaseline(fontSize); const baselineY = getBaseline(fontSize);
@@ -6,8 +6,9 @@ import {
segmentSentences, segmentSentences,
} from '@blocksuite/affine-gfx-turbo-renderer'; } from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di'; import type { Container } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std'; import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord } from '@blocksuite/std/gfx'; import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ParagraphLayout } from './paragraph-painter.worker'; 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 = const paragraphSelector =
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'; '.affine-paragraph-rich-text-wrapper [data-v-text="true"]';
const paragraphNodes = component.querySelectorAll(paragraphSelector); const paragraphNode = component.querySelector(paragraphSelector);
if (!paragraphNode) return null;
if (paragraphNodes.length === 0) return null;
const viewportRecord = component.gfx.viewport.deserializeRecord(
component.dataset.viewportState
);
if (!viewportRecord) return null;
const { zoom, viewScale } = viewportRecord; const { zoom, viewScale } = viewportRecord;
const paragraph: ParagraphLayout = { const paragraph: ParagraphLayout = {
type: 'affine:paragraph', type: 'affine:paragraph',
sentences: [], sentences: [],
blockId: model.id,
rect: { x: 0, y: 0, w: 0, h: 0 },
}; };
paragraphNodes.forEach(paragraphNode => { const computedStyle = window.getComputedStyle(paragraphNode);
const computedStyle = window.getComputedStyle(paragraphNode); const fontSizeStr = computedStyle.fontSize;
const fontSizeStr = computedStyle.fontSize; const fontSize = parseInt(fontSizeStr);
const fontSize = parseInt(fontSizeStr);
const sentences = segmentSentences(paragraphNode.textContent || ''); const sentences = segmentSentences(paragraphNode.textContent || '');
const sentenceLayouts = sentences.map(sentence => { const sentenceLayouts = sentences.map(sentence => {
const sentenceRects = getSentenceRects(paragraphNode, sentence); const sentenceRects = getSentenceRects(paragraphNode, sentence);
const rects = sentenceRects.map(({ text, rect }) => { const rects = sentenceRects.map(({ text, rect }) => {
const [modelX, modelY] = clientToModelCoord(viewportRecord, [ const [modelX, modelY] = clientToModelCoord(viewportRecord, [
rect.x, rect.x,
rect.y, rect.y,
]); ]);
return {
text,
rect: {
x: modelX,
y: modelY,
w: rect.w / zoom / viewScale,
h: rect.h / zoom / viewScale,
},
};
});
return { return {
text: sentence, text,
rects, rect: {
fontSize, x: modelX,
y: modelY,
w: rect.w / zoom / viewScale,
h: rect.h / zoom / viewScale,
},
}; };
}); });
return {
paragraph.sentences.push(...sentenceLayouts); text: sentence,
rects,
fontSize,
};
}); });
paragraph.sentences.push(...sentenceLayouts);
return paragraph; return paragraph;
} }
@@ -73,10 +73,15 @@ class ParagraphLayoutPainter implements BlockLayoutPainter {
return; 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>(); const renderedPositions = new Set<string>();
layout.sentences.forEach(sentence => { layout.sentences.forEach(sentence => {
const fontSize = sentence.fontSize; const fontSize = sentence.fontSize;
const baselineY = getBaseline(fontSize); const baselineY = getBaseline(fontSize);
@@ -1,5 +1,7 @@
import { createIdentifier } from '@blocksuite/global/di'; import { createIdentifier } from '@blocksuite/global/di';
import type { GfxBlockComponent } from '@blocksuite/std'; import type { EditorHost } from '@blocksuite/std';
import type { ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { Extension } from '@blocksuite/store'; import { Extension } from '@blocksuite/store';
import type { BlockLayout, Rect } from '../types'; import type { BlockLayout, Rect } from '../types';
@@ -8,7 +10,13 @@ export abstract class BlockLayoutHandlerExtension<
T extends BlockLayout = BlockLayout, T extends BlockLayout = BlockLayout,
> extends Extension { > extends Extension {
abstract readonly blockType: string; abstract readonly blockType: string;
abstract queryLayout(component: GfxBlockComponent): T | null;
abstract queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): T | null;
abstract calculateBound(layout: T): { abstract calculateBound(layout: T): {
rect: Rect; rect: Rect;
subRects: Rect[]; subRects: Rect[];
@@ -7,8 +7,9 @@ import type { ExtensionType } from '@blocksuite/store';
import type { import type {
BlockLayoutPainter, BlockLayoutPainter,
BlockLayoutTreeNode,
HostToWorkerMessage, HostToWorkerMessage,
ViewportLayout, ViewportLayoutTree,
WorkerToHostMessage, WorkerToHostMessage,
} from '../types'; } from '../types';
@@ -33,8 +34,8 @@ export class ViewportLayoutPainter {
private zoom = 1; private zoom = 1;
public provider: ServiceProvider; public provider: ServiceProvider;
getPainter(type: string): BlockLayoutPainter | undefined { getPainter(type: string): BlockLayoutPainter | null {
return this.provider.get(BlockPainterProvider(type)); return this.provider.getOptional(BlockPainterProvider(type));
} }
constructor(extensions: ExtensionType[]) { constructor(extensions: ExtensionType[]) {
@@ -66,24 +67,28 @@ export class ViewportLayoutPainter {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
} }
paint(layout: ViewportLayout, version: number) { paint(layout: ViewportLayoutTree, version: number) {
const { canvas, ctx } = this; const { canvas, ctx } = this;
if (!canvas || !ctx) return; if (!canvas || !ctx) return;
if (layout.rect.w === 0 || layout.rect.h === 0) {
console.warn('empty layout rect'); this.paintTree(layout, version);
return; }
}
paintTree(layout: ViewportLayoutTree, version: number) {
const { canvas, ctx } = this;
const { overallRect } = layout;
if (!canvas || !ctx) return;
this.clearBackground(); this.clearBackground();
ctx.scale(this.zoom, this.zoom); ctx.scale(this.zoom, this.zoom);
layout.blocks.forEach(blockLayout => { const paintNode = (node: BlockLayoutTreeNode) => {
const painter = this.getPainter(blockLayout.type); const painter = this.getPainter(node.type);
if (!painter) return; painter?.paint(ctx, node.layout, overallRect.x, overallRect.y);
painter.paint(ctx, blockLayout, layout.rect.x, layout.rect.y); node.children.forEach(paintNode);
}); };
layout.roots.forEach(root => paintNode(root));
const bitmap = canvas.transferToImageBitmap(); const bitmap = canvas.transferToImageBitmap();
const message: WorkerToHostMessage = { const message: WorkerToHostMessage = {
type: 'bitmapPainted', type: 'bitmapPainted',
@@ -1,12 +1,14 @@
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std'; import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { import { type Viewport } from '@blocksuite/std/gfx';
GfxBlockElementModel, import type { BlockModel } from '@blocksuite/store';
GfxControllerIdentifier,
type Viewport,
} from '@blocksuite/std/gfx';
import { BlockLayoutHandlersIdentifier } from './layout/block-layout-provider'; import { BlockLayoutHandlersIdentifier } from './layout/block-layout-provider';
import type { BlockLayout, RenderingState, ViewportLayout } from './types'; import type {
BlockLayout,
BlockLayoutTreeNode,
RenderingState,
ViewportLayoutTree,
} from './types';
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) { export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
const hostRect = host.getBoundingClientRect(); const hostRect = host.getBoundingClientRect();
@@ -21,33 +23,10 @@ export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
canvas.style.pointerEvents = 'none'; canvas.style.pointerEvents = 'none';
} }
function getBlockLayouts(host: EditorHost): BlockLayout[] { export function getViewportLayoutTree(
const gfx = host.std.get(GfxControllerIdentifier);
const models = gfx.gfxElements.filter(e => e instanceof GfxBlockElementModel);
const components = models
.map(model => gfx.view.get(model.id))
.filter(Boolean) as GfxBlockComponent[];
const layouts: BlockLayout[] = [];
components.forEach(component => {
const layoutHandlers = host.std.provider.getAll(
BlockLayoutHandlersIdentifier
);
const handlersArray = Array.from(layoutHandlers.values());
for (const handler of handlersArray) {
const layout = handler.queryLayout(component);
if (layout) {
layouts.push(layout);
}
}
});
return layouts;
}
export function getViewportLayout(
host: EditorHost, host: EditorHost,
viewport: Viewport viewport: Viewport
): ViewportLayout { ): ViewportLayoutTree {
const zoom = viewport.zoom; const zoom = viewport.zoom;
let layoutMinX = Infinity; let layoutMinX = Infinity;
@@ -55,36 +34,106 @@ export function getViewportLayout(
let layoutMaxX = -Infinity; let layoutMaxX = -Infinity;
let layoutMaxY = -Infinity; let layoutMaxY = -Infinity;
const blockLayouts = getBlockLayouts(host); const store = host.std.store;
const rootModel = store.root;
if (!rootModel) {
return { roots: [], overallRect: { x: 0, y: 0, w: 0, h: 0 } };
}
const providers = host.std.provider.getAll(BlockLayoutHandlersIdentifier); const providers = host.std.provider.getAll(BlockLayoutHandlersIdentifier);
const providersArray = Array.from(providers.values()); const providersArray = Array.from(providers.values());
blockLayouts.forEach(blockLayout => { // Recursive function to build the tree structure
const provider = providersArray.find(p => p.blockType === blockLayout.type); const buildLayoutTreeNode = (
if (!provider) return; model: BlockModel,
ancestorViewportState?: string | null
): BlockLayoutTreeNode | null => {
const baseLayout: BlockLayout = {
blockId: model.id,
type: model.flavour,
rect: { x: 0, y: 0, w: 0, h: 0 },
};
const { rect } = provider.calculateBound(blockLayout); const handler = providersArray.find(p => p.blockType === model.flavour);
layoutMinX = Math.min(layoutMinX, rect.x); // Determine the correct viewport state to use
layoutMinY = Math.min(layoutMinY, rect.y); const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w); const currentViewportState = component?.dataset.viewportState;
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h); const effectiveViewportState =
}); currentViewportState ?? ancestorViewportState;
const defaultViewportState = {
left: 0,
top: 0,
viewportX: 0,
viewportY: 0,
zoom: 1,
viewScale: 1,
};
const layoutModelCoord = [layoutMinX, layoutMinY]; const viewportRecord = effectiveViewportState
? viewport.deserializeRecord(effectiveViewportState) ||
defaultViewportState
: defaultViewportState;
const layoutData = handler?.queryLayout(model, host, viewportRecord);
if (handler && layoutData) {
const { rect } = handler.calculateBound(layoutData);
baseLayout.rect = rect;
layoutMinX = Math.min(layoutMinX, rect.x);
layoutMinY = Math.min(layoutMinY, rect.y);
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
}
const children: BlockLayoutTreeNode[] = [];
for (const childModel of model.children) {
const childNode = buildLayoutTreeNode(childModel, effectiveViewportState);
if (childNode) {
children.push(childNode);
}
}
// Create node for this block - ALWAYS return a node
// Return the node structure including the layout (either real or fallback)
return {
blockId: model.id,
type: model.flavour,
layout: layoutData ? { ...baseLayout, ...layoutData } : baseLayout,
children,
};
};
const roots: BlockLayoutTreeNode[] = [];
const rootNode = buildLayoutTreeNode(rootModel);
if (rootNode) {
roots.push(rootNode);
}
// If no valid layouts were found, use default values
if (layoutMinX === Infinity) {
layoutMinX = 0;
layoutMinY = 0;
layoutMaxX = 0;
layoutMaxY = 0;
}
// Calculate overall rectangle
const w = (layoutMaxX - layoutMinX) / zoom / viewport.viewScale; const w = (layoutMaxX - layoutMinX) / zoom / viewport.viewScale;
const h = (layoutMaxY - layoutMinY) / zoom / viewport.viewScale; const h = (layoutMaxY - layoutMinY) / zoom / viewport.viewScale;
const layout: ViewportLayout = {
blocks: blockLayouts, const result = {
rect: { roots,
x: layoutModelCoord[0], overallRect: {
y: layoutModelCoord[1], x: layoutMinX,
y: layoutMinY,
w: Math.max(w, 0), w: Math.max(w, 0),
h: Math.max(h, 0), h: Math.max(h, 0),
}, },
}; };
return layout;
return result;
} }
export function debugLog(message: string, state: RenderingState) { export function debugLog(message: string, state: RenderingState) {
@@ -98,14 +147,15 @@ export function debugLog(message: string, state: RenderingState) {
export function paintPlaceholder( export function paintPlaceholder(
host: EditorHost, host: EditorHost,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
layout: ViewportLayout | null, layout: ViewportLayoutTree | null,
viewport: Viewport viewport: Viewport
) { ) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx || !layout) return;
if (!layout) return;
const dpr = window.devicePixelRatio; const dpr = window.devicePixelRatio;
const layoutViewCoord = viewport.toViewCoord(layout.rect.x, layout.rect.y); const { overallRect } = layout;
const layoutViewCoord = viewport.toViewCoord(overallRect.x, overallRect.y);
const offsetX = layoutViewCoord[0]; const offsetX = layoutViewCoord[0];
const offsetY = layoutViewCoord[1]; const offsetY = layoutViewCoord[1];
@@ -120,30 +170,28 @@ export function paintPlaceholder(
); );
const handlersArray = Array.from(layoutHandlers.values()); const handlersArray = Array.from(layoutHandlers.values());
layout.blocks.forEach((blockLayout, blockIndex) => { const paintNode = (node: BlockLayoutTreeNode, depth: number = 0) => {
ctx.fillStyle = colors[blockIndex % colors.length]; const { layout: nodeLayout, type } = node;
const renderedPositions = new Set<string>(); const handler = handlersArray.find(h => h.blockType === type);
if (handler) {
const handler = handlersArray.find(h => h.blockType === blockLayout.type); ctx.fillStyle = colors[depth % colors.length];
if (!handler) return; const rect = nodeLayout.rect;
const { subRects } = handler.calculateBound(blockLayout); const x = ((rect.x - overallRect.x) * viewport.zoom + offsetX) * dpr;
const y = ((rect.y - overallRect.y) * viewport.zoom + offsetY) * dpr;
subRects.forEach(rect => {
const x = ((rect.x - layout.rect.x) * viewport.zoom + offsetX) * dpr;
const y = ((rect.y - layout.rect.y) * viewport.zoom + offsetY) * dpr;
const width = rect.w * viewport.zoom * dpr; const width = rect.w * viewport.zoom * dpr;
const height = rect.h * viewport.zoom * dpr; const height = rect.h * viewport.zoom * dpr;
const posKey = `${x},${y}`;
if (renderedPositions.has(posKey)) return;
ctx.fillRect(x, y, width, height); ctx.fillRect(x, y, width, height);
if (width > 10 && height > 5) { if (width > 10 && height > 5) {
ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)'; ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)';
ctx.strokeRect(x, y, width, height); ctx.strokeRect(x, y, width, height);
} }
}
renderedPositions.add(posKey); if (node.children.length > 0) {
}); node.children.forEach(childNode => paintNode(childNode, depth + 1));
}); }
};
layout.roots.forEach(rootNode => paintNode(rootNode));
} }
@@ -19,7 +19,7 @@ import { debounceTime } from 'rxjs/operators';
import { import {
debugLog, debugLog,
getViewportLayout, getViewportLayoutTree,
paintPlaceholder, paintPlaceholder,
syncCanvasSize, syncCanvasSize,
} from './renderer-utils'; } from './renderer-utils';
@@ -28,7 +28,7 @@ import type {
RendererOptions, RendererOptions,
RenderingState, RenderingState,
TurboRendererConfig, TurboRendererConfig,
ViewportLayout, ViewportLayoutTree,
WorkerToHostMessage, WorkerToHostMessage,
} from './types'; } from './types';
@@ -49,7 +49,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
public readonly canvas: HTMLCanvasElement = document.createElement('canvas'); public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
private readonly worker: Worker; private readonly worker: Worker;
private readonly disposables = new DisposableGroup(); private readonly disposables = new DisposableGroup();
private layoutCacheData: ViewportLayout | null = null; private layoutCacheData: ViewportLayoutTree | null = null;
private layoutVersion = 0; private layoutVersion = 0;
private bitmap: ImageBitmap | null = null; private bitmap: ImageBitmap | null = null;
private viewportElement: GfxViewportElement | null = null; private viewportElement: GfxViewportElement | null = null;
@@ -172,9 +172,9 @@ export class ViewportTurboRendererExtension extends GfxExtension {
get layoutCache() { get layoutCache() {
if (this.layoutCacheData) return this.layoutCacheData; if (this.layoutCacheData) return this.layoutCacheData;
const layout = getViewportLayout(this.std.host, this.viewport); const layoutTree = getViewportLayoutTree(this.std.host, this.viewport);
this.debugLog('Layout cache updated'); this.debugLog('Layout cache updated');
return (this.layoutCacheData = layout); return (this.layoutCacheData = layoutTree);
} }
async refresh() { async refresh() {
@@ -248,8 +248,8 @@ export class ViewportTurboRendererExtension extends GfxExtension {
type: 'paintLayout', type: 'paintLayout',
data: { data: {
layout, layout,
width: layout.rect.w, width: layout.overallRect.w,
height: layout.rect.h, height: layout.overallRect.h,
dpr, dpr,
zoom: this.viewport.zoom, zoom: this.viewport.zoom,
version: currentVersion, version: currentVersion,
@@ -316,17 +316,18 @@ export class ViewportTurboRendererExtension extends GfxExtension {
if (!ctx) return; if (!ctx) return;
this.clearCanvas(); this.clearCanvas();
const layoutViewCoord = this.viewport.toViewCoord( const layoutViewCoord = this.viewport.toViewCoord(
layout.rect.x, layout.overallRect.x,
layout.rect.y layout.overallRect.y
); );
ctx.drawImage( ctx.drawImage(
bitmap, bitmap,
layoutViewCoord[0] * window.devicePixelRatio, layoutViewCoord[0] * window.devicePixelRatio,
layoutViewCoord[1] * window.devicePixelRatio, layoutViewCoord[1] * window.devicePixelRatio,
layout.rect.w * window.devicePixelRatio * this.viewport.zoom, layout.overallRect.w * window.devicePixelRatio * this.viewport.zoom,
layout.rect.h * window.devicePixelRatio * this.viewport.zoom layout.overallRect.h * window.devicePixelRatio * this.viewport.zoom
); );
this.debugLog('Bitmap drawn to canvas'); this.debugLog('Bitmap drawn to canvas');
@@ -14,13 +14,14 @@ export interface ViewportState {
} }
export interface BlockLayout extends Record<string, unknown> { export interface BlockLayout extends Record<string, unknown> {
blockId: string;
type: string; type: string;
rect?: Rect; rect: {
} x: number;
y: number;
export interface ViewportLayout { w: number;
blocks: BlockLayout[]; h: number;
rect: Rect; };
} }
export interface TextRect { export interface TextRect {
@@ -60,7 +61,7 @@ export type WorkerToHostMessage = MessageBitmapPainted | MessagePaintError;
export type MessagePaint = { export type MessagePaint = {
type: 'paintLayout'; type: 'paintLayout';
data: { data: {
layout: ViewportLayout; layout: ViewportLayoutTree;
width: number; width: number;
height: number; height: number;
dpr: number; dpr: number;
@@ -89,3 +90,15 @@ export interface TurboRendererConfig {
} }
export type HostToWorkerMessage = MessagePaint; export type HostToWorkerMessage = MessagePaint;
export interface BlockLayoutTreeNode {
blockId: string;
type: string;
layout: BlockLayout;
children: BlockLayoutTreeNode[];
}
export interface ViewportLayoutTree {
roots: BlockLayoutTreeNode[];
overallRect: BlockLayout['rect'];
}
@@ -7,7 +7,7 @@ import {
} from '@blocksuite/affine/gfx/turbo-renderer'; } from '@blocksuite/affine/gfx/turbo-renderer';
function createPainterWorker() { function createPainterWorker() {
const worker = new Worker(getWorkerUrl('turbo-painter-entry.worker.js')); const worker = new Worker(getWorkerUrl('turbo-painter.worker.js'));
return worker; return worker;
} }