Files
wraith-executioner-year-2-c…/js/split_primitives.js
T
wraith_executioner_year_2andCursor eb7bcaa385 ✨ ForEach / End ForEach と stabilize 共通化・Prettier 導入
- combined のリスト実行ループ(ForEach / End ForEach)を追加
- ForEach 出力 * と End ForEach value は接続後も常に *
- attachStabilizeHooks / pinWildcardSlot / scheduleStabilizeRetries で JS を整理
- Prettier(JS は trailing comma なし)、README 開発セクションを整理

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 03:11:45 +09:00

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);
}
});