Add custom nodes, Civitai loras (LFS), and vast.ai setup script
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>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,24 @@
import { app } from "../../../scripts/app.js";
import { NodeTypesString } from "../constants.js";
import { reduceNodesDepthFirst } from "../utils.js";
const SHORTCUT_DEFAULTS = "1234567890abcdefghijklmnopqrstuvwxyz".split("");
class BookmarksService {
getCurrentBookmarks() {
return reduceNodesDepthFirst(app.graph.nodes, (n, acc) => {
if (n.type === NodeTypesString.BOOKMARK) {
acc.push(n);
}
}, []).sort((a, b) => a.title.localeCompare(b.title));
}
getExistingShortcuts() {
const bookmarkNodes = this.getCurrentBookmarks();
const usedShortcuts = new Set(bookmarkNodes.map((n) => n.shortcutKey));
return usedShortcuts;
}
getNextShortcut() {
var _a;
const existingShortcuts = this.getExistingShortcuts();
return (_a = SHORTCUT_DEFAULTS.find((char) => !existingShortcuts.has(char))) !== null && _a !== void 0 ? _a : "1";
}
}
export const SERVICE = new BookmarksService();

View File

@@ -0,0 +1,28 @@
import { rgthreeConfig } from "../../../rgthree/config.js";
import { getObjectValue, setObjectValue } from "../../../rgthree/common/shared_utils.js";
import { rgthreeApi } from "../../../rgthree/common/rgthree_api.js";
class ConfigService extends EventTarget {
getConfigValue(key, def) {
return getObjectValue(rgthreeConfig, key, def);
}
getFeatureValue(key, def) {
key = "features." + key.replace(/^features\./, "");
return getObjectValue(rgthreeConfig, key, def);
}
async setConfigValues(changed) {
const body = new FormData();
body.append("json", JSON.stringify(changed));
const response = await rgthreeApi.fetchJson("/config", { method: "POST", body });
if (response.status === "ok") {
for (const [key, value] of Object.entries(changed)) {
setObjectValue(rgthreeConfig, key, value);
this.dispatchEvent(new CustomEvent("config-change", { detail: { key, value } }));
}
}
else {
return false;
}
return true;
}
}
export const SERVICE = new ConfigService();

View File

@@ -0,0 +1,51 @@
import { getConnectedOutputNodesAndFilterPassThroughs } from "../utils.js";
export let SERVICE;
const OWNED_PREFIX = "+";
const REGEX_PREFIX = /^[\+⚠️]\s*/;
const REGEX_EMPTY_INPUT = /^\+\s*$/;
export function stripContextInputPrefixes(name) {
return name.replace(REGEX_PREFIX, "");
}
export function getContextOutputName(inputName) {
if (inputName === "base_ctx")
return "CONTEXT";
return stripContextInputPrefixes(inputName).toUpperCase();
}
export var InputMutationOperation;
(function (InputMutationOperation) {
InputMutationOperation[InputMutationOperation["UNKNOWN"] = 0] = "UNKNOWN";
InputMutationOperation[InputMutationOperation["ADDED"] = 1] = "ADDED";
InputMutationOperation[InputMutationOperation["REMOVED"] = 2] = "REMOVED";
InputMutationOperation[InputMutationOperation["RENAMED"] = 3] = "RENAMED";
})(InputMutationOperation || (InputMutationOperation = {}));
export class ContextService {
constructor() {
if (SERVICE) {
throw new Error("ContextService was already instantiated.");
}
}
onInputChanges(node, mutation) {
const childCtxs = getConnectedOutputNodesAndFilterPassThroughs(node, node, 0);
for (const childCtx of childCtxs) {
childCtx.handleUpstreamMutation(mutation);
}
}
getDynamicContextInputsData(node) {
return node
.getContextInputsList()
.map((input, index) => ({
name: stripContextInputPrefixes(input.name),
type: String(input.type),
index,
}))
.filter((i) => i.type !== "*");
}
getDynamicContextOutputsData(node) {
return node.outputs.map((output, index) => ({
name: stripContextInputPrefixes(output.name),
type: String(output.type),
index,
}));
}
}
SERVICE = new ContextService();

View File

@@ -0,0 +1,163 @@
import { app } from "../../../scripts/app.js";
import { getGraphDependantNodeKey, getGroupNodes, reduceNodesDepthFirst } from "../utils.js";
class FastGroupsService {
constructor() {
this.msThreshold = 400;
this.msLastUnsorted = 0;
this.msLastAlpha = 0;
this.msLastPosition = 0;
this.groupsUnsorted = [];
this.groupsSortedAlpha = [];
this.groupsSortedPosition = [];
this.fastGroupNodes = [];
this.runScheduledForMs = null;
this.runScheduleTimeout = null;
this.runScheduleAnimation = null;
this.cachedNodeBoundings = null;
}
addFastGroupNode(node) {
this.fastGroupNodes.push(node);
this.scheduleRun(8);
}
removeFastGroupNode(node) {
var _a;
const index = this.fastGroupNodes.indexOf(node);
if (index > -1) {
this.fastGroupNodes.splice(index, 1);
}
if (!((_a = this.fastGroupNodes) === null || _a === void 0 ? void 0 : _a.length)) {
this.clearScheduledRun();
this.groupsUnsorted = [];
this.groupsSortedAlpha = [];
this.groupsSortedPosition = [];
}
}
run() {
if (!this.runScheduledForMs) {
return;
}
for (const node of this.fastGroupNodes) {
node.refreshWidgets();
}
this.clearScheduledRun();
this.scheduleRun();
}
scheduleRun(ms = 500) {
if (this.runScheduledForMs && ms < this.runScheduledForMs) {
this.clearScheduledRun();
}
if (!this.runScheduledForMs && this.fastGroupNodes.length) {
this.runScheduledForMs = ms;
this.runScheduleTimeout = setTimeout(() => {
this.runScheduleAnimation = requestAnimationFrame(() => this.run());
}, ms);
}
}
clearScheduledRun() {
this.runScheduleTimeout && clearTimeout(this.runScheduleTimeout);
this.runScheduleAnimation && cancelAnimationFrame(this.runScheduleAnimation);
this.runScheduleTimeout = null;
this.runScheduleAnimation = null;
this.runScheduledForMs = null;
}
getBoundingsForAllNodes() {
if (!this.cachedNodeBoundings) {
this.cachedNodeBoundings = reduceNodesDepthFirst(app.graph._nodes, (node, acc) => {
var _a, _b;
let bounds = node.getBounding();
if (bounds[0] === 0 && bounds[1] === 0 && bounds[2] === 0 && bounds[3] === 0) {
const ctx = (_b = (_a = node.graph) === null || _a === void 0 ? void 0 : _a.primaryCanvas) === null || _b === void 0 ? void 0 : _b.canvas.getContext("2d");
if (ctx) {
node.updateArea(ctx);
bounds = node.getBounding();
}
}
acc[getGraphDependantNodeKey(node)] = bounds;
}, {});
setTimeout(() => {
this.cachedNodeBoundings = null;
}, 50);
}
return this.cachedNodeBoundings;
}
recomputeInsideNodesForGroup(group) {
if (app.canvas.isDragging)
return;
const cachedBoundings = this.getBoundingsForAllNodes();
const nodes = group.graph.nodes;
group._children.clear();
group.nodes.length = 0;
for (const node of nodes) {
const nodeBounding = cachedBoundings[getGraphDependantNodeKey(node)];
const nodeCenter = nodeBounding &&
[nodeBounding[0] + nodeBounding[2] * 0.5, nodeBounding[1] + nodeBounding[3] * 0.5];
if (nodeCenter) {
const grouBounds = group._bounding;
if (nodeCenter[0] >= grouBounds[0] &&
nodeCenter[0] < grouBounds[0] + grouBounds[2] &&
nodeCenter[1] >= grouBounds[1] &&
nodeCenter[1] < grouBounds[1] + grouBounds[3]) {
group._children.add(node);
group.nodes.push(node);
}
}
}
}
getGroupsUnsorted(now) {
var _a, _b, _c;
const canvas = app.canvas;
const graph = (_a = canvas.getCurrentGraph()) !== null && _a !== void 0 ? _a : app.graph;
if (!canvas.selected_group_moving &&
(!this.groupsUnsorted.length || now - this.msLastUnsorted > this.msThreshold)) {
this.groupsUnsorted = [...graph._groups];
const subgraphs = (_b = graph.subgraphs) === null || _b === void 0 ? void 0 : _b.values();
if (subgraphs) {
let s;
while ((s = subgraphs.next().value))
this.groupsUnsorted.push(...((_c = s.groups) !== null && _c !== void 0 ? _c : []));
}
for (const group of this.groupsUnsorted) {
this.recomputeInsideNodesForGroup(group);
group.rgthree_hasAnyActiveNode = getGroupNodes(group).some((n) => n.mode === LiteGraph.ALWAYS);
}
this.msLastUnsorted = now;
}
return this.groupsUnsorted;
}
getGroupsAlpha(now) {
if (!this.groupsSortedAlpha.length || now - this.msLastAlpha > this.msThreshold) {
this.groupsSortedAlpha = [...this.getGroupsUnsorted(now)].sort((a, b) => {
return a.title.localeCompare(b.title);
});
this.msLastAlpha = now;
}
return this.groupsSortedAlpha;
}
getGroupsPosition(now) {
if (!this.groupsSortedPosition.length || now - this.msLastPosition > this.msThreshold) {
this.groupsSortedPosition = [...this.getGroupsUnsorted(now)].sort((a, b) => {
const aY = Math.floor(a._pos[1] / 30);
const bY = Math.floor(b._pos[1] / 30);
if (aY == bY) {
const aX = Math.floor(a._pos[0] / 30);
const bX = Math.floor(b._pos[0] / 30);
return aX - bX;
}
return aY - bY;
});
this.msLastPosition = now;
}
return this.groupsSortedPosition;
}
getGroups(sort) {
const now = +new Date();
if (sort === "alphanumeric") {
return this.getGroupsAlpha(now);
}
if (sort === "position") {
return this.getGroupsPosition(now);
}
return this.getGroupsUnsorted(now);
}
}
export const SERVICE = new FastGroupsService();

View File

@@ -0,0 +1,115 @@
class KeyEventService extends EventTarget {
constructor() {
var _a, _b, _c;
super();
this.downKeys = {};
this.shiftDownKeys = {};
this.ctrlKey = false;
this.altKey = false;
this.metaKey = false;
this.shiftKey = false;
this.isMac = !!(((_a = navigator.platform) === null || _a === void 0 ? void 0 : _a.toLocaleUpperCase().startsWith("MAC")) ||
((_c = (_b = navigator.userAgentData) === null || _b === void 0 ? void 0 : _b.platform) === null || _c === void 0 ? void 0 : _c.toLocaleUpperCase().startsWith("MAC")));
this.initialize();
}
initialize() {
const that = this;
const processKey = LGraphCanvas.prototype.processKey;
LGraphCanvas.prototype.processKey = function (e) {
if (e.type === "keydown" || e.type === "keyup") {
that.handleKeyDownOrUp(e);
}
return processKey.apply(this, [...arguments]);
};
window.addEventListener("keydown", (e) => {
that.handleKeyDownOrUp(e);
});
window.addEventListener("keyup", (e) => {
that.handleKeyDownOrUp(e);
});
document.addEventListener("visibilitychange", (e) => {
this.clearKeydowns();
});
window.addEventListener("blur", (e) => {
this.clearKeydowns();
});
}
handleKeyDownOrUp(e) {
const key = e.key.toLocaleUpperCase();
if ((e.type === 'keydown' && this.downKeys[key] === true)
|| (e.type === 'keyup' && this.downKeys[key] === undefined)) {
return;
}
this.ctrlKey = !!e.ctrlKey;
this.altKey = !!e.altKey;
this.metaKey = !!e.metaKey;
this.shiftKey = !!e.shiftKey;
if (e.type === "keydown") {
this.downKeys[key] = true;
this.dispatchCustomEvent("keydown", { originalEvent: e });
if (this.shiftKey && key !== 'SHIFT') {
this.shiftDownKeys[key] = true;
}
}
else if (e.type === "keyup") {
if (key === "META" && this.isMac) {
this.clearKeydowns();
}
else {
delete this.downKeys[key];
}
if (key === 'SHIFT') {
for (const key in this.shiftDownKeys) {
delete this.downKeys[key];
delete this.shiftDownKeys[key];
}
}
this.dispatchCustomEvent("keyup", { originalEvent: e });
}
}
clearKeydowns() {
this.ctrlKey = false;
this.altKey = false;
this.metaKey = false;
this.shiftKey = false;
for (const key in this.downKeys)
delete this.downKeys[key];
}
dispatchCustomEvent(event, detail) {
if (detail != null) {
return this.dispatchEvent(new CustomEvent(event, { detail }));
}
return this.dispatchEvent(new CustomEvent(event));
}
getKeysFromShortcut(shortcut) {
let keys;
if (typeof shortcut === "string") {
shortcut = shortcut.replace(/\s/g, "");
shortcut = shortcut.replace(/^\+/, "__PLUS__").replace(/\+\+/, "+__PLUS__");
keys = shortcut.split("+").map((i) => i.replace("__PLUS__", "+"));
}
else {
keys = [...shortcut];
}
return keys.map((k) => k.toLocaleUpperCase());
}
areAllKeysDown(keys) {
keys = this.getKeysFromShortcut(keys);
return keys.every((k) => {
return this.downKeys[k];
});
}
areOnlyKeysDown(keys, alsoAllowShift = false) {
keys = this.getKeysFromShortcut(keys);
const allKeysDown = this.areAllKeysDown(keys);
const downKeysLength = Object.values(this.downKeys).length;
if (allKeysDown && keys.length === downKeysLength) {
return true;
}
if (alsoAllowShift && !keys.includes("SHIFT") && keys.length === downKeysLength - 1) {
return allKeysDown && this.areAllKeysDown(["SHIFT"]);
}
return false;
}
}
export const SERVICE = new KeyEventService();