修复画布生成面板打开后不可见
新增 composer 可见安全区计算,打开后自动平移画布避开视口边界和工具栏。 统一在生成面板 surface 中按真实 DOM 尺寸触发避让,覆盖生成器、快速编辑、裁扩和角色动作面板。 补充 overlay 与 generation surface 回归测试。 同步 Lovart 面板方案和共享踩坑文档。
This commit is contained in:
@@ -263,6 +263,14 @@
|
||||
- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationPlacementModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`。
|
||||
- 关联:`src/components/image-editor/ImageCanvasGenerationPlacementModel.ts`、`src/components/image-editor/useImageCanvasGenerationWorkflow.ts`、`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`。
|
||||
|
||||
## 图片画布生成类 composer 打开后必须自动进入可见安全区
|
||||
|
||||
- 现象:生成器、快速编辑、裁扩或角色动作面板打开后,面板可能在当前画布视口外,或被底部工具栏 / 左下 dock 盖住,用户只看到一部分甚至完全看不到输入框。
|
||||
- 原因:placement 只负责选择画布世界坐标里的占位落点,面板实际 DOM 宽高、`translateX(-50%)`、移动端 fixed 样式和工具栏覆盖区域没有反向修正 viewport。
|
||||
- 处理:所有画布内 composer / 面板渲染后统一走 `resolveViewportForOverlayVisibility(...)`,用真实 DOM 矩形和工具栏安全边界只平移 viewport;新增入口不要在各自按钮 handler 里写独立偏移。
|
||||
- 验证:`npm run test -- src/components/image-editor/ImageCanvasOverlayModel.test.ts src/components/image-editor/useImageCanvasGenerationSurface.test.tsx`。
|
||||
- 关联:`src/components/image-editor/ImageCanvasOverlayModel.ts`、`src/components/image-editor/useImageCanvasGenerationSurface.tsx`、`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`。
|
||||
|
||||
## 图片画布图片改造也必须创建独立生成器占位
|
||||
|
||||
- 现象:点击图片图层的“改造”后,输入框直接挂在原图上,提交时既不像其它生成入口一样有独立占位,也容易让用户误以为会覆盖源图。
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
- 视频待生成占位必须与面板当前比例和清晰度同步:默认 `16:9 · 480p` 为 `854 x 480`,切换比例、`720p` 或 `1080p` 后按比例和清晰度重算偶数宽度;调整参数时保持占位中心点不变。
|
||||
- 面板中用户修改比例、尺寸或清晰度后,已有空白待生成占位立即同步更新 `width / height / originalWidth / originalHeight`,且保持中心点不跳动。
|
||||
- 快速编辑点击生成后不在原图上播放生成中遮罩,而是立即创建独立 `Quick Edit Generator` 画布生成占位并播放生成中动画;该占位必须复用新建图片的 placement 避让逻辑,和已有素材 / 生成占位至少保留 32px 画布间距,不允许固定放到原图右侧后压住其它素材;生成成功后结果落在该占位框位置,失败时占位标记失败并恢复快速编辑面板。
|
||||
- 任何会打开画布内 composer / 面板的入口,必须在面板渲染后通过统一 overlay 可见性校正检查真实 DOM 矩形;如果面板超出画布视口,或底部工具栏 / 左下 dock 会遮住面板,就只平移当前 viewport 让面板完整进入安全区域。新增生成类入口不要在按钮 handler 里手写单独的避让偏移。
|
||||
|
||||
## 画布悬浮信息
|
||||
|
||||
|
||||
@@ -763,6 +763,7 @@ export function ImageCanvasEditorView() {
|
||||
canvasSize,
|
||||
viewport,
|
||||
setViewport,
|
||||
canvasViewportRef,
|
||||
setLayers,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveQuickEditFocusViewport,
|
||||
resolveQuickEditPanelStyle,
|
||||
resolveSelectedToolbarStyle,
|
||||
resolveViewportForOverlayVisibility,
|
||||
} from './ImageCanvasOverlayModel';
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
@@ -182,6 +183,66 @@ describe('ImageCanvasOverlayModel', () => {
|
||||
).toEqual({ left: 536, top: 120 });
|
||||
});
|
||||
|
||||
it('pans the viewport until an opened composer is inside the visible safe area', () => {
|
||||
expect(
|
||||
resolveViewportForOverlayVisibility({
|
||||
viewport: { x: 10, y: 20, scale: 1 },
|
||||
overlayBounds: {
|
||||
left: 210,
|
||||
top: 430,
|
||||
right: 690,
|
||||
bottom: 650,
|
||||
},
|
||||
safeBounds: {
|
||||
left: 12,
|
||||
top: 12,
|
||||
right: 888,
|
||||
bottom: 560,
|
||||
},
|
||||
}),
|
||||
).toEqual({ x: 10, y: -70, scale: 1 });
|
||||
});
|
||||
|
||||
it('pans sideways when a centered composer would leave the viewport', () => {
|
||||
expect(
|
||||
resolveViewportForOverlayVisibility({
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
overlayBounds: {
|
||||
left: -80,
|
||||
top: 120,
|
||||
right: 420,
|
||||
bottom: 320,
|
||||
},
|
||||
safeBounds: {
|
||||
left: 12,
|
||||
top: 12,
|
||||
right: 888,
|
||||
bottom: 560,
|
||||
},
|
||||
}),
|
||||
).toEqual({ x: 92, y: 0, scale: 1 });
|
||||
});
|
||||
|
||||
it('centers an oversized composer within the available safe area', () => {
|
||||
expect(
|
||||
resolveViewportForOverlayVisibility({
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
overlayBounds: {
|
||||
left: 20,
|
||||
top: 20,
|
||||
right: 700,
|
||||
bottom: 620,
|
||||
},
|
||||
safeBounds: {
|
||||
left: 12,
|
||||
top: 12,
|
||||
right: 612,
|
||||
bottom: 512,
|
||||
},
|
||||
}),
|
||||
).toEqual({ x: -48, y: -58, scale: 1 });
|
||||
});
|
||||
|
||||
it('recognizes canvas generation composer dialog modes', () => {
|
||||
expect(isCanvasGenerationComposerVisible(createDialog({ mode: 'generate' }))).toBe(
|
||||
true,
|
||||
|
||||
@@ -25,6 +25,7 @@ const QUICK_EDIT_FOCUS_SOURCE_TOP = 56;
|
||||
const QUICK_EDIT_PANEL_GAP = 12;
|
||||
const QUICK_EDIT_PANEL_ESTIMATED_HEIGHT = 260;
|
||||
const QUICK_EDIT_PANEL_BOTTOM_MARGIN = 4;
|
||||
const OVERLAY_VISIBILITY_EPSILON = 0.5;
|
||||
|
||||
export type CanvasOverlayStyle = {
|
||||
left: number;
|
||||
@@ -35,6 +36,72 @@ export type CanvasComposerOverlayStyle = CanvasOverlayStyle & {
|
||||
width?: string;
|
||||
};
|
||||
|
||||
export type CanvasOverlayBounds = {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
};
|
||||
|
||||
function resolveOverlayAxisShift({
|
||||
start,
|
||||
end,
|
||||
safeStart,
|
||||
safeEnd,
|
||||
}: {
|
||||
start: number;
|
||||
end: number;
|
||||
safeStart: number;
|
||||
safeEnd: number;
|
||||
}) {
|
||||
const size = end - start;
|
||||
const safeSize = safeEnd - safeStart;
|
||||
if (size > safeSize) {
|
||||
return safeStart + (safeSize - size) / 2 - start;
|
||||
}
|
||||
if (start < safeStart) {
|
||||
return safeStart - start;
|
||||
}
|
||||
if (end > safeEnd) {
|
||||
return safeEnd - end;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function resolveViewportForOverlayVisibility({
|
||||
viewport,
|
||||
overlayBounds,
|
||||
safeBounds,
|
||||
}: {
|
||||
viewport: CanvasViewport;
|
||||
overlayBounds: CanvasOverlayBounds;
|
||||
safeBounds: CanvasOverlayBounds;
|
||||
}): CanvasViewport {
|
||||
const shiftX = resolveOverlayAxisShift({
|
||||
start: overlayBounds.left,
|
||||
end: overlayBounds.right,
|
||||
safeStart: safeBounds.left,
|
||||
safeEnd: safeBounds.right,
|
||||
});
|
||||
const shiftY = resolveOverlayAxisShift({
|
||||
start: overlayBounds.top,
|
||||
end: overlayBounds.bottom,
|
||||
safeStart: safeBounds.top,
|
||||
safeEnd: safeBounds.bottom,
|
||||
});
|
||||
if (
|
||||
Math.abs(shiftX) <= OVERLAY_VISIBILITY_EPSILON &&
|
||||
Math.abs(shiftY) <= OVERLAY_VISIBILITY_EPSILON
|
||||
) {
|
||||
return viewport;
|
||||
}
|
||||
return {
|
||||
...viewport,
|
||||
x: viewport.x + shiftX,
|
||||
y: viewport.y + shiftY,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGenerationAnchor({
|
||||
dialog,
|
||||
generatedLayer,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
@@ -31,6 +31,52 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
let mockedComposerRect: DOMRect | null = null;
|
||||
let originalGetBoundingClientRect: typeof Element.prototype.getBoundingClientRect;
|
||||
|
||||
beforeEach(() => {
|
||||
originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
||||
Element.prototype.getBoundingClientRect = function getBoundingClientRect() {
|
||||
if (
|
||||
mockedComposerRect &&
|
||||
this instanceof HTMLElement &&
|
||||
this.classList.contains('image-canvas-editor__generation-composer')
|
||||
) {
|
||||
return mockedComposerRect;
|
||||
}
|
||||
return originalGetBoundingClientRect.call(this);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockedComposerRect = null;
|
||||
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
||||
});
|
||||
|
||||
function createDomRect({
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}) {
|
||||
return {
|
||||
x: left,
|
||||
y: top,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect;
|
||||
}
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
const id = overrides.id ?? 'layer-a';
|
||||
return {
|
||||
@@ -66,6 +112,7 @@ function GenerationSurfaceHarness() {
|
||||
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const publicationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const canvasViewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const [viewport, setViewport] = useState({ x: 10, y: 20, scale: 2 });
|
||||
const dialogs = useCanvasGenerationDialogs();
|
||||
const activeDialog = dialogs.generateDialog;
|
||||
@@ -78,6 +125,7 @@ function GenerationSurfaceHarness() {
|
||||
canvasSize: { width: 900, height: 640 },
|
||||
viewport,
|
||||
setViewport,
|
||||
canvasViewportRef,
|
||||
setLayers,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
@@ -136,6 +184,8 @@ function GenerationSurfaceHarness() {
|
||||
<span data-testid="composer-top">
|
||||
{surface.generationComposerStyle?.top ?? '-'}
|
||||
</span>
|
||||
<span data-testid="viewport-x">{viewport.x}</span>
|
||||
<span data-testid="viewport-y">{viewport.y}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => surface.switchGenerationTool('generate')}
|
||||
@@ -204,7 +254,10 @@ function GenerationSurfaceHarness() {
|
||||
>
|
||||
切换选择
|
||||
</button>
|
||||
{surface.generationComposerNode}
|
||||
<div ref={canvasViewportRef} data-testid="viewport">
|
||||
<div className="image-canvas-editor__bottom-toolbar">底部工具栏</div>
|
||||
{surface.generationComposerNode}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -357,4 +410,54 @@ describe('useImageCanvasGenerationSurface', () => {
|
||||
screen.getByRole('dialog', { name: '生成游戏背景音乐' }),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('pans the canvas when an opened composer would be covered by the bottom toolbar', async () => {
|
||||
render(<GenerationSurfaceHarness />);
|
||||
const viewportElement = screen.getByTestId('viewport');
|
||||
const toolbarElement = viewportElement.querySelector(
|
||||
'.image-canvas-editor__bottom-toolbar',
|
||||
) as HTMLElement;
|
||||
|
||||
Object.defineProperty(viewportElement, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 900,
|
||||
bottom: 640,
|
||||
width: 900,
|
||||
height: 640,
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
});
|
||||
Object.defineProperty(toolbarElement, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => ({
|
||||
x: 250,
|
||||
y: 570,
|
||||
left: 250,
|
||||
top: 570,
|
||||
right: 650,
|
||||
bottom: 620,
|
||||
width: 400,
|
||||
height: 50,
|
||||
toJSON: () => ({}),
|
||||
}),
|
||||
});
|
||||
mockedComposerRect = createDomRect({
|
||||
left: 114,
|
||||
top: 520,
|
||||
right: 786,
|
||||
bottom: 700,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '切换生成' }));
|
||||
await screen.findByRole('dialog', { name: '生成图片' });
|
||||
|
||||
await act(async () => {});
|
||||
|
||||
expect(Number(screen.getByTestId('viewport-y').textContent)).toBe(1086);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
resolveGenerationComposerStyle,
|
||||
resolveIconComposerStyle,
|
||||
resolveQuickEditPanelStyle,
|
||||
resolveViewportForOverlayVisibility,
|
||||
} from './ImageCanvasOverlayModel';
|
||||
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
||||
|
||||
@@ -54,6 +56,7 @@ type ImageCanvasGenerationSurfaceOptions = {
|
||||
canvasSize: { width: number; height: number };
|
||||
viewport: CanvasViewport;
|
||||
setViewport: Dispatch<SetStateAction<CanvasViewport>>;
|
||||
canvasViewportRef: RefObject<HTMLDivElement | null>;
|
||||
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
|
||||
layerCounterRef: MutableRefObject<number>;
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
@@ -133,12 +136,73 @@ function buildToolbarPortalMenuStyle(
|
||||
}
|
||||
|
||||
const TOOLBAR_OPTION_CLOSE_DELAY_MS = 160;
|
||||
const COMPOSER_SAFE_MARGIN = 12;
|
||||
const COMPOSER_MIN_SAFE_HEIGHT = 120;
|
||||
|
||||
function getViewportLocalBounds(
|
||||
element: Element | null,
|
||||
viewportRect: DOMRect,
|
||||
) {
|
||||
const rect = element?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
left: rect.left - viewportRect.left,
|
||||
top: rect.top - viewportRect.top,
|
||||
right: rect.right - viewportRect.left,
|
||||
bottom: rect.bottom - viewportRect.top,
|
||||
};
|
||||
}
|
||||
|
||||
function getComposerSafeBounds(viewportElement: HTMLElement) {
|
||||
const viewportRect = viewportElement.getBoundingClientRect();
|
||||
const bottomToolbarBounds = getViewportLocalBounds(
|
||||
viewportElement.querySelector('.image-canvas-editor__bottom-toolbar'),
|
||||
viewportRect,
|
||||
);
|
||||
const panelDockBounds = getViewportLocalBounds(
|
||||
viewportElement.querySelector('.image-canvas-editor__panel-dock'),
|
||||
viewportRect,
|
||||
);
|
||||
const coveredBottom = Math.min(
|
||||
viewportRect.height,
|
||||
...[bottomToolbarBounds, panelDockBounds]
|
||||
.map((bounds) => bounds?.top)
|
||||
.filter((top): top is number => typeof top === 'number'),
|
||||
);
|
||||
const safeBottom = Math.max(
|
||||
COMPOSER_SAFE_MARGIN + COMPOSER_MIN_SAFE_HEIGHT,
|
||||
coveredBottom - COMPOSER_SAFE_MARGIN,
|
||||
);
|
||||
return {
|
||||
left: COMPOSER_SAFE_MARGIN,
|
||||
top: COMPOSER_SAFE_MARGIN,
|
||||
right: Math.max(
|
||||
COMPOSER_SAFE_MARGIN,
|
||||
viewportRect.width - COMPOSER_SAFE_MARGIN,
|
||||
),
|
||||
bottom: safeBottom,
|
||||
};
|
||||
}
|
||||
|
||||
function getActiveComposerElement(viewportElement: HTMLElement) {
|
||||
return viewportElement.querySelector<HTMLElement>(
|
||||
[
|
||||
'.image-canvas-editor__generation-composer[role="dialog"]',
|
||||
'.image-canvas-editor__quick-edit-panel[role="dialog"]',
|
||||
'.image-canvas-editor__crop-expand-panel[role="dialog"]',
|
||||
'.image-canvas-editor__character-animation-panel[role="dialog"]',
|
||||
].join(', '),
|
||||
);
|
||||
}
|
||||
|
||||
export function useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
setViewport,
|
||||
canvasViewportRef,
|
||||
setLayers,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
@@ -179,6 +243,7 @@ export function useImageCanvasGenerationSurface({
|
||||
const toolbarOptionCloseTimerRef = useRef<ReturnType<
|
||||
typeof setTimeout
|
||||
> | null>(null);
|
||||
const adjustedComposerVisibilityKeyRef = useRef<string | null>(null);
|
||||
const generationWorkflow = useImageCanvasGenerationWorkflow({
|
||||
layers,
|
||||
canvasSize,
|
||||
@@ -251,6 +316,72 @@ export function useImageCanvasGenerationSurface({
|
||||
viewport,
|
||||
canvasSize,
|
||||
});
|
||||
const activeComposerVisibilityKey =
|
||||
activeCanvasGenerationDialog &&
|
||||
activeCanvasGenerationDialog.status !== 'generating' &&
|
||||
activeCanvasGenerationDialog.composerOpen !== false &&
|
||||
generationComposerStyle
|
||||
? `${activeCanvasGenerationDialog.id}:${activeCanvasGenerationDialog.mode}:${activeCanvasGenerationDialog.placeholder?.x ?? '-'}:${activeCanvasGenerationDialog.placeholder?.y ?? '-'}:${activeCanvasGenerationDialog.placeholder?.width ?? '-'}:${activeCanvasGenerationDialog.placeholder?.height ?? '-'}`
|
||||
: generationWorkflow.quickEditPanel &&
|
||||
generationWorkflow.quickEditSourceLayer &&
|
||||
quickEditPanelStyle
|
||||
? `quick-edit-panel:${generationWorkflow.quickEditPanel.sourceLayerId}:${generationWorkflow.quickEditPanel.mode ?? 'quick-edit'}`
|
||||
: generationWorkflow.cropExpandPanel &&
|
||||
generationWorkflow.cropExpandSourceLayer &&
|
||||
cropExpandPanelStyle
|
||||
? `crop-expand-panel:${generationWorkflow.cropExpandPanel.sourceLayerId}`
|
||||
: generationWorkflow.characterAnimationPanel &&
|
||||
generationWorkflow.characterAnimationSourceLayer &&
|
||||
characterAnimationPanelStyle
|
||||
? `character-animation-panel:${generationWorkflow.characterAnimationPanel.sourceLayerId}`
|
||||
: null;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!activeComposerVisibilityKey) {
|
||||
adjustedComposerVisibilityKeyRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
adjustedComposerVisibilityKeyRef.current === activeComposerVisibilityKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const viewportElement = canvasViewportRef.current;
|
||||
if (!viewportElement) {
|
||||
return;
|
||||
}
|
||||
const composerElement = getActiveComposerElement(viewportElement);
|
||||
if (!composerElement) {
|
||||
return;
|
||||
}
|
||||
const viewportRect = viewportElement.getBoundingClientRect();
|
||||
if (viewportRect.width <= 0 || viewportRect.height <= 0) {
|
||||
return;
|
||||
}
|
||||
const composerRect = composerElement.getBoundingClientRect();
|
||||
if (composerRect.width <= 0 || composerRect.height <= 0) {
|
||||
return;
|
||||
}
|
||||
const nextViewport = resolveViewportForOverlayVisibility({
|
||||
viewport,
|
||||
overlayBounds: {
|
||||
left: composerRect.left - viewportRect.left,
|
||||
top: composerRect.top - viewportRect.top,
|
||||
right: composerRect.right - viewportRect.left,
|
||||
bottom: composerRect.bottom - viewportRect.top,
|
||||
},
|
||||
safeBounds: getComposerSafeBounds(viewportElement),
|
||||
});
|
||||
if (nextViewport !== viewport) {
|
||||
adjustedComposerVisibilityKeyRef.current = activeComposerVisibilityKey;
|
||||
setViewport(nextViewport);
|
||||
}
|
||||
}, [
|
||||
activeComposerVisibilityKey,
|
||||
canvasViewportRef,
|
||||
setViewport,
|
||||
viewport,
|
||||
]);
|
||||
|
||||
const clearToolbarOptionCloseTimer = useCallback(() => {
|
||||
if (!toolbarOptionCloseTimerRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user