From 3252dd7a310ca9ab69d543d69ab842fd1add023c Mon Sep 17 00:00:00 2001
From: doodlewind <7312949+doodlewind@users.noreply.github.com>
Date: Thu, 27 Feb 2025 02:10:49 +0000
Subject: [PATCH] feat(editor): automatically hide canvas optimized blocks
(#10451)
Qualified DOM blocks can now be optimized away automatically.
Since this is under development, verifying state correctness is more important than rendering details.
This won't affect current production version since the passive APIs added to `GfxViewportElement` are disabled by the `enableOptimization = false` config.
---
.../viewport-renderer/viewport-renderer.ts | 93 ++++++++++++++-----
.../block-std/src/gfx/viewport-element.ts | 63 +++++++++----
2 files changed, 114 insertions(+), 42 deletions(-)
diff --git a/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts b/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts
index b3c7b9b41..86a43f423 100644
--- a/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts
+++ b/blocksuite/affine/shared/src/viewport-renderer/viewport-renderer.ts
@@ -4,10 +4,12 @@ import {
LifeCycleWatcherIdentifier,
StdIdentifier,
} from '@blocksuite/block-std';
-import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
+import {
+ GfxControllerIdentifier,
+ type GfxViewportElement,
+} from '@blocksuite/block-std/gfx';
import type { Container, ServiceIdentifier } from '@blocksuite/global/di';
import { debounce, DisposableGroup } from '@blocksuite/global/utils';
-import { type Pane } from 'tweakpane';
import {
debugLog,
@@ -28,12 +30,12 @@ interface Tile {
// With high enough zoom, fallback to DOM rendering
const zoomThreshold = 1;
+const debug = false; // Toggle for debug logs
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
state: RenderingState = 'inactive';
disposables = new DisposableGroup();
private layoutVersion = 0;
- private readonly debug = false; // Toggle for debug logs
static override setup(di: Container) {
di.addImpl(ViewportTurboRendererIdentifier, this, [StdIdentifier]);
@@ -43,7 +45,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
private readonly worker: Worker;
private layoutCache: ViewportLayout | null = null;
private tile: Tile | null = null;
- private debugPane: Pane | null = null;
+ private viewportElement: GfxViewportElement | null = null;
constructor(std: BlockStdScope) {
super(std);
@@ -60,7 +62,8 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
initTweakpane(this, mountPoint as HTMLElement);
}
- this.viewport.elementReady.once(() => {
+ this.viewport.elementReady.once(element => {
+ this.viewportElement = element;
syncCanvasSize(this.canvas, this.std.host);
this.setState('pending');
this.disposables.add(
@@ -81,10 +84,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
override unmounted() {
this.debugLog('Unmounting renderer');
this.clearTile();
- if (this.debugPane) {
- this.debugPane.dispose();
- this.debugPane = null;
- }
+ this.clearOptimizedBlocks();
this.worker.terminate();
this.canvas.remove();
this.disposables.dispose();
@@ -99,23 +99,31 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
if (this.state === 'inactive') return;
this.clearCanvas();
+ // -> pending
if (this.viewport.zoom > zoomThreshold) {
this.debugLog('Zoom above threshold, falling back to DOM rendering');
this.setState('pending');
- return;
- } else if (this.canUseBitmapCache()) {
+ this.toggleOptimization(false);
+ this.clearOptimizedBlocks();
+ }
+ // -> ready
+ else if (this.canUseBitmapCache()) {
this.debugLog('Using cached bitmap');
- this.drawCachedBitmap(this.layoutCache!);
this.setState('ready');
- } else {
+ this.drawCachedBitmap(this.layoutCache!);
+ this.updateOptimizedBlocks();
+ }
+ // -> rendering
+ else {
if (!this.layoutCache) {
this.updateLayoutCache();
}
const layout = this.layoutCache!;
this.setState('rendering');
+ this.toggleOptimization(false);
await this.paintLayout(layout);
this.drawCachedBitmap(layout);
- // State will be updated to 'ready' in handlePaintedBitmap if successful
+ this.updateOptimizedBlocks();
}
}
@@ -131,13 +139,14 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.layoutVersion++;
this.layoutCache = null;
this.clearTile();
- this.clearCanvas(); // Should clear immediately after content updates
+ this.clearCanvas();
+ this.clearOptimizedBlocks();
this.setState('pending');
this.debugLog(`Invalidated renderer (layoutVersion=${this.layoutVersion})`);
}
- private debugLog(message: string): void {
- if (!this.debug) return;
+ private debugLog(message: string) {
+ if (!debug) return;
debugLog(message, this.state);
}
@@ -148,11 +157,10 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
}
private clearTile() {
- if (this.tile) {
- this.tile.bitmap.close();
- this.tile = null;
- this.debugLog('Tile cleared');
- }
+ if (!this.tile) return;
+ this.tile.bitmap.close();
+ this.tile = null;
+ this.debugLog('Tile cleared');
}
private async paintLayout(layout: ViewportLayout): Promise {
@@ -224,7 +232,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
if (!this.tile) {
this.debugLog('No cached bitmap available, requesting refresh');
this.debouncedRefresh();
- return; // version mismatch
+ return;
}
const bitmap = this.tile.bitmap;
@@ -248,16 +256,51 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.debugLog('Bitmap drawn to canvas');
}
- setState(newState: RenderingState): void {
+ setState(newState: RenderingState) {
if (this.state === newState) return;
this.debugLog(`State change: ${this.state} -> ${newState}`);
this.state = newState;
}
- canOptimizeDOM(): boolean {
+ private updateOptimizedBlocks() {
+ requestAnimationFrame(() => {
+ if (!this.viewportElement || !this.layoutCache) return;
+ if (!this.canOptimize()) return;
+ if (this.state !== 'ready') {
+ this.debugLog('Unexpected state updating optimized blocks');
+ console.warn('Unexpected state', this.tile, this.layoutCache);
+ return;
+ }
+
+ this.toggleOptimization(true);
+ const blockElements = this.viewportElement.getModelsInViewport();
+ const blockIds = Array.from(blockElements).map(model => model.id);
+ this.viewportElement.updateOptimizedBlocks(blockIds, true);
+ this.debugLog(`Optimized ${blockIds.length} blocks`);
+ });
+ }
+
+ private clearOptimizedBlocks() {
+ if (this.viewportElement) {
+ this.viewportElement.clearOptimizedBlocks();
+ this.debugLog('Cleared optimized blocks');
+ }
+ }
+
+ canOptimize(): boolean {
const isReady = this.state === 'ready';
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
const result = isReady && isBelowZoomThreshold;
return result;
}
+
+ private toggleOptimization(value: boolean) {
+ if (
+ this.viewportElement &&
+ this.viewportElement.enableOptimization !== value
+ ) {
+ this.viewportElement.enableOptimization = value;
+ this.debugLog(`${value ? 'Enabled' : 'Disabled'} optimization`);
+ }
+ }
}
diff --git a/blocksuite/framework/block-std/src/gfx/viewport-element.ts b/blocksuite/framework/block-std/src/gfx/viewport-element.ts
index 5f740528d..c770d1916 100644
--- a/blocksuite/framework/block-std/src/gfx/viewport-element.ts
+++ b/blocksuite/framework/block-std/src/gfx/viewport-element.ts
@@ -57,27 +57,30 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
}
`;
+ optimizedBlocks = new Set();
+
private readonly _hideOutsideBlock = () => {
- if (this.getModelsInViewport && this.host) {
- const host = this.host;
- const modelsInViewport = this.getModelsInViewport();
+ if (!this.host) return;
- modelsInViewport.forEach(model => {
- const view = host.std.view.getBlock(model.id);
- setDisplay(view, 'block');
+ const { host, optimizedBlocks, enableOptimization } = this;
+ const modelsInViewport = this.getModelsInViewport();
+ modelsInViewport.forEach(model => {
+ const view = host.std.view.getBlock(model.id);
+ const canOptimize = optimizedBlocks.has(model.id) && enableOptimization;
+ const display = canOptimize ? 'none' : 'block';
+ setDisplay(view, display);
- if (this._lastVisibleModels?.has(model)) {
- this._lastVisibleModels!.delete(model);
- }
- });
+ if (this._lastVisibleModels?.has(model)) {
+ this._lastVisibleModels!.delete(model);
+ }
+ });
- this._lastVisibleModels?.forEach(model => {
- const view = host.std.view.getBlock(model.id);
- setDisplay(view, 'none');
- });
+ this._lastVisibleModels?.forEach(model => {
+ const view = host.std.view.getBlock(model.id);
+ setDisplay(view, 'none');
+ });
- this._lastVisibleModels = modelsInViewport;
- }
+ this._lastVisibleModels = modelsInViewport;
};
private _lastVisibleModels?: Set;
@@ -154,7 +157,8 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
};
@property({ attribute: false })
- accessor getModelsInViewport: undefined | (() => Set);
+ accessor getModelsInViewport: () => Set = () =>
+ new Set();
@property({ attribute: false })
accessor host: undefined | EditorHost;
@@ -167,4 +171,29 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor viewport!: Viewport;
+
+ @property({ attribute: false })
+ accessor enableOptimization: boolean = false;
+
+ updateOptimizedBlocks(blockIds: string[], optimized: boolean): void {
+ let changed = false;
+
+ blockIds.forEach(id => {
+ if (optimized && !this.optimizedBlocks.has(id)) {
+ this.optimizedBlocks.add(id);
+ changed = true;
+ } else if (!optimized && this.optimizedBlocks.has(id)) {
+ this.optimizedBlocks.delete(id);
+ changed = true;
+ }
+ });
+
+ if (changed) this._refreshViewport();
+ }
+
+ clearOptimizedBlocks(): void {
+ if (this.optimizedBlocks.size === 0) return;
+ this.optimizedBlocks.clear();
+ this._refreshViewport();
+ }
}