| const MODEL_CHAIN_TYPES = Object.freeze(['MODEL', 'CLIP']); |
|
|
| function normalizeSlotType(type) { |
| return String(type ?? '').trim().toUpperCase(); |
| } |
|
|
| function findSlotIndex(slots, requiredType) { |
| const wanted = normalizeSlotType(requiredType); |
| return Array.isArray(slots) |
| ? slots.findIndex(slot => normalizeSlotType(slot?.type) === wanted) |
| : -1; |
| } |
|
|
| function getGraphLink(graph, linkId) { |
| if (linkId === null || linkId === undefined) return null; |
| return graph?.links?.[linkId] || graph?._links?.[linkId] || null; |
| } |
|
|
| function getGraphNode(graph, nodeId) { |
| if (graph?.getNodeById) return graph.getNodeById(nodeId) || null; |
| return Array.isArray(graph?._nodes) |
| ? graph._nodes.find(node => node?.id === nodeId) || null |
| : null; |
| } |
|
|
| function unsupported(direction, code, details = {}) { |
| return { supported: false, direction, code, channels: [], ...details }; |
| } |
|
|
| export function analyzeModelChainInsertion(graph, anchorNode, direction) { |
| if (!graph || !anchorNode) { |
| return unsupported(direction, 'missing_graph_or_node'); |
| } |
| if (direction !== 'before' && direction !== 'after') { |
| return unsupported(direction, 'invalid_direction'); |
| } |
|
|
| const channels = []; |
| for (const type of MODEL_CHAIN_TYPES) { |
| const anchorSlot = direction === 'before' |
| ? findSlotIndex(anchorNode.inputs, type) |
| : findSlotIndex(anchorNode.outputs, type); |
| if (anchorSlot < 0) { |
| return unsupported(direction, direction === 'before' ? 'missing_chain_inputs' : 'missing_chain_outputs', { missingType: type }); |
| } |
|
|
| if (direction === 'before') { |
| const linkId = anchorNode.inputs[anchorSlot]?.link; |
| const link = getGraphLink(graph, linkId); |
| const sourceNode = link ? getGraphNode(graph, link.origin_id) : null; |
| if (!link || !sourceNode) { |
| return unsupported(direction, 'unconnected_chain_inputs', { missingType: type }); |
| } |
| channels.push({ |
| type, |
| anchorSlot, |
| originalLinks: [{ |
| originNode: sourceNode, |
| originSlot: link.origin_slot, |
| targetNode: anchorNode, |
| targetSlot: anchorSlot, |
| }], |
| }); |
| continue; |
| } |
|
|
| const linkIds = Array.isArray(anchorNode.outputs[anchorSlot]?.links) |
| ? anchorNode.outputs[anchorSlot].links.filter(id => id !== null && id !== undefined) |
| : []; |
| if (linkIds.length > 1) { |
| return unsupported(direction, 'ambiguous_downstream_branches', { ambiguousType: type, branchCount: linkIds.length }); |
| } |
|
|
| const originalLinks = []; |
| for (const linkId of linkIds) { |
| const link = getGraphLink(graph, linkId); |
| const targetNode = link ? getGraphNode(graph, link.target_id) : null; |
| if (!link || !targetNode) { |
| return unsupported(direction, 'invalid_downstream_link', { invalidType: type }); |
| } |
| originalLinks.push({ |
| originNode: anchorNode, |
| originSlot: anchorSlot, |
| targetNode, |
| targetSlot: link.target_slot, |
| }); |
| } |
| channels.push({ type, anchorSlot, originalLinks }); |
| } |
|
|
| return { supported: true, direction, code: 'ready', channels }; |
| } |
|
|
| export function getModelChainInsertionCapabilities(graph, anchorNode) { |
| return { |
| before: analyzeModelChainInsertion(graph, anchorNode, 'before'), |
| after: analyzeModelChainInsertion(graph, anchorNode, 'after'), |
| }; |
| } |
|
|
| function assertInsertedNodeSlots(insertedNode) { |
| const slots = {}; |
| for (const type of MODEL_CHAIN_TYPES) { |
| const input = findSlotIndex(insertedNode?.inputs, type); |
| const output = findSlotIndex(insertedNode?.outputs, type); |
| if (input < 0 || output < 0) { |
| const error = new Error(`Inserted node does not expose ${type} input/output slots.`); |
| error.code = 'inserted_node_missing_chain_slots'; |
| throw error; |
| } |
| slots[type] = { input, output }; |
| } |
| return slots; |
| } |
|
|
| function connectOrThrow(originNode, originSlot, targetNode, targetSlot, type) { |
| const link = originNode?.connect?.(originSlot, targetNode, targetSlot); |
| if (!link) { |
| const error = new Error(`Failed to connect ${type} while inserting the model node.`); |
| error.code = 'connection_failed'; |
| error.channelType = type; |
| throw error; |
| } |
| } |
|
|
| function restoreConnections(originalConnections) { |
| for (const connection of originalConnections) { |
| connection.originNode?.connect?.( |
| connection.originSlot, |
| connection.targetNode, |
| connection.targetSlot, |
| ); |
| } |
| } |
|
|
| function placeInsertedNode(graph, anchorNode, insertedNode, direction) { |
| const anchorX = Number(anchorNode?.pos?.[0]) || 0; |
| const anchorY = Number(anchorNode?.pos?.[1]) || 0; |
| const anchorWidth = Number(anchorNode?.size?.[0]) || 220; |
| const insertedWidth = Number(insertedNode?.size?.[0]) || 220; |
| const horizontalGap = 90; |
| let x = direction === 'before' |
| ? anchorX - insertedWidth - horizontalGap |
| : anchorX + anchorWidth + horizontalGap; |
| let y = anchorY; |
|
|
| const overlaps = (candidateX, candidateY) => (graph?._nodes || []).some(node => { |
| if (!node || node === anchorNode || node === insertedNode) return false; |
| const nodeX = Number(node.pos?.[0]) || 0; |
| const nodeY = Number(node.pos?.[1]) || 0; |
| const nodeWidth = Number(node.size?.[0]) || 220; |
| const nodeHeight = Number(node.size?.[1]) || 120; |
| const insertedHeight = Number(insertedNode?.size?.[1]) || 120; |
| return candidateX < nodeX + nodeWidth + 20 |
| && candidateX + insertedWidth + 20 > nodeX |
| && candidateY < nodeY + nodeHeight + 20 |
| && candidateY + insertedHeight + 20 > nodeY; |
| }); |
|
|
| for (const offset of [0, 80, -80, 160, -160, 240, -240]) { |
| if (!overlaps(x, anchorY + offset)) { |
| y = anchorY + offset; |
| break; |
| } |
| } |
| insertedNode.pos = [x, y]; |
| } |
|
|
| export function spliceModelChainNode({ graph, anchorNode, insertedNode, direction }) { |
| const analysis = analyzeModelChainInsertion(graph, anchorNode, direction); |
| if (!analysis.supported) { |
| const error = new Error(`Model-chain insertion is not available: ${analysis.code}`); |
| error.code = analysis.code; |
| error.analysis = analysis; |
| throw error; |
| } |
|
|
| const insertedSlots = assertInsertedNodeSlots(insertedNode); |
| const originalConnections = analysis.channels.flatMap(channel => channel.originalLinks); |
| let added = false; |
| graph.beforeChange?.(anchorNode); |
| try { |
| graph.add(insertedNode); |
| added = true; |
| placeInsertedNode(graph, anchorNode, insertedNode, direction); |
|
|
| for (const channel of analysis.channels) { |
| const slots = insertedSlots[channel.type]; |
| if (direction === 'before') { |
| const original = channel.originalLinks[0]; |
| connectOrThrow(original.originNode, original.originSlot, insertedNode, slots.input, channel.type); |
| connectOrThrow(insertedNode, slots.output, anchorNode, channel.anchorSlot, channel.type); |
| continue; |
| } |
|
|
| connectOrThrow(anchorNode, channel.anchorSlot, insertedNode, slots.input, channel.type); |
| for (const original of channel.originalLinks) { |
| connectOrThrow(insertedNode, slots.output, original.targetNode, original.targetSlot, channel.type); |
| } |
| } |
|
|
| graph.afterChange?.(anchorNode); |
| graph.change?.(); |
| graph.setDirtyCanvas?.(true, true); |
| return insertedNode; |
| } catch (error) { |
| if (added) graph.remove?.(insertedNode); |
| restoreConnections(originalConnections); |
| graph.afterChange?.(anchorNode); |
| graph.change?.(); |
| graph.setDirtyCanvas?.(true, true); |
| throw error; |
| } |
| } |
|
|