- combined のリスト実行ループ(ForEach / End ForEach)を追加 - ForEach 出力 * と End ForEach value は接続後も常に * - attachStabilizeHooks / pinWildcardSlot / scheduleStabilizeRetries で JS を整理 - Prettier(JS は trailing comma なし)、README 開発セクションを整理 Co-authored-by: Cursor <cursoragent@cursor.com>
368 lines
10 KiB
JavaScript
368 lines
10 KiB
JavaScript
/** 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);
|
|
}
|
|
});
|