ecac0dc3fc
画板参考图选择提示改为持续显示并支持手动关闭。 显式项目访问失效时同步切回项目页状态。 补充提示关闭和项目失效回退测试。
2338 lines
72 KiB
TypeScript
2338 lines
72 KiB
TypeScript
import {
|
||
type Dispatch,
|
||
type MutableRefObject,
|
||
type PointerEvent as ReactPointerEvent,
|
||
type SetStateAction,
|
||
useCallback,
|
||
useEffect,
|
||
useMemo,
|
||
useRef,
|
||
useState,
|
||
} from 'react';
|
||
|
||
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
|
||
import type {
|
||
EditorAssetSnapshot,
|
||
EditorProjectSnapshot,
|
||
} from '../../services/image-editor/editorProjectClient';
|
||
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
|
||
import type {
|
||
CanvasGenerationDialogState,
|
||
CanvasLayer,
|
||
CanvasTool,
|
||
CanvasViewport,
|
||
CharacterAnimationPanelState,
|
||
CharacterReferenceImage,
|
||
CropExpandPanelState,
|
||
CropExpandResizeHandle,
|
||
GenerateDialogState,
|
||
ImageContextMenuState,
|
||
PublicationMaterialsWorkflowId,
|
||
QuickEditPanelState,
|
||
SidebarPanel,
|
||
SpecFormValues,
|
||
SpecGenerationType,
|
||
} from './ImageCanvasEditorTypes';
|
||
import {
|
||
appendCharacterReference,
|
||
appendGenerationReference,
|
||
appendPublicationReference,
|
||
assignCharacterSpecReference,
|
||
assignIconSpecReference,
|
||
assignUiDesignSpecReference,
|
||
closeGenerateComposerDialog,
|
||
createAudioRedrawGenerationDialogDraft,
|
||
createBackgroundMusicGenerationDialogDraft,
|
||
createCharacterAnimationGenerationDialogDraft,
|
||
createCharacterGenerationDialogDraft,
|
||
createEditDialogDraft,
|
||
createGenerateDialogDraft,
|
||
createIconGenerationDialogDraft,
|
||
createPublicationGenerationDialogDraft,
|
||
createQuickEditGenerationDialogDraft,
|
||
createQuickEditPanelDraft,
|
||
createRedrawPanelDraft,
|
||
createSameSourceGenerationDialogDraft,
|
||
createSoundEffectGenerationDialogDraft,
|
||
createSpecDialogDraft,
|
||
createUiDesignGenerationDialogDraft,
|
||
createVideoGenerationDialogDraft,
|
||
createVideoRedrawGenerationDialogDraft,
|
||
hideGeneratedLayerComposerAfterBlur,
|
||
isCharacterSpecReferenceLayer,
|
||
isIconSpecReferenceLayer,
|
||
updateCharacterAnimationDurationPanel,
|
||
updateIconDescriptionsTextInDialog,
|
||
updateSpecFormDialogValue,
|
||
} from './ImageCanvasGenerationDialogModel';
|
||
import {
|
||
calculateCharacterAnimationPrice,
|
||
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
||
CHARACTER_ANIMATION_MODEL,
|
||
DEFAULT_ICON_DESCRIPTIONS,
|
||
DEFAULT_IMAGE_MODEL,
|
||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||
ICON_DESCRIPTION_LIMIT,
|
||
IMAGE_MODEL_GPT_IMAGE_2,
|
||
isCanvasGenerationDialog,
|
||
normalizeEditorImageModel,
|
||
resizeGenerationPlaceholderToImageSelection,
|
||
resolveEditorImageGenerationPixelSize,
|
||
} from './ImageCanvasGenerationModel';
|
||
import {
|
||
centerViewportOnPlacement,
|
||
chooseGenerationPlacement,
|
||
} from './ImageCanvasGenerationPlacementModel';
|
||
import { resolveQuickEditFocusViewport } from './ImageCanvasOverlayModel';
|
||
import {
|
||
buildCropExpandInsetsFromFrame,
|
||
removeImageBackground,
|
||
renderCropExpandImage,
|
||
} from './ImageCanvasRasterEditModel';
|
||
import {
|
||
createUiAssetExtractionDraftMark,
|
||
createUiAssetExtractionState,
|
||
normalizeUiAssetExtractionMark,
|
||
type UiAssetExtractionState,
|
||
type UiAssetExtractionTool,
|
||
updateUiAssetExtractionDraftMark,
|
||
} from './ImageCanvasUiAssetExtractionModel';
|
||
import {
|
||
applyQueuedEditorGenerationProject,
|
||
useImageCanvasGenerationSubmissionWorkflow,
|
||
} from './useImageCanvasGenerationSubmissionWorkflow';
|
||
|
||
type CanvasSize = { width: number; height: number };
|
||
|
||
type CropExpandResizeDragState = {
|
||
pointerId: number;
|
||
handle: CropExpandResizeHandle;
|
||
sourceLayerId: string;
|
||
startClientX: number;
|
||
startClientY: number;
|
||
startScale: number;
|
||
startFrame: CropExpandPanelState['frame'];
|
||
ratio: CropExpandPanelState['ratio'];
|
||
};
|
||
|
||
type CanvasGenerationDialogUpdater = (
|
||
dialog: CanvasGenerationDialogState,
|
||
) => CanvasGenerationDialogState | null;
|
||
|
||
type RememberedImageGenerationOptions = {
|
||
model: string;
|
||
aspectRatio: string;
|
||
imageSize: string;
|
||
};
|
||
|
||
function findSourceGenerationDialog(
|
||
dialogs: CanvasGenerationDialogState[],
|
||
sourceLayer: CanvasLayer,
|
||
) {
|
||
return [...dialogs]
|
||
.reverse()
|
||
.find(
|
||
(dialog) =>
|
||
dialog.generatedLayerId === sourceLayer.id ||
|
||
(!dialog.generatedLayerId && dialog.sourceLayerId === sourceLayer.id),
|
||
);
|
||
}
|
||
|
||
function findSourceAnimationLayer(
|
||
layers: CanvasLayer[],
|
||
sourceLayer: CanvasLayer,
|
||
) {
|
||
if (sourceLayer.assetKind !== 'character-animation') {
|
||
return null;
|
||
}
|
||
const sourceResourceId = sourceLayer.sourceResourceId?.trim();
|
||
if (!sourceResourceId) {
|
||
return null;
|
||
}
|
||
return (
|
||
layers.find(
|
||
(layer) =>
|
||
layer.resourceId === sourceResourceId &&
|
||
layer.assetKind === 'character',
|
||
) ?? null
|
||
);
|
||
}
|
||
|
||
function getCanvasToolForGenerationMode(
|
||
mode: CanvasGenerationDialogState['mode'],
|
||
): CanvasTool {
|
||
if (mode === 'video') {
|
||
return 'video';
|
||
}
|
||
if (mode === 'audio-sound-effect' || mode === 'audio-background-music') {
|
||
return 'music';
|
||
}
|
||
if (mode === 'character' || mode === 'character-animation') {
|
||
return 'character';
|
||
}
|
||
if (mode === 'icon') {
|
||
return 'icon';
|
||
}
|
||
if (mode === 'publication') {
|
||
return 'publication';
|
||
}
|
||
if (mode === 'ui-design') {
|
||
return 'ui-design';
|
||
}
|
||
if (mode === 'spec') {
|
||
return 'spec';
|
||
}
|
||
return 'generate';
|
||
}
|
||
|
||
const LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY =
|
||
'genarrative.imageCanvas.lastCharacterSpecReference';
|
||
const LAST_ICON_SPEC_REFERENCE_CACHE_KEY =
|
||
'genarrative.imageCanvas.lastIconSpecReference';
|
||
const INVALID_CHARACTER_SPEC_WARNING =
|
||
'选择的图片不是角色规范图,请选择生成规范里的角色规范。';
|
||
const INVALID_ICON_SPEC_WARNING =
|
||
'选择的图片不是图标规范图,请选择生成规范里的图标规范。';
|
||
|
||
function normalizeRememberedImageGenerationOptions({
|
||
model,
|
||
aspectRatio,
|
||
imageSize,
|
||
}: Partial<RememberedImageGenerationOptions>): RememberedImageGenerationOptions {
|
||
const normalizedModel = normalizeEditorImageModel(model);
|
||
const dimensionOptions =
|
||
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||
normalizedModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
||
const aspectRatios = dimensionOptions.aspectRatios as readonly string[];
|
||
const imageSizes = dimensionOptions.imageSizes as readonly string[];
|
||
|
||
return {
|
||
model: normalizedModel,
|
||
aspectRatio:
|
||
aspectRatio && aspectRatios.includes(aspectRatio)
|
||
? aspectRatio
|
||
: (dimensionOptions.aspectRatios[0] ?? '1:1'),
|
||
imageSize:
|
||
imageSize && imageSizes.includes(imageSize)
|
||
? imageSize
|
||
: (dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
||
dimensionOptions.imageSizes[0] ??
|
||
'1K'),
|
||
};
|
||
}
|
||
|
||
function applyRememberedImageGenerationOptions(
|
||
draft: Omit<CanvasGenerationDialogState, 'id'>,
|
||
options: RememberedImageGenerationOptions,
|
||
): Omit<CanvasGenerationDialogState, 'id'> {
|
||
return resizeGenerationPlaceholderToImageSelection({
|
||
...draft,
|
||
...normalizeRememberedImageGenerationOptions(options),
|
||
}) as Omit<CanvasGenerationDialogState, 'id'>;
|
||
}
|
||
|
||
function shouldRememberImageGenerationOptions(
|
||
dialog: GenerateDialogState | null,
|
||
): dialog is GenerateDialogState & {
|
||
mode: 'generate' | 'character' | 'icon';
|
||
} {
|
||
return (
|
||
dialog?.mode === 'generate' ||
|
||
dialog?.mode === 'character' ||
|
||
dialog?.mode === 'icon'
|
||
);
|
||
}
|
||
|
||
function appendQuickEditSelectionPrompt(prompt: string, markNumber: number) {
|
||
const nextLine = `对${markNumber}号红色圈选框里的内容做以下修改:`;
|
||
const trimmedPrompt = prompt.trimEnd();
|
||
return trimmedPrompt ? `${trimmedPrompt}\n${nextLine}` : nextLine;
|
||
}
|
||
|
||
function getLocalGenerationReferenceStorage() {
|
||
if (typeof window === 'undefined') {
|
||
return null;
|
||
}
|
||
try {
|
||
return window.localStorage;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function readOptionalCachedString(
|
||
record: Record<string, unknown>,
|
||
key: keyof CharacterReferenceImage,
|
||
) {
|
||
const value = record[key];
|
||
return typeof value === 'string' ? value : undefined;
|
||
}
|
||
|
||
function readOptionalCachedNullableString(
|
||
record: Record<string, unknown>,
|
||
key: keyof CharacterReferenceImage,
|
||
) {
|
||
const value = record[key];
|
||
if (value === null) {
|
||
return null;
|
||
}
|
||
return typeof value === 'string' ? value : undefined;
|
||
}
|
||
|
||
function readCachedGenerationReference(
|
||
key: string,
|
||
currentUserId?: string | null,
|
||
): CharacterReferenceImage | null {
|
||
const storage = getLocalGenerationReferenceStorage();
|
||
if (!storage) {
|
||
return null;
|
||
}
|
||
try {
|
||
const rawValue = storage.getItem(key);
|
||
if (!rawValue) {
|
||
return null;
|
||
}
|
||
const parsedValue: unknown = JSON.parse(rawValue);
|
||
if (!parsedValue || typeof parsedValue !== 'object') {
|
||
return null;
|
||
}
|
||
const record = parsedValue as Record<string, unknown>;
|
||
const normalizedCurrentUserId = currentUserId?.trim();
|
||
const ownerUserId =
|
||
typeof record.ownerUserId === 'string' ? record.ownerUserId.trim() : '';
|
||
if (normalizedCurrentUserId && ownerUserId !== normalizedCurrentUserId) {
|
||
storage.removeItem(key);
|
||
return null;
|
||
}
|
||
const id = readOptionalCachedString(record, 'id');
|
||
const label = readOptionalCachedString(record, 'label');
|
||
const src = readOptionalCachedString(record, 'src');
|
||
if (!id || !label || !src) {
|
||
return null;
|
||
}
|
||
const reference: CharacterReferenceImage = { id, label, src };
|
||
const mediaType = readOptionalCachedString(record, 'mediaType');
|
||
if (
|
||
mediaType === 'image' ||
|
||
mediaType === 'video' ||
|
||
mediaType === 'audio'
|
||
) {
|
||
reference.mediaType = mediaType;
|
||
}
|
||
const mimeType = readOptionalCachedString(record, 'mimeType');
|
||
if (mimeType) {
|
||
reference.mimeType = mimeType;
|
||
}
|
||
const sizeBytes = record.sizeBytes;
|
||
if (typeof sizeBytes === 'number') {
|
||
reference.sizeBytes = sizeBytes;
|
||
}
|
||
const durationSeconds = record.durationSeconds;
|
||
if (typeof durationSeconds === 'number') {
|
||
reference.durationSeconds = durationSeconds;
|
||
}
|
||
for (const keyName of [
|
||
'objectKey',
|
||
'assetObjectId',
|
||
'resourceId',
|
||
'sourceAssetId',
|
||
] as const) {
|
||
const value = readOptionalCachedNullableString(record, keyName);
|
||
if (value !== undefined) {
|
||
reference[keyName] = value;
|
||
}
|
||
}
|
||
return reference;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function writeCachedGenerationReference(
|
||
key: string,
|
||
reference: CharacterReferenceImage,
|
||
currentUserId?: string | null,
|
||
) {
|
||
const storage = getLocalGenerationReferenceStorage();
|
||
if (!storage) {
|
||
return;
|
||
}
|
||
try {
|
||
const ownerUserId = currentUserId?.trim();
|
||
storage.setItem(
|
||
key,
|
||
JSON.stringify(ownerUserId ? { ...reference, ownerUserId } : reference),
|
||
);
|
||
} catch {
|
||
// 中文注释:本地缓存只提升下次新建素材的便捷性,写入失败不阻断生成流程。
|
||
}
|
||
}
|
||
|
||
function createCharacterAnimationPanelFromDialog(
|
||
dialog: CanvasGenerationDialogState | null,
|
||
): CharacterAnimationPanelState | null {
|
||
if (dialog?.mode !== 'character-animation' || !dialog.sourceLayerId) {
|
||
return null;
|
||
}
|
||
return {
|
||
sourceLayerId: dialog.sourceLayerId,
|
||
promptText: dialog.prompt,
|
||
resolution: dialog.characterAnimationResolution ?? '480p',
|
||
ratio: dialog.characterAnimationRatio ?? 'same',
|
||
frameCount: dialog.characterAnimationFrameCount ?? 32,
|
||
durationSeconds: dialog.characterAnimationDurationSeconds ?? 4,
|
||
status:
|
||
dialog.status === 'generating'
|
||
? 'generating'
|
||
: dialog.status === 'failed'
|
||
? 'failed'
|
||
: dialog.characterAnimationResult
|
||
? 'completed'
|
||
: 'idle',
|
||
errorMessage: dialog.errorMessage,
|
||
result: dialog.characterAnimationResult,
|
||
};
|
||
}
|
||
|
||
function applyCharacterAnimationPanelToDialog(
|
||
dialog: CanvasGenerationDialogState,
|
||
panel: CharacterAnimationPanelState | null,
|
||
): CanvasGenerationDialogState {
|
||
if (dialog.mode !== 'character-animation') {
|
||
return dialog;
|
||
}
|
||
if (!panel) {
|
||
return {
|
||
...dialog,
|
||
composerOpen: false,
|
||
};
|
||
}
|
||
return {
|
||
...dialog,
|
||
prompt: panel.promptText,
|
||
characterAnimationResolution: panel.resolution,
|
||
characterAnimationRatio: panel.ratio,
|
||
characterAnimationFrameCount: panel.frameCount,
|
||
characterAnimationDurationSeconds: panel.durationSeconds,
|
||
characterAnimationResult: panel.result,
|
||
status:
|
||
panel.status === 'generating'
|
||
? 'generating'
|
||
: panel.status === 'failed'
|
||
? 'failed'
|
||
: 'idle',
|
||
errorMessage: panel.errorMessage,
|
||
};
|
||
}
|
||
|
||
type GenerationWorkflowOptions = {
|
||
layers: CanvasLayer[];
|
||
canvasSize: CanvasSize;
|
||
viewport: CanvasViewport;
|
||
setViewport: Dispatch<SetStateAction<CanvasViewport>>;
|
||
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
|
||
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
||
layerCounterRef: MutableRefObject<number>;
|
||
generateDialog: GenerateDialogState | null;
|
||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||
openCanvasGenerationDialog: (
|
||
dialog: Omit<CanvasGenerationDialogState, 'id'>,
|
||
) => string;
|
||
updateCanvasGenerationDialogById: (
|
||
dialogId: string,
|
||
updater: CanvasGenerationDialogUpdater,
|
||
) => void;
|
||
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
|
||
archiveActiveCanvasGenerationDialog: () => void;
|
||
removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void;
|
||
getGeneratingDialogPlaceholder: (
|
||
dialog: GenerateDialogState,
|
||
) => GenerateDialogState['placeholder'];
|
||
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
|
||
selectSingleLayer: (layerId: string | null) => void;
|
||
fitLayers: (targetLayers?: CanvasLayer[]) => void;
|
||
captureCanvasHistory: () => void;
|
||
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
|
||
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
|
||
setMetadataLayer: Dispatch<SetStateAction<CanvasLayer | null>>;
|
||
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
||
persistGeneratedAsset?: (layer: CanvasLayer) => void;
|
||
persistUpdatedLayerResource?: (layer: CanvasLayer) => void;
|
||
projectId?: string | null;
|
||
currentUserId?: string | null;
|
||
assetFolderId?: string | null;
|
||
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
|
||
applyProjectSnapshot?: (project: EditorProjectSnapshot) => void;
|
||
onWalletBalanceMayHaveChanged?: () => void;
|
||
};
|
||
|
||
export function useImageCanvasGenerationWorkflow({
|
||
layers,
|
||
canvasSize,
|
||
viewport,
|
||
setViewport,
|
||
setLayers,
|
||
canvasGenerationDialogs,
|
||
layerCounterRef,
|
||
generateDialog,
|
||
setGenerateDialog,
|
||
openCanvasGenerationDialog,
|
||
updateCanvasGenerationDialogById,
|
||
hasCanvasGenerationDialogById,
|
||
archiveActiveCanvasGenerationDialog,
|
||
removeCanvasGenerationDialogsByLayerId,
|
||
getGeneratingDialogPlaceholder,
|
||
appendCanvasLayersWithResources,
|
||
selectSingleLayer,
|
||
fitLayers,
|
||
captureCanvasHistory,
|
||
setActiveTool,
|
||
setActiveSidebarPanel,
|
||
setMetadataLayer,
|
||
setImageContextMenu,
|
||
persistGeneratedAsset,
|
||
persistUpdatedLayerResource,
|
||
projectId,
|
||
currentUserId,
|
||
assetFolderId,
|
||
upsertGeneratedAsset,
|
||
applyProjectSnapshot,
|
||
onWalletBalanceMayHaveChanged,
|
||
}: GenerationWorkflowOptions) {
|
||
const [isTaskSidebarOpen, setIsTaskSidebarOpen] = useState(false);
|
||
const [taskListRefreshKey, setTaskListRefreshKey] = useState(0);
|
||
const refreshTaskListForQueuedGeneration = useCallback(() => {
|
||
setIsTaskSidebarOpen(true);
|
||
setTaskListRefreshKey((key) => key + 1);
|
||
}, []);
|
||
const previousTaskCountRef = useRef(canvasGenerationDialogs.length);
|
||
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
||
const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] =
|
||
useState(false);
|
||
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false);
|
||
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
|
||
useState(false);
|
||
const [
|
||
isPickingGenerationReferenceFromCanvas,
|
||
setIsPickingGenerationReferenceFromCanvas,
|
||
] = useState(false);
|
||
const [
|
||
isPickingQuickEditReferenceFromCanvas,
|
||
setIsPickingQuickEditReferenceFromCanvas,
|
||
] = useState(false);
|
||
const [
|
||
isPickingCharacterSpecFromCanvas,
|
||
setIsPickingCharacterSpecFromCanvas,
|
||
] = useState(false);
|
||
const [
|
||
isPickingCharacterReferenceFromCanvas,
|
||
setIsPickingCharacterReferenceFromCanvas,
|
||
] = useState(false);
|
||
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false);
|
||
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
||
useState(false);
|
||
const [isUiDesignSpecMenuOpen, setIsUiDesignSpecMenuOpen] = useState(false);
|
||
const [isPickingUiDesignSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas] =
|
||
useState(false);
|
||
const [isMusicMenuOpen, setIsMusicMenuOpen] = useState(false);
|
||
const [isPublicationMenuOpen, setIsPublicationMenuOpen] = useState(false);
|
||
const [isPublicationReferenceMenuOpen, setIsPublicationReferenceMenuOpen] =
|
||
useState(false);
|
||
const [
|
||
isPickingPublicationReferenceFromCanvas,
|
||
setIsPickingPublicationReferenceFromCanvas,
|
||
] = useState(false);
|
||
const [quickEditPanel, setQuickEditPanel] =
|
||
useState<QuickEditPanelState | null>(null);
|
||
const [referencePickWarning, setReferencePickWarning] = useState<
|
||
string | null
|
||
>(null);
|
||
const [cropExpandPanel, setCropExpandPanel] =
|
||
useState<CropExpandPanelState | null>(null);
|
||
const [uiAssetExtractionState, setUiAssetExtractionState] =
|
||
useState<UiAssetExtractionState | null>(null);
|
||
const [quickEditSelectionState, setQuickEditSelectionState] =
|
||
useState<UiAssetExtractionState | null>(null);
|
||
const quickEditSelectionStateRef = useRef<UiAssetExtractionState | null>(
|
||
null,
|
||
);
|
||
const cropExpandPanelRef = useRef<CropExpandPanelState | null>(null);
|
||
const cropExpandResizeDragRef = useRef<CropExpandResizeDragState | null>(
|
||
null,
|
||
);
|
||
const uiAssetExtractionMarkCounterRef = useRef(0);
|
||
|
||
useEffect(() => {
|
||
if (canvasGenerationDialogs.length > previousTaskCountRef.current) {
|
||
setIsTaskSidebarOpen(true);
|
||
}
|
||
previousTaskCountRef.current = canvasGenerationDialogs.length;
|
||
}, [canvasGenerationDialogs.length]);
|
||
const quickEditSelectionMarkCounterRef = useRef(0);
|
||
const [characterAnimationPanel, setCharacterAnimationPanel] =
|
||
useState<CharacterAnimationPanelState | null>(null);
|
||
const [rememberedImageOptions, setRememberedImageOptions] =
|
||
useState<RememberedImageGenerationOptions>(() =>
|
||
normalizeRememberedImageGenerationOptions({ model: DEFAULT_IMAGE_MODEL }),
|
||
);
|
||
const [initialCharacterSpecReference] =
|
||
useState<CharacterReferenceImage | null>(() =>
|
||
readCachedGenerationReference(
|
||
LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY,
|
||
currentUserId,
|
||
),
|
||
);
|
||
const [initialIconSpecReference] = useState<CharacterReferenceImage | null>(
|
||
() =>
|
||
readCachedGenerationReference(
|
||
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
|
||
currentUserId,
|
||
),
|
||
);
|
||
const currentUserIdRef = useRef(currentUserId);
|
||
const lastCharacterSpecReferenceRef = useRef<CharacterReferenceImage | null>(
|
||
initialCharacterSpecReference,
|
||
);
|
||
const lastIconSpecReferenceRef = useRef<CharacterReferenceImage | null>(
|
||
initialIconSpecReference,
|
||
);
|
||
const characterAnimationDialog =
|
||
isCanvasGenerationDialog(generateDialog) &&
|
||
generateDialog.mode === 'character-animation'
|
||
? generateDialog
|
||
: null;
|
||
const characterAnimationDialogPanel = createCharacterAnimationPanelFromDialog(
|
||
characterAnimationDialog,
|
||
);
|
||
const effectiveCharacterAnimationPanel =
|
||
characterAnimationDialogPanel ?? characterAnimationPanel;
|
||
|
||
cropExpandPanelRef.current = cropExpandPanel;
|
||
quickEditSelectionStateRef.current = quickEditSelectionState;
|
||
|
||
useEffect(() => {
|
||
currentUserIdRef.current = currentUserId;
|
||
lastCharacterSpecReferenceRef.current = readCachedGenerationReference(
|
||
LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY,
|
||
currentUserId,
|
||
);
|
||
lastIconSpecReferenceRef.current = readCachedGenerationReference(
|
||
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
|
||
currentUserId,
|
||
);
|
||
}, [currentUserId]);
|
||
|
||
const updateQuickEditSelectionState = useCallback(
|
||
(
|
||
updater: (
|
||
currentState: UiAssetExtractionState | null,
|
||
) => UiAssetExtractionState | null,
|
||
) => {
|
||
const nextState = updater(quickEditSelectionStateRef.current);
|
||
quickEditSelectionStateRef.current = nextState;
|
||
setQuickEditSelectionState(nextState);
|
||
return nextState;
|
||
},
|
||
[],
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (
|
||
generateDialog?.mode === 'character' &&
|
||
generateDialog.characterSpecReference
|
||
) {
|
||
lastCharacterSpecReferenceRef.current =
|
||
generateDialog.characterSpecReference;
|
||
writeCachedGenerationReference(
|
||
LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY,
|
||
generateDialog.characterSpecReference,
|
||
currentUserIdRef.current,
|
||
);
|
||
return;
|
||
}
|
||
if (generateDialog?.mode === 'icon' && generateDialog.iconSpecReference) {
|
||
lastIconSpecReferenceRef.current = generateDialog.iconSpecReference;
|
||
writeCachedGenerationReference(
|
||
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
|
||
generateDialog.iconSpecReference,
|
||
currentUserIdRef.current,
|
||
);
|
||
return;
|
||
}
|
||
if (
|
||
generateDialog?.mode === 'ui-design' &&
|
||
generateDialog.uiDesignSpecReference
|
||
) {
|
||
lastIconSpecReferenceRef.current = generateDialog.uiDesignSpecReference;
|
||
writeCachedGenerationReference(
|
||
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
|
||
generateDialog.uiDesignSpecReference,
|
||
currentUserIdRef.current,
|
||
);
|
||
}
|
||
}, [generateDialog]);
|
||
|
||
useEffect(() => {
|
||
if (!shouldRememberImageGenerationOptions(generateDialog)) {
|
||
return;
|
||
}
|
||
const nextOptions = normalizeRememberedImageGenerationOptions({
|
||
model: generateDialog.imageModel,
|
||
aspectRatio: generateDialog.aspectRatio,
|
||
imageSize: generateDialog.imageSize,
|
||
});
|
||
setRememberedImageOptions((currentOptions) =>
|
||
currentOptions.model === nextOptions.model &&
|
||
currentOptions.aspectRatio === nextOptions.aspectRatio &&
|
||
currentOptions.imageSize === nextOptions.imageSize
|
||
? currentOptions
|
||
: nextOptions,
|
||
);
|
||
}, [
|
||
generateDialog?.aspectRatio,
|
||
generateDialog?.imageModel,
|
||
generateDialog?.imageSize,
|
||
generateDialog?.mode,
|
||
]);
|
||
|
||
const quickEditSourceLayer = quickEditPanel
|
||
? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
|
||
null)
|
||
: null;
|
||
const cropExpandSourceLayer = cropExpandPanel
|
||
? (layers.find((layer) => layer.id === cropExpandPanel.sourceLayerId) ??
|
||
null)
|
||
: null;
|
||
const uiAssetExtractionSourceLayer = uiAssetExtractionState
|
||
? (layers.find(
|
||
(layer) => layer.id === uiAssetExtractionState.sourceLayerId,
|
||
) ?? null)
|
||
: null;
|
||
const quickEditSelectionSourceLayer = quickEditSelectionState
|
||
? (layers.find(
|
||
(layer) => layer.id === quickEditSelectionState.sourceLayerId,
|
||
) ?? null)
|
||
: null;
|
||
const characterAnimationSourceLayer = effectiveCharacterAnimationPanel
|
||
? (layers.find(
|
||
(layer) => layer.id === effectiveCharacterAnimationPanel.sourceLayerId,
|
||
) ?? null)
|
||
: null;
|
||
const characterAnimationPrice = effectiveCharacterAnimationPanel
|
||
? calculateCharacterAnimationPrice(
|
||
CHARACTER_ANIMATION_MODEL,
|
||
effectiveCharacterAnimationPanel.resolution,
|
||
effectiveCharacterAnimationPanel.durationSeconds,
|
||
)
|
||
: 0;
|
||
const iconDescriptionValues =
|
||
generateDialog?.mode === 'icon'
|
||
? generateDialog.prompt.trim()
|
||
? generateDialog.prompt
|
||
.split(/[\r\n,,、;;/|]+/u)
|
||
.map((description) => description.trim())
|
||
.filter(Boolean)
|
||
.slice(0, ICON_DESCRIPTION_LIMIT)
|
||
: (generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
|
||
: DEFAULT_ICON_DESCRIPTIONS;
|
||
|
||
const closeGenerationTransientState = useCallback(() => {
|
||
setIsSpecMenuOpen(false);
|
||
setIsGenerationReferenceMenuOpen(false);
|
||
setIsPickingQuickEditReferenceFromCanvas(false);
|
||
setIsCharacterSpecMenuOpen(false);
|
||
setIsCharacterReferenceMenuOpen(false);
|
||
setIsPickingGenerationReferenceFromCanvas(false);
|
||
setIsPickingCharacterSpecFromCanvas(false);
|
||
setIsPickingCharacterReferenceFromCanvas(false);
|
||
setIsIconSpecMenuOpen(false);
|
||
setIsPickingIconSpecFromCanvas(false);
|
||
setIsUiDesignSpecMenuOpen(false);
|
||
setIsPickingUiDesignSpecFromCanvas(false);
|
||
setIsMusicMenuOpen(false);
|
||
setIsPublicationMenuOpen(false);
|
||
setIsPublicationReferenceMenuOpen(false);
|
||
setIsPickingPublicationReferenceFromCanvas(false);
|
||
setUiAssetExtractionState(null);
|
||
setQuickEditSelectionState(null);
|
||
setImageContextMenu(null);
|
||
}, [setImageContextMenu]);
|
||
|
||
const openPlacedCanvasGenerationDialog = useCallback(
|
||
(draft: Omit<CanvasGenerationDialogState, 'id'>) => {
|
||
const draftPlaceholder = draft.placeholder;
|
||
if (!draftPlaceholder) {
|
||
return {
|
||
dialogId: openCanvasGenerationDialog(draft),
|
||
placeholder: undefined,
|
||
};
|
||
}
|
||
// 中文注释:所有画布生成入口统一先走 placement 模型,避免新占位压住已有图层或生成占位。
|
||
const placement = chooseGenerationPlacement({
|
||
canvasSize,
|
||
viewport,
|
||
frame: draftPlaceholder,
|
||
layers,
|
||
generationDialogs: canvasGenerationDialogs,
|
||
});
|
||
const dialogId = openCanvasGenerationDialog({
|
||
...draft,
|
||
placeholder: placement,
|
||
});
|
||
setViewport(
|
||
centerViewportOnPlacement({
|
||
canvasSize,
|
||
viewport,
|
||
placement,
|
||
}),
|
||
);
|
||
return { dialogId, placeholder: placement };
|
||
},
|
||
[
|
||
canvasGenerationDialogs,
|
||
canvasSize,
|
||
layers,
|
||
openCanvasGenerationDialog,
|
||
setViewport,
|
||
viewport,
|
||
],
|
||
);
|
||
|
||
const activateCanvasGenerationEntry = useCallback(
|
||
(activeTool: CanvasTool) => {
|
||
closeGenerationTransientState();
|
||
setActiveTool(activeTool);
|
||
selectSingleLayer(null);
|
||
setQuickEditPanel(null);
|
||
setCropExpandPanel(null);
|
||
setCharacterAnimationPanel(null);
|
||
setUiAssetExtractionState(null);
|
||
setQuickEditSelectionState(null);
|
||
},
|
||
[closeGenerationTransientState, selectSingleLayer, setActiveTool],
|
||
);
|
||
|
||
const openGenerateDialog = useCallback(() => {
|
||
openPlacedCanvasGenerationDialog(
|
||
applyRememberedImageGenerationOptions(
|
||
createGenerateDialogDraft({ canvasSize, viewport }),
|
||
rememberedImageOptions,
|
||
),
|
||
);
|
||
activateCanvasGenerationEntry('generate');
|
||
}, [
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
rememberedImageOptions,
|
||
viewport,
|
||
]);
|
||
|
||
const openSpecDialog = useCallback(
|
||
(specType: SpecGenerationType) => {
|
||
openPlacedCanvasGenerationDialog(
|
||
createSpecDialogDraft({ canvasSize, viewport, specType }),
|
||
);
|
||
activateCanvasGenerationEntry('generate');
|
||
},
|
||
[
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
viewport,
|
||
],
|
||
);
|
||
|
||
const openCharacterAnimationPanel = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
const draft = createCharacterAnimationGenerationDialogDraft({
|
||
canvasSize,
|
||
viewport,
|
||
layer,
|
||
});
|
||
if (!draft) {
|
||
return;
|
||
}
|
||
openPlacedCanvasGenerationDialog(draft);
|
||
activateCanvasGenerationEntry('character');
|
||
},
|
||
[
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
viewport,
|
||
],
|
||
);
|
||
|
||
const openCharacterGenerationDialog = useCallback(() => {
|
||
const draft = applyRememberedImageGenerationOptions(
|
||
createCharacterGenerationDialogDraft({
|
||
canvasSize,
|
||
viewport,
|
||
imageModel: rememberedImageOptions.model,
|
||
}),
|
||
rememberedImageOptions,
|
||
);
|
||
openPlacedCanvasGenerationDialog({
|
||
...draft,
|
||
characterSpecReference:
|
||
lastCharacterSpecReferenceRef.current ?? draft.characterSpecReference,
|
||
});
|
||
activateCanvasGenerationEntry('character');
|
||
}, [
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
rememberedImageOptions,
|
||
viewport,
|
||
]);
|
||
|
||
const openIconGenerationDialog = useCallback(() => {
|
||
const draft = applyRememberedImageGenerationOptions(
|
||
createIconGenerationDialogDraft({
|
||
canvasSize,
|
||
viewport,
|
||
imageModel: rememberedImageOptions.model,
|
||
}),
|
||
rememberedImageOptions,
|
||
);
|
||
openPlacedCanvasGenerationDialog({
|
||
...draft,
|
||
iconSpecReference:
|
||
lastIconSpecReferenceRef.current ?? draft.iconSpecReference,
|
||
});
|
||
activateCanvasGenerationEntry('icon');
|
||
}, [
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
rememberedImageOptions,
|
||
viewport,
|
||
]);
|
||
|
||
const openPublicationGenerationDialog = useCallback(
|
||
(workflowId: PublicationMaterialsWorkflowId) => {
|
||
openPlacedCanvasGenerationDialog(
|
||
createPublicationGenerationDialogDraft({
|
||
canvasSize,
|
||
viewport,
|
||
workflowId,
|
||
}),
|
||
);
|
||
activateCanvasGenerationEntry('publication');
|
||
},
|
||
[
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
viewport,
|
||
],
|
||
);
|
||
|
||
const openVideoGenerationDialog = useCallback(() => {
|
||
openPlacedCanvasGenerationDialog(
|
||
createVideoGenerationDialogDraft({ canvasSize, viewport }),
|
||
);
|
||
activateCanvasGenerationEntry('video');
|
||
}, [
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
viewport,
|
||
]);
|
||
|
||
const openUiDesignGenerationDialog = useCallback(() => {
|
||
const draft = createUiDesignGenerationDialogDraft({
|
||
canvasSize,
|
||
viewport,
|
||
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||
});
|
||
openPlacedCanvasGenerationDialog({
|
||
...draft,
|
||
uiDesignSpecReference:
|
||
lastIconSpecReferenceRef.current ?? draft.uiDesignSpecReference,
|
||
});
|
||
activateCanvasGenerationEntry('ui-design');
|
||
}, [
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
viewport,
|
||
]);
|
||
|
||
const openSoundEffectGenerationDialog = useCallback(() => {
|
||
openPlacedCanvasGenerationDialog(
|
||
createSoundEffectGenerationDialogDraft({ canvasSize, viewport }),
|
||
);
|
||
activateCanvasGenerationEntry('music');
|
||
}, [
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
viewport,
|
||
]);
|
||
|
||
const openBackgroundMusicGenerationDialog = useCallback(() => {
|
||
openPlacedCanvasGenerationDialog(
|
||
createBackgroundMusicGenerationDialogDraft({ canvasSize, viewport }),
|
||
);
|
||
activateCanvasGenerationEntry('music');
|
||
}, [
|
||
activateCanvasGenerationEntry,
|
||
canvasSize,
|
||
openPlacedCanvasGenerationDialog,
|
||
viewport,
|
||
]);
|
||
|
||
const openEditDialog = useCallback(
|
||
(sourceLayer: CanvasLayer) => {
|
||
setMetadataLayer(null);
|
||
setImageContextMenu(null);
|
||
setQuickEditPanel(null);
|
||
setCropExpandPanel(null);
|
||
setUiAssetExtractionState(null);
|
||
archiveActiveCanvasGenerationDialog();
|
||
setGenerateDialog(createEditDialogDraft(sourceLayer));
|
||
setActiveTool('generate');
|
||
},
|
||
[
|
||
archiveActiveCanvasGenerationDialog,
|
||
setActiveTool,
|
||
setGenerateDialog,
|
||
setImageContextMenu,
|
||
setMetadataLayer,
|
||
],
|
||
);
|
||
|
||
const createQuickEditModeDraft = useCallback(
|
||
(sourceLayer: CanvasLayer) => {
|
||
const sourceDialog = findSourceGenerationDialog(
|
||
canvasGenerationDialogs,
|
||
sourceLayer,
|
||
);
|
||
return createQuickEditPanelDraft(sourceLayer, {
|
||
imageModel: sourceDialog?.imageModel,
|
||
});
|
||
},
|
||
[canvasGenerationDialogs],
|
||
);
|
||
|
||
const openQuickEditPanel = useCallback(
|
||
(sourceLayer: CanvasLayer) => {
|
||
setImageContextMenu(null);
|
||
setMetadataLayer(null);
|
||
setUiAssetExtractionState(null);
|
||
setCropExpandPanel(null);
|
||
setCharacterAnimationPanel(null);
|
||
const quickEditDraft = createQuickEditModeDraft(sourceLayer);
|
||
archiveActiveCanvasGenerationDialog();
|
||
setGenerateDialog(null);
|
||
setQuickEditPanel(quickEditDraft);
|
||
selectSingleLayer(sourceLayer.id);
|
||
const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, {
|
||
initialTool: null,
|
||
model: DEFAULT_IMAGE_MODEL,
|
||
});
|
||
quickEditSelectionStateRef.current = nextSelectionState;
|
||
setQuickEditSelectionState(nextSelectionState);
|
||
quickEditSelectionMarkCounterRef.current = 0;
|
||
setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize }));
|
||
setActiveTool('generate');
|
||
},
|
||
[
|
||
archiveActiveCanvasGenerationDialog,
|
||
canvasSize,
|
||
createQuickEditModeDraft,
|
||
selectSingleLayer,
|
||
setActiveTool,
|
||
setGenerateDialog,
|
||
setImageContextMenu,
|
||
setMetadataLayer,
|
||
setQuickEditPanel,
|
||
setViewport,
|
||
],
|
||
);
|
||
|
||
const updateSourceLayer = useCallback(
|
||
(
|
||
sourceLayerId: string,
|
||
updater: (layer: CanvasLayer) => CanvasLayer,
|
||
options: { fit?: boolean; persist?: boolean } = {},
|
||
) => {
|
||
const sourceLayer = layers.find((layer) => layer.id === sourceLayerId);
|
||
if (!sourceLayer) {
|
||
return;
|
||
}
|
||
const updatedLayer = updater(sourceLayer);
|
||
captureCanvasHistory();
|
||
setLayers((currentLayers) =>
|
||
currentLayers.map((layer) =>
|
||
layer.id === sourceLayerId ? updatedLayer : layer,
|
||
),
|
||
);
|
||
if (options.persist !== false) {
|
||
persistUpdatedLayerResource?.(updatedLayer);
|
||
}
|
||
selectSingleLayer(sourceLayerId);
|
||
if (options.fit !== false) {
|
||
fitLayers([updatedLayer]);
|
||
}
|
||
},
|
||
[
|
||
captureCanvasHistory,
|
||
fitLayers,
|
||
layers,
|
||
persistUpdatedLayerResource,
|
||
selectSingleLayer,
|
||
setLayers,
|
||
],
|
||
);
|
||
|
||
const openRedrawPanel = useCallback(
|
||
(sourceLayer: CanvasLayer) => {
|
||
setImageContextMenu(null);
|
||
setMetadataLayer(null);
|
||
setCropExpandPanel(null);
|
||
setCharacterAnimationPanel(null);
|
||
setUiAssetExtractionState(null);
|
||
setQuickEditSelectionState(null);
|
||
if (sourceLayer.mediaType === 'audio') {
|
||
const audioDraft = createAudioRedrawGenerationDialogDraft(sourceLayer);
|
||
if (!audioDraft) {
|
||
return;
|
||
}
|
||
setQuickEditPanel(null);
|
||
selectSingleLayer(sourceLayer.id);
|
||
openCanvasGenerationDialog(audioDraft);
|
||
setActiveTool('music');
|
||
return;
|
||
}
|
||
if (sourceLayer.mediaType === 'video') {
|
||
const videoDraft = createVideoRedrawGenerationDialogDraft(sourceLayer);
|
||
if (!videoDraft) {
|
||
return;
|
||
}
|
||
setQuickEditPanel(null);
|
||
selectSingleLayer(sourceLayer.id);
|
||
openCanvasGenerationDialog(videoDraft);
|
||
setActiveTool('video');
|
||
return;
|
||
}
|
||
if (
|
||
sourceLayer.mediaType === undefined ||
|
||
sourceLayer.mediaType === 'image'
|
||
) {
|
||
const sourceDialog = findSourceGenerationDialog(
|
||
canvasGenerationDialogs,
|
||
sourceLayer,
|
||
);
|
||
const sameSourceDraft = createSameSourceGenerationDialogDraft({
|
||
sourceLayer,
|
||
canvasSize,
|
||
viewport,
|
||
sourceDialog,
|
||
mode: 'redraw',
|
||
sourceAnimationLayer: findSourceAnimationLayer(layers, sourceLayer),
|
||
});
|
||
if (sameSourceDraft) {
|
||
openPlacedCanvasGenerationDialog(sameSourceDraft);
|
||
setQuickEditPanel(null);
|
||
selectSingleLayer(null);
|
||
setActiveTool(getCanvasToolForGenerationMode(sameSourceDraft.mode));
|
||
return;
|
||
}
|
||
const redrawPanel = createRedrawPanelDraft(sourceLayer, {
|
||
imageModel: sourceDialog?.imageModel,
|
||
aspectRatio: sourceDialog?.aspectRatio,
|
||
imageSize: sourceDialog?.imageSize,
|
||
});
|
||
const redrawSize = resolveEditorImageGenerationPixelSize({
|
||
model: redrawPanel.model,
|
||
aspectRatio: redrawPanel.aspectRatio,
|
||
imageSize: redrawPanel.imageSize,
|
||
});
|
||
openPlacedCanvasGenerationDialog({
|
||
...createQuickEditGenerationDialogDraft({
|
||
sourceLayer,
|
||
prompt: redrawPanel.prompt,
|
||
model: redrawPanel.model,
|
||
aspectRatio: redrawPanel.aspectRatio,
|
||
imageSize: redrawPanel.imageSize,
|
||
frame: {
|
||
width: redrawSize.width,
|
||
height: redrawSize.height,
|
||
},
|
||
}),
|
||
composerOpen: true,
|
||
});
|
||
setQuickEditPanel(null);
|
||
selectSingleLayer(null);
|
||
setActiveTool('generate');
|
||
return;
|
||
}
|
||
},
|
||
[
|
||
openCanvasGenerationDialog,
|
||
openPlacedCanvasGenerationDialog,
|
||
selectSingleLayer,
|
||
setActiveTool,
|
||
setImageContextMenu,
|
||
setMetadataLayer,
|
||
canvasGenerationDialogs,
|
||
canvasSize,
|
||
layers,
|
||
viewport,
|
||
],
|
||
);
|
||
|
||
const openCropExpandPanel = useCallback(
|
||
(sourceLayer: CanvasLayer) => {
|
||
setImageContextMenu(null);
|
||
setMetadataLayer(null);
|
||
setUiAssetExtractionState(null);
|
||
archiveActiveCanvasGenerationDialog();
|
||
setGenerateDialog(null);
|
||
setQuickEditPanel(null);
|
||
setCharacterAnimationPanel(null);
|
||
setCropExpandPanel({
|
||
sourceLayerId: sourceLayer.id,
|
||
frame: {
|
||
x: sourceLayer.x,
|
||
y: sourceLayer.y,
|
||
width: sourceLayer.width,
|
||
height: sourceLayer.height,
|
||
},
|
||
ratio: 'free',
|
||
status: 'idle',
|
||
});
|
||
selectSingleLayer(sourceLayer.id);
|
||
setActiveTool('select');
|
||
},
|
||
[
|
||
archiveActiveCanvasGenerationDialog,
|
||
selectSingleLayer,
|
||
setActiveTool,
|
||
setGenerateDialog,
|
||
setImageContextMenu,
|
||
setMetadataLayer,
|
||
],
|
||
);
|
||
|
||
const startCropExpandFrameResize = useCallback(
|
||
(
|
||
event: ReactPointerEvent<HTMLButtonElement>,
|
||
handle: CropExpandResizeHandle,
|
||
) => {
|
||
const currentPanel = cropExpandPanelRef.current;
|
||
if (!currentPanel) {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
cropExpandResizeDragRef.current = {
|
||
pointerId: event.pointerId,
|
||
handle,
|
||
sourceLayerId: currentPanel.sourceLayerId,
|
||
startClientX: event.clientX,
|
||
startClientY: event.clientY,
|
||
startScale:
|
||
Number.isFinite(viewport.scale) && viewport.scale > 0
|
||
? viewport.scale
|
||
: 1,
|
||
startFrame: currentPanel.frame,
|
||
ratio: currentPanel.ratio,
|
||
};
|
||
selectSingleLayer(currentPanel.sourceLayerId);
|
||
},
|
||
[selectSingleLayer, viewport.scale],
|
||
);
|
||
|
||
useEffect(() => {
|
||
const handlePointerMove = (event: PointerEvent) => {
|
||
const dragState = cropExpandResizeDragRef.current;
|
||
if (!dragState || dragState.pointerId !== event.pointerId) {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
const deltaX =
|
||
(event.clientX - dragState.startClientX) / dragState.startScale;
|
||
const deltaY =
|
||
(event.clientY - dragState.startClientY) / dragState.startScale;
|
||
const nextFrame = resizeCropExpandFrame({
|
||
frame: dragState.startFrame,
|
||
handle: dragState.handle,
|
||
deltaX,
|
||
deltaY,
|
||
ratio: dragState.ratio,
|
||
});
|
||
setCropExpandPanel((currentPanel) =>
|
||
currentPanel?.sourceLayerId === dragState.sourceLayerId
|
||
? {
|
||
...currentPanel,
|
||
frame: nextFrame,
|
||
status:
|
||
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
|
||
errorMessage:
|
||
currentPanel.status === 'failed'
|
||
? undefined
|
||
: currentPanel.errorMessage,
|
||
}
|
||
: currentPanel,
|
||
);
|
||
};
|
||
const handlePointerUp = (event: PointerEvent) => {
|
||
const dragState = cropExpandResizeDragRef.current;
|
||
if (!dragState || dragState.pointerId !== event.pointerId) {
|
||
return;
|
||
}
|
||
cropExpandResizeDragRef.current = null;
|
||
};
|
||
|
||
window.addEventListener('pointermove', handlePointerMove);
|
||
window.addEventListener('pointerup', handlePointerUp);
|
||
window.addEventListener('pointercancel', handlePointerUp);
|
||
return () => {
|
||
window.removeEventListener('pointermove', handlePointerMove);
|
||
window.removeEventListener('pointerup', handlePointerUp);
|
||
window.removeEventListener('pointercancel', handlePointerUp);
|
||
};
|
||
}, [setCropExpandPanel]);
|
||
|
||
const submitCropExpand = useCallback(async () => {
|
||
if (!cropExpandPanel || !cropExpandSourceLayer) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const insets = buildCropExpandInsetsFromFrame({
|
||
layer: cropExpandSourceLayer,
|
||
frame: cropExpandPanel.frame,
|
||
});
|
||
setCropExpandPanel({
|
||
...cropExpandPanel,
|
||
status: 'processing',
|
||
errorMessage: undefined,
|
||
});
|
||
const sourceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||
cropExpandSourceLayer.objectKey?.trim() || cropExpandSourceLayer.src,
|
||
);
|
||
const result = await renderCropExpandImage({
|
||
source: sourceImageSrc,
|
||
insets,
|
||
});
|
||
layerCounterRef.current += 1;
|
||
const cropExpandIndex = layerCounterRef.current;
|
||
const nextLayer: CanvasLayer = {
|
||
...cropExpandSourceLayer,
|
||
id: `layer-crop-expand-${cropExpandIndex}`,
|
||
resourceId: `local-resource-crop-expand-${cropExpandIndex}`,
|
||
title: `${cropExpandSourceLayer.title} 裁扩`,
|
||
src: result.imageSrc,
|
||
x: cropExpandSourceLayer.x + cropExpandSourceLayer.width + 32,
|
||
y: cropExpandSourceLayer.y,
|
||
width: result.width,
|
||
height: result.height,
|
||
originalWidth: result.width,
|
||
originalHeight: result.height,
|
||
zIndex: cropExpandIndex + 10,
|
||
sourceType: 'generated',
|
||
sourceResourceId: cropExpandSourceLayer.resourceId,
|
||
objectKey: null,
|
||
assetObjectId: null,
|
||
sourceAssetId: null,
|
||
};
|
||
captureCanvasHistory();
|
||
appendCanvasLayersWithResources([nextLayer]);
|
||
persistGeneratedAsset?.(nextLayer);
|
||
selectSingleLayer(nextLayer.id);
|
||
fitLayers([cropExpandSourceLayer, nextLayer]);
|
||
setCropExpandPanel(null);
|
||
setActiveSidebarPanel('layers');
|
||
} catch (error) {
|
||
setCropExpandPanel({
|
||
...cropExpandPanel,
|
||
status: 'failed',
|
||
errorMessage:
|
||
error instanceof Error && error.message.trim()
|
||
? error.message
|
||
: '裁扩图片失败',
|
||
});
|
||
}
|
||
}, [
|
||
cropExpandPanel,
|
||
cropExpandSourceLayer,
|
||
appendCanvasLayersWithResources,
|
||
captureCanvasHistory,
|
||
fitLayers,
|
||
layerCounterRef,
|
||
persistGeneratedAsset,
|
||
selectSingleLayer,
|
||
setActiveSidebarPanel,
|
||
]);
|
||
|
||
const removeSelectedLayerBackground = useCallback(
|
||
async (sourceLayer: CanvasLayer) => {
|
||
setImageContextMenu(null);
|
||
setMetadataLayer(null);
|
||
setCropExpandPanel(null);
|
||
setQuickEditPanel(null);
|
||
const assetLabel = `${sourceLayer.title} 去背景`;
|
||
const backgroundRemovalDialog =
|
||
projectId && applyProjectSnapshot
|
||
? createQuickEditGenerationDialogDraft({
|
||
sourceLayer,
|
||
prompt: '去除背景',
|
||
status: 'generating',
|
||
frame: {
|
||
width: sourceLayer.width,
|
||
height: sourceLayer.height,
|
||
},
|
||
})
|
||
: null;
|
||
const backgroundRemovalPlacement = backgroundRemovalDialog
|
||
? openPlacedCanvasGenerationDialog({
|
||
...backgroundRemovalDialog,
|
||
composerOpen: false,
|
||
})
|
||
: undefined;
|
||
const backgroundRemovalDialogId = backgroundRemovalPlacement?.dialogId;
|
||
try {
|
||
const sourceObjectKey = sourceLayer.objectKey?.trim();
|
||
const sourceImageSrc = sourceObjectKey
|
||
? `/${sourceObjectKey.replace(/^\/+/u, '')}`
|
||
: await resolveEditorImageReferenceDataUrl(sourceLayer.src);
|
||
const result = await removeImageBackground({
|
||
sourceImageSrc,
|
||
projectId,
|
||
targetLayerId: sourceLayer.id,
|
||
assetKind: sourceLayer.assetKind,
|
||
generationInputs: sourceLayer.generationInputs,
|
||
assetFolderId,
|
||
assetLabel,
|
||
sourceResourceId: sourceLayer.resourceId,
|
||
...(backgroundRemovalPlacement?.placeholder
|
||
? {
|
||
canvasCompletion: {
|
||
dialogId: backgroundRemovalDialogId,
|
||
title: assetLabel,
|
||
placeholder: backgroundRemovalPlacement.placeholder,
|
||
},
|
||
}
|
||
: {}),
|
||
});
|
||
if (
|
||
await applyQueuedEditorGenerationProject(
|
||
result,
|
||
projectId,
|
||
applyProjectSnapshot,
|
||
refreshTaskListForQueuedGeneration,
|
||
onWalletBalanceMayHaveChanged,
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
if (result.project && applyProjectSnapshot) {
|
||
applyProjectSnapshot(result.project);
|
||
return;
|
||
}
|
||
if (backgroundRemovalPlacement?.placeholder && applyProjectSnapshot) {
|
||
return;
|
||
}
|
||
updateSourceLayer(sourceLayer.id, (layer) => ({
|
||
...layer,
|
||
resourceId: result.resource?.resourceId ?? layer.resourceId,
|
||
src: result.imageSrc,
|
||
width: result.width,
|
||
height: result.height,
|
||
originalWidth: result.width,
|
||
originalHeight: result.height,
|
||
sourceType: result.sourceType ?? 'generated',
|
||
provider: result.provider ?? 'BiRefNet',
|
||
taskId: result.taskId ?? null,
|
||
objectKey: result.resource?.objectKey ?? result.objectKey ?? null,
|
||
assetObjectId:
|
||
result.resource?.assetObjectId ?? result.assetObjectId ?? null,
|
||
sourceResourceId:
|
||
result.resource?.sourceResourceId ?? sourceLayer.resourceId,
|
||
sourceAssetId: null,
|
||
prompt: result.resource?.prompt ?? layer.prompt,
|
||
actualPrompt: result.resource?.actualPrompt ?? layer.actualPrompt,
|
||
model: result.resource?.model ?? layer.model,
|
||
assetKind:
|
||
(result.resource?.assetKind as CanvasLayer['assetKind']) ??
|
||
layer.assetKind,
|
||
generationInputs:
|
||
result.resource?.generationInputs ?? layer.generationInputs,
|
||
generatedAssetSnapshot: result.asset ?? layer.generatedAssetSnapshot,
|
||
}));
|
||
setActiveSidebarPanel('layers');
|
||
} catch (error) {
|
||
if (backgroundRemovalDialogId) {
|
||
updateCanvasGenerationDialogById(backgroundRemovalDialogId, (dialog) => ({
|
||
...dialog,
|
||
status: 'failed',
|
||
errorMessage:
|
||
error instanceof Error && error.message.trim()
|
||
? error.message
|
||
: '去除背景失败',
|
||
}));
|
||
}
|
||
throw error;
|
||
}
|
||
},
|
||
[
|
||
applyProjectSnapshot,
|
||
assetFolderId,
|
||
onWalletBalanceMayHaveChanged,
|
||
openPlacedCanvasGenerationDialog,
|
||
projectId,
|
||
refreshTaskListForQueuedGeneration,
|
||
resolveEditorImageReferenceDataUrl,
|
||
setActiveSidebarPanel,
|
||
setImageContextMenu,
|
||
setMetadataLayer,
|
||
updateCanvasGenerationDialogById,
|
||
updateSourceLayer,
|
||
],
|
||
);
|
||
|
||
const pickCharacterSpecFromLayer = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
if (!isCharacterSpecReferenceLayer(layer)) {
|
||
setReferencePickWarning(INVALID_CHARACTER_SPEC_WARNING);
|
||
return;
|
||
}
|
||
setReferencePickWarning(null);
|
||
setGenerateDialog((currentDialog) => {
|
||
const nextDialog = assignCharacterSpecReference(currentDialog, layer);
|
||
if (
|
||
nextDialog?.mode === 'character' &&
|
||
nextDialog.characterSpecReference
|
||
) {
|
||
lastCharacterSpecReferenceRef.current =
|
||
nextDialog.characterSpecReference;
|
||
}
|
||
return nextDialog;
|
||
});
|
||
setIsPickingCharacterSpecFromCanvas(false);
|
||
setIsCharacterSpecMenuOpen(false);
|
||
setImageContextMenu(null);
|
||
},
|
||
[setGenerateDialog, setImageContextMenu],
|
||
);
|
||
|
||
const pickGenerationReferenceFromLayer = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
setGenerateDialog((currentDialog) =>
|
||
appendGenerationReference(currentDialog, layer),
|
||
);
|
||
setIsPickingGenerationReferenceFromCanvas(false);
|
||
setIsGenerationReferenceMenuOpen(false);
|
||
setImageContextMenu(null);
|
||
},
|
||
[setGenerateDialog, setImageContextMenu],
|
||
);
|
||
|
||
const pickQuickEditReferenceFromLayer = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
setGenerateDialog((currentDialog) =>
|
||
appendGenerationReference(currentDialog, layer),
|
||
);
|
||
setIsPickingQuickEditReferenceFromCanvas(false);
|
||
setIsGenerationReferenceMenuOpen(false);
|
||
setImageContextMenu(null);
|
||
},
|
||
[setGenerateDialog, setImageContextMenu],
|
||
);
|
||
|
||
const pickCharacterReferenceFromLayer = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
setGenerateDialog((currentDialog) =>
|
||
appendCharacterReference(currentDialog, layer),
|
||
);
|
||
setIsPickingCharacterReferenceFromCanvas(false);
|
||
setImageContextMenu(null);
|
||
},
|
||
[setGenerateDialog, setImageContextMenu],
|
||
);
|
||
|
||
const pickIconSpecFromLayer = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
if (!isIconSpecReferenceLayer(layer)) {
|
||
setReferencePickWarning(INVALID_ICON_SPEC_WARNING);
|
||
return;
|
||
}
|
||
setReferencePickWarning(null);
|
||
const nextDialog = assignIconSpecReference(generateDialog, layer);
|
||
if (nextDialog?.mode === 'icon' && nextDialog.iconSpecReference) {
|
||
lastIconSpecReferenceRef.current = nextDialog.iconSpecReference;
|
||
}
|
||
setGenerateDialog(nextDialog);
|
||
if (nextDialog === generateDialog) {
|
||
return;
|
||
}
|
||
setIsPickingIconSpecFromCanvas(false);
|
||
setIsIconSpecMenuOpen(false);
|
||
setImageContextMenu(null);
|
||
},
|
||
[generateDialog, setGenerateDialog, setImageContextMenu],
|
||
);
|
||
|
||
const pickUiDesignSpecFromLayer = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
if (!isIconSpecReferenceLayer(layer)) {
|
||
setReferencePickWarning(INVALID_ICON_SPEC_WARNING);
|
||
return;
|
||
}
|
||
setReferencePickWarning(null);
|
||
const nextDialog = assignUiDesignSpecReference(generateDialog, layer);
|
||
if (
|
||
nextDialog?.mode === 'ui-design' &&
|
||
nextDialog.uiDesignSpecReference
|
||
) {
|
||
lastIconSpecReferenceRef.current = nextDialog.uiDesignSpecReference;
|
||
}
|
||
setGenerateDialog(nextDialog);
|
||
if (nextDialog === generateDialog) {
|
||
return;
|
||
}
|
||
setIsPickingUiDesignSpecFromCanvas(false);
|
||
setIsUiDesignSpecMenuOpen(false);
|
||
setImageContextMenu(null);
|
||
},
|
||
[generateDialog, setGenerateDialog, setImageContextMenu],
|
||
);
|
||
|
||
const pickPublicationReferenceFromLayer = useCallback(
|
||
(layer: CanvasLayer) => {
|
||
setGenerateDialog((currentDialog) =>
|
||
appendPublicationReference(currentDialog, layer),
|
||
);
|
||
setIsPickingPublicationReferenceFromCanvas(false);
|
||
setIsPublicationReferenceMenuOpen(false);
|
||
setImageContextMenu(null);
|
||
},
|
||
[setGenerateDialog, setImageContextMenu],
|
||
);
|
||
|
||
const updateIconDescriptionsText = useCallback(
|
||
(value: string) => {
|
||
setGenerateDialog((currentDialog) =>
|
||
updateIconDescriptionsTextInDialog(currentDialog, value),
|
||
);
|
||
},
|
||
[setGenerateDialog],
|
||
);
|
||
|
||
const rememberImageModel = useCallback((model: string) => {
|
||
setRememberedImageOptions((currentOptions) =>
|
||
normalizeRememberedImageGenerationOptions({
|
||
...currentOptions,
|
||
model,
|
||
}),
|
||
);
|
||
}, []);
|
||
const generationSubmissionWorkflow =
|
||
useImageCanvasGenerationSubmissionWorkflow({
|
||
layers,
|
||
canvasSize,
|
||
viewport,
|
||
layerCounterRef,
|
||
quickEditPanel,
|
||
quickEditSourceLayer,
|
||
quickEditSelectionState,
|
||
canvasGenerationDialogs,
|
||
setQuickEditPanel,
|
||
characterAnimationPanel: effectiveCharacterAnimationPanel,
|
||
characterAnimationDialog,
|
||
characterAnimationSourceLayer,
|
||
setCharacterAnimationPanel,
|
||
setGenerateDialog,
|
||
openCanvasGenerationDialog,
|
||
updateCanvasGenerationDialogById,
|
||
hasCanvasGenerationDialogById,
|
||
getGeneratingDialogPlaceholder,
|
||
appendCanvasLayersWithResources,
|
||
updateSourceLayer,
|
||
selectSingleLayer,
|
||
fitLayers,
|
||
setActiveTool,
|
||
setActiveSidebarPanel,
|
||
rememberImageModel,
|
||
projectId,
|
||
assetFolderId,
|
||
upsertGeneratedAsset,
|
||
applyProjectSnapshot,
|
||
onQueuedGenerationTask: refreshTaskListForQueuedGeneration,
|
||
onWalletBalanceMayHaveChanged,
|
||
});
|
||
|
||
const setEffectiveCharacterAnimationPanel: Dispatch<
|
||
SetStateAction<CharacterAnimationPanelState | null>
|
||
> = useCallback(
|
||
(updater) => {
|
||
setGenerateDialog((currentDialog) => {
|
||
if (
|
||
!isCanvasGenerationDialog(currentDialog) ||
|
||
currentDialog.mode !== 'character-animation'
|
||
) {
|
||
return currentDialog;
|
||
}
|
||
const currentPanel =
|
||
createCharacterAnimationPanelFromDialog(currentDialog);
|
||
const nextPanel =
|
||
typeof updater === 'function' ? updater(currentPanel) : updater;
|
||
return applyCharacterAnimationPanelToDialog(currentDialog, nextPanel);
|
||
});
|
||
setCharacterAnimationPanel((currentPanel) => {
|
||
if (characterAnimationDialog) {
|
||
return currentPanel;
|
||
}
|
||
return typeof updater === 'function' ? updater(currentPanel) : updater;
|
||
});
|
||
},
|
||
[characterAnimationDialog, setGenerateDialog],
|
||
);
|
||
const {
|
||
extractUiDesignAssets: submitUiDesignAssetExtraction,
|
||
submitCharacterAnimation,
|
||
submitIconSpritesheetGeneration,
|
||
submitImageGeneration,
|
||
submitQuickEdit,
|
||
} = generationSubmissionWorkflow;
|
||
|
||
const extractUiDesignAssets = useCallback(
|
||
(sourceLayer: CanvasLayer) => {
|
||
if (sourceLayer.assetKind !== 'ui-design') {
|
||
return;
|
||
}
|
||
closeGenerationTransientState();
|
||
setMetadataLayer(null);
|
||
setQuickEditPanel(null);
|
||
setCropExpandPanel(null);
|
||
setCharacterAnimationPanel(null);
|
||
selectSingleLayer(sourceLayer.id);
|
||
setActiveTool('select');
|
||
setUiAssetExtractionState(
|
||
createUiAssetExtractionState(sourceLayer.id, {
|
||
initialTool: 'rect',
|
||
model: DEFAULT_IMAGE_MODEL,
|
||
}),
|
||
);
|
||
setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize }));
|
||
},
|
||
[
|
||
canvasSize,
|
||
closeGenerationTransientState,
|
||
selectSingleLayer,
|
||
setActiveTool,
|
||
setCharacterAnimationPanel,
|
||
setCropExpandPanel,
|
||
setMetadataLayer,
|
||
setQuickEditPanel,
|
||
],
|
||
);
|
||
|
||
const changeUiAssetExtractionTool = useCallback(
|
||
(tool: UiAssetExtractionTool | null) => {
|
||
setUiAssetExtractionState((currentState) =>
|
||
currentState
|
||
? {
|
||
...currentState,
|
||
tool: currentState.tool === tool ? null : tool,
|
||
draftMark: null,
|
||
status:
|
||
currentState.status === 'failed' ? 'idle' : currentState.status,
|
||
errorMessage:
|
||
currentState.status === 'failed'
|
||
? undefined
|
||
: currentState.errorMessage,
|
||
}
|
||
: currentState,
|
||
);
|
||
},
|
||
[],
|
||
);
|
||
|
||
const changeUiAssetExtractionModel = useCallback(
|
||
(model: string) => {
|
||
const normalizedModel = normalizeEditorImageModel(model);
|
||
rememberImageModel(normalizedModel);
|
||
setUiAssetExtractionState((currentState) =>
|
||
currentState
|
||
? {
|
||
...currentState,
|
||
model: normalizedModel,
|
||
status:
|
||
currentState.status === 'failed' ? 'idle' : currentState.status,
|
||
errorMessage:
|
||
currentState.status === 'failed'
|
||
? undefined
|
||
: currentState.errorMessage,
|
||
}
|
||
: currentState,
|
||
);
|
||
},
|
||
[rememberImageModel],
|
||
);
|
||
|
||
const appendUiAssetExtractionReferences = useCallback(
|
||
(references: CharacterReferenceImage[]) => {
|
||
if (!references.length) {
|
||
return;
|
||
}
|
||
setUiAssetExtractionState((currentState) =>
|
||
currentState
|
||
? {
|
||
...currentState,
|
||
references: [...currentState.references, ...references],
|
||
status:
|
||
currentState.status === 'failed' ? 'idle' : currentState.status,
|
||
errorMessage:
|
||
currentState.status === 'failed'
|
||
? undefined
|
||
: currentState.errorMessage,
|
||
}
|
||
: currentState,
|
||
);
|
||
},
|
||
[],
|
||
);
|
||
|
||
const removeUiAssetExtractionReference = useCallback(
|
||
(referenceId: string) => {
|
||
setUiAssetExtractionState((currentState) =>
|
||
currentState
|
||
? {
|
||
...currentState,
|
||
references: currentState.references.filter(
|
||
(reference) => reference.id !== referenceId,
|
||
),
|
||
status:
|
||
currentState.status === 'failed' ? 'idle' : currentState.status,
|
||
errorMessage:
|
||
currentState.status === 'failed'
|
||
? undefined
|
||
: currentState.errorMessage,
|
||
}
|
||
: currentState,
|
||
);
|
||
},
|
||
[],
|
||
);
|
||
|
||
const changeQuickEditSelectionTool = useCallback(
|
||
(tool: UiAssetExtractionTool | null) => {
|
||
updateQuickEditSelectionState((currentState) =>
|
||
currentState
|
||
? {
|
||
...currentState,
|
||
tool: currentState.tool === tool ? null : tool,
|
||
draftMark: null,
|
||
status: 'idle',
|
||
errorMessage: undefined,
|
||
}
|
||
: currentState,
|
||
);
|
||
},
|
||
[updateQuickEditSelectionState],
|
||
);
|
||
|
||
const startUiAssetExtractionPointer = useCallback(
|
||
(point: { x: number; y: number }) => {
|
||
setUiAssetExtractionState((currentState) => {
|
||
if (
|
||
!currentState ||
|
||
!currentState.tool ||
|
||
currentState.status === 'extracting'
|
||
) {
|
||
return currentState;
|
||
}
|
||
uiAssetExtractionMarkCounterRef.current += 1;
|
||
return {
|
||
...currentState,
|
||
draftMark: createUiAssetExtractionDraftMark({
|
||
tool: currentState.tool,
|
||
id: `ui-extraction-mark-${uiAssetExtractionMarkCounterRef.current}`,
|
||
point,
|
||
}),
|
||
status: 'idle',
|
||
errorMessage: undefined,
|
||
};
|
||
});
|
||
},
|
||
[],
|
||
);
|
||
|
||
const startQuickEditSelectionPointer = useCallback(
|
||
(point: { x: number; y: number }) => {
|
||
updateQuickEditSelectionState((currentState) => {
|
||
if (!currentState || !currentState.tool) {
|
||
return currentState;
|
||
}
|
||
quickEditSelectionMarkCounterRef.current += 1;
|
||
return {
|
||
...currentState,
|
||
draftMark: createUiAssetExtractionDraftMark({
|
||
tool: currentState.tool,
|
||
id: `quick-edit-selection-mark-${quickEditSelectionMarkCounterRef.current}`,
|
||
point,
|
||
}),
|
||
status: 'idle',
|
||
errorMessage: undefined,
|
||
};
|
||
});
|
||
},
|
||
[updateQuickEditSelectionState],
|
||
);
|
||
|
||
const moveUiAssetExtractionPointer = useCallback(
|
||
(point: { x: number; y: number }) => {
|
||
setUiAssetExtractionState((currentState) =>
|
||
currentState?.draftMark
|
||
? {
|
||
...currentState,
|
||
draftMark: updateUiAssetExtractionDraftMark(
|
||
currentState.draftMark,
|
||
point,
|
||
),
|
||
}
|
||
: currentState,
|
||
);
|
||
},
|
||
[],
|
||
);
|
||
|
||
const moveQuickEditSelectionPointer = useCallback(
|
||
(point: { x: number; y: number }) => {
|
||
updateQuickEditSelectionState((currentState) =>
|
||
currentState?.draftMark
|
||
? {
|
||
...currentState,
|
||
draftMark: updateUiAssetExtractionDraftMark(
|
||
currentState.draftMark,
|
||
point,
|
||
),
|
||
}
|
||
: currentState,
|
||
);
|
||
},
|
||
[updateQuickEditSelectionState],
|
||
);
|
||
|
||
const endUiAssetExtractionPointer = useCallback(() => {
|
||
setUiAssetExtractionState((currentState) => {
|
||
if (!currentState?.draftMark) {
|
||
return currentState;
|
||
}
|
||
const normalizedMark = normalizeUiAssetExtractionMark(
|
||
currentState.draftMark,
|
||
);
|
||
return {
|
||
...currentState,
|
||
draftMark: null,
|
||
marks: normalizedMark
|
||
? [...currentState.marks, normalizedMark]
|
||
: currentState.marks,
|
||
};
|
||
});
|
||
}, []);
|
||
|
||
const endQuickEditSelectionPointer = useCallback(() => {
|
||
const currentSelectionState = quickEditSelectionStateRef.current;
|
||
if (!currentSelectionState?.draftMark) {
|
||
return;
|
||
}
|
||
const normalizedMark = normalizeUiAssetExtractionMark(
|
||
currentSelectionState.draftMark,
|
||
);
|
||
if (!normalizedMark) {
|
||
updateQuickEditSelectionState(() => ({
|
||
...currentSelectionState,
|
||
draftMark: null,
|
||
}));
|
||
return;
|
||
}
|
||
const nextMarkNumber = currentSelectionState.marks.length + 1;
|
||
updateQuickEditSelectionState(() => ({
|
||
...currentSelectionState,
|
||
draftMark: null,
|
||
marks: [...currentSelectionState.marks, normalizedMark],
|
||
}));
|
||
setQuickEditPanel((currentPanel) =>
|
||
currentPanel?.sourceLayerId === currentSelectionState.sourceLayerId
|
||
? {
|
||
...currentPanel,
|
||
status:
|
||
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
|
||
errorMessage:
|
||
currentPanel.status === 'failed'
|
||
? undefined
|
||
: currentPanel.errorMessage,
|
||
prompt: appendQuickEditSelectionPrompt(
|
||
currentPanel.prompt,
|
||
nextMarkNumber,
|
||
),
|
||
}
|
||
: currentPanel,
|
||
);
|
||
setGenerateDialog((currentDialog) =>
|
||
currentDialog?.mode === 'quick-edit' &&
|
||
currentDialog.sourceLayerId === currentSelectionState.sourceLayerId
|
||
? {
|
||
...currentDialog,
|
||
status:
|
||
currentDialog.status === 'failed' ? 'idle' : currentDialog.status,
|
||
errorMessage:
|
||
currentDialog.status === 'failed'
|
||
? undefined
|
||
: currentDialog.errorMessage,
|
||
prompt: appendQuickEditSelectionPrompt(
|
||
currentDialog.prompt,
|
||
nextMarkNumber,
|
||
),
|
||
composerOpen: true,
|
||
}
|
||
: currentDialog,
|
||
);
|
||
}, [setGenerateDialog, setQuickEditPanel, updateQuickEditSelectionState]);
|
||
|
||
useEffect(() => {
|
||
if (!quickEditSelectionState) {
|
||
return;
|
||
}
|
||
const hasActiveQuickEditDialog =
|
||
generateDialog?.mode === 'quick-edit' &&
|
||
generateDialog.sourceLayerId === quickEditSelectionState.sourceLayerId;
|
||
if (!hasActiveQuickEditDialog && !quickEditPanel) {
|
||
quickEditSelectionStateRef.current = null;
|
||
setQuickEditSelectionState(null);
|
||
}
|
||
}, [generateDialog, quickEditPanel, quickEditSelectionState]);
|
||
|
||
const cancelUiAssetExtraction = useCallback(() => {
|
||
setUiAssetExtractionState(null);
|
||
}, []);
|
||
|
||
const submitUiAssetExtraction = useCallback(async () => {
|
||
const currentState = uiAssetExtractionState;
|
||
const sourceLayer = uiAssetExtractionSourceLayer;
|
||
if (!currentState || !sourceLayer || !currentState.marks.length) {
|
||
return;
|
||
}
|
||
setUiAssetExtractionState({
|
||
...currentState,
|
||
status: 'extracting',
|
||
errorMessage: undefined,
|
||
});
|
||
try {
|
||
await submitUiDesignAssetExtraction(sourceLayer, {
|
||
marks: currentState.marks,
|
||
model: currentState.model,
|
||
references: currentState.references,
|
||
suppressAlert: true,
|
||
});
|
||
setUiAssetExtractionState(null);
|
||
} catch (error) {
|
||
setUiAssetExtractionState((latestState) =>
|
||
latestState?.sourceLayerId === currentState.sourceLayerId
|
||
? {
|
||
...latestState,
|
||
status: 'failed',
|
||
errorMessage:
|
||
error instanceof Error && error.message.trim()
|
||
? error.message
|
||
: '提取素材失败',
|
||
}
|
||
: latestState,
|
||
);
|
||
}
|
||
}, [
|
||
submitUiDesignAssetExtraction,
|
||
uiAssetExtractionSourceLayer,
|
||
uiAssetExtractionState,
|
||
]);
|
||
|
||
const updateSpecFormValue = useCallback(
|
||
(key: keyof SpecFormValues, value: string) => {
|
||
setGenerateDialog((currentDialog) =>
|
||
updateSpecFormDialogValue(currentDialog, key, value),
|
||
);
|
||
},
|
||
[setGenerateDialog],
|
||
);
|
||
|
||
const updateCharacterAnimationDuration = useCallback(
|
||
(frameCountValue: string) => {
|
||
const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find(
|
||
(item) => String(item.frameCount) === frameCountValue,
|
||
);
|
||
if (option) {
|
||
setGenerateDialog((currentDialog) =>
|
||
currentDialog?.mode === 'character-animation'
|
||
? {
|
||
...currentDialog,
|
||
characterAnimationFrameCount: option.frameCount,
|
||
characterAnimationDurationSeconds: option.durationSeconds,
|
||
status:
|
||
currentDialog.status === 'failed'
|
||
? 'idle'
|
||
: currentDialog.status,
|
||
errorMessage:
|
||
currentDialog.status === 'failed'
|
||
? undefined
|
||
: currentDialog.errorMessage,
|
||
}
|
||
: currentDialog,
|
||
);
|
||
}
|
||
setCharacterAnimationPanel((currentPanel) =>
|
||
updateCharacterAnimationDurationPanel(currentPanel, frameCountValue),
|
||
);
|
||
},
|
||
[setGenerateDialog],
|
||
);
|
||
|
||
const hideGeneratedLayerPanelAfterBlur = useCallback(() => {
|
||
setGenerateDialog((currentDialog) =>
|
||
hideGeneratedLayerComposerAfterBlur(currentDialog),
|
||
);
|
||
}, [setGenerateDialog]);
|
||
|
||
const closeGenerateComposer = useCallback(() => {
|
||
setGenerateDialog(closeGenerateComposerDialog);
|
||
setActiveTool('select');
|
||
}, [setActiveTool, setGenerateDialog]);
|
||
|
||
const clearDeletedLayerGenerationState = useCallback(
|
||
(targetLayerId: string) => {
|
||
setQuickEditPanel((currentPanel) =>
|
||
currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
|
||
);
|
||
setQuickEditPanel((currentPanel) =>
|
||
currentPanel
|
||
? {
|
||
...currentPanel,
|
||
quickEditReferences: (
|
||
currentPanel.quickEditReferences ?? []
|
||
).filter(
|
||
(reference) => reference.id !== `canvas-${targetLayerId}`,
|
||
),
|
||
}
|
||
: currentPanel,
|
||
);
|
||
setCropExpandPanel((currentPanel) =>
|
||
currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
|
||
);
|
||
setUiAssetExtractionState((currentState) =>
|
||
currentState?.sourceLayerId === targetLayerId ? null : currentState,
|
||
);
|
||
setQuickEditSelectionState((currentState) =>
|
||
currentState?.sourceLayerId === targetLayerId ? null : currentState,
|
||
);
|
||
setCharacterAnimationPanel((currentPanel) =>
|
||
currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
|
||
);
|
||
setGenerateDialog((currentDialog) =>
|
||
currentDialog?.mode === 'edit' &&
|
||
currentDialog.sourceLayerId === targetLayerId
|
||
? null
|
||
: currentDialog,
|
||
);
|
||
removeCanvasGenerationDialogsByLayerId(targetLayerId);
|
||
},
|
||
[removeCanvasGenerationDialogsByLayerId, setGenerateDialog],
|
||
);
|
||
|
||
return useMemo(
|
||
() => ({
|
||
quickEditPanel,
|
||
setQuickEditPanel,
|
||
quickEditSourceLayer,
|
||
cropExpandPanel,
|
||
setCropExpandPanel,
|
||
cropExpandSourceLayer,
|
||
uiAssetExtractionState,
|
||
uiAssetExtractionSourceLayer,
|
||
quickEditSelectionState,
|
||
quickEditSelectionSourceLayer,
|
||
changeUiAssetExtractionTool,
|
||
changeUiAssetExtractionModel,
|
||
appendUiAssetExtractionReferences,
|
||
removeUiAssetExtractionReference,
|
||
changeQuickEditSelectionTool,
|
||
startUiAssetExtractionPointer,
|
||
startQuickEditSelectionPointer,
|
||
moveUiAssetExtractionPointer,
|
||
moveQuickEditSelectionPointer,
|
||
endUiAssetExtractionPointer,
|
||
endQuickEditSelectionPointer,
|
||
cancelUiAssetExtraction,
|
||
submitUiAssetExtraction,
|
||
characterAnimationPanel: effectiveCharacterAnimationPanel,
|
||
setCharacterAnimationPanel: setEffectiveCharacterAnimationPanel,
|
||
characterAnimationSourceLayer,
|
||
characterAnimationPrice,
|
||
iconDescriptionValues,
|
||
isSpecMenuOpen,
|
||
setIsSpecMenuOpen,
|
||
isGenerationReferenceMenuOpen,
|
||
setIsGenerationReferenceMenuOpen,
|
||
isCharacterSpecMenuOpen,
|
||
setIsCharacterSpecMenuOpen,
|
||
isCharacterReferenceMenuOpen,
|
||
setIsCharacterReferenceMenuOpen,
|
||
isPickingGenerationReferenceFromCanvas,
|
||
setIsPickingGenerationReferenceFromCanvas,
|
||
isPickingQuickEditReferenceFromCanvas,
|
||
setIsPickingQuickEditReferenceFromCanvas,
|
||
isPickingCharacterSpecFromCanvas,
|
||
setIsPickingCharacterSpecFromCanvas,
|
||
isPickingCharacterReferenceFromCanvas,
|
||
setIsPickingCharacterReferenceFromCanvas,
|
||
isIconSpecMenuOpen,
|
||
setIsIconSpecMenuOpen,
|
||
isPickingIconSpecFromCanvas,
|
||
setIsPickingIconSpecFromCanvas,
|
||
isUiDesignSpecMenuOpen,
|
||
setIsUiDesignSpecMenuOpen,
|
||
isPickingUiDesignSpecFromCanvas,
|
||
setIsPickingUiDesignSpecFromCanvas,
|
||
isMusicMenuOpen,
|
||
setIsMusicMenuOpen,
|
||
isPublicationMenuOpen,
|
||
setIsPublicationMenuOpen,
|
||
isPublicationReferenceMenuOpen,
|
||
setIsPublicationReferenceMenuOpen,
|
||
isPickingPublicationReferenceFromCanvas,
|
||
setIsPickingPublicationReferenceFromCanvas,
|
||
referencePickWarning,
|
||
clearReferencePickWarning: () => setReferencePickWarning(null),
|
||
openGenerateDialog,
|
||
openSpecDialog,
|
||
openCharacterAnimationPanel,
|
||
openCharacterGenerationDialog,
|
||
openIconGenerationDialog,
|
||
openPublicationGenerationDialog,
|
||
openVideoGenerationDialog,
|
||
openUiDesignGenerationDialog,
|
||
openSoundEffectGenerationDialog,
|
||
openBackgroundMusicGenerationDialog,
|
||
openEditDialog,
|
||
openQuickEditPanel,
|
||
openRedrawPanel,
|
||
openCropExpandPanel,
|
||
startCropExpandFrameResize,
|
||
removeSelectedLayerBackground,
|
||
taskListRefreshKey,
|
||
isTaskSidebarOpen,
|
||
toggleTaskSidebar: () => setIsTaskSidebarOpen((open) => !open),
|
||
extractUiDesignAssets,
|
||
pickCharacterSpecFromLayer,
|
||
pickGenerationReferenceFromLayer,
|
||
pickQuickEditReferenceFromLayer,
|
||
pickCharacterReferenceFromLayer,
|
||
pickIconSpecFromLayer,
|
||
pickUiDesignSpecFromLayer,
|
||
pickPublicationReferenceFromLayer,
|
||
submitIconSpritesheetGeneration,
|
||
submitQuickEdit,
|
||
submitCropExpand,
|
||
submitImageGeneration,
|
||
updateSpecFormValue,
|
||
updateIconDescriptionsText,
|
||
updateCharacterAnimationDuration,
|
||
rememberImageModel,
|
||
submitCharacterAnimation,
|
||
hideGeneratedLayerPanelAfterBlur,
|
||
closeGenerateComposer,
|
||
clearDeletedLayerGenerationState,
|
||
}),
|
||
[
|
||
effectiveCharacterAnimationPanel,
|
||
characterAnimationPrice,
|
||
characterAnimationSourceLayer,
|
||
clearDeletedLayerGenerationState,
|
||
closeGenerateComposer,
|
||
cropExpandPanel,
|
||
cropExpandSourceLayer,
|
||
uiAssetExtractionState,
|
||
uiAssetExtractionSourceLayer,
|
||
quickEditSelectionState,
|
||
quickEditSelectionSourceLayer,
|
||
changeUiAssetExtractionTool,
|
||
changeUiAssetExtractionModel,
|
||
appendUiAssetExtractionReferences,
|
||
removeUiAssetExtractionReference,
|
||
changeQuickEditSelectionTool,
|
||
startUiAssetExtractionPointer,
|
||
startQuickEditSelectionPointer,
|
||
moveUiAssetExtractionPointer,
|
||
moveQuickEditSelectionPointer,
|
||
endUiAssetExtractionPointer,
|
||
endQuickEditSelectionPointer,
|
||
cancelUiAssetExtraction,
|
||
submitUiAssetExtraction,
|
||
setEffectiveCharacterAnimationPanel,
|
||
hideGeneratedLayerPanelAfterBlur,
|
||
iconDescriptionValues,
|
||
isCharacterReferenceMenuOpen,
|
||
isCharacterSpecMenuOpen,
|
||
isIconSpecMenuOpen,
|
||
isGenerationReferenceMenuOpen,
|
||
isPickingCharacterReferenceFromCanvas,
|
||
isPickingCharacterSpecFromCanvas,
|
||
isPickingGenerationReferenceFromCanvas,
|
||
isPickingQuickEditReferenceFromCanvas,
|
||
isPickingIconSpecFromCanvas,
|
||
isPickingUiDesignSpecFromCanvas,
|
||
isUiDesignSpecMenuOpen,
|
||
isMusicMenuOpen,
|
||
isTaskSidebarOpen,
|
||
taskListRefreshKey,
|
||
isPublicationMenuOpen,
|
||
isPublicationReferenceMenuOpen,
|
||
isSpecMenuOpen,
|
||
isPickingPublicationReferenceFromCanvas,
|
||
referencePickWarning,
|
||
openBackgroundMusicGenerationDialog,
|
||
openCharacterAnimationPanel,
|
||
openCharacterGenerationDialog,
|
||
openCropExpandPanel,
|
||
openEditDialog,
|
||
extractUiDesignAssets,
|
||
openGenerateDialog,
|
||
openIconGenerationDialog,
|
||
openPublicationGenerationDialog,
|
||
openRedrawPanel,
|
||
openUiDesignGenerationDialog,
|
||
openQuickEditPanel,
|
||
openSpecDialog,
|
||
startCropExpandFrameResize,
|
||
openSoundEffectGenerationDialog,
|
||
openVideoGenerationDialog,
|
||
pickCharacterReferenceFromLayer,
|
||
pickCharacterSpecFromLayer,
|
||
pickGenerationReferenceFromLayer,
|
||
pickQuickEditReferenceFromLayer,
|
||
pickIconSpecFromLayer,
|
||
pickPublicationReferenceFromLayer,
|
||
pickUiDesignSpecFromLayer,
|
||
quickEditPanel,
|
||
quickEditSourceLayer,
|
||
removeSelectedLayerBackground,
|
||
submitCharacterAnimation,
|
||
submitCropExpand,
|
||
submitIconSpritesheetGeneration,
|
||
submitImageGeneration,
|
||
submitQuickEdit,
|
||
updateCharacterAnimationDuration,
|
||
updateIconDescriptionsText,
|
||
updateSpecFormValue,
|
||
rememberImageModel,
|
||
refreshTaskListForQueuedGeneration,
|
||
],
|
||
);
|
||
}
|