完善快速编辑参考图与生成占位
- 快速编辑支持最多8张额外参考图,并将原图自动作为最后一张参考图提交 - 快速编辑尺寸和模型沿用原图配置,按禁用态参数胶囊展示 - 快速编辑提交前同步改写原图引用为实际图序号 - 快速编辑生成改为创建独立画布生成占位,并支持键盘删除生成中对象 - 对齐生成类面板参考图图标、规范类固定参数和相关文档测试
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type MutableRefObject,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
@@ -14,6 +17,7 @@ import type {
|
||||
CanvasViewport,
|
||||
CharacterAnimationPanelState,
|
||||
CropExpandPanelState,
|
||||
CropExpandResizeHandle,
|
||||
GenerateDialogState,
|
||||
ImageContextMenuState,
|
||||
QuickEditPanelState,
|
||||
@@ -24,6 +28,7 @@ import type {
|
||||
import {
|
||||
appendCharacterReference,
|
||||
appendGenerationReference,
|
||||
appendQuickEditReference,
|
||||
assignCharacterSpecReference,
|
||||
assignIconSpecReference,
|
||||
assignUiDesignSpecReference,
|
||||
@@ -46,8 +51,6 @@ import {
|
||||
updateSpecFormDialogValue,
|
||||
} from './ImageCanvasGenerationDialogModel';
|
||||
import {
|
||||
buildQuickEditModelOptions,
|
||||
buildQuickEditSizeOptions,
|
||||
calculateCharacterAnimationPrice,
|
||||
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
||||
DEFAULT_ICON_DESCRIPTIONS,
|
||||
@@ -60,15 +63,26 @@ import {
|
||||
chooseGenerationPlacement,
|
||||
} from './ImageCanvasGenerationPlacementModel';
|
||||
import {
|
||||
buildCropExpandLayerPatch,
|
||||
parseCropExpandInsetValue,
|
||||
buildCropExpandInsetsFromFrame,
|
||||
removeImageBackground,
|
||||
renderCropExpandImage,
|
||||
} from './ImageCanvasRasterEditModel';
|
||||
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
|
||||
import { 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;
|
||||
@@ -142,7 +156,7 @@ type GenerationWorkflowOptions = {
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
openCanvasGenerationDialog: (
|
||||
dialog: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
) => void;
|
||||
) => string;
|
||||
updateCanvasGenerationDialogById: (
|
||||
dialogId: string,
|
||||
updater: CanvasGenerationDialogUpdater,
|
||||
@@ -200,6 +214,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
] = useState(false);
|
||||
const [
|
||||
isPickingQuickEditReferenceFromCanvas,
|
||||
setIsPickingQuickEditReferenceFromCanvas,
|
||||
] = useState(false);
|
||||
const [
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
@@ -219,6 +237,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
useState<QuickEditPanelState | null>(null);
|
||||
const [cropExpandPanel, setCropExpandPanel] =
|
||||
useState<CropExpandPanelState | null>(null);
|
||||
const cropExpandPanelRef = useRef<CropExpandPanelState | null>(null);
|
||||
const cropExpandResizeDragRef = useRef<CropExpandResizeDragState | null>(
|
||||
null,
|
||||
);
|
||||
const [characterAnimationPanel, setCharacterAnimationPanel] =
|
||||
useState<CharacterAnimationPanelState | null>(null);
|
||||
const [lastImageModel, setLastImageModel] = useState(DEFAULT_IMAGE_MODEL);
|
||||
@@ -232,6 +254,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const effectiveCharacterAnimationPanel =
|
||||
characterAnimationDialogPanel ?? characterAnimationPanel;
|
||||
|
||||
cropExpandPanelRef.current = cropExpandPanel;
|
||||
|
||||
const quickEditSourceLayer = quickEditPanel
|
||||
? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
|
||||
null)
|
||||
@@ -245,16 +269,6 @@ export function useImageCanvasGenerationWorkflow({
|
||||
(layer) => layer.id === effectiveCharacterAnimationPanel.sourceLayerId,
|
||||
) ?? null)
|
||||
: null;
|
||||
const quickEditSizeOptions = useMemo(
|
||||
() =>
|
||||
quickEditPanel ? buildQuickEditSizeOptions(quickEditPanel.size) : [],
|
||||
[quickEditPanel],
|
||||
);
|
||||
const quickEditModelOptions = useMemo(
|
||||
() =>
|
||||
quickEditPanel ? buildQuickEditModelOptions(quickEditPanel.model) : [],
|
||||
[quickEditPanel],
|
||||
);
|
||||
const characterAnimationPrice = effectiveCharacterAnimationPanel
|
||||
? calculateCharacterAnimationPrice(
|
||||
effectiveCharacterAnimationPanel.resolution,
|
||||
@@ -275,6 +289,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const closeGenerationTransientState = useCallback(() => {
|
||||
setIsSpecMenuOpen(false);
|
||||
setIsGenerationReferenceMenuOpen(false);
|
||||
setIsPickingQuickEditReferenceFromCanvas(false);
|
||||
setIsCharacterSpecMenuOpen(false);
|
||||
setIsCharacterReferenceMenuOpen(false);
|
||||
setIsPickingGenerationReferenceFromCanvas(false);
|
||||
@@ -562,12 +577,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setCharacterAnimationPanel(null);
|
||||
setCropExpandPanel({
|
||||
sourceLayerId: sourceLayer.id,
|
||||
insets: {
|
||||
left: '0',
|
||||
top: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
frame: {
|
||||
x: sourceLayer.x,
|
||||
y: sourceLayer.y,
|
||||
width: sourceLayer.width,
|
||||
height: sourceLayer.height,
|
||||
},
|
||||
ratio: 'free',
|
||||
status: 'idle',
|
||||
});
|
||||
selectSingleLayer(sourceLayer.id);
|
||||
@@ -582,18 +598,98 @@ export function useImageCanvasGenerationWorkflow({
|
||||
],
|
||||
);
|
||||
|
||||
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 = {
|
||||
left: parseCropExpandInsetValue(cropExpandPanel.insets.left),
|
||||
top: parseCropExpandInsetValue(cropExpandPanel.insets.top),
|
||||
right: parseCropExpandInsetValue(cropExpandPanel.insets.right),
|
||||
bottom: parseCropExpandInsetValue(cropExpandPanel.insets.bottom),
|
||||
};
|
||||
const insets = buildCropExpandInsetsFromFrame({
|
||||
layer: cropExpandSourceLayer,
|
||||
frame: cropExpandPanel.frame,
|
||||
});
|
||||
setCropExpandPanel({
|
||||
...cropExpandPanel,
|
||||
status: 'processing',
|
||||
@@ -603,21 +699,32 @@ export function useImageCanvasGenerationWorkflow({
|
||||
source: cropExpandSourceLayer.src,
|
||||
insets,
|
||||
});
|
||||
const layerPatch = buildCropExpandLayerPatch({
|
||||
layerX: cropExpandSourceLayer.x,
|
||||
layerY: cropExpandSourceLayer.y,
|
||||
insets,
|
||||
outputWidth: result.width,
|
||||
outputHeight: result.height,
|
||||
});
|
||||
updateSourceLayer(cropExpandSourceLayer.id, (layer) => ({
|
||||
...layer,
|
||||
...layerPatch,
|
||||
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) {
|
||||
@@ -633,8 +740,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
}, [
|
||||
cropExpandPanel,
|
||||
cropExpandSourceLayer,
|
||||
appendCanvasLayersWithResources,
|
||||
captureCanvasHistory,
|
||||
fitLayers,
|
||||
layerCounterRef,
|
||||
persistGeneratedAsset,
|
||||
selectSingleLayer,
|
||||
setActiveSidebarPanel,
|
||||
updateSourceLayer,
|
||||
]);
|
||||
|
||||
const removeSelectedLayerBackground = useCallback(
|
||||
@@ -697,6 +809,17 @@ export function useImageCanvasGenerationWorkflow({
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
);
|
||||
|
||||
const pickQuickEditReferenceFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
setQuickEditPanel((currentPanel) =>
|
||||
appendQuickEditReference(currentPanel, layer),
|
||||
);
|
||||
setIsPickingQuickEditReferenceFromCanvas(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[setImageContextMenu],
|
||||
);
|
||||
|
||||
const pickCharacterReferenceFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
@@ -710,29 +833,30 @@ export function useImageCanvasGenerationWorkflow({
|
||||
|
||||
const pickIconSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
assignIconSpecReference(currentDialog, layer),
|
||||
);
|
||||
if (layer.assetKind !== 'icon-spec') {
|
||||
const nextDialog = assignIconSpecReference(generateDialog, layer);
|
||||
setGenerateDialog(nextDialog);
|
||||
if (nextDialog === generateDialog) {
|
||||
return;
|
||||
}
|
||||
setIsPickingIconSpecFromCanvas(false);
|
||||
setIsIconSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
[generateDialog, setGenerateDialog, setImageContextMenu],
|
||||
);
|
||||
|
||||
const pickUiDesignSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
assignUiDesignSpecReference(currentDialog, layer),
|
||||
);
|
||||
const nextDialog = assignUiDesignSpecReference(generateDialog, layer);
|
||||
setGenerateDialog(nextDialog);
|
||||
if (nextDialog === generateDialog) {
|
||||
return;
|
||||
}
|
||||
setIsPickingUiDesignSpecFromCanvas(false);
|
||||
setIsUiDesignSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
[generateDialog, setGenerateDialog, setImageContextMenu],
|
||||
);
|
||||
|
||||
const updateIconDescriptionsText = useCallback(
|
||||
@@ -757,6 +881,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
characterAnimationSourceLayer,
|
||||
setCharacterAnimationPanel,
|
||||
setGenerateDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
getGeneratingDialogPlaceholder,
|
||||
@@ -865,6 +990,16 @@ export function useImageCanvasGenerationWorkflow({
|
||||
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,
|
||||
);
|
||||
@@ -890,8 +1025,6 @@ export function useImageCanvasGenerationWorkflow({
|
||||
cropExpandPanel,
|
||||
setCropExpandPanel,
|
||||
cropExpandSourceLayer,
|
||||
quickEditSizeOptions,
|
||||
quickEditModelOptions,
|
||||
characterAnimationPanel: effectiveCharacterAnimationPanel,
|
||||
setCharacterAnimationPanel: setEffectiveCharacterAnimationPanel,
|
||||
characterAnimationSourceLayer,
|
||||
@@ -907,6 +1040,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setIsCharacterReferenceMenuOpen,
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
isPickingQuickEditReferenceFromCanvas,
|
||||
setIsPickingQuickEditReferenceFromCanvas,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
@@ -934,9 +1069,11 @@ export function useImageCanvasGenerationWorkflow({
|
||||
openQuickEditPanel,
|
||||
openRedrawPanel,
|
||||
openCropExpandPanel,
|
||||
startCropExpandFrameResize,
|
||||
removeSelectedLayerBackground,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickGenerationReferenceFromLayer,
|
||||
pickQuickEditReferenceFromLayer,
|
||||
pickCharacterReferenceFromLayer,
|
||||
pickIconSpecFromLayer,
|
||||
pickUiDesignSpecFromLayer,
|
||||
@@ -971,6 +1108,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
isPickingQuickEditReferenceFromCanvas,
|
||||
isPickingIconSpecFromCanvas,
|
||||
isPickingUiDesignSpecFromCanvas,
|
||||
isUiDesignSpecMenuOpen,
|
||||
@@ -987,16 +1125,16 @@ export function useImageCanvasGenerationWorkflow({
|
||||
openUiDesignGenerationDialog,
|
||||
openQuickEditPanel,
|
||||
openSpecDialog,
|
||||
startCropExpandFrameResize,
|
||||
openSoundEffectGenerationDialog,
|
||||
openVideoGenerationDialog,
|
||||
pickCharacterReferenceFromLayer,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickGenerationReferenceFromLayer,
|
||||
pickQuickEditReferenceFromLayer,
|
||||
pickIconSpecFromLayer,
|
||||
pickUiDesignSpecFromLayer,
|
||||
quickEditModelOptions,
|
||||
quickEditPanel,
|
||||
quickEditSizeOptions,
|
||||
quickEditSourceLayer,
|
||||
removeSelectedLayerBackground,
|
||||
submitCharacterAnimation,
|
||||
|
||||
Reference in New Issue
Block a user