/** Split Primitives (misc) */ import { app } from "../../scripts/app.js" import { attachStabilizeHooks, PRIMITIVES_TYPE, syncPrimitivesLinkSlot, COMBINE_PRIMITIVES_NODE_CLASS, IoDirection, PRIMITIVE_SLOT_TYPE, SPLIT_PRIMITIVES_NODE_CLASS, LENGTH_OUTPUT_NAME, findLinkedCombineNode, isPrimitiveRelayNode, propagatePrimitiveSplitSync, getStoredPrimitiveSlotTypes, resolvePrimitiveSlotType, reconcilePrimitiveSlotType, scheduleStabilizeRetries, syncNodeSizeToContent, } from "./utils.js" import { createTrailingOutputHelpers } from "./utils/trailing-output.js" import { formatTypedOutputName, typeNameForPrimitiveSlotType, listLinkedPrimitiveInputs, pickDesiredDuringSync, resolveDesiredPrimitiveSlots, } from "./logic/split-primitives-names.js" import { MISC_GRAPH_RESTORE_WINDOW_MS } from "./logic/restore-window.js" const NODE_CLASS = SPLIT_PRIMITIVES_NODE_CLASS const COMBINE_NODE_CLASS = COMBINE_PRIMITIVES_NODE_CLASS const RELAY_PATCH_KEY = "__miscPrimitiveRelayPatch" const lengthTrailing = createTrailingOutputHelpers(LENGTH_OUTPUT_NAME) const countPrimitiveDataOutputs = lengthTrailing.countDataSlots const ensureLengthOutput = lengthTrailing.ensureMeta const isLengthOutputSlot = lengthTrailing.isMetaSlot function patchRelayNodeConnectionCallbacks() { if (LGraphNode.prototype[RELAY_PATCH_KEY]) { return } LGraphNode.prototype[RELAY_PATCH_KEY] = true const origChange = LGraphNode.prototype.onConnectionsChange LGraphNode.prototype.onConnectionsChange = function (...args) { const ret = origChange?.apply(this, args) if (isPrimitiveRelayNode(this)) { propagatePrimitiveSplitSync(this) } return ret } const origChain = LGraphNode.prototype.onConnectionsChainChange if (origChain) { LGraphNode.prototype.onConnectionsChainChange = function (...args) { const ret = origChain.apply(this, args) if (isPrimitiveRelayNode(this)) { propagatePrimitiveSplitSync(this) } return ret } } } function addPrimitiveOutputBeforeLength(node, name, type) { ensureLengthOutput(node) if (isLengthOutputSlot(node)) { const lengthSlot = node.outputs.pop() node.addOutput(name, type) node.outputs.push(lengthSlot) } else { node.addOutput(name, type) ensureLengthOutput(node) } } function collectExistingOutputSnapshot(splitNode) { const end = countPrimitiveDataOutputs(splitNode) const snapshot = [] for (let i = 0; i < end; i++) { const out = splitNode.outputs?.[i] if (!out?.name) { continue } snapshot.push({ name: out.name, type: out.type }) } return snapshot } function ensurePrimitiveOutputsCount(splitNode, count, defaultType = PRIMITIVE_SLOT_TYPE) { ensureLengthOutput(splitNode) while (countPrimitiveDataOutputs(splitNode) < count) { const idx = countPrimitiveDataOutputs(splitNode) addPrimitiveOutputBeforeLength(splitNode, `primitive_${String(idx + 1).padStart(2, "0")}`, defaultType) } } function normalizeLengthOutput(splitNode) { const lengthOut = splitNode.outputs[splitNode.outputs.length - 1] if (!lengthOut) { return } lengthOut.type = "INT" lengthOut.name = LENGTH_OUTPUT_NAME lengthOut.label = LENGTH_OUTPUT_NAME } function ensureSplitOutputLabels(splitNode) { const end = countPrimitiveDataOutputs(splitNode) for (let i = 0; i < end; i++) { const out = splitNode.outputs?.[i] if (!out?.name) { continue } if (out.label !== out.name) { out.label = out.name } } normalizeLengthOutput(splitNode) } function setGenericPrimitiveOutputs(splitNode) { const end = countPrimitiveDataOutputs(splitNode) for (let i = 0; i < end; i++) { const out = splitNode.outputs[i] if (!out?.name?.includes("_")) { continue } out.type = PRIMITIVE_SLOT_TYPE out.label = out.name } } function applySnapshotPlaceholders(splitNode, snapshot) { if (!snapshot?.length) { return } ensurePrimitiveOutputsCount(splitNode, snapshot.length, PRIMITIVE_SLOT_TYPE) for (let i = 0; i < snapshot.length; i++) { const out = splitNode.outputs?.[i] const snap = snapshot[i] if (!out || !snap) { continue } out.name = snap.name out.type = PRIMITIVE_SLOT_TYPE out.label = snap.name } normalizeLengthOutput(splitNode) } function listLinkedWithResolvedTypes(combineNode) { const linked = [] for (let i = 0; i < (combineNode.inputs ?? []).length; i++) { const inp = combineNode.inputs[i] if (!inp?.name?.startsWith("primitive_") || !inp.link) { continue } linked.push({ name: inp.name, type: resolvePrimitiveSlotType(combineNode, i, inp), }) } return linked } function getCombinedInput(splitNode) { return splitNode.inputs?.find((inp) => inp.name === "combined") } function syncSplitFromCombine(splitNode, combineNode) { ensureLengthOutput(splitNode) const restoring = Date.now() < (splitNode._miscSplitRestoringUntil || 0) const combinedInput = getCombinedInput(splitNode) const hasCombinedLink = !!combinedInput?.link if (!restoring && !combineNode && hasCombinedLink) { ensurePrimitiveOutputsCount(splitNode, 1, PRIMITIVE_SLOT_TYPE) normalizeLengthOutput(splitNode) ensureSplitOutputLabels(splitNode) return } if (restoring && !combineNode) { setGenericPrimitiveOutputs(splitNode) normalizeLengthOutput(splitNode) return } const desired = pickDesiredDuringSync({ restoring, combineNode, hasCombinedLink, cachedDesired: splitNode._miscSplitCachedDesired, linked: combineNode ? listLinkedWithResolvedTypes(combineNode) : [], stored: combineNode ? getStoredPrimitiveSlotTypes(combineNode) : null, }) const desiredCount = desired.length while (countPrimitiveDataOutputs(splitNode) < desiredCount) { const idx = countPrimitiveDataOutputs(splitNode) const slot = desired[idx] if (!slot) { break } addPrimitiveOutputBeforeLength(splitNode, formatTypedOutputName(slot.type, idx), slot.type) } while (countPrimitiveDataOutputs(splitNode) > desiredCount) { const removeIndex = countPrimitiveDataOutputs(splitNode) - 1 const output = splitNode.outputs[removeIndex] if (restoring) { break } if (output?.links?.length) { break } splitNode.removeOutput(removeIndex) } for (let i = 0; i < desiredCount; i++) { const output = splitNode.outputs[i] if (!output) { break } const want = desired[i] if (!want) { continue } const typedName = formatTypedOutputName(want.type, i) if (output.name !== typedName) { output.name = typedName } if (restoring) { output.type = PRIMITIVE_SLOT_TYPE } else { output.type = reconcilePrimitiveSlotType(output.type, want.type) } output.label = output.name } ensureSplitOutputLabels(splitNode) syncNodeSizeToContent(splitNode) } function initSplitRestoreState(node) { node._miscSplitGraphReady = true node._miscSplitRestoringUntil = 0 node._miscSplitCachedOutputs = null node._miscSplitCachedDesired = null } function setupSplitPrimitives(nodeType) { const onDrawForeground = nodeType.prototype.onDrawForeground attachStabilizeHooks(nodeType, { configureStabilize: "none", afterBind(node) { initSplitRestoreState(node) syncPrimitivesLinkSlot(getCombinedInput(node)) }, afterStabilizeOnCreate(node) { scheduleStabilizeRetries(node, [80]) }, onConfigure(node) { initSplitRestoreState(node) node._miscSplitRestoringUntil = Date.now() + MISC_GRAPH_RESTORE_WINDOW_MS node._miscSplitCachedOutputs = collectExistingOutputSnapshot(node) node._miscSplitCachedDesired = node._miscSplitCachedOutputs ? node._miscSplitCachedOutputs.map((s) => ({ name: s.name, type: s.type })) : null applySnapshotPlaceholders(node, node._miscSplitCachedOutputs) node.stabilize() scheduleStabilizeRetries(node, [80, 200, 500, 1200], { doubleRaf: true }) }, onConnectionsChange(node, type, slotIndex, isConnected, _linkInfo, ioSlot) { if (ioSlot && node.outputs?.includes(ioSlot)) { return false } if (type === IoDirection.OUTPUT) { return false } if (!node._miscSplitGraphReady) { return false } const slot = ioSlot ?? node.inputs?.[slotIndex] if (type === IoDirection.INPUT && isConnected && slot?.name === "combined") { const combineNode = findLinkedCombineNode(node, COMBINE_NODE_CLASS) combineNode?.stabilize?.() node.stabilize() scheduleStabilizeRetries(node, [80]) return false } }, onConnectionsChainChange(node) { if (!node._miscSplitGraphReady) { return false } }, }) nodeType.prototype.onDrawForeground = function (ctx) { onDrawForeground?.call(this, ctx) if (this._miscSplitGraphReady) { ensureSplitOutputLabels(this) } } nodeType.prototype.stabilize = function () { if (!this._miscSplitGraphReady) { return } syncPrimitivesLinkSlot(getCombinedInput(this)) const combineNode = findLinkedCombineNode(this, COMBINE_NODE_CLASS) const restoring = Date.now() < (this._miscSplitRestoringUntil || 0) syncSplitFromCombine(this, combineNode) if (restoring && combineNode && this._miscSplitCachedOutputs) { const current = collectExistingOutputSnapshot(this) const cached = this._miscSplitCachedOutputs const differs = current.length !== cached.length || current.some((s, i) => s.name !== cached[i]?.name || s.type !== cached[i]?.type) if (differs) { requestAnimationFrame(() => this.scheduleStabilize(0)) } } syncNodeSizeToContent(this) } } app.registerExtension({ name: "comfyui-misc.SplitPrimitives", init() { patchRelayNodeConnectionCallbacks() }, async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== NODE_CLASS) { return } nodeData.input = nodeData.input ?? {} nodeData.input.required = { ...(nodeData.input.required ?? {}), combined: [PRIMITIVES_TYPE], } nodeData.output = [PRIMITIVE_SLOT_TYPE, "INT"] nodeData.output_name = ["primitive_01", "length"] setupSplitPrimitives(nodeType) }, })