feat(editor): automatically hide canvas optimized blocks (#10451)
Qualified DOM blocks can now be optimized away automatically. <img alt="image.png" width="500" src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/102bf813-154a-4816-9eb0-2c9c0ce01fe7.png"> 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.
This commit is contained in:
@@ -4,10 +4,12 @@ import {
|
|||||||
LifeCycleWatcherIdentifier,
|
LifeCycleWatcherIdentifier,
|
||||||
StdIdentifier,
|
StdIdentifier,
|
||||||
} from '@blocksuite/block-std';
|
} 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 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 {
|
import {
|
||||||
debugLog,
|
debugLog,
|
||||||
@@ -28,12 +30,12 @@ interface Tile {
|
|||||||
|
|
||||||
// With high enough zoom, fallback to DOM rendering
|
// With high enough zoom, fallback to DOM rendering
|
||||||
const zoomThreshold = 1;
|
const zoomThreshold = 1;
|
||||||
|
const debug = false; // Toggle for debug logs
|
||||||
|
|
||||||
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
||||||
state: RenderingState = 'inactive';
|
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]);
|
||||||
@@ -43,7 +45,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
private readonly worker: Worker;
|
private readonly worker: Worker;
|
||||||
private layoutCache: ViewportLayout | null = null;
|
private layoutCache: ViewportLayout | null = null;
|
||||||
private tile: Tile | null = null;
|
private tile: Tile | null = null;
|
||||||
private debugPane: Pane | null = null;
|
private viewportElement: GfxViewportElement | null = null;
|
||||||
|
|
||||||
constructor(std: BlockStdScope) {
|
constructor(std: BlockStdScope) {
|
||||||
super(std);
|
super(std);
|
||||||
@@ -60,7 +62,8 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
initTweakpane(this, mountPoint as HTMLElement);
|
initTweakpane(this, mountPoint as HTMLElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewport.elementReady.once(() => {
|
this.viewport.elementReady.once(element => {
|
||||||
|
this.viewportElement = element;
|
||||||
syncCanvasSize(this.canvas, this.std.host);
|
syncCanvasSize(this.canvas, this.std.host);
|
||||||
this.setState('pending');
|
this.setState('pending');
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
@@ -81,10 +84,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
override unmounted() {
|
override unmounted() {
|
||||||
this.debugLog('Unmounting renderer');
|
this.debugLog('Unmounting renderer');
|
||||||
this.clearTile();
|
this.clearTile();
|
||||||
if (this.debugPane) {
|
this.clearOptimizedBlocks();
|
||||||
this.debugPane.dispose();
|
|
||||||
this.debugPane = null;
|
|
||||||
}
|
|
||||||
this.worker.terminate();
|
this.worker.terminate();
|
||||||
this.canvas.remove();
|
this.canvas.remove();
|
||||||
this.disposables.dispose();
|
this.disposables.dispose();
|
||||||
@@ -99,23 +99,31 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
if (this.state === 'inactive') return;
|
if (this.state === 'inactive') return;
|
||||||
|
|
||||||
this.clearCanvas();
|
this.clearCanvas();
|
||||||
|
// -> pending
|
||||||
if (this.viewport.zoom > zoomThreshold) {
|
if (this.viewport.zoom > 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');
|
||||||
return;
|
this.toggleOptimization(false);
|
||||||
} else if (this.canUseBitmapCache()) {
|
this.clearOptimizedBlocks();
|
||||||
|
}
|
||||||
|
// -> ready
|
||||||
|
else if (this.canUseBitmapCache()) {
|
||||||
this.debugLog('Using cached bitmap');
|
this.debugLog('Using cached bitmap');
|
||||||
this.drawCachedBitmap(this.layoutCache!);
|
|
||||||
this.setState('ready');
|
this.setState('ready');
|
||||||
} else {
|
this.drawCachedBitmap(this.layoutCache!);
|
||||||
|
this.updateOptimizedBlocks();
|
||||||
|
}
|
||||||
|
// -> rendering
|
||||||
|
else {
|
||||||
if (!this.layoutCache) {
|
if (!this.layoutCache) {
|
||||||
this.updateLayoutCache();
|
this.updateLayoutCache();
|
||||||
}
|
}
|
||||||
const layout = this.layoutCache!;
|
const layout = this.layoutCache!;
|
||||||
this.setState('rendering');
|
this.setState('rendering');
|
||||||
|
this.toggleOptimization(false);
|
||||||
await this.paintLayout(layout);
|
await this.paintLayout(layout);
|
||||||
this.drawCachedBitmap(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.layoutVersion++;
|
||||||
this.layoutCache = null;
|
this.layoutCache = null;
|
||||||
this.clearTile();
|
this.clearTile();
|
||||||
this.clearCanvas(); // Should clear immediately after content updates
|
this.clearCanvas();
|
||||||
|
this.clearOptimizedBlocks();
|
||||||
this.setState('pending');
|
this.setState('pending');
|
||||||
this.debugLog(`Invalidated renderer (layoutVersion=${this.layoutVersion})`);
|
this.debugLog(`Invalidated renderer (layoutVersion=${this.layoutVersion})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private debugLog(message: string): void {
|
private debugLog(message: string) {
|
||||||
if (!this.debug) return;
|
if (!debug) return;
|
||||||
debugLog(message, this.state);
|
debugLog(message, this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,11 +157,10 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private clearTile() {
|
private clearTile() {
|
||||||
if (this.tile) {
|
if (!this.tile) return;
|
||||||
this.tile.bitmap.close();
|
this.tile.bitmap.close();
|
||||||
this.tile = null;
|
this.tile = null;
|
||||||
this.debugLog('Tile cleared');
|
this.debugLog('Tile cleared');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async paintLayout(layout: ViewportLayout): Promise<void> {
|
private async paintLayout(layout: ViewportLayout): Promise<void> {
|
||||||
@@ -224,7 +232,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
if (!this.tile) {
|
if (!this.tile) {
|
||||||
this.debugLog('No cached bitmap available, requesting refresh');
|
this.debugLog('No cached bitmap available, requesting refresh');
|
||||||
this.debouncedRefresh();
|
this.debouncedRefresh();
|
||||||
return; // version mismatch
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitmap = this.tile.bitmap;
|
const bitmap = this.tile.bitmap;
|
||||||
@@ -248,16 +256,51 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
|||||||
this.debugLog('Bitmap drawn to canvas');
|
this.debugLog('Bitmap drawn to canvas');
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(newState: RenderingState): void {
|
setState(newState: RenderingState) {
|
||||||
if (this.state === newState) return;
|
if (this.state === newState) return;
|
||||||
this.debugLog(`State change: ${this.state} -> ${newState}`);
|
this.debugLog(`State change: ${this.state} -> ${newState}`);
|
||||||
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 isReady = this.state === 'ready';
|
||||||
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
|
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
|
||||||
const result = isReady && isBelowZoomThreshold;
|
const result = isReady && isBelowZoomThreshold;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private toggleOptimization(value: boolean) {
|
||||||
|
if (
|
||||||
|
this.viewportElement &&
|
||||||
|
this.viewportElement.enableOptimization !== value
|
||||||
|
) {
|
||||||
|
this.viewportElement.enableOptimization = value;
|
||||||
|
this.debugLog(`${value ? 'Enabled' : 'Disabled'} optimization`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,27 +57,30 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
optimizedBlocks = new Set<string>();
|
||||||
|
|
||||||
private readonly _hideOutsideBlock = () => {
|
private readonly _hideOutsideBlock = () => {
|
||||||
if (this.getModelsInViewport && this.host) {
|
if (!this.host) return;
|
||||||
const host = this.host;
|
|
||||||
const modelsInViewport = this.getModelsInViewport();
|
|
||||||
|
|
||||||
modelsInViewport.forEach(model => {
|
const { host, optimizedBlocks, enableOptimization } = this;
|
||||||
const view = host.std.view.getBlock(model.id);
|
const modelsInViewport = this.getModelsInViewport();
|
||||||
setDisplay(view, 'block');
|
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)) {
|
if (this._lastVisibleModels?.has(model)) {
|
||||||
this._lastVisibleModels!.delete(model);
|
this._lastVisibleModels!.delete(model);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._lastVisibleModels?.forEach(model => {
|
this._lastVisibleModels?.forEach(model => {
|
||||||
const view = host.std.view.getBlock(model.id);
|
const view = host.std.view.getBlock(model.id);
|
||||||
setDisplay(view, 'none');
|
setDisplay(view, 'none');
|
||||||
});
|
});
|
||||||
|
|
||||||
this._lastVisibleModels = modelsInViewport;
|
this._lastVisibleModels = modelsInViewport;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private _lastVisibleModels?: Set<GfxBlockElementModel>;
|
private _lastVisibleModels?: Set<GfxBlockElementModel>;
|
||||||
@@ -154,7 +157,8 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor getModelsInViewport: undefined | (() => Set<GfxBlockElementModel>);
|
accessor getModelsInViewport: () => Set<GfxBlockElementModel> = () =>
|
||||||
|
new Set();
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor host: undefined | EditorHost;
|
accessor host: undefined | EditorHost;
|
||||||
@@ -167,4 +171,29 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor viewport!: Viewport;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user