Browse Source

feat(annotate): 接入编辑工具宿主与节点菜单

- nodeToolStore / ImageEditTool / ImageEditSession / ActiveImageEditHost 注册 annotate
- MediaEditActionDropdown 标签
- GenerateImageNode 与 ImageInputNode 编辑菜单新增"标注"入口、图标与激活判定

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feature/handleReconstruction
Luckyu_js 1 day ago
parent
commit
3f09f2b8f1
  1. 1
      src/components/canvas/ActiveImageEditHost.tsx
  2. 4
      src/components/media/MediaEditActionDropdown.tsx
  3. 3
      src/components/media/image-edit/ImageEditSession.tsx
  4. 2
      src/components/media/image-edit/types.ts
  5. 30
      src/components/nodes/GenerateImageNode.tsx
  6. 25
      src/components/nodes/ImageInputNode.tsx
  7. 3
      src/store/nodeToolStore.ts

1
src/components/canvas/ActiveImageEditHost.tsx

@ -18,6 +18,7 @@ const IMAGE_EDIT_TOOLS: ReadonlySet<string> = new Set([
"outpainting",
"inpainting",
"splitGrid",
"annotate",
]);
function asImageDimensions(value: unknown): { width: number; height: number } | null {

4
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 "编辑";
}

3
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 <InpaintingEditSession {...commonProps} model={models.inpainting} />;
case "splitGrid":
return splitGridSelection ? <SplitGridEditSession {...commonProps} selection={splitGridSelection} /> : null;
case "annotate":
return <AnnotationEditSession {...commonProps} />;
default:
return null;
}

2
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;

30
src/components/nodes/GenerateImageNode.tsx

@ -64,6 +64,14 @@ function GenerateImageEditMenuIcon({ tool, className = "h-5 w-5" }: { tool: Medi
</svg>
);
}
if (tool === "annotate") {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 20h9" />
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z" />
</svg>
);
}
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<rect x="7" y="7" width="10" height="10" rx="1.5" />
@ -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<HTMLButtonElement>) => {
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: <GenerateImageEditMenuIcon tool="crop" className="h-4 w-4" />,
label: t("imageInput.crop"),
},
{
key: "annotate",
icon: <GenerateImageEditMenuIcon tool="annotate" className="h-4 w-4" />,
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();
}
},
}),
{

25
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 <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="m4 16 8.5-8.5a3 3 0 0 1 4.25 0l1.75 1.75a3 3 0 0 1 0 4.25L10 22H4v-6Z" /><path strokeLinecap="round" strokeLinejoin="round" d="m9 11 4 4M13 22h7" /></svg>;
case "highDefinition":
return <svg {...commonProps}><rect x="4" y="5" width="16" height="14" rx="2" /><path strokeLinecap="round" strokeLinejoin="round" d="M8 15V9m0 3h4m4-3v6M12 9v6" /></svg>;
case "annotate":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="M12 20h9" /><path strokeLinecap="round" strokeLinejoin="round" d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z" /></svg>;
}
}
@ -107,7 +110,9 @@ function getImageInputEditMenuIcon(tool: MediaEditMenuTool) {
? "crop"
: tool === "inpainting"
? "inpainting"
: "outpainting";
: tool === "annotate"
? "annotate"
: "outpainting";
return <span className="[&_svg]:h-5 [&_svg]:w-5"><ImageToolbarIcon kind={kind} /></span>;
}
@ -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: <span className="[&_svg]:h-4 [&_svg]:w-4"><ImageToolbarIcon kind="crop" /></span>,
label: t("imageInput.crop"),
},
{
key: "annotate",
icon: <span className="[&_svg]:h-4 [&_svg]:w-4"><ImageToolbarIcon kind="annotate" /></span>,
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");
}
},
}),
{

3
src/store/nodeToolStore.ts

@ -8,7 +8,8 @@ export type NodeToolType =
| "crop"
| "splitGrid"
| "outpainting"
| "inpainting";
| "inpainting"
| "annotate";
interface NodeToolState {
activeNodeId: string | null;

Loading…
Cancel
Save