From d021e4cddc719cc8aa7532fe322a935a519ba59b Mon Sep 17 00:00:00 2001
From: doodlewind <7312949+doodlewind@users.noreply.github.com>
Date: Tue, 11 Feb 2025 14:12:41 +0000
Subject: [PATCH] refactor(editor): mount worker renderer in editor host
(#10055)
This would allow for easier integration with current test runner, since the two column layout is removed.
The `ViewportTurboRender` canvas and its debug UI are only enabled if the extension is added, which won't affect the AFFiNE entry.
---
blocksuite/affine/shared/package.json | 1 +
.../src/viewport-renderer/painter.worker.ts | 28 ++++++++--
.../viewport-renderer/viewport-renderer.ts | 56 +++++++++++++++++--
.../playground/examples/renderer/editor.ts | 7 ++-
.../playground/examples/renderer/index.html | 34 +----------
.../playground/examples/renderer/main.ts | 56 +------------------
yarn.lock | 3 +-
7 files changed, 84 insertions(+), 101 deletions(-)
diff --git a/blocksuite/affine/shared/package.json b/blocksuite/affine/shared/package.json
index 684d610d50..dfe905b26d 100644
--- a/blocksuite/affine/shared/package.json
+++ b/blocksuite/affine/shared/package.json
@@ -46,6 +46,7 @@
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
+ "tweakpane": "^4.0.5",
"unified": "^11.0.5",
"yjs": "^13.6.21",
"zod": "^3.23.8"
diff --git a/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts b/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts
index 5ee60505c0..be7c13958f 100644
--- a/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts
+++ b/blocksuite/affine/shared/src/viewport-renderer/painter.worker.ts
@@ -31,7 +31,6 @@ const font = new FontFace(
);
// @ts-expect-error worker env
self.fonts && self.fonts.add(font);
-font.load().catch(console.error);
function getBaseline() {
const fontSize = 15;
@@ -57,9 +56,15 @@ class SectionPainter {
this.canvas = new OffscreenCanvas(width, height);
this.ctx = this.canvas.getContext('2d')!;
this.ctx.scale(dpr, dpr);
- this.ctx.fillStyle = 'lightgrey';
- this.ctx.fillRect(0, 0, width, height);
this.zoom = zoom;
+ this.clearBackground();
+ }
+
+ private clearBackground() {
+ if (!this.canvas || !this.ctx) return;
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.ctx.fillStyle = 'white';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
paint(section: SectionLayout) {
@@ -70,6 +75,8 @@ class SectionPainter {
return;
}
+ this.clearBackground();
+
ctx.scale(this.zoom, this.zoom);
// Track rendered positions to avoid duplicate rendering across all paragraphs and sentences
@@ -105,9 +112,23 @@ class SectionPainter {
}
const painter = new SectionPainter();
+let fontLoaded = false;
+
+font
+ .load()
+ .then(() => {
+ fontLoaded = true;
+ })
+ .catch(console.error);
self.onmessage = async (e: MessageEvent) => {
const { type, data } = e.data;
+
+ if (!fontLoaded) {
+ await font.load();
+ fontLoaded = true;
+ }
+
switch (type) {
case 'initSection': {
const { width, height, dpr, zoom } = data;
@@ -115,7 +136,6 @@ self.onmessage = async (e: MessageEvent) => {
break;
}
case 'paintSection': {
- await font.load();
const { section } = data;
painter.paint(section);
break;
diff --git a/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts b/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts
index b84a9fb68c..ea53fe0d18 100644
--- a/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts
+++ b/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts
@@ -6,6 +6,7 @@ import {
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { type Container, type ServiceIdentifier } from '@blocksuite/global/di';
+import { Pane } from 'tweakpane';
import { getSentenceRects, segmentSentences } from './text-utils.js';
import { type ParagraphLayout, type SectionLayout } from './types.js';
@@ -24,6 +25,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
private lastZoom: number | null = null;
private lastSection: SectionLayout | null = null;
private lastBitmap: ImageBitmap | null = null;
+ private debugPane: Pane | null = null;
constructor(std: BlockStdScope) {
super(std);
@@ -33,20 +35,57 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
}
override mounted() {
- const targetContainer = document.querySelector('#right-column')!;
- if (!targetContainer.querySelector('canvas')) {
- targetContainer.append(this.canvas);
+ const viewportElement = document.querySelector('.affine-edgeless-viewport');
+ if (viewportElement) {
+ viewportElement.append(this.canvas);
+ this.debugPane = new Pane({ container: viewportElement as HTMLElement });
+ this.initTweakpane();
}
+ this.viewport.viewportUpdated.on(async () => {
+ await this.render();
+ });
+
+ document.fonts.load('15px Inter').then(async () => {
+ await this.render();
+ });
}
override unmounted() {
if (this.lastBitmap) {
this.lastBitmap.close();
}
+ if (this.debugPane) {
+ this.debugPane.dispose();
+ this.debugPane = null;
+ }
this.worker.terminate();
this.canvas.remove();
}
+ private initTweakpane() {
+ if (!this.debugPane) return;
+
+ const paneElement = this.debugPane.element;
+ paneElement.style.position = 'absolute';
+ paneElement.style.top = '10px';
+ paneElement.style.left = '10px';
+ paneElement.style.width = '250px';
+
+ this.debugPane.title = 'Viewport Turbo Renderer';
+
+ const params = {
+ enabled: true,
+ };
+
+ this.debugPane
+ .addBinding(params, 'enabled', {
+ label: 'Enable',
+ })
+ .on('change', ({ value }) => {
+ this.canvas.style.display = value ? 'block' : 'none';
+ });
+ }
+
get viewport() {
return this.std.get(GfxControllerIdentifier).viewport;
}
@@ -56,6 +95,8 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
}
getHostLayout() {
+ if (!document.fonts.check('15px Inter')) return null;
+
const paragraphBlocks = this.std.host.querySelectorAll(
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
);
@@ -169,10 +210,14 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
private syncCanvasSize() {
const hostRect = this.getHostRect();
const dpr = window.devicePixelRatio;
- this.canvas.style.width = `${hostRect.width}px`;
- this.canvas.style.height = `${hostRect.height}px`;
+ this.canvas.style.position = 'absolute';
+ this.canvas.style.left = '0px';
+ this.canvas.style.top = '0px';
+ this.canvas.style.width = '100%';
+ this.canvas.style.height = '100%';
this.canvas.width = hostRect.width * dpr;
this.canvas.height = hostRect.height * dpr;
+ this.canvas.style.pointerEvents = 'none';
}
private updateCacheState(section: SectionLayout, bitmapCopy: ImageBitmap) {
@@ -225,7 +270,6 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
public async render(): Promise {
const hostLayout = this.getHostLayout();
-
if (!hostLayout) return;
const { section } = hostLayout;
diff --git a/blocksuite/playground/examples/renderer/editor.ts b/blocksuite/playground/examples/renderer/editor.ts
index cb6ef5a4f6..192f163a1b 100644
--- a/blocksuite/playground/examples/renderer/editor.ts
+++ b/blocksuite/playground/examples/renderer/editor.ts
@@ -12,10 +12,11 @@ presetsEffects();
export const doc = createEmptyDoc().init();
export const editor = new AffineEditorContainer();
-editor.pageSpecs = editor.pageSpecs.concat([ViewportTurboRendererExtension]);
-editor.edgelessSpecs = editor.edgelessSpecs.concat([
+editor.pageSpecs = [...editor.pageSpecs, ViewportTurboRendererExtension];
+editor.edgelessSpecs = [
+ ...editor.edgelessSpecs,
ViewportTurboRendererExtension,
-]);
+];
editor.doc = doc;
editor.mode = 'edgeless';
diff --git a/blocksuite/playground/examples/renderer/index.html b/blocksuite/playground/examples/renderer/index.html
index 8f39feffc6..2cc387fc4b 100644
--- a/blocksuite/playground/examples/renderer/index.html
+++ b/blocksuite/playground/examples/renderer/index.html
@@ -15,45 +15,15 @@
background-color: var(--affine-white-90);
transition: background-color 0.3s;
}
-
#container {
- display: flex;
+ position: relative;
width: 100%;
height: 100%;
}
-
- #left-column {
- flex: 1;
- padding: 20px;
- border-right: 1px solid #e0e0e0;
- }
-
- #right-column {
- flex: 1;
- padding: 20px;
- position: relative;
- display: flex;
- align-items: flex-end;
- justify-content: center;
- }
-
- .top-bar {
- position: absolute;
- top: 0;
- left: 0;
- padding: 5px;
- }
-
+