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. <img width="945" alt="image" src="https://github.com/user-attachments/assets/dc82daa4-cbed-4eb9-9660-28c3f7d35722" />
This commit is contained in:
@@ -46,6 +46,7 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-stringify": "^11.0.0",
|
"remark-stringify": "^11.0.0",
|
||||||
|
"tweakpane": "^4.0.5",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"yjs": "^13.6.21",
|
"yjs": "^13.6.21",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const font = new FontFace(
|
|||||||
);
|
);
|
||||||
// @ts-expect-error worker env
|
// @ts-expect-error worker env
|
||||||
self.fonts && self.fonts.add(font);
|
self.fonts && self.fonts.add(font);
|
||||||
font.load().catch(console.error);
|
|
||||||
|
|
||||||
function getBaseline() {
|
function getBaseline() {
|
||||||
const fontSize = 15;
|
const fontSize = 15;
|
||||||
@@ -57,9 +56,15 @@ class SectionPainter {
|
|||||||
this.canvas = new OffscreenCanvas(width, height);
|
this.canvas = new OffscreenCanvas(width, height);
|
||||||
this.ctx = this.canvas.getContext('2d')!;
|
this.ctx = this.canvas.getContext('2d')!;
|
||||||
this.ctx.scale(dpr, dpr);
|
this.ctx.scale(dpr, dpr);
|
||||||
this.ctx.fillStyle = 'lightgrey';
|
|
||||||
this.ctx.fillRect(0, 0, width, height);
|
|
||||||
this.zoom = zoom;
|
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) {
|
paint(section: SectionLayout) {
|
||||||
@@ -70,6 +75,8 @@ class SectionPainter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.clearBackground();
|
||||||
|
|
||||||
ctx.scale(this.zoom, this.zoom);
|
ctx.scale(this.zoom, this.zoom);
|
||||||
|
|
||||||
// Track rendered positions to avoid duplicate rendering across all paragraphs and sentences
|
// Track rendered positions to avoid duplicate rendering across all paragraphs and sentences
|
||||||
@@ -105,9 +112,23 @@ class SectionPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const painter = new SectionPainter();
|
const painter = new SectionPainter();
|
||||||
|
let fontLoaded = false;
|
||||||
|
|
||||||
|
font
|
||||||
|
.load()
|
||||||
|
.then(() => {
|
||||||
|
fontLoaded = true;
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
||||||
const { type, data } = e.data;
|
const { type, data } = e.data;
|
||||||
|
|
||||||
|
if (!fontLoaded) {
|
||||||
|
await font.load();
|
||||||
|
fontLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'initSection': {
|
case 'initSection': {
|
||||||
const { width, height, dpr, zoom } = data;
|
const { width, height, dpr, zoom } = data;
|
||||||
@@ -115,7 +136,6 @@ self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'paintSection': {
|
case 'paintSection': {
|
||||||
await font.load();
|
|
||||||
const { section } = data;
|
const { section } = data;
|
||||||
painter.paint(section);
|
painter.paint(section);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '@blocksuite/block-std';
|
} from '@blocksuite/block-std';
|
||||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||||
import { type Container, type ServiceIdentifier } from '@blocksuite/global/di';
|
import { type Container, type ServiceIdentifier } from '@blocksuite/global/di';
|
||||||
|
import { Pane } from 'tweakpane';
|
||||||
|
|
||||||
import { getSentenceRects, segmentSentences } from './text-utils.js';
|
import { getSentenceRects, segmentSentences } from './text-utils.js';
|
||||||
import { type ParagraphLayout, type SectionLayout } from './types.js';
|
import { type ParagraphLayout, type SectionLayout } from './types.js';
|
||||||
@@ -24,6 +25,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
private lastZoom: number | null = null;
|
private lastZoom: number | null = null;
|
||||||
private lastSection: SectionLayout | null = null;
|
private lastSection: SectionLayout | null = null;
|
||||||
private lastBitmap: ImageBitmap | null = null;
|
private lastBitmap: ImageBitmap | null = null;
|
||||||
|
private debugPane: Pane | null = null;
|
||||||
|
|
||||||
constructor(std: BlockStdScope) {
|
constructor(std: BlockStdScope) {
|
||||||
super(std);
|
super(std);
|
||||||
@@ -33,20 +35,57 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
const targetContainer = document.querySelector('#right-column')!;
|
const viewportElement = document.querySelector('.affine-edgeless-viewport');
|
||||||
if (!targetContainer.querySelector('canvas')) {
|
if (viewportElement) {
|
||||||
targetContainer.append(this.canvas);
|
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() {
|
override unmounted() {
|
||||||
if (this.lastBitmap) {
|
if (this.lastBitmap) {
|
||||||
this.lastBitmap.close();
|
this.lastBitmap.close();
|
||||||
}
|
}
|
||||||
|
if (this.debugPane) {
|
||||||
|
this.debugPane.dispose();
|
||||||
|
this.debugPane = null;
|
||||||
|
}
|
||||||
this.worker.terminate();
|
this.worker.terminate();
|
||||||
this.canvas.remove();
|
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() {
|
get viewport() {
|
||||||
return this.std.get(GfxControllerIdentifier).viewport;
|
return this.std.get(GfxControllerIdentifier).viewport;
|
||||||
}
|
}
|
||||||
@@ -56,6 +95,8 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHostLayout() {
|
getHostLayout() {
|
||||||
|
if (!document.fonts.check('15px Inter')) return null;
|
||||||
|
|
||||||
const paragraphBlocks = this.std.host.querySelectorAll(
|
const paragraphBlocks = this.std.host.querySelectorAll(
|
||||||
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
|
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
|
||||||
);
|
);
|
||||||
@@ -169,10 +210,14 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
private syncCanvasSize() {
|
private syncCanvasSize() {
|
||||||
const hostRect = this.getHostRect();
|
const hostRect = this.getHostRect();
|
||||||
const dpr = window.devicePixelRatio;
|
const dpr = window.devicePixelRatio;
|
||||||
this.canvas.style.width = `${hostRect.width}px`;
|
this.canvas.style.position = 'absolute';
|
||||||
this.canvas.style.height = `${hostRect.height}px`;
|
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.width = hostRect.width * dpr;
|
||||||
this.canvas.height = hostRect.height * dpr;
|
this.canvas.height = hostRect.height * dpr;
|
||||||
|
this.canvas.style.pointerEvents = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateCacheState(section: SectionLayout, bitmapCopy: ImageBitmap) {
|
private updateCacheState(section: SectionLayout, bitmapCopy: ImageBitmap) {
|
||||||
@@ -225,7 +270,6 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
|
|
||||||
public async render(): Promise<void> {
|
public async render(): Promise<void> {
|
||||||
const hostLayout = this.getHostLayout();
|
const hostLayout = this.getHostLayout();
|
||||||
|
|
||||||
if (!hostLayout) return;
|
if (!hostLayout) return;
|
||||||
|
|
||||||
const { section } = hostLayout;
|
const { section } = hostLayout;
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ presetsEffects();
|
|||||||
|
|
||||||
export const doc = createEmptyDoc().init();
|
export const doc = createEmptyDoc().init();
|
||||||
export const editor = new AffineEditorContainer();
|
export const editor = new AffineEditorContainer();
|
||||||
editor.pageSpecs = editor.pageSpecs.concat([ViewportTurboRendererExtension]);
|
editor.pageSpecs = [...editor.pageSpecs, ViewportTurboRendererExtension];
|
||||||
editor.edgelessSpecs = editor.edgelessSpecs.concat([
|
editor.edgelessSpecs = [
|
||||||
|
...editor.edgelessSpecs,
|
||||||
ViewportTurboRendererExtension,
|
ViewportTurboRendererExtension,
|
||||||
]);
|
];
|
||||||
|
|
||||||
editor.doc = doc;
|
editor.doc = doc;
|
||||||
editor.mode = 'edgeless';
|
editor.mode = 'edgeless';
|
||||||
|
|||||||
@@ -15,45 +15,15 @@
|
|||||||
background-color: var(--affine-white-90);
|
background-color: var(--affine-white-90);
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#container {
|
#container {
|
||||||
display: flex;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container"></div>
|
||||||
<div id="left-column"></div>
|
|
||||||
<div id="right-column">
|
|
||||||
<div class="top-bar">
|
|
||||||
<div id="tweakpane-container"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script type="module" src="./main.ts"></script>
|
<script type="module" src="./main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,59 +1,7 @@
|
|||||||
import { ViewportTurboRendererIdentifier } from '@blocksuite/affine-shared/viewport-renderer';
|
|
||||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
|
||||||
import { nextTick } from '@blocksuite/global/utils';
|
|
||||||
import { Text } from '@blocksuite/store';
|
import { Text } from '@blocksuite/store';
|
||||||
import { Pane } from 'tweakpane';
|
|
||||||
|
|
||||||
import { doc, editor } from './editor.js';
|
import { doc, editor } from './editor.js';
|
||||||
|
|
||||||
type DocMode = 'page' | 'edgeless';
|
|
||||||
|
|
||||||
async function handleToCanvasClick() {
|
|
||||||
const renderer = editor.std.get(ViewportTurboRendererIdentifier);
|
|
||||||
await renderer.render();
|
|
||||||
const viewport = editor.std.get(GfxControllerIdentifier).viewport;
|
|
||||||
viewport.viewportUpdated.on(async () => {
|
|
||||||
await renderer.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleModeChange(mode: DocMode) {
|
|
||||||
editor.mode = mode;
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const renderer = editor.std.get(ViewportTurboRendererIdentifier);
|
|
||||||
await renderer.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initUI() {
|
|
||||||
const pane = new Pane({
|
|
||||||
container: document.querySelector('#tweakpane-container') as HTMLElement,
|
|
||||||
});
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
mode: 'edgeless' as DocMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
pane
|
|
||||||
.addButton({
|
|
||||||
title: 'To Canvas',
|
|
||||||
})
|
|
||||||
.on('click', () => {
|
|
||||||
handleToCanvasClick().catch(console.error);
|
|
||||||
});
|
|
||||||
pane
|
|
||||||
.addBinding(params, 'mode', {
|
|
||||||
label: 'Editor Mode',
|
|
||||||
options: {
|
|
||||||
Doc: 'page',
|
|
||||||
Edgeless: 'edgeless',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.on('change', ({ value }) => {
|
|
||||||
handleModeChange(value as DocMode).catch(console.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addParagraph(content: string) {
|
function addParagraph(content: string) {
|
||||||
const note = doc.getBlocksByFlavour('affine:note')[0];
|
const note = doc.getBlocksByFlavour('affine:note')[0];
|
||||||
const props = {
|
const props = {
|
||||||
@@ -63,9 +11,7 @@ function addParagraph(content: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
initUI();
|
document.querySelector('#container')?.append(editor);
|
||||||
|
|
||||||
document.querySelector('#left-column')?.append(editor);
|
|
||||||
const firstParagraph = doc.getBlockByFlavour('affine:paragraph')[0];
|
const firstParagraph = doc.getBlockByFlavour('affine:paragraph')[0];
|
||||||
doc.updateBlock(firstParagraph, { text: new Text('Renderer') });
|
doc.updateBlock(firstParagraph, { text: new Text('Renderer') });
|
||||||
|
|
||||||
|
|||||||
@@ -3754,6 +3754,7 @@ __metadata:
|
|||||||
remark-math: "npm:^6.0.0"
|
remark-math: "npm:^6.0.0"
|
||||||
remark-parse: "npm:^11.0.0"
|
remark-parse: "npm:^11.0.0"
|
||||||
remark-stringify: "npm:^11.0.0"
|
remark-stringify: "npm:^11.0.0"
|
||||||
|
tweakpane: "npm:^4.0.5"
|
||||||
unified: "npm:^11.0.5"
|
unified: "npm:^11.0.5"
|
||||||
vitest: "npm:3.0.5"
|
vitest: "npm:3.0.5"
|
||||||
yjs: "npm:^13.6.21"
|
yjs: "npm:^13.6.21"
|
||||||
@@ -32935,7 +32936,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"tweakpane@npm:^4.0.4":
|
"tweakpane@npm:^4.0.4, tweakpane@npm:^4.0.5":
|
||||||
version: 4.0.5
|
version: 4.0.5
|
||||||
resolution: "tweakpane@npm:4.0.5"
|
resolution: "tweakpane@npm:4.0.5"
|
||||||
checksum: 10/7719a15ce96dd2b936b277239ccb18ee6a75ed2416a6bdacfc537515d909da6edd50161b12e91441ace5243efad3a14a98fe6e5475cae2617d7647a197117e64
|
checksum: 10/7719a15ce96dd2b936b277239ccb18ee6a75ed2416a6bdacfc537515d909da6edd50161b12e91441ace5243efad3a14a98fe6e5475cae2617d7647a197117e64
|
||||||
|
|||||||
Reference in New Issue
Block a user