From 1e8bd05938bd06f42a4ff2ba1bc956376e4944a1 Mon Sep 17 00:00:00 2001 From: jessesep Date: Fri, 5 Dec 2025 02:25:53 +0100 Subject: [PATCH] Hybrid approach: Python stubs + JS virtual override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Python defines nodes with ANY type so they appear in menus/drag connections - JS overrides with isVirtualNode for actual graph-redirect behavior - SimpleSet accepts any input, passes through - SimpleGet dropdown shows available setter names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- __init__.py | 69 +++++++++-- web/simplevariables.js | 257 ++++++++++++++--------------------------- 2 files changed, 151 insertions(+), 175 deletions(-) diff --git a/__init__.py b/__init__.py index 2ec4cd5..eee46e6 100644 --- a/__init__.py +++ b/__init__.py @@ -1,13 +1,68 @@ """ -SimpleVariables - Pure frontend Set/Get Variable nodes for ComfyUI - -These nodes are implemented entirely in JavaScript (web/simplevariables.js). -They work by redirecting graph connections - no Python execution needed. +SimpleVariables - Set/Get Variable nodes for ComfyUI +JavaScript handles the actual logic (virtual nodes), Python stubs make them appear in menus. """ -# No Python nodes - everything is in JavaScript -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} + +class AnyType(str): + """Matches any type for ComfyUI type checking""" + def __eq__(self, _): + return True + def __ne__(self, _): + return False + + +ANY = AnyType("*") + + +class SimpleSet: + """Set a variable by name""" + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "name": ("STRING", {"default": ""}), + }, + "optional": { + "value": (ANY,), + } + } + RETURN_TYPES = (ANY,) + RETURN_NAMES = ("value",) + FUNCTION = "passthrough" + CATEGORY = "SimpleVariables" + + def passthrough(self, name, value=None): + return (value,) + + +class SimpleGet: + """Get a variable by name""" + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "name": ("STRING", {"default": ""}), + } + } + RETURN_TYPES = (ANY,) + RETURN_NAMES = ("value",) + FUNCTION = "get" + CATEGORY = "SimpleVariables" + + def get(self, name): + return (None,) + + +NODE_CLASS_MAPPINGS = { + "SimpleSet": SimpleSet, + "SimpleGet": SimpleGet, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SimpleSet": "SimpleSet", + "SimpleGet": "SimpleGet", +} WEB_DIRECTORY = "./web" __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/web/simplevariables.js b/web/simplevariables.js index 8f22cd5..ed40966 100644 --- a/web/simplevariables.js +++ b/web/simplevariables.js @@ -1,200 +1,121 @@ import { app } from "/scripts/app.js"; -// Pure frontend Set/Get Variable nodes - based on KJNodes pattern -const LGraphNode = LiteGraph.LGraphNode; - -// Shared storage for variables (persists during session) -const variableStorage = {}; - +// Override Python nodes with virtual node behavior app.registerExtension({ - name: "SimpleVariables.Set", - registerCustomNodes() { - class SimpleSetNode extends LGraphNode { - constructor(title) { - super(title); + name: "SimpleVariables", + + beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "SimpleSet") { + // Make it a virtual node - passes data through without Python execution + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + if (onNodeCreated) onNodeCreated.apply(this, arguments); + const node = this; + this.isVirtualNode = true; - this.properties = { variableName: "" }; - - this.addWidget("text", "name", "", (value) => { - node.properties.variableName = value; - node.title = value ? `Set: ${value}` : "SimpleSet"; - }); - - this.addInput("value", "*"); - this.addOutput("value", "*"); + // Update title when name changes + const nameWidget = this.widgets?.find(w => w.name === "name"); + if (nameWidget) { + const origCallback = nameWidget.callback; + nameWidget.callback = function(value) { + if (origCallback) origCallback.apply(this, arguments); + node.title = value ? `Set: ${value}` : "SimpleSet"; + }; + } + // Update slot types when connection changes this.onConnectionsChange = function(slotType, slot, isConnect, linkInfo) { - if (slotType === 1 && node.inputs[0]) { - if (isConnect) { - const link = node.graph?.links?.[node.inputs[0].link]; + if (slotType === 1 && node.inputs?.[0]) { + if (isConnect && node.graph) { + const link = node.graph.links?.[node.inputs[0].link]; if (link) { const originNode = node.graph._nodes_by_id[link.origin_id]; if (originNode?.outputs?.[link.origin_slot]) { const type = originNode.outputs[link.origin_slot].type; node.inputs[0].type = type; node.inputs[0].name = type; - node.outputs[0].type = type; - node.outputs[0].name = type; + if (node.outputs?.[0]) { + node.outputs[0].type = type; + node.outputs[0].name = type; + } } } } else { node.inputs[0].type = "*"; node.inputs[0].name = "value"; - node.outputs[0].type = "*"; - node.outputs[0].name = "value"; + if (node.outputs?.[0]) { + node.outputs[0].type = "*"; + node.outputs[0].name = "value"; + } } } }; - - this.isVirtualNode = true; - } + }; } - LiteGraph.registerNodeType("SimpleSet", Object.assign(SimpleSetNode, { - title: "SimpleSet", - })); - SimpleSetNode.category = "SimpleVariables"; - } -}); + if (nodeData.name === "SimpleGet") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + if (onNodeCreated) onNodeCreated.apply(this, arguments); -app.registerExtension({ - name: "SimpleVariables.Get", - registerCustomNodes() { - class SimpleGetNode extends LGraphNode { - constructor(title) { - super(title); const node = this; - - this.properties = { variableName: "" }; - - this.addWidget("combo", "name", "", (value) => { - node.properties.variableName = value; - node.title = value ? `Get: ${value}` : "SimpleGet"; - node.syncType(); - }, { - values: () => { - if (!node.graph?._nodes) return [""]; - const names = node.graph._nodes - .filter(n => n.type === "SimpleSet") - .map(n => n.properties?.variableName || n.widgets?.[0]?.value || "") - .filter(n => n !== ""); - return ["", ...names.sort()]; - } - }); - - this.addOutput("value", "*"); this.isVirtualNode = true; - } - syncType() { - const setter = this.findSetter(); - if (setter?.inputs?.[0]) { - this.outputs[0].type = setter.inputs[0].type || "*"; - this.outputs[0].name = setter.inputs[0].name || "value"; - } else { - this.outputs[0].type = "*"; - this.outputs[0].name = "value"; - } - } - - findSetter() { - if (!this.graph || !this.properties.variableName) return null; - return this.graph._nodes.find(n => - n.type === "SimpleSet" && - (n.properties?.variableName === this.properties.variableName || - n.widgets?.[0]?.value === this.properties.variableName) - ); - } - - getInputLink(slot) { - const setter = this.findSetter(); - if (setter?.inputs?.[0]?.link != null) { - return this.graph.links[setter.inputs[0].link]; - } - return null; - } - } - - LiteGraph.registerNodeType("SimpleGet", Object.assign(SimpleGetNode, { - title: "SimpleGet", - })); - SimpleGetNode.category = "SimpleVariables"; - } -}); - -app.registerExtension({ - name: "SimpleVariables.List", - registerCustomNodes() { - class SimpleListNode extends LGraphNode { - constructor(title) { - super(title); - const node = this; - - this.addWidget("button", "Refresh", null, () => { - node.updateList(); - }); - - this.addOutput("list", "STRING"); - this.isVirtualNode = true; - this.size = [200, 100]; - } - - updateList() { - if (!this.graph?._nodes) return; - const setters = this.graph._nodes.filter(n => n.type === "SimpleSet"); - const names = setters - .map(n => n.properties?.variableName || n.widgets?.[0]?.value || "") - .filter(n => n !== ""); - - // Update title to show variables - if (names.length === 0) { - this.title = "SimpleList (empty)"; - } else { - this.title = "SimpleList:\n" + names.join("\n"); - } - } - - onAdded() { - this.updateList(); - } - } - - LiteGraph.registerNodeType("SimpleList", Object.assign(SimpleListNode, { - title: "SimpleList", - })); - SimpleListNode.category = "SimpleVariables"; - } -}); - -app.registerExtension({ - name: "SimpleVariables.Clear", - registerCustomNodes() { - class SimpleClearNode extends LGraphNode { - constructor(title) { - super(title); - const node = this; - - this.addWidget("button", "Clear All Variables", null, () => { - if (!node.graph?._nodes) return; - const setters = node.graph._nodes.filter(n => n.type === "SimpleSet"); - let count = 0; - setters.forEach(setter => { - if (setter.inputs?.[0]?.link != null) { - node.graph.removeLink(setter.inputs[0].link); - count++; + // Replace name widget with combo that shows available setters + const nameWidget = this.widgets?.find(w => w.name === "name"); + if (nameWidget) { + // Convert to combo widget + nameWidget.type = "combo"; + nameWidget.options = { + values: () => { + if (!node.graph?._nodes) return [""]; + const names = node.graph._nodes + .filter(n => n.type === "SimpleSet") + .map(n => n.widgets?.find(w => w.name === "name")?.value || "") + .filter(n => n !== ""); + return ["", ...names.sort()]; } - }); - node.title = `SimpleClear (cleared ${count})`; - }); + }; - this.isVirtualNode = true; - } + const origCallback = nameWidget.callback; + nameWidget.callback = function(value) { + if (origCallback) origCallback.apply(this, arguments); + node.title = value ? `Get: ${value}` : "SimpleGet"; + node.syncWithSetter(); + }; + } + + this.syncWithSetter = function() { + const setter = this.findSetter(); + if (setter?.inputs?.[0] && this.outputs?.[0]) { + this.outputs[0].type = setter.inputs[0].type || "*"; + this.outputs[0].name = setter.inputs[0].name || "value"; + } else if (this.outputs?.[0]) { + this.outputs[0].type = "*"; + this.outputs[0].name = "value"; + } + }; + + this.findSetter = function() { + if (!this.graph) return null; + const name = this.widgets?.find(w => w.name === "name")?.value; + if (!name) return null; + return this.graph._nodes.find(n => + n.type === "SimpleSet" && + n.widgets?.find(w => w.name === "name")?.value === name + ); + }; + + // Override getInputLink to get data from setter + this.getInputLink = function(slot) { + const setter = this.findSetter(); + if (setter?.inputs?.[0]?.link != null) { + return this.graph.links[setter.inputs[0].link]; + } + return null; + }; + }; } - - LiteGraph.registerNodeType("SimpleClear", Object.assign(SimpleClearNode, { - title: "SimpleClear", - })); - SimpleClearNode.category = "SimpleVariables"; } });