diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index d881abf57..5189b182c 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -956,6 +956,7 @@ const CANVAS_TOOLBAR_FALLBACK_ICONS: Record = imageEnhance: Sparkles, imageRemoveBackground: Scissors, imageBoxCutout: Scan, + imageLocalEdit: Wand2, imageRemoveWatermark: Eraser, imageResize: Maximize2, imageTextEdit: Type, diff --git a/components/canvas/CanvasWorkspace.tsx b/components/canvas/CanvasWorkspace.tsx index 108097a08..6b707d03e 100644 --- a/components/canvas/CanvasWorkspace.tsx +++ b/components/canvas/CanvasWorkspace.tsx @@ -33,6 +33,7 @@ import { Tag, type LucideIcon, UserRound, + Wand2, Zap, } from "lucide-react"; import { @@ -45,6 +46,7 @@ import { } from "@/lib/canvas/video-canvas-markers"; import { getCanvasToolbarItem, + isCanvasToolbarItemEnabled, normalizeCanvasToolbarConfig, type CanvasToolbarConfig, } from "@/lib/canvas-toolbar-config"; @@ -373,6 +375,14 @@ export function CanvasWorkspace({ ), [nodes, selectedNodeIds] ); + const selectedImageLocalEditItem = getCanvasToolbarItem( + canvasToolbarConfig, + "imageLocalEdit" + ); + const selectedImageLocalEditEnabled = isCanvasToolbarItemEnabled( + canvasToolbarConfig, + "imageLocalEdit" + ); // Store actions const setViewport = useCanvasStore((state) => state.setViewport); @@ -386,6 +396,9 @@ export function CanvasWorkspace({ const resizeNode = useCanvasStore((state) => state.resizeNode); const pushHistory = useCanvasStore((state) => state.pushHistory); const updateNode = useCanvasStore((state) => state.updateNode); + const requestImageNodeAction = useCanvasStore( + (state) => state.requestImageNodeAction + ); const clearLastAddedNodeId = useCanvasStore((state) => state.clearLastAddedNodeId); const centerTimeoutRef = useRef(null); const isMacPlatform = React.useMemo(() => { @@ -1870,9 +1883,34 @@ export function CanvasWorkspace({ )} - {/* 缩放控制器 */} + {/* 选中图片快捷操作 */} {selectedImageNodes.length > 0 && ( -
+
+ {selectedImageLocalEditEnabled && selectedImageNodes.length === 1 ? ( + + ) : null} + )) + ) : ( + + 暂无区域,直接在图片上拖拽框选。 + + )} +
+
+ + + +
+
+ {boxCutoutMetrics ? ( +
+ ) : null} + {localEditRegionOverlays.map(({ region, style }) => { + const left = Number(style.left || 0); + const top = Number(style.top || 0); + const width = Number(style.width || 0); + const height = Number(style.height || 0); + const imageBottom = boxCutoutMetrics + ? boxCutoutMetrics.offsetY + boxCutoutMetrics.renderedHeight + : node.size.height; + const placeAbove = top + height + 58 > imageBottom; + const promptTop = placeAbove ? top - 46 : top + height + 8; + const promptWidth = Math.min(Math.max(width, 176), 300); + const isEditing = + editingLocalEditRegionId === region.id || !region.prompt.trim(); + + return ( + +
+
+ {region.index} +
+
+
event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + style={{ + left, + top: Math.max(4, promptTop), + width: promptWidth, + }} + > + {isEditing ? ( + { + if (region.prompt.trim()) { + setEditingLocalEditRegionId(null); + } + }} + onChange={(event) => + updateLocalEditRegionPrompt(region.id, event.target.value) + } + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + setEditingLocalEditRegionId(null); + } + if (event.key === "Escape") { + event.preventDefault(); + setEditingLocalEditRegionId(null); + } + }} + placeholder={`区域 ${region.index} 提示词`} + value={region.prompt} + /> + ) : ( + + )} + +
+
+ ); + })} + {localEditDraftRectStyle ? ( +
+ ) : null} + + )} + {/* 🆕 拾取模式:当前点击位置(正在识别) */} {isThisNodePicking && currentPickerClickStyle && (
) : null} + {imageLocalEditEnabled ? ( + + ) : null} + {imageRemoveWatermarkEnabled ? (