diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index 62f13030..6b5574b5 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -6,7 +6,7 @@ import { Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { NodeHandle } from "./NodeHandle"; import { useWorkflowStore } from "@/store/workflowStore"; -import { getActiveNodeTool, useNodeToolStore, type NodeToolType } from "@/store/nodeToolStore"; +import { getActiveNodeTool, useNodeToolStore } from "@/store/nodeToolStore"; import { useGenerationPreferenceStore } from "@/store/generationPreferenceStore"; import { NanoBananaNodeData, SelectedModel } from "@/types"; import { ProviderModel } from "@/lib/providers/types"; @@ -29,28 +29,15 @@ import { SINGLE_INPUT_HANDLE_ID, SINGLE_OUTPUT_HANDLE_ID } from "@/utils/nodeHan import { useSelectedNodeCount } from "@/hooks/useSelectedNodeCount"; import { NodeActionCapsule, type NodeActionCapsuleAction } from "./NodeActionCapsule"; import { SaveToAssetLibraryModal } from "@/components/SaveToAssetLibraryModal"; -import { MediaCropOverlay, type MediaCropAspectRatio, type MediaCropConfirmPayload, type MediaEditFrameRect } from "@/components/media/MediaCropOverlay"; -import { CropRatioSelect, MEDIA_EDIT_TOOL, MediaEditToolbar, type MediaEditTool, type MediaEditToolOption } from "@/components/media/MediaEditToolbar"; import { createMediaEditActionDropdown, getMediaEditMenuLabel, type MediaEditMenuTool } from "@/components/media/MediaEditActionDropdown"; -import { MediaOutpaintingOverlay, type MediaOutpaintingFrameRect, type MediaOutpaintingParams } from "@/components/media/MediaOutpaintingOverlay"; -import { MediaOutpaintingToolbar, type OutpaintingBatchSize } from "@/components/media/MediaOutpaintingToolbar"; import { SplitGridCapsuleMenu, type SplitGridSelection } from "@/components/media/SplitGridCapsuleMenu"; -import { createSplitGridImageNodes } from "@/utils/splitGridNodes"; -import { createPendingDerivedImageNode } from "@/utils/derivedImageNodes"; -import { SplitGridSelectionOverlay } from "@/components/media/SplitGridSelectionOverlay"; -import { SplitGridSelectionToolbar } from "@/components/media/SplitGridSelectionToolbar"; import { ImageEditSession } from "@/components/media/image-edit/ImageEditSession"; +import type { ImageEditTool } from "@/components/media/image-edit/types"; import { GridToolIcon, MultiAngleToolIcon } from "@/components/media/MediaToolbarIcons"; import { getAutoMediaElementClassName, getAutoMediaFrameClassName } from "./mediaAutoSize"; import { chooseHighDefinitionModel, createHighDefinitionImageNode, readHighDefinitionSourceImageDimensions } from "@/utils/highDefinitionNodes"; -import { deriveOutpaintingOutputDimensions, OUTPAINTING_DEFAULT_PROMPT } from "@/utils/outpaintingNodes"; -import { resolveMediaNodeCreationSizeFromDimensions } from "@/utils/nodeSizing"; -import { toSelectedModel as providerModelToSelectedModel } from "@/utils/selectedModel"; const IMAGE_LOAD_RETRY_DELAYS_MS = [600, 1600]; -const IMAGE_MEDIA_EDIT_TOOLS: MediaEditToolOption[] = [ - { key: MEDIA_EDIT_TOOL.crop, label: "裁剪" }, -]; function GenerateImageEditMenuIcon({ tool, className = "h-5 w-5" }: { tool: MediaEditMenuTool; className?: string }) { if (tool === "high-definition") { @@ -138,17 +125,7 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isSaveToAssetLibraryOpen, setIsSaveToAssetLibraryOpen] = useState(false); const [activeEditMenuTool, setActiveEditMenuTool] = useState("crop"); - const [cropAspectRatio, setCropAspectRatio] = useState("original"); - const [cropPayload, setCropPayload] = useState(null); - const [cropFrameRect, setCropFrameRect] = useState(null); - const [cropImageBoundsRect, setCropImageBoundsRect] = useState(null); - const [outpaintingParams, setOutpaintingParams] = useState(null); - const [outpaintingFrameRect, setOutpaintingFrameRect] = useState(null); - const [outpaintingImageBoundsRect, setOutpaintingImageBoundsRect] = useState(null); - const [outpaintingBatchSize, setOutpaintingBatchSize] = useState(1); - const [isSplitGridRunning, setIsSplitGridRunning] = useState(false); - const [gridSelection, setGridSelection] = useState(null); - const [selectedGridCellKeys, setSelectedGridCellKeys] = useState>(() => new Set()); + const [splitGridSelection, setSplitGridSelection] = useState(null); const [cropImageDimensions, setCropImageDimensions] = useState<{ width: number; height: number } | null>(null); const imageRef = useRef(null); @@ -267,111 +244,21 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = }, nextItem.dimensions ?? null); }, [id, nodeData.imageHistory, nodeData.selectedHistoryIndex, updateMediaNodeData, updateNodeData]); - const resetToolDraftState = useCallback(() => { - setCropPayload(null); - setCropFrameRect(null); - setCropImageBoundsRect(null); - setOutpaintingParams(null); - setOutpaintingFrameRect(null); - setOutpaintingImageBoundsRect(null); - setGridSelection(null); - setSelectedGridCellKeys(new Set()); - }, []); - - const openNodeTool = useCallback((tool: NodeToolType) => { - resetToolDraftState(); - if (tool === "crop") setCropAspectRatio("original"); - if (tool === "outpainting") setOutpaintingBatchSize(1); + const openNodeTool = useCallback((tool: ImageEditTool) => { + setSplitGridSelection(null); switchNodeTool(id, tool); - }, [id, resetToolDraftState, switchNodeTool]); + }, [id, switchNodeTool]); const closeNodeTool = useCallback(() => { - resetToolDraftState(); + setSplitGridSelection(null); closeNodeToolForNode(id); - }, [closeNodeToolForNode, id, resetToolDraftState]); - - const handleConfirmCrop = useCallback((payload: MediaCropConfirmPayload) => { - const currentNode = getNodeById(id); - if (!currentNode) return; - - const currentPosition = currentNode.position ?? { x: 0, y: 0 }; - const currentWidth = typeof currentNode.style?.width === "number" - ? currentNode.style.width - : 300; - const filename = `crop-${Date.now()}.png`; - const dimensions = { width: payload.outputWidth, height: payload.outputHeight }; - const size = resolveMediaNodeCreationSizeFromDimensions(dimensions) ?? undefined; - const nodeId = createPendingDerivedImageNode({ - sourceNodeId: id, - position: { - x: currentPosition.x + currentWidth + 220, - y: currentPosition.y, - }, - filename, - dimensions, - customTitle: t("imageInput.crop"), - operation: "crop", - task: { - cropRect: payload.cropRect, - outputWidth: payload.outputWidth, - outputHeight: payload.outputHeight, - aspectRatio: payload.aspectRatio, - }, - size, - addNode, - onConnect, - selectSingleNode, - }); - closeNodeTool(); - void regenerateNode(nodeId); - }, [addNode, closeNodeTool, getNodeById, id, onConnect, regenerateNode, selectSingleNode, t]); + }, [closeNodeToolForNode, id]); const handleStartGridSelection = useCallback((selection: SplitGridSelection) => { - resetToolDraftState(); + setSplitGridSelection(null); switchNodeTool(id, "splitGrid"); - setGridSelection(selection); - setSelectedGridCellKeys(new Set()); - }, [id, resetToolDraftState, switchNodeTool]); - - const handleToggleGridCell = useCallback((cellKey: string) => { - setSelectedGridCellKeys((current) => { - const next = new Set(current); - if (next.has(cellKey)) { - next.delete(cellKey); - } else { - next.add(cellKey); - } - return next; - }); - }, []); - - const handleCancelGridSelection = useCallback(() => { - closeNodeTool(); - }, [closeNodeTool]); - - const handleConfirmSplitGrid = useCallback(async () => { - const sourceNode = getNodeById(id); - if (!sourceNode || !gridSelection || selectedGridCellKeys.size === 0) return; - - setIsSplitGridRunning(true); - try { - const createdNodeIds = createSplitGridImageNodes({ - sourceNode, - rows: gridSelection.rows, - cols: gridSelection.cols, - selectedCellKeys: selectedGridCellKeys, - addNode, - onConnect, - selectSingleNode, - }); - handleCancelGridSelection(); - createdNodeIds.forEach((nodeId) => void regenerateNode(nodeId)); - } catch (error) { - alert(t("workflow.splitGrid.failed", { error: error instanceof Error ? error.message : "Unknown error" })); - } finally { - setIsSplitGridRunning(false); - } - }, [addNode, getNodeById, gridSelection, handleCancelGridSelection, id, onConnect, regenerateNode, selectSingleNode, selectedGridCellKeys, t]); + setSplitGridSelection(selection); + }, [id, switchNodeTool]); const handleCreateHighDefinitionNode = useCallback(async () => { const currentNode = getNodeById(id); @@ -398,64 +285,6 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = }); }, [addNode, displayImage, getNodeById, highDefinitionModels, id, primaryImage, onConnect, selectSingleNode]); - const handleConfirmOutpainting = useCallback(async (params: MediaOutpaintingParams) => { - const currentNode = getNodeById(id); - const imageSource = primaryImage || displayImage; - if (!currentNode || !imageSource || !outpaintingModel) return; - - const currentPosition = currentNode.position ?? { x: 0, y: 0 }; - const currentWidth = typeof currentNode.style?.width === "number" - ? currentNode.style.width - : 300; - const sourceDimensions = await readHighDefinitionSourceImageDimensions(currentNode.data as { dimensions?: unknown }, imageSource); - const dimensions = deriveOutpaintingOutputDimensions(sourceDimensions, params) ?? sourceDimensions; - const size = resolveMediaNodeCreationSizeFromDimensions(dimensions) ?? undefined; - const selectedModel = providerModelToSelectedModel(outpaintingModel); - const nodeId = addNode("smartImage", { - x: currentPosition.x + currentWidth + 220, - y: currentPosition.y, - }, { - inputImages: [imageSource], - inputPrompt: OUTPAINTING_DEFAULT_PROMPT, - selectedModel, - dimensions, - batchSize: outpaintingBatchSize, - parameters: { - batchSize: outpaintingBatchSize, - }, - extraTaskParams: params, - filename: `outpainting-${Date.now()}.png`, - customTitle: "扩图", - derivedImage: { - sourceNodeId: id, - operation: "outpainting", - }, - status: "idle", - error: null, - }, size); - onConnect({ - source: id, - sourceHandle: SINGLE_OUTPUT_HANDLE_ID, - target: nodeId, - targetHandle: SINGLE_INPUT_HANDLE_ID, - }); - selectSingleNode(nodeId); - closeNodeTool(); - await regenerateNode(nodeId); - }, [ - addNode, - closeNodeTool, - displayImage, - getNodeById, - id, - primaryImage, - onConnect, - outpaintingBatchSize, - outpaintingModel, - regenerateNode, - selectSingleNode, - ]); - const startCropTool = useCallback(() => { openNodeTool("crop"); }, [openNodeTool]); @@ -586,12 +415,16 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = const downloadImage = nodeData.outputImage || editableImage; const overlayImageDimensions = cropImageDimensions || asImageDimensions(nodeData.dimensions); const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && editableImage); - const isCropping = activeNodeTool === "crop"; - const isOutpainting = activeNodeTool === "outpainting"; - const isInpainting = activeNodeTool === "inpainting"; - const isSplitGrid = activeNodeTool === "splitGrid"; + const activeImageEditTool: ImageEditTool | null = + activeNodeTool === "crop" || + activeNodeTool === "outpainting" || + activeNodeTool === "inpainting" || + activeNodeTool === "splitGrid" + ? activeNodeTool + : null; + const isEditingImage = Boolean(activeImageEditTool); const isMultiAnglePanelOpen = activeNodeTool === "multiAngle"; - const renderedImageSrc = isCropping || isOutpainting || isInpainting ? editableImage : outputImageSrc; + const renderedImageSrc = isEditingImage ? editableImage : outputImageSrc; const selectedActions: NodeActionCapsuleAction[] = editableImage ? [ createMediaEditActionDropdown({ @@ -654,7 +487,7 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = section: "feature", panel: ({ close }) => ( { close(); handleStartGridSelection(selection); @@ -768,15 +601,6 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = {showSelectedActions && !activeNodeTool && } - {isSplitGrid && gridSelection && ( - void handleConfirmSplitGrid()} - /> - )} -
- {isCropping && ( - - )} - {isOutpainting && ( - - )} - {isInpainting && editableImage && ( + {activeImageEditTool && editableImage && ( { - closeNodeTool(); - }} - /> - )} - {isCropping && ( - { - closeNodeTool(); - }} - context={{ - value: cropAspectRatio, - onChange: setCropAspectRatio, - onConfirm: () => { - if (cropPayload) void handleConfirmCrop(cropPayload); - }, - }} - toolComponents={{ - [MEDIA_EDIT_TOOL.crop]: CropRatioSelect, - }} - /> - )} - {isOutpainting && ( - { - closeNodeTool(); - }} - onConfirm={() => { - if (outpaintingParams) void handleConfirmOutpainting(outpaintingParams); - }} - /> - )} - {isSplitGrid && gridSelection && ( - )} {/* Loading overlay for generation */}