fix(editor): sync gfx block transform update with RAF to prevent stale transform (#11322)

Close [BS-2866](https://linear.app/affine-design/issue/BS-2866/presentation-mode中的note消失)

## Problem
When using RequestAnimationFrame (RAF) for GFX block updates, there was a timing issue where the transform update would lag behind the RAF callback, causing the block to display with the previous frame's transform state.

## Solution
1. Refactored the block state management to use signals for better reactivity
2. Moved block visibility state management from `viewport-element.ts` to `gfx-block-component.ts`
3. Added `transformState$` signal to track block state
4. Synchronized transform updates with RAF using `effect` to ensure updates happen in the correct frame
5. Added test case to verify note visibility in presentation mode
This commit is contained in:
L-Sun
2025-03-31 12:47:01 +00:00
parent fec698fd8b
commit c9e14ac0db
3 changed files with 108 additions and 47 deletions
@@ -1,15 +1,17 @@
import { WithDisposable } from '@blocksuite/global/lit';
import { batch } from '@preact/signals-core';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import { PropTypes, requiredProperties } from '../view/decorators/required.js';
import {
type BlockComponent,
type EditorHost,
isGfxBlockComponent,
ShadowlessElement,
} from '../view/index.js';
import type { GfxBlockElementModel } from './model/gfx-block-model.js';
import { Viewport } from './viewport.js';
} from '../view';
import { PropTypes, requiredProperties } from '../view/decorators/required';
import { GfxControllerIdentifier } from './identifiers';
import type { GfxBlockElementModel } from './model/gfx-block-model';
import { Viewport } from './viewport';
/**
* A wrapper around `requestConnectedFrame` that only calls at most once in one frame
@@ -35,24 +37,6 @@ export function requestThrottledConnectedFrame<
}) as T;
}
function setBlockState(view: BlockComponent | null, state: 'active' | 'idle') {
if (!view) return;
if (state === 'active') {
view.style.visibility = 'visible';
view.style.pointerEvents = 'auto';
view.classList.remove('block-idle');
view.classList.add('block-active');
view.dataset.blockState = 'active';
} else {
view.style.visibility = 'hidden';
view.style.pointerEvents = 'none';
view.classList.remove('block-active');
view.classList.add('block-idle');
view.dataset.blockState = 'idle';
}
}
@requiredProperties({
viewport: PropTypes.instanceOf(Viewport),
})
@@ -85,21 +69,27 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
private readonly _hideOutsideBlock = () => {
if (!this.host) return;
const { host } = this;
const gfx = this.host.std.get(GfxControllerIdentifier);
const modelsInViewport = this.getModelsInViewport();
modelsInViewport.forEach(model => {
const view = host.std.view.getBlock(model.id);
setBlockState(view, 'active');
batch(() => {
modelsInViewport.forEach(model => {
const view = gfx.view.get(model);
if (isGfxBlockComponent(view)) {
view.transformState$.value = 'active';
}
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);
setBlockState(view, 'idle');
this._lastVisibleModels?.forEach(model => {
const view = gfx.view.get(model);
if (isGfxBlockComponent(view)) {
view.transformState$.value = 'idle';
}
});
});
this._lastVisibleModels = modelsInViewport;
@@ -194,23 +184,29 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
setBlocksActive(blockIds: string[]): void {
if (!this.host) return;
const gfx = this.host.std.get(GfxControllerIdentifier);
blockIds.forEach(id => {
const view = this.host?.std.view.getBlock(id);
if (view) {
setBlockState(view, 'active');
}
batch(() => {
blockIds.forEach(id => {
const view = gfx.view.get(id);
if (isGfxBlockComponent(view)) {
view.transformState$.value = 'active';
}
});
});
}
setBlocksIdle(blockIds: string[]): void {
if (!this.host) return;
const gfx = this.host.std.get(GfxControllerIdentifier);
blockIds.forEach(id => {
const view = this.host?.std.view.getBlock(id);
if (view) {
setBlockState(view, 'idle');
}
batch(() => {
blockIds.forEach(id => {
const view = gfx.view.get(id);
if (isGfxBlockComponent(view)) {
view.transformState$.value = 'idle';
}
});
});
}
}