feat(canvas): 画布节点 - 恢复批量创作表与转换能力
This commit is contained in:
1 parent
dec5034578
commit
fca772a40c
36 files changed
+3341
-34
No files matched your search
@@ -0,0 +1,153 @@
|
||||
import { useCallback, type Dispatch, type SetStateAction } from "react";
|
||||
import { App } from "antd";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { batchInputColumns, batchReferenceColumns, createBatchRowsFromColumns, createInheritedBatchRow } from "@/lib/canvas/canvas-batch-table";
|
||||
import { createCanvasNode } from "@/lib/canvas/canvas-project-domain";
|
||||
import { buildGenerationConfig, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { modelDisplayName, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { CanvasNodeType, type CanvasBatchRow, type CanvasBatchTableData, type CanvasConnection, type CanvasGenerationBatchMode, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
type Options = {
|
||||
nodesRef: { current: CanvasNodeData[] };
|
||||
connectionsRef: { current: CanvasConnection[] };
|
||||
setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>;
|
||||
setConnections: Dispatch<SetStateAction<CanvasConnection[]>>;
|
||||
setSelectedNodeIds: Dispatch<SetStateAction<Set<string>>>;
|
||||
enqueueGenerationBatch: (sourceNodeId: string, mode: CanvasGenerationBatchMode, targets: Array<{ rowId: string; nodeId: string }>, options?: { concurrency?: number }) => string | undefined;
|
||||
};
|
||||
|
||||
export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setConnections, setSelectedNodeIds, enqueueGenerationBatch }: Options) {
|
||||
const { message, modal } = App.useApp();
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
|
||||
|
||||
const patchTable = useCallback((nodeId: string, patch: Partial<CanvasBatchTableData>) => {
|
||||
setNodes((current) => current.map((node) => node.id !== nodeId ? node : { ...node, metadata: { ...node.metadata, batchTable: { operation: "try_on", concurrency: 10, rows: [], ...node.metadata?.batchTable, ...patch } } }));
|
||||
}, [setNodes]);
|
||||
|
||||
const updateRow = useCallback((nodeId: string, rowId: string, patch: Partial<CanvasBatchRow>) => {
|
||||
const node = nodesRef.current.find((item) => item.id === nodeId);
|
||||
if (!node?.metadata?.batchTable) return;
|
||||
patchTable(nodeId, { rows: node.metadata.batchTable.rows.map((row) => row.id === rowId ? { ...row, ...patch } : row) });
|
||||
}, [nodesRef, patchTable]);
|
||||
|
||||
const addRow = useCallback((nodeId: string) => {
|
||||
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
|
||||
if (!table) return;
|
||||
patchTable(nodeId, { rows: [...table.rows, createInheritedBatchRow(table.operation, table.rows)] });
|
||||
}, [nodesRef, patchTable]);
|
||||
|
||||
const removeRow = useCallback((nodeId: string, rowId: string) => {
|
||||
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
|
||||
if (!table) return;
|
||||
patchTable(nodeId, { rows: table.rows.filter((row) => row.id !== rowId) });
|
||||
}, [nodesRef, patchTable]);
|
||||
|
||||
const addReferenceColumn = useCallback((nodeId: string) => {
|
||||
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
|
||||
if (!table) return;
|
||||
const columns = batchReferenceColumns(table);
|
||||
if (columns.length >= 6) return message.info("最多支持 6 组参考图");
|
||||
const nextIndex = columns.length + 1;
|
||||
patchTable(nodeId, { referenceColumns: [...columns, { id: `reference-${nanoid()}`, label: `参考图 ${nextIndex}` }] });
|
||||
}, [message, nodesRef, patchTable]);
|
||||
|
||||
const fillRowsFromConnections = useCallback((nodeId: string) => {
|
||||
const node = nodesRef.current.find((item) => item.id === nodeId);
|
||||
const table = node?.metadata?.batchTable;
|
||||
if (!node || !table) return;
|
||||
const nodeById = new Map(nodesRef.current.map((item) => [item.id, item]));
|
||||
const columns = batchInputColumns(node, connectionsRef.current).map((column) => column.filter((inputNodeId) => {
|
||||
const input = nodeById.get(inputNodeId);
|
||||
return input?.type === CanvasNodeType.Image && Boolean(input.metadata?.content || input.metadata?.storageKey);
|
||||
}));
|
||||
if (!columns.some((column) => column.length)) return message.warning("请先把图片节点连接到批量创作表");
|
||||
const rows = createBatchRowsFromColumns(table.operation, columns, table.rows);
|
||||
if (!rows.length) return message.warning("批量换装至少需要一张人物图和一张服装图");
|
||||
patchTable(nodeId, { rows });
|
||||
const added = Math.max(0, rows.length - table.rows.length);
|
||||
message.success(added ? `已同步连线并新增 ${added} 行,原有任务均已保留` : "已同步最新连线,原有任务均已保留");
|
||||
}, [connectionsRef, message, nodesRef, patchTable]);
|
||||
|
||||
const generateRows = useCallback(async (nodeId: string, requestedRowIds?: string[]) => {
|
||||
const sourceNode = nodesRef.current.find((item) => item.id === nodeId);
|
||||
const table = sourceNode?.metadata?.batchTable;
|
||||
if (!sourceNode || !table) return;
|
||||
const imageModel = effectiveConfig.imageModel || effectiveConfig.model;
|
||||
if (!isAiConfigReady(effectiveConfig, imageModel)) {
|
||||
navigateToSettings({ continueCreation: true });
|
||||
return;
|
||||
}
|
||||
const activeNodeIds = new Set((sourceNode.metadata?.generationBatches || []).filter((batch) => batch.mode === "batch_image").flatMap((batch) => batch.items.filter((item) => ["waiting", "submitting", "queued", "running"].includes(item.status)).map((item) => item.nodeId)));
|
||||
const requested = requestedRowIds?.length ? new Set(requestedRowIds) : null;
|
||||
const rows = table.rows.filter((row) => {
|
||||
if (!row.enabled || (requested && !requested.has(row.id)) || !row.prompt.trim()) return false;
|
||||
if (table.operation === "try_on" && row.inputNodeIds.length < 2) return false;
|
||||
if (!row.inputNodeIds.length || row.inputNodeIds.some((id) => !nodesRef.current.some((node) => node.id === id && node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey)))) return false;
|
||||
const output = row.outputNodeId ? nodesRef.current.find((node) => node.id === row.outputNodeId) : undefined;
|
||||
if (output && activeNodeIds.has(output.id)) return false;
|
||||
// 操作列的单行生成允许对已完成结果重新生成;顶部批量按钮仍只提交未完成项。
|
||||
if (requested) return true;
|
||||
return !output?.metadata?.content;
|
||||
});
|
||||
if (!rows.length) return message.info("没有可提交的未完成任务,请检查参考图和提示词");
|
||||
const confirmed = await new Promise<boolean>((resolve) => modal.confirm({
|
||||
title: `确认提交 ${rows.length} 个批量图片任务`,
|
||||
content: `模型:${modelDisplayName(effectiveConfig, imageModel)};并发上限:${table.concurrency}。这些任务可能消耗积分或产生外部模型费用。`,
|
||||
okText: "确认生成",
|
||||
cancelText: "取消",
|
||||
centered: true,
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
}));
|
||||
if (!confirmed) return;
|
||||
|
||||
const imageSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
||||
const nextNodes = [...nodesRef.current];
|
||||
let nextConnections = [...connectionsRef.current];
|
||||
const outputByRowId = new Map<string, string>();
|
||||
const targets: Array<{ rowId: string; nodeId: string }> = [];
|
||||
rows.forEach((row, index) => {
|
||||
const existingIndex = row.outputNodeId ? nextNodes.findIndex((node) => node.id === row.outputNodeId && node.type === CanvasNodeType.Image) : -1;
|
||||
const metadata = {
|
||||
...(existingIndex >= 0 ? resetGenerationTaskMetadata(nextNodes[existingIndex].metadata) : {}),
|
||||
prompt: row.prompt.trim(),
|
||||
composerContent: row.prompt.trim(),
|
||||
model: buildGenerationConfig(effectiveConfig, undefined, "image").model,
|
||||
generationMode: "image" as const,
|
||||
generationType: "edit" as const,
|
||||
workflowKind: "final" as const,
|
||||
workflowTitle: `${table.operation === "try_on" ? "换装" : "创意"}任务 ${index + 1}`,
|
||||
status: "idle" as const,
|
||||
batchSourceNodeId: nodeId,
|
||||
batchRowId: row.id,
|
||||
batchOperation: table.operation,
|
||||
batchInputNodeIds: row.inputNodeIds,
|
||||
};
|
||||
const output = existingIndex >= 0
|
||||
? { ...nextNodes[existingIndex], metadata }
|
||||
: createCanvasNode(CanvasNodeType.Image, { x: sourceNode.position.x + sourceNode.width + 120 + imageSpec.width / 2, y: sourceNode.position.y + index * (imageSpec.height + 32) + imageSpec.height / 2 }, metadata);
|
||||
output.title = `${table.operation === "try_on" ? "换装" : "创意"} · ${index + 1}`;
|
||||
if (existingIndex >= 0) nextNodes[existingIndex] = output;
|
||||
else nextNodes.push(output);
|
||||
nextConnections = nextConnections.filter((connection) => connection.toNodeId !== output.id);
|
||||
row.inputNodeIds.forEach((inputNodeId) => nextConnections.push({ id: nanoid(), fromNodeId: inputNodeId, toNodeId: output.id }));
|
||||
nextConnections.push({ id: nanoid(), fromNodeId: sourceNode.id, toNodeId: output.id, relation: "batch-output", storyboardRowId: row.id });
|
||||
outputByRowId.set(row.id, output.id);
|
||||
targets.push({ rowId: row.id, nodeId: output.id });
|
||||
});
|
||||
const sourceIndex = nextNodes.findIndex((node) => node.id === sourceNode.id);
|
||||
nextNodes[sourceIndex] = { ...sourceNode, metadata: { ...sourceNode.metadata, batchTable: { ...table, rows: table.rows.map((row) => outputByRowId.has(row.id) ? { ...row, outputNodeId: outputByRowId.get(row.id) } : row) } } };
|
||||
nodesRef.current = nextNodes;
|
||||
connectionsRef.current = nextConnections;
|
||||
setNodes(nextNodes);
|
||||
setConnections(nextConnections);
|
||||
setSelectedNodeIds(new Set(targets.map((target) => target.nodeId)));
|
||||
if (enqueueGenerationBatch(nodeId, "batch_image", targets, { concurrency: table.concurrency })) message.success(`${targets.length} 个任务已加入并发队列`);
|
||||
}, [connectionsRef, effectiveConfig, enqueueGenerationBatch, isAiConfigReady, message, modal, nodesRef, setConnections, setNodes, setSelectedNodeIds]);
|
||||
|
||||
return { addReferenceColumn, addRow, fillRowsFromConnections, generateRows, patchTable, removeRow, updateRow };
|
||||
}
|
||||
Reference in new issue
Block a user