mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
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:
@@ -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';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { computed, effect, signal } from '@preact/signals-core';
|
||||
import { nothing } from 'lit';
|
||||
|
||||
import type { BlockService } from '../../extension/index.js';
|
||||
@@ -23,7 +23,7 @@ export function isGfxBlockComponent(
|
||||
export const GfxElementSymbol = Symbol('GfxElement');
|
||||
|
||||
function updateTransform(element: GfxBlockComponent) {
|
||||
if (element.dataset.blockState === 'idle') return;
|
||||
if (element.transformState$.value === 'idle') return;
|
||||
|
||||
const { viewport } = element.gfx;
|
||||
element.dataset.viewportState = viewport.serializeRecord();
|
||||
@@ -31,6 +31,20 @@ function updateTransform(element: GfxBlockComponent) {
|
||||
element.style.transform = element.getCSSTransform();
|
||||
}
|
||||
|
||||
function updateBlockVisibility(view: GfxBlockComponent) {
|
||||
if (view.transformState$.value === 'active') {
|
||||
view.style.visibility = 'visible';
|
||||
view.style.pointerEvents = 'auto';
|
||||
view.classList.remove('block-idle');
|
||||
view.classList.add('block-active');
|
||||
} else {
|
||||
view.style.visibility = 'hidden';
|
||||
view.style.pointerEvents = 'none';
|
||||
view.classList.remove('block-active');
|
||||
view.classList.add('block-idle');
|
||||
}
|
||||
}
|
||||
|
||||
function handleGfxConnection(instance: GfxBlockComponent) {
|
||||
instance.style.position = 'absolute';
|
||||
|
||||
@@ -48,7 +62,12 @@ function handleGfxConnection(instance: GfxBlockComponent) {
|
||||
})
|
||||
);
|
||||
|
||||
updateTransform(instance);
|
||||
instance.disposables.add(
|
||||
effect(() => {
|
||||
updateBlockVisibility(instance);
|
||||
updateTransform(instance);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export abstract class GfxBlockComponent<
|
||||
@@ -61,6 +80,8 @@ export abstract class GfxBlockComponent<
|
||||
{
|
||||
[GfxElementSymbol] = true;
|
||||
|
||||
readonly transformState$ = signal<'idle' | 'active'>('active');
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
@@ -175,6 +196,8 @@ export function toGfxBlockComponent<
|
||||
return class extends CustomBlock {
|
||||
[GfxElementSymbol] = true;
|
||||
|
||||
readonly transformState$ = signal<'idle' | 'active'>('active');
|
||||
|
||||
override selected$ = computed(() => {
|
||||
const selection = this.std.selection.value.find(
|
||||
selection => selection.blockId === this.model?.id
|
||||
|
||||
Reference in New Issue
Block a user