refactor(editor): generic layout type support for turbo renderer (#10766)
This PR refactored the turbo renderer architecture to support multiple block layout types. - New base class `BlockLayoutPainter` and `BlockLayoutProvider` are introduced for writing extendable per-block layout querying and painting logic. - Paragraph-specific lines are all moved into dedicated classes (`ParagraphLayoutProvider` and `ParagraphLayoutPainter`) under the `/variants/paragraph` dir. - The `renderer-utils.ts` doesn't contain paragraph-specific logic now. - The `text-utils.ts` is also now scoped for paragraph only. - Worker messages are now strongly typed. Upcoming PR should further implement the block registration system using extension API. The `variants` dir could still exist, since there will be similar rendering logic that can be reused among block types (i.e., between paragraph block and list block).
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blocksuite/affine-components": "workspace:*",
|
"@blocksuite/affine-components": "workspace:*",
|
||||||
|
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
|
||||||
"@blocksuite/affine-model": "workspace:*",
|
"@blocksuite/affine-model": "workspace:*",
|
||||||
"@blocksuite/affine-rich-text": "workspace:*",
|
"@blocksuite/affine-rich-text": "workspace:*",
|
||||||
"@blocksuite/affine-shared": "workspace:*",
|
"@blocksuite/affine-shared": "workspace:*",
|
||||||
@@ -33,7 +34,8 @@
|
|||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./effects": "./src/effects.ts"
|
"./effects": "./src/effects.ts",
|
||||||
|
"./turbo-painter": "./src/turbo/paragraph-painter.worker.ts"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ export * from './commands';
|
|||||||
export * from './paragraph-block.js';
|
export * from './paragraph-block.js';
|
||||||
export * from './paragraph-service.js';
|
export * from './paragraph-service.js';
|
||||||
export * from './paragraph-spec.js';
|
export * from './paragraph-spec.js';
|
||||||
|
export * from './turbo/paragraph-layout-provider.js';
|
||||||
|
export * from './turbo/paragraph-painter-config.js';
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer';
|
||||||
|
import {
|
||||||
|
BlockLayoutHandlerExtension,
|
||||||
|
BlockLayoutHandlersIdentifier,
|
||||||
|
getSentenceRects,
|
||||||
|
segmentSentences,
|
||||||
|
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||||
|
import type { GfxBlockComponent } from '@blocksuite/block-std';
|
||||||
|
import { clientToModelCoord } from '@blocksuite/block-std/gfx';
|
||||||
|
import type { Container } from '@blocksuite/global/di';
|
||||||
|
|
||||||
|
import type { ParagraphLayout } from './paragraph-painter.worker';
|
||||||
|
|
||||||
|
export class ParagraphLayoutHandlerExtension extends BlockLayoutHandlerExtension<ParagraphLayout> {
|
||||||
|
readonly blockType = 'affine:paragraph';
|
||||||
|
|
||||||
|
static override setup(di: Container) {
|
||||||
|
const layoutHandler = new ParagraphLayoutHandlerExtension();
|
||||||
|
di.addImpl(BlockLayoutHandlersIdentifier, layoutHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryLayout(component: GfxBlockComponent): ParagraphLayout | null {
|
||||||
|
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 { zoom, viewScale } = viewportRecord;
|
||||||
|
const paragraph: ParagraphLayout = {
|
||||||
|
type: 'affine:paragraph',
|
||||||
|
sentences: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
paragraphNodes.forEach(paragraphNode => {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
text: sentence,
|
||||||
|
rects,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
paragraph.sentences.push(...sentenceLayouts);
|
||||||
|
});
|
||||||
|
|
||||||
|
return paragraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBound(layout: ParagraphLayout) {
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
layout.sentences.forEach(sentence => {
|
||||||
|
sentence.rects.forEach(r => {
|
||||||
|
minX = Math.min(minX, r.rect.x);
|
||||||
|
minY = Math.min(minY, r.rect.y);
|
||||||
|
maxX = Math.max(maxX, r.rect.x + r.rect.w);
|
||||||
|
maxY = Math.max(maxY, r.rect.y + r.rect.h);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const rect: Rect = {
|
||||||
|
x: minX,
|
||||||
|
y: minY,
|
||||||
|
w: maxX - minX,
|
||||||
|
h: maxY - minY,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
rect,
|
||||||
|
subRects: layout.sentences.flatMap(s => s.rects.map(r => r.rect)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { BlockPainterConfigIdentifier } from '@blocksuite/affine-gfx-turbo-renderer';
|
||||||
|
import type { Container } from '@blocksuite/global/di';
|
||||||
|
import { Extension } from '@blocksuite/store';
|
||||||
|
|
||||||
|
export class ParagraphPaintConfigExtension extends Extension {
|
||||||
|
static override setup(di: Container) {
|
||||||
|
const config = {
|
||||||
|
type: 'affine:paragraph',
|
||||||
|
path: new URL(
|
||||||
|
'@blocksuite/affine-block-paragraph/turbo-painter',
|
||||||
|
import.meta.url
|
||||||
|
).href,
|
||||||
|
};
|
||||||
|
di.addImpl(BlockPainterConfigIdentifier, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import type {
|
||||||
|
BlockLayout,
|
||||||
|
BlockLayoutPainter,
|
||||||
|
TextRect,
|
||||||
|
WorkerToHostMessage,
|
||||||
|
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||||
|
|
||||||
|
interface SentenceLayout {
|
||||||
|
text: string;
|
||||||
|
rects: TextRect[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParagraphLayout extends BlockLayout {
|
||||||
|
type: 'affine:paragraph';
|
||||||
|
sentences: SentenceLayout[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
emSize: 2048,
|
||||||
|
hHeadAscent: 1984,
|
||||||
|
hHeadDescent: -494,
|
||||||
|
};
|
||||||
|
|
||||||
|
const debugSentenceBorder = false;
|
||||||
|
|
||||||
|
function getBaseline() {
|
||||||
|
const fontSize = 15;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isParagraphLayout(layout: BlockLayout): layout is ParagraphLayout {
|
||||||
|
return layout.type === 'affine:paragraph';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ParagraphLayoutPainter implements BlockLayoutPainter {
|
||||||
|
static readonly font = new FontFace(
|
||||||
|
'Inter',
|
||||||
|
`url(https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ8UA3.woff2)`
|
||||||
|
);
|
||||||
|
|
||||||
|
static fontLoaded = false;
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (typeof self !== 'undefined' && 'fonts' in self) {
|
||||||
|
// @ts-expect-error worker fonts API
|
||||||
|
self.fonts.add(ParagraphLayoutPainter.font);
|
||||||
|
|
||||||
|
ParagraphLayoutPainter.font
|
||||||
|
.load()
|
||||||
|
.then(() => {
|
||||||
|
ParagraphLayoutPainter.fontLoaded = true;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to load Inter font:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paint(
|
||||||
|
ctx: OffscreenCanvasRenderingContext2D,
|
||||||
|
layout: BlockLayout,
|
||||||
|
layoutBaseX: number,
|
||||||
|
layoutBaseY: number
|
||||||
|
): void {
|
||||||
|
if (!ParagraphLayoutPainter.fontLoaded) {
|
||||||
|
const message: WorkerToHostMessage = {
|
||||||
|
type: 'paintError',
|
||||||
|
error: 'Font not loaded',
|
||||||
|
blockType: 'affine:paragraph',
|
||||||
|
};
|
||||||
|
self.postMessage(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isParagraphLayout(layout)) return; // cast to ParagraphLayout
|
||||||
|
|
||||||
|
const fontSize = 15;
|
||||||
|
ctx.font = `300 ${fontSize}px Inter`;
|
||||||
|
const baselineY = getBaseline();
|
||||||
|
const renderedPositions = new Set<string>();
|
||||||
|
|
||||||
|
layout.sentences.forEach(sentence => {
|
||||||
|
ctx.strokeStyle = 'yellow';
|
||||||
|
sentence.rects.forEach(textRect => {
|
||||||
|
const x = textRect.rect.x - layoutBaseX;
|
||||||
|
const y = textRect.rect.y - layoutBaseY;
|
||||||
|
|
||||||
|
const posKey = `${x},${y}`;
|
||||||
|
// Only render if we haven't rendered at this position before
|
||||||
|
if (renderedPositions.has(posKey)) return;
|
||||||
|
|
||||||
|
if (debugSentenceBorder) {
|
||||||
|
ctx.strokeRect(x, y, textRect.rect.w, textRect.rect.h);
|
||||||
|
}
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
ctx.fillText(textRect.text, x, y + baselineY);
|
||||||
|
|
||||||
|
renderedPositions.add(posKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,13 +15,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blocksuite/block-std": "workspace:*",
|
"@blocksuite/block-std": "workspace:*",
|
||||||
"@blocksuite/global": "workspace:*",
|
"@blocksuite/global": "workspace:*",
|
||||||
|
"@blocksuite/store": "workspace:*",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"tweakpane": "^4.0.5"
|
"tweakpane": "^4.0.5"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts",
|
||||||
|
"./painter": "./src/painter/painter.worker.ts"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './turbo-renderer.js';
|
export * from './layout/block-layout-provider';
|
||||||
export * from './types.js';
|
export * from './text-utils';
|
||||||
|
export * from './turbo-renderer';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { GfxBlockComponent } from '@blocksuite/block-std';
|
||||||
|
import { createIdentifier } from '@blocksuite/global/di';
|
||||||
|
import { Extension } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { BlockLayout, Rect } from '../types';
|
||||||
|
|
||||||
|
export abstract class BlockLayoutHandlerExtension<
|
||||||
|
T extends BlockLayout = BlockLayout,
|
||||||
|
> extends Extension {
|
||||||
|
abstract readonly blockType: string;
|
||||||
|
abstract queryLayout(component: GfxBlockComponent): T | null;
|
||||||
|
abstract calculateBound(layout: T): {
|
||||||
|
rect: Rect;
|
||||||
|
subRects: Rect[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockLayoutHandlersIdentifier =
|
||||||
|
createIdentifier<BlockLayoutHandlerExtension>(
|
||||||
|
'BlockLayoutHandlersIdentifier'
|
||||||
|
);
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import { type ViewportLayout } from './types.js';
|
|
||||||
|
|
||||||
type WorkerMessagePaint = {
|
|
||||||
type: 'paintLayout';
|
|
||||||
data: {
|
|
||||||
layout: ViewportLayout;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
dpr: number;
|
|
||||||
zoom: number;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type WorkerMessage = WorkerMessagePaint;
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
emSize: 2048,
|
|
||||||
hHeadAscent: 1984,
|
|
||||||
hHeadDescent: -494,
|
|
||||||
};
|
|
||||||
|
|
||||||
const font = new FontFace(
|
|
||||||
'Inter',
|
|
||||||
`url(https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ8UA3.woff2)`
|
|
||||||
);
|
|
||||||
// @ts-expect-error worker env
|
|
||||||
self.fonts && self.fonts.add(font);
|
|
||||||
|
|
||||||
const debugSentenceBoarder = false;
|
|
||||||
|
|
||||||
function getBaseline() {
|
|
||||||
const fontSize = 15;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Layout painter in worker */
|
|
||||||
class LayoutPainter {
|
|
||||||
private readonly canvas: OffscreenCanvas = new OffscreenCanvas(0, 0);
|
|
||||||
private ctx: OffscreenCanvasRenderingContext2D | null = null;
|
|
||||||
private zoom = 1;
|
|
||||||
|
|
||||||
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: ViewportLayout, version: number) {
|
|
||||||
const { canvas, ctx } = this;
|
|
||||||
if (!canvas || !ctx) return;
|
|
||||||
if (layout.rect.w === 0 || layout.rect.h === 0) {
|
|
||||||
console.warn('empty layout rect');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearBackground();
|
|
||||||
|
|
||||||
ctx.scale(this.zoom, this.zoom);
|
|
||||||
|
|
||||||
// Track rendered positions to avoid duplicate rendering across all paragraphs and sentences
|
|
||||||
const renderedPositions = new Set<string>();
|
|
||||||
|
|
||||||
layout.paragraphs.forEach(paragraph => {
|
|
||||||
const fontSize = 15;
|
|
||||||
ctx.font = `300 ${fontSize}px Inter`;
|
|
||||||
const baselineY = getBaseline();
|
|
||||||
|
|
||||||
paragraph.sentences.forEach(sentence => {
|
|
||||||
ctx.strokeStyle = 'yellow';
|
|
||||||
sentence.rects.forEach(textRect => {
|
|
||||||
const x = textRect.rect.x - layout.rect.x;
|
|
||||||
const y = textRect.rect.y - layout.rect.y;
|
|
||||||
|
|
||||||
const posKey = `${x},${y}`;
|
|
||||||
// Only render if we haven't rendered at this position before
|
|
||||||
if (renderedPositions.has(posKey)) return;
|
|
||||||
|
|
||||||
if (debugSentenceBoarder) {
|
|
||||||
ctx.strokeRect(x, y, textRect.rect.w, textRect.rect.h);
|
|
||||||
}
|
|
||||||
ctx.fillStyle = 'black';
|
|
||||||
ctx.fillText(textRect.text, x, y + baselineY);
|
|
||||||
|
|
||||||
renderedPositions.add(posKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const bitmap = canvas.transferToImageBitmap();
|
|
||||||
self.postMessage(
|
|
||||||
{ type: 'bitmapPainted', bitmap, version },
|
|
||||||
{ transfer: [bitmap] }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const painter = new LayoutPainter();
|
|
||||||
let fontLoaded = false;
|
|
||||||
|
|
||||||
font
|
|
||||||
.load()
|
|
||||||
.then(() => {
|
|
||||||
fontLoaded = true;
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
|
||||||
const { type, data } = e.data;
|
|
||||||
|
|
||||||
if (!fontLoaded) {
|
|
||||||
await font.load();
|
|
||||||
fontLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'paintLayout': {
|
|
||||||
const { layout, width, height, dpr, zoom, version } = data;
|
|
||||||
painter.setSize(width, height, dpr, zoom);
|
|
||||||
painter.paint(layout, version);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type {
|
||||||
|
BlockLayoutPainter,
|
||||||
|
HostToWorkerMessage,
|
||||||
|
ViewportLayout,
|
||||||
|
WorkerToHostMessage,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
class BlockPainterRegistry {
|
||||||
|
private readonly painters = new Map<string, BlockLayoutPainter>();
|
||||||
|
|
||||||
|
register(type: string, painter: BlockLayoutPainter) {
|
||||||
|
this.painters.set(type, painter);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPainter(type: string): BlockLayoutPainter | undefined {
|
||||||
|
return this.painters.get(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewportLayoutPainter {
|
||||||
|
private readonly canvas: OffscreenCanvas = new OffscreenCanvas(0, 0);
|
||||||
|
private ctx: OffscreenCanvasRenderingContext2D | null = null;
|
||||||
|
private zoom = 1;
|
||||||
|
public readonly registry = new BlockPainterRegistry();
|
||||||
|
|
||||||
|
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: ViewportLayout, version: number) {
|
||||||
|
const { canvas, ctx } = this;
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
if (layout.rect.w === 0 || layout.rect.h === 0) {
|
||||||
|
console.warn('empty layout rect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearBackground();
|
||||||
|
|
||||||
|
ctx.scale(this.zoom, this.zoom);
|
||||||
|
|
||||||
|
layout.blocks.forEach(blockLayout => {
|
||||||
|
const painter = this.registry.getPainter(blockLayout.type);
|
||||||
|
if (!painter) return;
|
||||||
|
painter.paint(ctx, blockLayout, layout.rect.x, layout.rect.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bitmap = canvas.transferToImageBitmap();
|
||||||
|
const message: WorkerToHostMessage = {
|
||||||
|
type: 'bitmapPainted',
|
||||||
|
bitmap,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
self.postMessage(message, { transfer: [bitmap] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const painter = new ViewportLayoutPainter();
|
||||||
|
|
||||||
|
self.onmessage = async (e: MessageEvent<HostToWorkerMessage>) => {
|
||||||
|
const { type, data } = e.data;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'paintLayout': {
|
||||||
|
const { layout, width, height, dpr, zoom, version } = data;
|
||||||
|
painter.setSize(width, height, dpr, zoom);
|
||||||
|
painter.paint(layout, version);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'registerPainter': {
|
||||||
|
const { painterConfigs } = data;
|
||||||
|
painterConfigs.forEach(async ({ type, path }) => {
|
||||||
|
painter.registry.register(type, new (await import(path)).default());
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import type { EditorHost, GfxBlockComponent } from '@blocksuite/block-std';
|
import type { EditorHost, GfxBlockComponent } from '@blocksuite/block-std';
|
||||||
import {
|
import {
|
||||||
clientToModelCoord,
|
|
||||||
GfxBlockElementModel,
|
GfxBlockElementModel,
|
||||||
GfxControllerIdentifier,
|
GfxControllerIdentifier,
|
||||||
type Viewport,
|
type Viewport,
|
||||||
} from '@blocksuite/block-std/gfx';
|
} from '@blocksuite/block-std/gfx';
|
||||||
import { Pane } from 'tweakpane';
|
import { Pane } from 'tweakpane';
|
||||||
|
|
||||||
import { getSentenceRects, segmentSentences } from './text-utils.js';
|
import { BlockLayoutHandlersIdentifier } from './layout/block-layout-provider';
|
||||||
import type { ViewportTurboRendererExtension } from './turbo-renderer.js';
|
import type { ViewportTurboRendererExtension } from './turbo-renderer';
|
||||||
import type {
|
import type { BlockLayout, RenderingState, ViewportLayout } from './types';
|
||||||
ParagraphLayout,
|
|
||||||
RenderingState,
|
|
||||||
ViewportLayout,
|
|
||||||
} from './types.js';
|
|
||||||
|
|
||||||
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
|
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
|
||||||
const hostRect = host.getBoundingClientRect();
|
const hostRect = host.getBoundingClientRect();
|
||||||
@@ -28,58 +23,27 @@ export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
|
|||||||
canvas.style.pointerEvents = 'none';
|
canvas.style.pointerEvents = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getParagraphs(host: EditorHost) {
|
function getBlockLayouts(host: EditorHost): BlockLayout[] {
|
||||||
const gfx = host.std.get(GfxControllerIdentifier);
|
const gfx = host.std.get(GfxControllerIdentifier);
|
||||||
const models = gfx.gfxElements.filter(e => e instanceof GfxBlockElementModel);
|
const models = gfx.gfxElements.filter(e => e instanceof GfxBlockElementModel);
|
||||||
const components = models
|
const components = models
|
||||||
.map(model => gfx.view.get(model.id))
|
.map(model => gfx.view.get(model.id))
|
||||||
.filter(Boolean) as GfxBlockComponent[];
|
.filter(Boolean) as GfxBlockComponent[];
|
||||||
|
|
||||||
const paragraphs: ParagraphLayout[] = [];
|
const layouts: BlockLayout[] = [];
|
||||||
const selector = '.affine-paragraph-rich-text-wrapper [data-v-text="true"]';
|
|
||||||
|
|
||||||
components.forEach(component => {
|
components.forEach(component => {
|
||||||
const paragraphNodes = component.querySelectorAll(selector);
|
const layoutHandlers = host.std.provider.getAll(
|
||||||
const viewportRecord = component.gfx.viewport.deserializeRecord(
|
BlockLayoutHandlersIdentifier
|
||||||
component.dataset.viewportState
|
|
||||||
);
|
);
|
||||||
if (!viewportRecord) return;
|
const handlersArray = Array.from(layoutHandlers.values());
|
||||||
const { zoom, viewScale } = viewportRecord;
|
for (const handler of handlersArray) {
|
||||||
|
const layout = handler.queryLayout(component);
|
||||||
paragraphNodes.forEach(paragraphNode => {
|
if (layout) {
|
||||||
const paragraph: ParagraphLayout = {
|
layouts.push(layout);
|
||||||
sentences: [],
|
}
|
||||||
};
|
}
|
||||||
const sentences = segmentSentences(paragraphNode.textContent || '');
|
|
||||||
paragraph.sentences = 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,
|
|
||||||
rect: {
|
|
||||||
x: modelX,
|
|
||||||
y: modelY,
|
|
||||||
w: rect.w / zoom / viewScale,
|
|
||||||
h: rect.h / zoom / viewScale,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
return {
|
return layouts;
|
||||||
text: sentence,
|
|
||||||
rects,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
paragraphs.push(paragraph);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return paragraphs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getViewportLayout(
|
export function getViewportLayout(
|
||||||
@@ -93,23 +57,28 @@ export function getViewportLayout(
|
|||||||
let layoutMaxX = -Infinity;
|
let layoutMaxX = -Infinity;
|
||||||
let layoutMaxY = -Infinity;
|
let layoutMaxY = -Infinity;
|
||||||
|
|
||||||
const paragraphs = getParagraphs(host);
|
const blockLayouts = getBlockLayouts(host);
|
||||||
paragraphs.forEach(paragraph => {
|
|
||||||
paragraph.sentences.forEach(sentence => {
|
const providers = host.std.provider.getAll(BlockLayoutHandlersIdentifier);
|
||||||
sentence.rects.forEach(r => {
|
const providersArray = Array.from(providers.values());
|
||||||
layoutMinX = Math.min(layoutMinX, r.rect.x);
|
|
||||||
layoutMinY = Math.min(layoutMinY, r.rect.y);
|
blockLayouts.forEach(blockLayout => {
|
||||||
layoutMaxX = Math.max(layoutMaxX, r.rect.x + r.rect.w);
|
const provider = providersArray.find(p => p.blockType === blockLayout.type);
|
||||||
layoutMaxY = Math.max(layoutMaxY, r.rect.y + r.rect.h);
|
if (!provider) return;
|
||||||
});
|
|
||||||
});
|
const { rect } = provider.calculateBound(blockLayout);
|
||||||
|
|
||||||
|
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 layoutModelCoord = [layoutMinX, layoutMinY];
|
const layoutModelCoord = [layoutMinX, layoutMinY];
|
||||||
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 = {
|
const layout: ViewportLayout = {
|
||||||
paragraphs,
|
blocks: blockLayouts,
|
||||||
rect: {
|
rect: {
|
||||||
x: layoutModelCoord[0],
|
x: layoutModelCoord[0],
|
||||||
y: layoutModelCoord[1],
|
y: layoutModelCoord[1],
|
||||||
@@ -145,6 +114,7 @@ export function debugLog(message: string, state: RenderingState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function paintPlaceholder(
|
export function paintPlaceholder(
|
||||||
|
host: EditorHost,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
layout: ViewportLayout | null,
|
layout: ViewportLayout | null,
|
||||||
viewport: Viewport
|
viewport: Viewport
|
||||||
@@ -163,19 +133,25 @@ export function paintPlaceholder(
|
|||||||
'rgba(160, 160, 160, 0.7)',
|
'rgba(160, 160, 160, 0.7)',
|
||||||
];
|
];
|
||||||
|
|
||||||
layout.paragraphs.forEach((paragraph, paragraphIndex) => {
|
const layoutHandlers = host.std.provider.getAll(
|
||||||
ctx.fillStyle = colors[paragraphIndex % colors.length];
|
BlockLayoutHandlersIdentifier
|
||||||
|
);
|
||||||
|
const handlersArray = Array.from(layoutHandlers.values());
|
||||||
|
|
||||||
|
layout.blocks.forEach((blockLayout, blockIndex) => {
|
||||||
|
ctx.fillStyle = colors[blockIndex % colors.length];
|
||||||
const renderedPositions = new Set<string>();
|
const renderedPositions = new Set<string>();
|
||||||
|
|
||||||
paragraph.sentences.forEach(sentence => {
|
const handler = handlersArray.find(h => h.blockType === blockLayout.type);
|
||||||
sentence.rects.forEach(textRect => {
|
if (!handler) return;
|
||||||
const x =
|
const { subRects } = handler.calculateBound(blockLayout);
|
||||||
((textRect.rect.x - layout.rect.x) * viewport.zoom + offsetX) * dpr;
|
|
||||||
const y =
|
subRects.forEach(rect => {
|
||||||
((textRect.rect.y - layout.rect.y) * viewport.zoom + offsetY) * dpr;
|
const x = ((rect.x - layout.rect.x) * viewport.zoom + offsetX) * dpr;
|
||||||
dpr;
|
const y = ((rect.y - layout.rect.y) * viewport.zoom + offsetY) * dpr;
|
||||||
const width = textRect.rect.w * viewport.zoom * dpr;
|
|
||||||
const height = textRect.rect.h * viewport.zoom * dpr;
|
const width = rect.w * viewport.zoom * dpr;
|
||||||
|
const height = rect.h * viewport.zoom * dpr;
|
||||||
|
|
||||||
const posKey = `${x},${y}`;
|
const posKey = `${x},${y}`;
|
||||||
if (renderedPositions.has(posKey)) return;
|
if (renderedPositions.has(posKey)) return;
|
||||||
@@ -188,5 +164,4 @@ export function paintPlaceholder(
|
|||||||
renderedPositions.add(posKey);
|
renderedPositions.add(posKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TextRect } from './types.js';
|
import type { TextRect } from './types';
|
||||||
|
|
||||||
interface WordSegment {
|
interface WordSegment {
|
||||||
text: string;
|
text: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ConfigExtensionFactory,
|
||||||
LifeCycleWatcher,
|
LifeCycleWatcher,
|
||||||
LifeCycleWatcherIdentifier,
|
LifeCycleWatcherIdentifier,
|
||||||
StdIdentifier,
|
StdIdentifier,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
type GfxViewportElement,
|
type GfxViewportElement,
|
||||||
} from '@blocksuite/block-std/gfx';
|
} from '@blocksuite/block-std/gfx';
|
||||||
import type { Container, ServiceIdentifier } from '@blocksuite/global/di';
|
import type { Container, ServiceIdentifier } from '@blocksuite/global/di';
|
||||||
|
import { createIdentifier } from '@blocksuite/global/di';
|
||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
|
|
||||||
@@ -17,13 +19,31 @@ import {
|
|||||||
initTweakpane,
|
initTweakpane,
|
||||||
paintPlaceholder,
|
paintPlaceholder,
|
||||||
syncCanvasSize,
|
syncCanvasSize,
|
||||||
} from './renderer-utils.js';
|
} from './renderer-utils';
|
||||||
import type { RenderingState, ViewportLayout } from './types.js';
|
import type {
|
||||||
|
BlockPainterConfig,
|
||||||
|
MessagePaint,
|
||||||
|
MessageRegisterPainter,
|
||||||
|
RendererOptions,
|
||||||
|
RenderingState,
|
||||||
|
TurboRendererConfig,
|
||||||
|
ViewportLayout,
|
||||||
|
WorkerToHostMessage,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
const debug = false; // Toggle for debug logs
|
const debug = false; // Toggle for debug logs
|
||||||
const zoomThreshold = 1; // With high enough zoom, fallback to DOM rendering
|
const workerUrl = new URL('./painter/painter.worker.ts', import.meta.url);
|
||||||
const debounceTime = 1000; // During this period, fallback to DOM
|
|
||||||
const workerUrl = new URL('./painter.worker.ts', import.meta.url);
|
const defaultOptions: RendererOptions = {
|
||||||
|
zoomThreshold: 1, // With high enough zoom, fallback to DOM rendering
|
||||||
|
debounceTime: 1000, // During this period, fallback to DOM
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlockPainterConfigIdentifier =
|
||||||
|
createIdentifier<BlockPainterConfig>('block-painter-config');
|
||||||
|
|
||||||
|
export const TurboRendererConfigFactory =
|
||||||
|
ConfigExtensionFactory<TurboRendererConfig>('viewport-turbo-renderer');
|
||||||
|
|
||||||
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
||||||
public state: RenderingState = 'inactive';
|
public state: RenderingState = 'inactive';
|
||||||
@@ -39,6 +59,22 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
|
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get options(): RendererOptions {
|
||||||
|
const id = TurboRendererConfigFactory.identifier;
|
||||||
|
const { options } = this.std.getOptional(id) || {};
|
||||||
|
return {
|
||||||
|
...defaultOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get painterConfigs() {
|
||||||
|
const painterConfigMap = this.std.provider.getAll(
|
||||||
|
BlockPainterConfigIdentifier
|
||||||
|
);
|
||||||
|
return Array.from(painterConfigMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
const mountPoint = document.querySelector('.affine-edgeless-viewport');
|
const mountPoint = document.querySelector('.affine-edgeless-viewport');
|
||||||
if (mountPoint) {
|
if (mountPoint) {
|
||||||
@@ -80,6 +116,13 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.std.store.slots.blockUpdated.subscribe(() => this.invalidate())
|
this.std.store.slots.blockUpdated.subscribe(() => this.invalidate())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const painterConfigs = this.painterConfigs;
|
||||||
|
const message: MessageRegisterPainter = {
|
||||||
|
type: 'registerPainter',
|
||||||
|
data: { painterConfigs },
|
||||||
|
};
|
||||||
|
this.worker.postMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
override unmounted() {
|
override unmounted() {
|
||||||
@@ -116,7 +159,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
|
|
||||||
this.clearCanvas();
|
this.clearCanvas();
|
||||||
// -> pending
|
// -> pending
|
||||||
if (this.viewport.zoom > zoomThreshold) {
|
if (this.viewport.zoom > this.options.zoomThreshold) {
|
||||||
this.debugLog('Zoom above threshold, falling back to DOM rendering');
|
this.debugLog('Zoom above threshold, falling back to DOM rendering');
|
||||||
this.setState('pending');
|
this.setState('pending');
|
||||||
this.clearOptimizedBlocks();
|
this.clearOptimizedBlocks();
|
||||||
@@ -146,7 +189,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
|
|
||||||
debouncedRefresh = debounce(() => {
|
debouncedRefresh = debounce(() => {
|
||||||
this.refresh().catch(console.error);
|
this.refresh().catch(console.error);
|
||||||
}, debounceTime);
|
}, this.options.debounceTime);
|
||||||
|
|
||||||
invalidate() {
|
invalidate() {
|
||||||
this.layoutVersion++;
|
this.layoutVersion++;
|
||||||
@@ -179,7 +222,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
const currentVersion = this.layoutVersion;
|
const currentVersion = this.layoutVersion;
|
||||||
|
|
||||||
this.debugLog(`Requesting bitmap painting (version=${currentVersion})`);
|
this.debugLog(`Requesting bitmap painting (version=${currentVersion})`);
|
||||||
this.worker.postMessage({
|
const message: MessagePaint = {
|
||||||
type: 'paintLayout',
|
type: 'paintLayout',
|
||||||
data: {
|
data: {
|
||||||
layout,
|
layout,
|
||||||
@@ -189,9 +232,10 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
zoom: this.viewport.zoom,
|
zoom: this.viewport.zoom,
|
||||||
version: currentVersion,
|
version: currentVersion,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
this.worker.postMessage(message);
|
||||||
|
|
||||||
this.worker.onmessage = (e: MessageEvent) => {
|
this.worker.onmessage = (e: MessageEvent<WorkerToHostMessage>) => {
|
||||||
if (e.data.type === 'bitmapPainted') {
|
if (e.data.type === 'bitmapPainted') {
|
||||||
if (e.data.version === this.layoutVersion) {
|
if (e.data.version === this.layoutVersion) {
|
||||||
this.debugLog(
|
this.debugLog(
|
||||||
@@ -209,6 +253,12 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
this.setState('pending');
|
this.setState('pending');
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
} else if (e.data.type === 'paintError') {
|
||||||
|
this.debugLog(
|
||||||
|
`Paint error: ${e.data.error} for blockType: ${e.data.blockType}`
|
||||||
|
);
|
||||||
|
this.setState('pending');
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -267,7 +317,8 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private canOptimize(): boolean {
|
private canOptimize(): boolean {
|
||||||
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
|
const isBelowZoomThreshold =
|
||||||
|
this.viewport.zoom <= this.options.zoomThreshold;
|
||||||
return (
|
return (
|
||||||
(this.state === 'ready' || this.state === 'zooming') &&
|
(this.state === 'ready' || this.state === 'zooming') &&
|
||||||
isBelowZoomThreshold
|
isBelowZoomThreshold
|
||||||
@@ -297,7 +348,12 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private paintPlaceholder() {
|
private paintPlaceholder() {
|
||||||
paintPlaceholder(this.canvas, this.layoutCache, this.viewport);
|
paintPlaceholder(
|
||||||
|
this.std.host,
|
||||||
|
this.canvas,
|
||||||
|
this.layoutCache,
|
||||||
|
this.viewport
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,13 @@ export interface ViewportState {
|
|||||||
viewportY: number;
|
viewportY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SentenceLayout {
|
export interface BlockLayout extends Record<string, unknown> {
|
||||||
text: string;
|
type: string;
|
||||||
rects: TextRect[];
|
rect?: Rect;
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParagraphLayout {
|
|
||||||
sentences: SentenceLayout[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewportLayout {
|
export interface ViewportLayout {
|
||||||
paragraphs: ParagraphLayout[];
|
blocks: BlockLayout[];
|
||||||
rect: Rect;
|
rect: Rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,3 +42,61 @@ export type RenderingState =
|
|||||||
| 'zooming'
|
| 'zooming'
|
||||||
| 'rendering'
|
| 'rendering'
|
||||||
| 'ready';
|
| 'ready';
|
||||||
|
|
||||||
|
export type MessageBitmapPainted = {
|
||||||
|
type: 'bitmapPainted';
|
||||||
|
bitmap: ImageBitmap;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessagePaintError = {
|
||||||
|
type: 'paintError';
|
||||||
|
error: string;
|
||||||
|
blockType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkerToHostMessage = MessageBitmapPainted | MessagePaintError;
|
||||||
|
|
||||||
|
export type MessagePaint = {
|
||||||
|
type: 'paintLayout';
|
||||||
|
data: {
|
||||||
|
layout: ViewportLayout;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
dpr: number;
|
||||||
|
zoom: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BlockLayoutPainter {
|
||||||
|
paint(
|
||||||
|
ctx: OffscreenCanvasRenderingContext2D,
|
||||||
|
block: BlockLayout,
|
||||||
|
layoutBaseX: number,
|
||||||
|
layoutBaseY: number
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RendererOptions {
|
||||||
|
zoomThreshold: number;
|
||||||
|
debounceTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockPainterConfig {
|
||||||
|
type: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TurboRendererConfig {
|
||||||
|
options?: Partial<RendererOptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageRegisterPainter = {
|
||||||
|
type: 'registerPainter';
|
||||||
|
data: {
|
||||||
|
painterConfigs: BlockPainterConfig[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HostToWorkerMessage = MessagePaint | MessageRegisterPainter;
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
ParagraphLayoutHandlerExtension,
|
||||||
|
ParagraphPaintConfigExtension,
|
||||||
|
} from '@blocksuite/affine/blocks/paragraph';
|
||||||
|
import {
|
||||||
|
TurboRendererConfigFactory,
|
||||||
ViewportTurboRendererExtension,
|
ViewportTurboRendererExtension,
|
||||||
ViewportTurboRendererIdentifier,
|
ViewportTurboRendererIdentifier,
|
||||||
} from '@blocksuite/affine/gfx/turbo-renderer';
|
} from '@blocksuite/affine/gfx/turbo-renderer';
|
||||||
@@ -7,7 +12,17 @@ import { addSampleNotes } from './doc-generator.js';
|
|||||||
import { setupEditor } from './setup.js';
|
import { setupEditor } from './setup.js';
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
setupEditor('edgeless', [ViewportTurboRendererExtension]);
|
setupEditor('edgeless', [
|
||||||
|
ParagraphLayoutHandlerExtension,
|
||||||
|
ParagraphPaintConfigExtension,
|
||||||
|
TurboRendererConfigFactory({
|
||||||
|
options: {
|
||||||
|
zoomThreshold: 1,
|
||||||
|
debounceTime: 1000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ViewportTurboRendererExtension,
|
||||||
|
]);
|
||||||
addSampleNotes(doc, 100);
|
addSampleNotes(doc, 100);
|
||||||
doc.load();
|
doc.load();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { MemberSearchService } from '@affine/core/modules/permissions';
|
|||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
import type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
|
import type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
|
||||||
import { ViewportTurboRendererExtension } from '@blocksuite/affine/gfx/turbo-renderer';
|
|
||||||
import type { DocMode } from '@blocksuite/affine/model';
|
import type { DocMode } from '@blocksuite/affine/model';
|
||||||
import type { Store } from '@blocksuite/affine/store';
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +68,7 @@ import {
|
|||||||
type ReferenceReactRenderer,
|
type ReferenceReactRenderer,
|
||||||
} from '../extensions/reference-renderer';
|
} from '../extensions/reference-renderer';
|
||||||
import { patchSideBarService } from '../extensions/side-bar-service';
|
import { patchSideBarService } from '../extensions/side-bar-service';
|
||||||
|
import { patchTurboRendererExtension } from '../extensions/turbo-renderer';
|
||||||
import { patchUserExtensions } from '../extensions/user';
|
import { patchUserExtensions } from '../extensions/user';
|
||||||
import { patchUserListExtensions } from '../extensions/user-list';
|
import { patchUserListExtensions } from '../extensions/user-list';
|
||||||
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
||||||
@@ -169,7 +169,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
mode === 'edgeless' && enableTurboRenderer
|
mode === 'edgeless' && enableTurboRenderer
|
||||||
? [ViewportTurboRendererExtension]
|
? patchTurboRendererExtension()
|
||||||
: [],
|
: [],
|
||||||
].flat()
|
].flat()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
ParagraphLayoutHandlerExtension,
|
||||||
|
ParagraphPaintConfigExtension,
|
||||||
|
} from '@blocksuite/affine/blocks/paragraph';
|
||||||
|
import {
|
||||||
|
TurboRendererConfigFactory,
|
||||||
|
ViewportTurboRendererExtension,
|
||||||
|
} from '@blocksuite/affine/gfx/turbo-renderer';
|
||||||
|
|
||||||
|
export function patchTurboRendererExtension() {
|
||||||
|
return [
|
||||||
|
ParagraphLayoutHandlerExtension,
|
||||||
|
ParagraphPaintConfigExtension,
|
||||||
|
TurboRendererConfigFactory({
|
||||||
|
options: {
|
||||||
|
zoomThreshold: 1,
|
||||||
|
debounceTime: 1000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ViewportTurboRendererExtension,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -2593,6 +2593,7 @@ __metadata:
|
|||||||
resolution: "@blocksuite/affine-block-paragraph@workspace:blocksuite/affine/blocks/block-paragraph"
|
resolution: "@blocksuite/affine-block-paragraph@workspace:blocksuite/affine/blocks/block-paragraph"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@blocksuite/affine-components": "workspace:*"
|
"@blocksuite/affine-components": "workspace:*"
|
||||||
|
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*"
|
||||||
"@blocksuite/affine-model": "workspace:*"
|
"@blocksuite/affine-model": "workspace:*"
|
||||||
"@blocksuite/affine-rich-text": "workspace:*"
|
"@blocksuite/affine-rich-text": "workspace:*"
|
||||||
"@blocksuite/affine-shared": "workspace:*"
|
"@blocksuite/affine-shared": "workspace:*"
|
||||||
@@ -2900,6 +2901,7 @@ __metadata:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@blocksuite/block-std": "workspace:*"
|
"@blocksuite/block-std": "workspace:*"
|
||||||
"@blocksuite/global": "workspace:*"
|
"@blocksuite/global": "workspace:*"
|
||||||
|
"@blocksuite/store": "workspace:*"
|
||||||
"@types/lodash-es": "npm:^4.17.12"
|
"@types/lodash-es": "npm:^4.17.12"
|
||||||
lodash-es: "npm:^4.17.21"
|
lodash-es: "npm:^4.17.21"
|
||||||
rxjs: "npm:^7.8.1"
|
rxjs: "npm:^7.8.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user