mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 01:29:31 +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,
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user