新增 ImageCanvasOverlayModel 承载生成输入框、图片工具栏、快速编辑和角色动画面板定位规则 新增浮层定位模型单测覆盖锚定、边界限制和生成面板可见模式 更新图片画布前端拆分计划和 TRACKING 记录本阶段验证 精简 ImageCanvasEditorView 中的浮层坐标计算
189 lines
4.2 KiB
TypeScript
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'
|
|
);
|
|
}
|