diff --git a/src/components/canvas/ActiveImageEditHost.tsx b/src/components/canvas/ActiveImageEditHost.tsx index 4a46b910..a9311123 100644 --- a/src/components/canvas/ActiveImageEditHost.tsx +++ b/src/components/canvas/ActiveImageEditHost.tsx @@ -18,6 +18,7 @@ const IMAGE_EDIT_TOOLS: ReadonlySet = new Set([ "outpainting", "inpainting", "splitGrid", + "annotate", ]); function asImageDimensions(value: unknown): { width: number; height: number } | null { diff --git a/src/components/media/MediaEditActionDropdown.tsx b/src/components/media/MediaEditActionDropdown.tsx index 81b4ed5f..89f7badf 100644 --- a/src/components/media/MediaEditActionDropdown.tsx +++ b/src/components/media/MediaEditActionDropdown.tsx @@ -4,7 +4,7 @@ import type { ReactNode } from "react"; import type { MenuProps } from "antd"; import type { NodeActionCapsuleAction } from "@/components/nodes/NodeActionCapsule"; -export type MediaEditMenuTool = "edit" | "high-definition" | "outpainting" | "inpainting" | "crop"; +export type MediaEditMenuTool = "edit" | "high-definition" | "outpainting" | "inpainting" | "crop" | "annotate"; interface MediaEditActionDropdownOptions { label: string; @@ -46,6 +46,8 @@ export function getMediaEditMenuLabel(tool: MediaEditMenuTool) { return "擦除"; case "crop": return "裁剪"; + case "annotate": + return "标注"; default: return "编辑"; } diff --git a/src/components/media/image-edit/ImageEditSession.tsx b/src/components/media/image-edit/ImageEditSession.tsx index 586eebff..c004855f 100644 --- a/src/components/media/image-edit/ImageEditSession.tsx +++ b/src/components/media/image-edit/ImageEditSession.tsx @@ -4,6 +4,7 @@ import { CropEditSession } from "./CropEditSession"; import { InpaintingEditSession } from "./InpaintingEditSession"; import { OutpaintingEditSession } from "./OutpaintingEditSession"; import { SplitGridEditSession } from "./SplitGridEditSession"; +import { AnnotationEditSession } from "./AnnotationEditSession"; import type { ImageEditSessionProps } from "./types"; export function ImageEditSession({ @@ -35,6 +36,8 @@ export function ImageEditSession({ return ; case "splitGrid": return splitGridSelection ? : null; + case "annotate": + return ; default: return null; } diff --git a/src/components/media/image-edit/types.ts b/src/components/media/image-edit/types.ts index 24b5fd9d..259d0371 100644 --- a/src/components/media/image-edit/types.ts +++ b/src/components/media/image-edit/types.ts @@ -2,7 +2,7 @@ import type { RefObject } from "react"; import type { ProviderModel } from "@/lib/providers/types"; import type { SplitGridSelection } from "@/components/media/SplitGridCapsuleMenu"; -export type ImageEditTool = "crop" | "outpainting" | "inpainting" | "splitGrid"; +export type ImageEditTool = "crop" | "outpainting" | "inpainting" | "splitGrid" | "annotate"; export interface ImageEditSessionCommonProps { nodeId: string; diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index 6ffb0a84..5eedf37f 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -64,6 +64,14 @@ function GenerateImageEditMenuIcon({ tool, className = "h-5 w-5" }: { tool: Medi ); } + if (tool === "annotate") { + return ( + + + + + ); + } return ( @@ -297,6 +305,10 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = openNodeTool("inpainting"); }, [openNodeTool]); + const startAnnotationTool = useCallback(() => { + openNodeTool("annotate"); + }, [openNodeTool]); + const handleEditMenuPrimaryClick = useCallback((event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); @@ -312,9 +324,13 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = startInpaintingTool(); return; } + if (activeEditMenuTool === "annotate") { + startAnnotationTool(); + return; + } setActiveEditMenuTool("crop"); startCropTool(); - }, [activeEditMenuTool, handleCreateHighDefinitionNode, startCropTool, startInpaintingTool, startOutpaintingTool]); + }, [activeEditMenuTool, handleCreateHighDefinitionNode, startAnnotationTool, startCropTool, startInpaintingTool, startOutpaintingTool]); const handleCarouselPrevious = useCallback(async () => { const history = nodeData.imageHistory || []; @@ -437,7 +453,8 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = activeNodeTool === "crop" || activeNodeTool === "outpainting" || activeNodeTool === "inpainting" || - activeNodeTool === "splitGrid" + activeNodeTool === "splitGrid" || + activeNodeTool === "annotate" ? activeNodeTool : null; const isEditingImage = Boolean(activeImageEditTool); @@ -472,6 +489,11 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = icon: , label: t("imageInput.crop"), }, + { + key: "annotate", + icon: , + label: "标注", + }, ], menuOnClick: ({ key, domEvent }) => { domEvent.preventDefault(); @@ -495,6 +517,10 @@ export function GenerateImageNodeView({ id, data, selected, renderInputHandle = setActiveEditMenuTool("crop"); startCropTool(); } + if (key === "annotate") { + setActiveEditMenuTool("annotate"); + startAnnotationTool(); + } }, }), { diff --git a/src/components/nodes/ImageInputNode.tsx b/src/components/nodes/ImageInputNode.tsx index b80b665a..269d65b0 100644 --- a/src/components/nodes/ImageInputNode.tsx +++ b/src/components/nodes/ImageInputNode.tsx @@ -63,7 +63,8 @@ type ImageToolbarIconKind = | "multiAngle" | "outpainting" | "inpainting" - | "highDefinition"; + | "highDefinition" + | "annotate"; function ImageToolbarIcon({ kind }: { kind: ImageToolbarIconKind }) { const commonProps = { @@ -97,6 +98,8 @@ function ImageToolbarIcon({ kind }: { kind: ImageToolbarIconKind }) { return ; case "highDefinition": return ; + case "annotate": + return ; } } @@ -107,7 +110,9 @@ function getImageInputEditMenuIcon(tool: MediaEditMenuTool) { ? "crop" : tool === "inpainting" ? "inpainting" - : "outpainting"; + : tool === "annotate" + ? "annotate" + : "outpainting"; return ; } @@ -144,7 +149,8 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal activeNodeTool === "crop" || activeNodeTool === "outpainting" || activeNodeTool === "inpainting" || - activeNodeTool === "splitGrid" + activeNodeTool === "splitGrid" || + activeNodeTool === "annotate" ? activeNodeTool : null; const isEditingImage = Boolean(activeImageEditTool); @@ -324,6 +330,10 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal openImageEditTool("inpainting"); return; } + if (activeEditMenuTool === "annotate") { + openImageEditTool("annotate"); + return; + } setActiveEditMenuTool("crop"); openImageEditTool("crop"); }, [activeEditMenuTool, handleCreateHighDefinitionNode, openImageEditTool]); @@ -422,6 +432,11 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal icon: , label: t("imageInput.crop"), }, + { + key: "annotate", + icon: , + label: "标注", + }, ], menuOnClick: ({ key, domEvent }) => { domEvent.preventDefault(); @@ -445,6 +460,10 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal setActiveEditMenuTool("crop"); openImageEditTool("crop"); } + if (key === "annotate") { + setActiveEditMenuTool("annotate"); + openImageEditTool("annotate"); + } }, }), { diff --git a/src/store/nodeToolStore.ts b/src/store/nodeToolStore.ts index 9b4f42c3..278cc699 100644 --- a/src/store/nodeToolStore.ts +++ b/src/store/nodeToolStore.ts @@ -8,7 +8,8 @@ export type NodeToolType = | "crop" | "splitGrid" | "outpainting" - | "inpainting"; + | "inpainting" + | "annotate"; interface NodeToolState { activeNodeId: string | null;