Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
245 lines
5.6 KiB
JavaScript
245 lines
5.6 KiB
JavaScript
// @ts-check
|
|
// @ts-ignore
|
|
import { ComfyWidgets } from "../../../../scripts/widgets.js";
|
|
// @ts-ignore
|
|
import { api } from "../../../../scripts/api.js";
|
|
// @ts-ignore
|
|
import { app } from "../../../../scripts/app.js";
|
|
|
|
const PathHelper = {
|
|
get(obj, path) {
|
|
if (typeof path !== "string") {
|
|
// Hardcoded value
|
|
return path;
|
|
}
|
|
|
|
if (path[0] === '"' && path[path.length - 1] === '"') {
|
|
// Hardcoded string
|
|
return JSON.parse(path);
|
|
}
|
|
|
|
// Evaluate the path
|
|
path = path.split(".").filter(Boolean);
|
|
for (const p of path) {
|
|
const k = isNaN(+p) ? p : +p;
|
|
obj = obj[k];
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
set(obj, path, value) {
|
|
// https://stackoverflow.com/a/54733755
|
|
if (Object(obj) !== obj) return obj; // When obj is not an object
|
|
// If not yet an array, get the keys from the string-path
|
|
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
|
path.slice(0, -1).reduce(
|
|
(
|
|
a,
|
|
c,
|
|
i // Iterate all of them except the last one
|
|
) =>
|
|
Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
|
? // Yes: then follow that path
|
|
a[c]
|
|
: // No: create the key. Is the next key a potential array-index?
|
|
(a[c] =
|
|
Math.abs(path[i + 1]) >> 0 === +path[i + 1]
|
|
? [] // Yes: assign a new array object
|
|
: {}), // No: assign a new plain object
|
|
obj
|
|
)[path[path.length - 1]] = value; // Finally assign the value to the last key
|
|
return obj; // Return the top-level object to allow chaining
|
|
},
|
|
};
|
|
|
|
/***
|
|
@typedef { {
|
|
left: string;
|
|
op: "eq" | "ne",
|
|
right: string
|
|
} } IfCondition
|
|
|
|
@typedef { {
|
|
type: "if",
|
|
condition: Array<IfCondition>,
|
|
true?: Array<BindingCallback>,
|
|
false?: Array<BindingCallback>
|
|
} } IfCallback
|
|
|
|
@typedef { {
|
|
type: "fetch",
|
|
url: string,
|
|
then: Array<BindingCallback>
|
|
} } FetchCallback
|
|
|
|
@typedef { {
|
|
type: "set",
|
|
target: string,
|
|
value: string
|
|
} } SetCallback
|
|
|
|
@typedef { {
|
|
type: "validate-combo",
|
|
} } ValidateComboCallback
|
|
|
|
@typedef { IfCallback | FetchCallback | SetCallback | ValidateComboCallback } BindingCallback
|
|
|
|
@typedef { {
|
|
source: string,
|
|
callback: Array<BindingCallback>
|
|
} } Binding
|
|
***/
|
|
|
|
/**
|
|
* @param {IfCondition} condition
|
|
*/
|
|
function evaluateCondition(condition, state) {
|
|
const left = PathHelper.get(state, condition.left);
|
|
const right = PathHelper.get(state, condition.right);
|
|
|
|
let r;
|
|
if (condition.op === "eq") {
|
|
r = left === right;
|
|
} else {
|
|
r = left !== right;
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
/**
|
|
* @type { Record<BindingCallback["type"], (cb: any, state: Record<string, any>) => Promise<void>> }
|
|
*/
|
|
const callbacks = {
|
|
/**
|
|
* @param {IfCallback} cb
|
|
*/
|
|
async if(cb, state) {
|
|
// For now only support ANDs
|
|
let success = true;
|
|
for (const condition of cb.condition) {
|
|
const r = evaluateCondition(condition, state);
|
|
if (!r) {
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (const m of cb[success + ""] ?? []) {
|
|
await invokeCallback(m, state);
|
|
}
|
|
},
|
|
/**
|
|
* @param {FetchCallback} cb
|
|
*/
|
|
async fetch(cb, state) {
|
|
const url = cb.url.replace(/\{([^\}]+)\}/g, (m, v) => {
|
|
return PathHelper.get(state, v);
|
|
});
|
|
const res = await (await api.fetchApi(url)).json();
|
|
state["$result"] = res;
|
|
for (const m of cb.then) {
|
|
await invokeCallback(m, state);
|
|
}
|
|
},
|
|
/**
|
|
* @param {SetCallback} cb
|
|
*/
|
|
async set(cb, state) {
|
|
const value = PathHelper.get(state, cb.value);
|
|
PathHelper.set(state, cb.target, value);
|
|
},
|
|
async "validate-combo"(cb, state) {
|
|
const w = state["$this"];
|
|
const valid = w.options.values.includes(w.value);
|
|
if (!valid) {
|
|
w.value = w.options.values[0];
|
|
}
|
|
},
|
|
};
|
|
|
|
async function invokeCallback(callback, state) {
|
|
if (callback.type in callbacks) {
|
|
// @ts-ignore
|
|
await callbacks[callback.type](callback, state);
|
|
} else {
|
|
console.warn(
|
|
"%c[🐍 pysssss]",
|
|
"color: limegreen",
|
|
`[binding ${state.$node.comfyClass}.${state.$this.name}]`,
|
|
"unsupported binding callback type:",
|
|
callback.type
|
|
);
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "pysssss.Binding",
|
|
beforeRegisterNodeDef(node, nodeData) {
|
|
const hasBinding = (v) => {
|
|
if (!v) return false;
|
|
return Object.values(v).find((c) => c[1]?.["pysssss.binding"]);
|
|
};
|
|
const inputs = { ...nodeData.input?.required, ...nodeData.input?.optional };
|
|
if (hasBinding(inputs)) {
|
|
const onAdded = node.prototype.onAdded;
|
|
node.prototype.onAdded = function () {
|
|
const r = onAdded?.apply(this, arguments);
|
|
|
|
for (const widget of this.widgets || []) {
|
|
const bindings = inputs[widget.name][1]?.["pysssss.binding"];
|
|
if (!bindings) continue;
|
|
|
|
for (const binding of bindings) {
|
|
/**
|
|
* @type {import("../../../../../web/types/litegraph.d.ts").IWidget}
|
|
*/
|
|
const source = this.widgets.find((w) => w.name === binding.source);
|
|
if (!source) {
|
|
console.warn(
|
|
"%c[🐍 pysssss]",
|
|
"color: limegreen",
|
|
`[binding ${node.comfyClass}.${widget.name}]`,
|
|
"unable to find source binding widget:",
|
|
binding.source,
|
|
binding
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let lastValue;
|
|
async function valueChanged() {
|
|
const state = {
|
|
$this: widget,
|
|
$source: source,
|
|
$node: node,
|
|
};
|
|
|
|
for (const callback of binding.callback) {
|
|
await invokeCallback(callback, state);
|
|
}
|
|
|
|
app.graph.setDirtyCanvas(true, false);
|
|
}
|
|
|
|
const cb = source.callback;
|
|
source.callback = function () {
|
|
const v = cb?.apply(this, arguments) ?? source.value;
|
|
if (v !== lastValue) {
|
|
lastValue = v;
|
|
valueChanged();
|
|
}
|
|
return v;
|
|
};
|
|
|
|
lastValue = source.value;
|
|
valueChanged();
|
|
}
|
|
}
|
|
|
|
return r;
|
|
};
|
|
}
|
|
},
|
|
});
|