mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
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,
|
||||
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<void> {
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,27 +57,30 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
`;
|
||||
|
||||
optimizedBlocks = new Set<string>();
|
||||
|
||||
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<GfxBlockElementModel>;
|
||||
@@ -154,7 +157,8 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getModelsInViewport: undefined | (() => Set<GfxBlockElementModel>);
|
||||
accessor getModelsInViewport: () => Set<GfxBlockElementModel> = () =>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user