refactor(editor): simplify renderer state (#10441)
The redundant `monitoring` state has been removed. The new `ready` state is used for querying if DOM elements can be safely optimized away.
This commit is contained in:
@@ -20,10 +20,8 @@ import { choose } from 'lit/directives/choose.js';
|
|||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import {
|
import type { PageRootBlockComponent } from '../../page/page-root-block.js';
|
||||||
type PageRootBlockComponent,
|
import { RootBlockConfigExtension } from '../../root-config.js';
|
||||||
RootBlockConfigExtension,
|
|
||||||
} from '../../index.js';
|
|
||||||
import {
|
import {
|
||||||
type AFFINE_LINKED_DOC_WIDGET,
|
type AFFINE_LINKED_DOC_WIDGET,
|
||||||
getMenus,
|
getMenus,
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { type Viewport } from '@blocksuite/block-std/gfx';
|
|||||||
import { Pane } from 'tweakpane';
|
import { Pane } from 'tweakpane';
|
||||||
|
|
||||||
import { getSentenceRects, segmentSentences } from './text-utils.js';
|
import { getSentenceRects, segmentSentences } from './text-utils.js';
|
||||||
import type { ParagraphLayout, ViewportLayout } from './types.js';
|
import type {
|
||||||
|
ParagraphLayout,
|
||||||
|
RenderingState,
|
||||||
|
ViewportLayout,
|
||||||
|
} from './types.js';
|
||||||
import type { ViewportTurboRendererExtension } from './viewport-renderer.js';
|
import type { ViewportTurboRendererExtension } from './viewport-renderer.js';
|
||||||
|
|
||||||
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
|
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
|
||||||
@@ -98,15 +102,15 @@ export function initTweakpane(
|
|||||||
paneElement.style.right = '10px';
|
paneElement.style.right = '10px';
|
||||||
paneElement.style.width = '250px';
|
paneElement.style.width = '250px';
|
||||||
debugPane.title = 'Viewport Turbo Renderer';
|
debugPane.title = 'Viewport Turbo Renderer';
|
||||||
|
|
||||||
debugPane
|
|
||||||
.addBinding({ paused: false }, 'paused', {
|
|
||||||
label: 'Paused',
|
|
||||||
})
|
|
||||||
.on('change', ({ value }) => {
|
|
||||||
renderer.state = value ? 'paused' : 'monitoring';
|
|
||||||
});
|
|
||||||
debugPane.addButton({ title: 'Invalidate' }).on('click', () => {
|
debugPane.addButton({ title: 'Invalidate' }).on('click', () => {
|
||||||
renderer.invalidate();
|
renderer.invalidate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function debugLog(message: string, state: RenderingState) {
|
||||||
|
console.log(
|
||||||
|
`%c[ViewportTurboRenderer]%c ${message} | state=${state}`,
|
||||||
|
'color: #4285f4; font-weight: bold;',
|
||||||
|
'color: inherit;'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,3 +32,12 @@ export interface TextRect {
|
|||||||
rect: Rect;
|
rect: Rect;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the rendering state of the ViewportTurboRenderer
|
||||||
|
* - inactive: Renderer is not active
|
||||||
|
* - pending: Bitmap is invalid or not yet available, falling back to DOM rendering
|
||||||
|
* - rendering: Currently rendering to a bitmap (async operation in progress)
|
||||||
|
* - ready: Bitmap is valid and rendered, DOM elements can be safely removed
|
||||||
|
*/
|
||||||
|
export type RenderingState = 'inactive' | 'pending' | 'rendering' | 'ready';
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import {
|
|||||||
StdIdentifier,
|
StdIdentifier,
|
||||||
} 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, ServiceIdentifier } from '@blocksuite/global/di';
|
||||||
import { debounce, DisposableGroup } from '@blocksuite/global/utils';
|
import { debounce, DisposableGroup } from '@blocksuite/global/utils';
|
||||||
import { type Pane } from 'tweakpane';
|
import { type Pane } from 'tweakpane';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
debugLog,
|
||||||
getViewportLayout,
|
getViewportLayout,
|
||||||
initTweakpane,
|
initTweakpane,
|
||||||
syncCanvasSize,
|
syncCanvasSize,
|
||||||
} from './dom-utils.js';
|
} from './dom-utils.js';
|
||||||
import { type ViewportLayout } from './types.js';
|
import type { RenderingState, ViewportLayout } from './types.js';
|
||||||
|
|
||||||
export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier(
|
export const ViewportTurboRendererIdentifier = LifeCycleWatcherIdentifier(
|
||||||
'ViewportTurboRenderer'
|
'ViewportTurboRenderer'
|
||||||
@@ -29,9 +30,10 @@ interface Tile {
|
|||||||
const zoomThreshold = 1;
|
const zoomThreshold = 1;
|
||||||
|
|
||||||
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
||||||
state: 'monitoring' | 'paused' = 'paused';
|
state: RenderingState = 'inactive';
|
||||||
disposables = new DisposableGroup();
|
disposables = new DisposableGroup();
|
||||||
private layoutVersion = 0;
|
private layoutVersion = 0;
|
||||||
|
private readonly debug = false; // Toggle for debug logs
|
||||||
|
|
||||||
static override setup(di: Container) {
|
static override setup(di: Container) {
|
||||||
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
|
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
|
||||||
@@ -48,6 +50,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
this.worker = new Worker(new URL('./painter.worker.ts', import.meta.url), {
|
this.worker = new Worker(new URL('./painter.worker.ts', import.meta.url), {
|
||||||
type: 'module',
|
type: 'module',
|
||||||
});
|
});
|
||||||
|
this.debugLog('Initialized ViewportTurboRenderer');
|
||||||
}
|
}
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
@@ -59,7 +62,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
|
|
||||||
this.viewport.elementReady.once(() => {
|
this.viewport.elementReady.once(() => {
|
||||||
syncCanvasSize(this.canvas, this.std.host);
|
syncCanvasSize(this.canvas, this.std.host);
|
||||||
this.state = 'monitoring';
|
this.setState('pending');
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.viewport.viewportUpdated.on(() => {
|
this.viewport.viewportUpdated.on(() => {
|
||||||
this.refresh().catch(console.error);
|
this.refresh().catch(console.error);
|
||||||
@@ -76,6 +79,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override unmounted() {
|
override unmounted() {
|
||||||
|
this.debugLog('Unmounting renderer');
|
||||||
this.clearTile();
|
this.clearTile();
|
||||||
if (this.debugPane) {
|
if (this.debugPane) {
|
||||||
this.debugPane.dispose();
|
this.debugPane.dispose();
|
||||||
@@ -84,6 +88,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
this.worker.terminate();
|
this.worker.terminate();
|
||||||
this.canvas.remove();
|
this.canvas.remove();
|
||||||
this.disposables.dispose();
|
this.disposables.dispose();
|
||||||
|
this.setState('inactive');
|
||||||
}
|
}
|
||||||
|
|
||||||
get viewport() {
|
get viewport() {
|
||||||
@@ -91,20 +96,26 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
if (this.state === 'paused') return;
|
if (this.state === 'inactive') return;
|
||||||
|
|
||||||
this.clearCanvas();
|
this.clearCanvas();
|
||||||
if (this.viewport.zoom > zoomThreshold) {
|
if (this.viewport.zoom > zoomThreshold) {
|
||||||
|
this.debugLog('Zoom above threshold, falling back to DOM rendering');
|
||||||
|
this.setState('pending');
|
||||||
return;
|
return;
|
||||||
} else if (this.canUseBitmapCache()) {
|
} else if (this.canUseBitmapCache()) {
|
||||||
|
this.debugLog('Using cached bitmap');
|
||||||
this.drawCachedBitmap(this.layoutCache!);
|
this.drawCachedBitmap(this.layoutCache!);
|
||||||
|
this.setState('ready');
|
||||||
} else {
|
} else {
|
||||||
if (!this.layoutCache) {
|
if (!this.layoutCache) {
|
||||||
this.updateLayoutCache();
|
this.updateLayoutCache();
|
||||||
}
|
}
|
||||||
const layout = this.layoutCache!;
|
const layout = this.layoutCache!;
|
||||||
|
this.setState('rendering');
|
||||||
await this.paintLayout(layout);
|
await this.paintLayout(layout);
|
||||||
this.drawCachedBitmap(layout);
|
this.drawCachedBitmap(layout);
|
||||||
|
// State will be updated to 'ready' in handlePaintedBitmap if successful
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,17 +132,26 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
this.layoutCache = null;
|
this.layoutCache = null;
|
||||||
this.clearTile();
|
this.clearTile();
|
||||||
this.clearCanvas(); // Should clear immediately after content updates
|
this.clearCanvas(); // Should clear immediately after content updates
|
||||||
|
this.setState('pending');
|
||||||
|
this.debugLog(`Invalidated renderer (layoutVersion=${this.layoutVersion})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private debugLog(message: string): void {
|
||||||
|
if (!this.debug) return;
|
||||||
|
debugLog(message, this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLayoutCache() {
|
private updateLayoutCache() {
|
||||||
const layout = getViewportLayout(this.std.host, this.viewport);
|
const layout = getViewportLayout(this.std.host, this.viewport);
|
||||||
this.layoutCache = layout;
|
this.layoutCache = layout;
|
||||||
|
this.debugLog('Layout cache updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearTile() {
|
private clearTile() {
|
||||||
if (this.tile) {
|
if (this.tile) {
|
||||||
this.tile.bitmap.close();
|
this.tile.bitmap.close();
|
||||||
this.tile = null;
|
this.tile = null;
|
||||||
|
this.debugLog('Tile cleared');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +162,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
const dpr = window.devicePixelRatio;
|
const dpr = window.devicePixelRatio;
|
||||||
const currentVersion = this.layoutVersion;
|
const currentVersion = this.layoutVersion;
|
||||||
|
|
||||||
|
this.debugLog(`Requesting bitmap painting (version=${currentVersion})`);
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: 'paintLayout',
|
type: 'paintLayout',
|
||||||
data: {
|
data: {
|
||||||
@@ -157,9 +178,16 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
this.worker.onmessage = (e: MessageEvent) => {
|
this.worker.onmessage = (e: MessageEvent) => {
|
||||||
if (e.data.type === 'bitmapPainted') {
|
if (e.data.type === 'bitmapPainted') {
|
||||||
if (e.data.version === this.layoutVersion) {
|
if (e.data.version === this.layoutVersion) {
|
||||||
|
this.debugLog(
|
||||||
|
`Bitmap painted successfully (version=${e.data.version})`
|
||||||
|
);
|
||||||
this.handlePaintedBitmap(e.data.bitmap, resolve);
|
this.handlePaintedBitmap(e.data.bitmap, resolve);
|
||||||
} else {
|
} else {
|
||||||
|
this.debugLog(
|
||||||
|
`Received outdated bitmap (got=${e.data.version}, current=${this.layoutVersion})`
|
||||||
|
);
|
||||||
e.data.bitmap.close();
|
e.data.bitmap.close();
|
||||||
|
this.setState('pending');
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +203,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
bitmap,
|
bitmap,
|
||||||
zoom: this.viewport.zoom,
|
zoom: this.viewport.zoom,
|
||||||
};
|
};
|
||||||
|
this.setState('ready');
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +217,12 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
const ctx = this.canvas.getContext('2d');
|
const ctx = this.canvas.getContext('2d');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
this.debugLog('Canvas cleared');
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawCachedBitmap(layout: ViewportLayout) {
|
private drawCachedBitmap(layout: ViewportLayout) {
|
||||||
if (!this.tile) {
|
if (!this.tile) {
|
||||||
|
this.debugLog('No cached bitmap available, requesting refresh');
|
||||||
this.debouncedRefresh();
|
this.debouncedRefresh();
|
||||||
return; // version mismatch
|
return; // version mismatch
|
||||||
}
|
}
|
||||||
@@ -213,5 +244,20 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
layout.rect.w * window.devicePixelRatio * this.viewport.zoom,
|
layout.rect.w * window.devicePixelRatio * this.viewport.zoom,
|
||||||
layout.rect.h * window.devicePixelRatio * this.viewport.zoom
|
layout.rect.h * window.devicePixelRatio * this.viewport.zoom
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.debugLog('Bitmap drawn to canvas');
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(newState: RenderingState): void {
|
||||||
|
if (this.state === newState) return;
|
||||||
|
this.debugLog(`State change: ${this.state} -> ${newState}`);
|
||||||
|
this.state = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
canOptimizeDOM(): boolean {
|
||||||
|
const isReady = this.state === 'ready';
|
||||||
|
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
|
||||||
|
const result = isReady && isBelowZoomThreshold;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user