mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-06 01:14:36 +08:00
feat(toolbar): add mask editing feature to image node toolbar for localized modifications
This commit is contained in:
@@ -29,6 +29,7 @@ import { CanvasAssistantPanel } from "../components/canvas-assistant-panel";
|
||||
import { CanvasNodeContextMenu } from "../components/canvas-context-menu";
|
||||
import { CanvasNodeAngleDialog, type CanvasImageAngleParams } from "../components/canvas-node-angle-dialog";
|
||||
import { CanvasNodeCropDialog, type CanvasImageCropRect } from "../components/canvas-node-crop-dialog";
|
||||
import { CanvasNodeMaskEditDialog, type CanvasImageMaskEditPayload } from "../components/canvas-node-mask-edit-dialog";
|
||||
import { CanvasNodeUpscaleDialog, type CanvasImageUpscaleParams } from "../components/canvas-node-upscale-dialog";
|
||||
import { buildNodeChatMessages, buildNodeGenerationContext, buildNodeGenerationInputs, hydrateNodeGenerationContext, type NodeGenerationInput } from "../components/canvas-node-generation";
|
||||
import { CanvasNodeHoverToolbar, CanvasNodeInfoModal } from "../components/canvas-node-hover-toolbar";
|
||||
@@ -273,6 +274,7 @@ function InfiniteCanvasPage() {
|
||||
const [editRequestNonce, setEditRequestNonce] = useState(0);
|
||||
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
|
||||
const [cropNodeId, setCropNodeId] = useState<string | null>(null);
|
||||
const [maskEditNodeId, setMaskEditNodeId] = useState<string | null>(null);
|
||||
const [upscaleNodeId, setUpscaleNodeId] = useState<string | null>(null);
|
||||
const [superResolveNodeId, setSuperResolveNodeId] = useState<string | null>(null);
|
||||
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
|
||||
@@ -576,6 +578,7 @@ function InfiniteCanvasPage() {
|
||||
const toolbarNode = toolbarNodeId ? nodeById.get(toolbarNodeId) || null : null;
|
||||
const infoNode = infoNodeId ? nodeById.get(infoNodeId) || null : null;
|
||||
const cropNode = cropNodeId ? nodeById.get(cropNodeId) || null : null;
|
||||
const maskEditNode = maskEditNodeId ? nodeById.get(maskEditNodeId) || null : null;
|
||||
const upscaleNode = upscaleNodeId ? nodeById.get(upscaleNodeId) || null : null;
|
||||
const superResolveNode = superResolveNodeId ? nodeById.get(superResolveNodeId) || null : null;
|
||||
const angleNode = angleNodeId ? nodeById.get(angleNodeId) || null : null;
|
||||
@@ -698,6 +701,7 @@ function InfiniteCanvasPage() {
|
||||
setEditingNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||
setInfoNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||
setCropNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||
setMaskEditNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||
setAngleNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||
setPreviewNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||
setRunningNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||
@@ -730,6 +734,7 @@ function InfiniteCanvasPage() {
|
||||
setConnections([]);
|
||||
setInfoNodeId(null);
|
||||
setCropNodeId(null);
|
||||
setMaskEditNodeId(null);
|
||||
setAngleNodeId(null);
|
||||
setPreviewNodeId(null);
|
||||
setRunningNodeId(null);
|
||||
@@ -1285,6 +1290,7 @@ function InfiniteCanvasPage() {
|
||||
setEditingNodeId(null);
|
||||
setInfoNodeId(null);
|
||||
setCropNodeId(null);
|
||||
setMaskEditNodeId(null);
|
||||
setPendingConnectionCreate(null);
|
||||
}
|
||||
};
|
||||
@@ -1464,6 +1470,53 @@ function InfiniteCanvasPage() {
|
||||
setCropNodeId(null);
|
||||
}, []);
|
||||
|
||||
const maskEditImageNode = useCallback(
|
||||
async (node: CanvasNodeData, payload: CanvasImageMaskEditPayload) => {
|
||||
if (!node.metadata?.content) return;
|
||||
const generationConfig = { ...buildGenerationConfig(effectiveConfig, node, "image"), count: "1", size: node.metadata?.size || "auto" };
|
||||
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
|
||||
openConfigDialog(true);
|
||||
return;
|
||||
}
|
||||
const userPrompt = payload.prompt.trim();
|
||||
const prompt = `只修改蒙版透明区域,其他区域保持不变。${userPrompt}`;
|
||||
const childId = nanoid();
|
||||
const source = { id: node.id, name: `${node.title || node.id}.png`, type: node.metadata.mimeType || "image/png", dataUrl: node.metadata.content, storageKey: node.metadata.storageKey };
|
||||
const generationMetadata = buildImageGenerationMetadata("edit", generationConfig, 1, [source]);
|
||||
setMaskEditNodeId(null);
|
||||
setRunningNodeId(childId);
|
||||
setNodes((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: childId,
|
||||
type: CanvasNodeType.Image,
|
||||
title: userPrompt.slice(0, 32) || "局部编辑结果",
|
||||
position: { x: node.position.x + node.width + 96, y: node.position.y },
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
metadata: { prompt, status: NODE_STATUS_LOADING, ...generationMetadata },
|
||||
},
|
||||
]);
|
||||
setConnections((prev) => [...prev, { id: nanoid(), fromNodeId: node.id, toNodeId: childId }]);
|
||||
setSelectedNodeIds(new Set([childId]));
|
||||
setSelectedConnectionId(null);
|
||||
setDialogNodeId(childId);
|
||||
try {
|
||||
const image = await requestEdit(generationConfig, prompt, [source], { id: `${node.id}-mask`, name: "mask.png", type: "image/png", dataUrl: payload.maskDataUrl }).then((items) => items[0]);
|
||||
const uploaded = await uploadImage(image.dataUrl);
|
||||
const size = fitNodeSize(uploaded.width, uploaded.height, node.width, node.height);
|
||||
setNodes((prev) => prev.map((item) => (item.id === childId ? { ...item, width: size.width, height: size.height, metadata: { ...item.metadata, ...imageMetadata(uploaded), prompt, ...generationMetadata } } : item)));
|
||||
} catch (error) {
|
||||
const errorDetails = error instanceof Error ? error.message : "局部修改失败";
|
||||
message.error(errorDetails);
|
||||
setNodes((prev) => prev.map((item) => (item.id === childId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails } } : item)));
|
||||
} finally {
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
},
|
||||
[effectiveConfig, isAiConfigReady, message, openConfigDialog],
|
||||
);
|
||||
|
||||
const upscaleImageNode = useCallback(async (node: CanvasNodeData, params: CanvasImageUpscaleParams) => {
|
||||
if (!node.metadata?.content) return;
|
||||
setUpscaleNodeId(null);
|
||||
@@ -2324,6 +2377,7 @@ function InfiniteCanvasPage() {
|
||||
onUpload={(node) => handleUploadRequest(node.id)}
|
||||
onDownload={downloadNodeImage}
|
||||
onSaveAsset={(node) => void saveNodeAsset(node)}
|
||||
onMaskEdit={(node) => setMaskEditNodeId(node.id)}
|
||||
onCrop={(node) => setCropNodeId(node.id)}
|
||||
onUpscale={(node) => setUpscaleNodeId(node.id)}
|
||||
onSuperResolve={(node) => setSuperResolveNodeId(node.id)}
|
||||
@@ -2393,6 +2447,8 @@ function InfiniteCanvasPage() {
|
||||
|
||||
{cropNode?.metadata?.content ? <CanvasNodeCropDialog dataUrl={cropNode.metadata.content} open={Boolean(cropNode)} onClose={() => setCropNodeId(null)} onConfirm={(crop) => void cropImageNode(cropNode!, crop)} /> : null}
|
||||
|
||||
{maskEditNode?.metadata?.content ? <CanvasNodeMaskEditDialog dataUrl={maskEditNode.metadata.content} open={Boolean(maskEditNode)} onClose={() => setMaskEditNodeId(null)} onConfirm={(payload) => void maskEditImageNode(maskEditNode!, payload)} /> : null}
|
||||
|
||||
{upscaleNode?.metadata?.content ? <CanvasNodeUpscaleDialog dataUrl={upscaleNode.metadata.content} open={Boolean(upscaleNode)} onClose={() => setUpscaleNodeId(null)} onConfirm={(params) => void upscaleImageNode(upscaleNode!, params)} /> : null}
|
||||
|
||||
<Modal title="AI 超分" open={Boolean(superResolveNode?.metadata?.content)} centered footer={null} onCancel={() => setSuperResolveNodeId(null)}>
|
||||
|
||||
Reference in New Issue
Block a user