diff --git a/src/areas/workflows/nodes/ExtensionNode.tsx b/src/areas/workflows/nodes/ExtensionNode.tsx index cc38df9..69f0c34 100644 --- a/src/areas/workflows/nodes/ExtensionNode.tsx +++ b/src/areas/workflows/nodes/ExtensionNode.tsx @@ -120,11 +120,8 @@ export default function ExtensionNode({ id, data, selected }: { id: string; data const { updateNodeData } = useReactFlow() const running = useWorkflowRunStore((s) => s.activeNodeId === id) - // Refs for handle alignment — support up to 2 inputs - const ioRowRef = useRef(null) - const ioRow2Ref = useRef(null) - const [handleTop, setHandleTop] = useState('50%') - const [handle2Top, setHandle2Top] = useState('50%') + const handleRefs = useRef([]) + const [handleTops, setHandleTops] = useState([]) const { modelExtensions, processExtensions } = useExtensionsStore() const allExtensions = buildAllWorkflowExtensions(modelExtensions, processExtensions) @@ -138,15 +135,14 @@ export default function ExtensionNode({ id, data, selected }: { id: string; data // Align handles with their respective IO rows after mount useLayoutEffect(() => { - if (ioRowRef.current) { - const center = ioRowRef.current.offsetTop + ioRowRef.current.offsetHeight / 2 - setHandleTop(`${center}px`) - } - if (ioRow2Ref.current) { - const center = ioRow2Ref.current.offsetTop + ioRow2Ref.current.offsetHeight / 2 - setHandle2Top(`${center}px`) - } - }, [isMulti]) + setHandleTops(handleRefs.current.map((ref) => { + if (ref) { + const center = ref.offsetTop + ref.offsetHeight / 2 + return `${center}px` + } + return '50%' + })) + }, [isMulti, inputs?.length]) const patchParam = useCallback((key: string, val: number | string) => { updateNodeData(id, { params: { ...data.params, [key]: val } }) @@ -166,30 +162,27 @@ export default function ExtensionNode({ id, data, selected }: { id: string; data const ioSubheader = isMulti ? ( // Multi-input layout: one row per input, output on first row
-
- - {inputs[0]} - - {!isTerminal && ( - <> - - - - - {ext?.output ?? '—'} - - - )} -
-
- - {inputs[1]} - -
+ {inputs.map((inputType, i) => ( +
{ if (el) handleRefs.current[i] = el }} className="flex items-center justify-between px-3 py-2"> + + {inputType} + + {i === 0 && !isTerminal && ( + <> + + + + + {ext?.output ?? '—'} + + + )} +
+ ))}
) : ( // Single-input layout (existing behavior) -
+
{ if (el) handleRefs.current[0] = el }} className="flex items-center justify-between px-3 py-2"> {ext?.input ?? '—'} @@ -207,31 +200,40 @@ export default function ExtensionNode({ id, data, selected }: { id: string; data ) // ── Handles ────────────────────────────────────────────────────────────── - const handlesEl = ( + const handlesEl = isMulti ? ( <> - {/* Primary input handle */} - - {/* Secondary input handle (multi-input only) */} - {isMulti && ( + {inputs.map((inputType, i) => ( + ))} + {!isTerminal && ( + )} - {/* Output handle */} + + ) : ( + <> + {!isTerminal && ( )} diff --git a/src/areas/workflows/workflowRunStore.ts b/src/areas/workflows/workflowRunStore.ts index 0f0341d..9ba7986 100644 --- a/src/areas/workflows/workflowRunStore.ts +++ b/src/areas/workflows/workflowRunStore.ts @@ -150,20 +150,41 @@ async function executeExtensionNode( return realId ? nodeOutputs.get(realId) : undefined } - let nodeInputPath: string | undefined - let nodeInputText: string | undefined - let nodeInputMeshPath: string | undefined + let nodeInputPath: string | undefined + let nodeInputText: string | undefined + let nodeInputMeshPath: string | undefined + const extraImagePaths: string[] = [] const incomingEdges = workflow.edges.filter((e) => e.target === node.id) if (ext?.inputs && ext.inputs.length > 1) { + const inputTypes = ext.inputs + const inputPaths = new Array(inputTypes.length).fill(undefined) + for (const edge of incomingEdges) { const src = resolveSource(edge.source) - if (!src) continue - if (src.outputType === 'mesh') nodeInputMeshPath = src.filePath - else if (src.outputType === 'image') nodeInputPath = src.filePath - else if (src.filePath !== undefined) nodeInputPath = src.filePath - if (src.text !== undefined) nodeInputText = src.text + if (!src || !src.filePath) continue + let slot = 0 + if (edge.targetHandle?.startsWith('input-')) { + slot = parseInt(edge.targetHandle.slice(6), 10) + } + if (slot >= 0 && slot < inputTypes.length) { + inputPaths[slot] = src.filePath + } + } + + for (let i = 0; i < inputTypes.length; i++) { + const fp = inputPaths[i] + if (!fp) continue + if (inputTypes[i] === 'mesh') { + nodeInputMeshPath = fp + } else if (inputTypes[i] === 'image') { + if (!nodeInputPath) { + nodeInputPath = fp + } else { + extraImagePaths.push(fp) + } + } } } else { for (const edge of incomingEdges) { @@ -194,6 +215,9 @@ async function executeExtensionNode( ? norm.slice(workspaceDir.length).replace(/^\//, '') : norm } + if (extraImagePaths.length > 0) { + extraParams.extra_image_paths = extraImagePaths + } const schemaDefaults = Object.fromEntries( (ext.params ?? []).map((p) => [p.id, p.default]), @@ -242,17 +266,22 @@ async function executeExtensionNode( useAppStore.getState().updateCurrentJob({ status: 'generating', progress: st.progress, step: st.step }) } } else { - if (ext?.input === 'mesh' && !nodeInputPath) throw new Error(`${ext.name} needs an incoming mesh connection`) - if (ext?.input === 'image' && !nodeInputPath) throw new Error(`${ext.name} needs an incoming image connection`) - if (ext?.input === 'text' && !nodeInputText) throw new Error(`${ext.name} needs an incoming text connection`) - const parts = (node.data.extensionId ?? '').split('/') const extId = parts[0] const nid = parts[1] ?? '' + + const processParams: Record = { ...(node.data.params as Record) } + if (nodeInputMeshPath && nodeInputPath) { + // Texture node: mesh is filePath, all images in extra_image_paths + processParams.extra_image_paths = [nodeInputPath, ...extraImagePaths] + } else if (extraImagePaths.length > 0) { + processParams.extra_image_paths = extraImagePaths + } + const result = await window.electron.extensions.runProcess( extId, - { filePath: nodeInputPath, text: nodeInputText, nodeId: nid }, - node.data.params as Record, + { filePath: nodeInputMeshPath ?? nodeInputPath, text: nodeInputText, nodeId: nid }, + processParams, ) if (!result.success) throw new Error(result.error ?? 'Process extension failed') nodeInputPath = result.result?.filePath ?? nodeInputPath