Files
Genarrative/src/components/image-editor/ImageCanvasOverlayModel.ts
kdletters d0ad8402de 拆分图片画布浮层定位模型
新增 ImageCanvasOverlayModel 承载生成输入框、图片工具栏、快速编辑和角色动画面板定位规则

新增浮层定位模型单测覆盖锚定、边界限制和生成面板可见模式

更新图片画布前端拆分计划和 TRACKING 记录本阶段验证

精简 ImageCanvasEditorView 中的浮层坐标计算
2026-06-17 12:46:41 +08:00

189 lines
4.2 KiB
TypeScript

import {
TOOLBAR_HALF_WIDTH,
clamp,
} from './ImageCanvasEditorModel';
import {
ICON_COMPOSER_HORIZONTAL_CHROME_REM,
ICON_COMPOSER_MIN_WIDTH_REM,
ICON_DESCRIPTION_CARD_WIDTH_REM,
} from './ImageCanvasGenerationModel';
import type {
CanvasGenerationDialogMode,
CanvasGenerationDialogState,
CanvasLayer,
CanvasViewport,
CharacterAnimationPanelState,
GenerateDialogState,
QuickEditPanelState,
} from './ImageCanvasEditorTypes';
type CanvasSize = {
width: number;
height: number;
};
type OverlayAnchor = Pick<CanvasLayer, 'x' | 'y' | 'width' | 'height'>;
export type CanvasOverlayStyle = {
left: number;
top: number;
};
export type CanvasComposerOverlayStyle = CanvasOverlayStyle & {
width?: string;
};
export function resolveGenerationAnchor({
dialog,
generatedLayer,
}: {
dialog: CanvasGenerationDialogState | null;
generatedLayer: CanvasLayer | null;
}) {
if (!dialog) {
return null;
}
return generatedLayer ?? dialog.placeholder ?? null;
}
export function resolveGenerationComposerStyle({
dialog,
anchor,
viewport,
}: {
dialog: CanvasGenerationDialogState | null;
anchor: OverlayAnchor | null;
viewport: CanvasViewport;
}): CanvasOverlayStyle | null {
if (
!dialog ||
dialog.status === 'generating' ||
dialog.composerOpen === false ||
!anchor
) {
return null;
}
return {
left: viewport.x + (anchor.x + anchor.width / 2) * viewport.scale,
top: viewport.y + (anchor.y + anchor.height) * viewport.scale + 10,
};
}
export function resolveIconComposerStyle({
dialog,
composerStyle,
iconDescriptionCount,
}: {
dialog: CanvasGenerationDialogState | null;
composerStyle: CanvasOverlayStyle | null;
iconDescriptionCount: number;
}): CanvasComposerOverlayStyle | null {
if (dialog?.mode !== 'icon' || !composerStyle) {
return null;
}
return {
...composerStyle,
width: `${Math.max(
ICON_COMPOSER_MIN_WIDTH_REM,
ICON_COMPOSER_HORIZONTAL_CHROME_REM +
iconDescriptionCount * ICON_DESCRIPTION_CARD_WIDTH_REM,
).toFixed(1)}rem`,
};
}
export function resolveSelectedToolbarStyle({
selectedLayer,
viewport,
canvasSize,
}: {
selectedLayer: CanvasLayer | null;
viewport: CanvasViewport;
canvasSize: CanvasSize;
}): CanvasOverlayStyle | null {
if (!selectedLayer) {
return null;
}
return {
left: clamp(
viewport.x +
selectedLayer.x * viewport.scale +
(selectedLayer.width * viewport.scale) / 2,
TOOLBAR_HALF_WIDTH,
Math.max(TOOLBAR_HALF_WIDTH, canvasSize.width - TOOLBAR_HALF_WIDTH),
),
top: Math.max(10, viewport.y + selectedLayer.y * viewport.scale - 12),
};
}
export function resolveQuickEditPanelStyle({
panel,
sourceLayer,
viewport,
canvasSize,
}: {
panel: QuickEditPanelState | null;
sourceLayer: CanvasLayer | null;
viewport: CanvasViewport;
canvasSize: CanvasSize;
}): CanvasOverlayStyle | null {
if (!panel || !sourceLayer) {
return null;
}
return {
left: clamp(
viewport.x +
(sourceLayer.x + sourceLayer.width / 2) * viewport.scale,
12,
Math.max(12, canvasSize.width - 12),
),
top: clamp(
viewport.y +
(sourceLayer.y + sourceLayer.height) * viewport.scale +
12,
12,
Math.max(12, canvasSize.height - 360),
),
};
}
export function resolveCharacterAnimationPanelStyle({
panel,
sourceLayer,
viewport,
canvasSize,
}: {
panel: CharacterAnimationPanelState | null;
sourceLayer: CanvasLayer | null;
viewport: CanvasViewport;
canvasSize: CanvasSize;
}): CanvasOverlayStyle | null {
if (!panel || !sourceLayer) {
return null;
}
return {
left: clamp(
viewport.x +
(sourceLayer.x + sourceLayer.width) * viewport.scale +
12,
12,
Math.max(12, canvasSize.width - 364),
),
top: clamp(
viewport.y + sourceLayer.y * viewport.scale,
12,
Math.max(12, canvasSize.height - 520),
),
};
}
export function isCanvasGenerationComposerVisible(
dialog: GenerateDialogState | null,
): dialog is GenerateDialogState & { mode: CanvasGenerationDialogMode } {
return (
dialog?.mode === 'generate' ||
dialog?.mode === 'spec' ||
dialog?.mode === 'character' ||
dialog?.mode === 'icon'
);
}