From 61a2c58a3ab3483a00dbaa871fb3b696b2b80efd Mon Sep 17 00:00:00 2001 From: jessesep Date: Fri, 5 Dec 2025 02:30:24 +0100 Subject: [PATCH] Back to Python nodes with preview + text input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SimpleSet/Get use text input (type any name) - Preview shows on node after execution - SimpleGet has trigger input for execution order - SimpleList shows all variables - SimpleClear clears all 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- __init__.py | 119 +++++++++++++++++++++++++++++++++---- web/simplevariables.js | 131 ++++++++++++----------------------------- 2 files changed, 146 insertions(+), 104 deletions(-) diff --git a/__init__.py b/__init__.py index eee46e6..95fbe0d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ """ SimpleVariables - Set/Get Variable nodes for ComfyUI -JavaScript handles the actual logic (virtual nodes), Python stubs make them appear in menus. """ +import time class AnyType(str): @@ -14,14 +14,17 @@ class AnyType(str): ANY = AnyType("*") +# Global storage +_variables = {} + class SimpleSet: - """Set a variable by name""" + """Set a variable by name - stores value and passes through""" @classmethod def INPUT_TYPES(cls): return { "required": { - "name": ("STRING", {"default": ""}), + "name": ("STRING", {"default": "var"}), }, "optional": { "value": (ANY,), @@ -29,11 +32,30 @@ class SimpleSet: } RETURN_TYPES = (ANY,) RETURN_NAMES = ("value",) - FUNCTION = "passthrough" + FUNCTION = "set_var" CATEGORY = "SimpleVariables" + OUTPUT_NODE = True - def passthrough(self, name, value=None): - return (value,) + @classmethod + def IS_CHANGED(cls, name, value=None): + return time.time() + + def set_var(self, name, value=None): + _variables[name] = value + + # Generate preview + if value is None: + preview = f"{name} = None" + elif hasattr(value, 'shape'): + preview = f"{name} = {type(value).__name__} {list(value.shape)}" + elif isinstance(value, (int, float, bool)): + preview = f"{name} = {value}" + elif isinstance(value, str): + preview = f'{name} = "{value[:30]}..."' if len(value) > 30 else f'{name} = "{value}"' + else: + preview = f"{name} = {type(value).__name__}" + + return {"ui": {"text": [preview]}, "result": (value,)} class SimpleGet: @@ -42,26 +64,103 @@ class SimpleGet: def INPUT_TYPES(cls): return { "required": { - "name": ("STRING", {"default": ""}), + "name": ("STRING", {"default": "var"}), + }, + "optional": { + "trigger": (ANY,), # Connect to SimpleSet output for execution order } } RETURN_TYPES = (ANY,) RETURN_NAMES = ("value",) - FUNCTION = "get" + FUNCTION = "get_var" CATEGORY = "SimpleVariables" + OUTPUT_NODE = True - def get(self, name): - return (None,) + @classmethod + def IS_CHANGED(cls, **kwargs): + return time.time() + + def get_var(self, name, trigger=None): + value = _variables.get(name) + + # Generate preview + if name not in _variables: + preview = f"{name} = NOT SET" + elif value is None: + preview = f"{name} = None" + elif hasattr(value, 'shape'): + preview = f"{name} = {type(value).__name__} {list(value.shape)}" + elif isinstance(value, (int, float, bool)): + preview = f"{name} = {value}" + elif isinstance(value, str): + preview = f'{name} = "{value[:30]}..."' if len(value) > 30 else f'{name} = "{value}"' + else: + preview = f"{name} = {type(value).__name__}" + + return {"ui": {"text": [preview]}, "result": (value,)} + + +class SimpleList: + """List all stored variables""" + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("list",) + FUNCTION = "list_vars" + CATEGORY = "SimpleVariables" + OUTPUT_NODE = True + + @classmethod + def IS_CHANGED(cls): + return time.time() + + def list_vars(self): + if not _variables: + text = "(no variables)" + else: + lines = [f"{k} = {type(v).__name__}" for k, v in _variables.items()] + text = "\n".join(lines) + return {"ui": {"text": [text]}, "result": (text,)} + + +class SimpleClear: + """Clear all stored variables""" + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "clear": ("BOOLEAN", {"default": False}), + } + } + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("status",) + FUNCTION = "clear_vars" + CATEGORY = "SimpleVariables" + OUTPUT_NODE = True + + def clear_vars(self, clear): + if clear: + count = len(_variables) + _variables.clear() + status = f"Cleared {count} variable(s)" + else: + status = "Set clear=True to clear" + return {"ui": {"text": [status]}, "result": (status,)} NODE_CLASS_MAPPINGS = { "SimpleSet": SimpleSet, "SimpleGet": SimpleGet, + "SimpleList": SimpleList, + "SimpleClear": SimpleClear, } NODE_DISPLAY_NAME_MAPPINGS = { "SimpleSet": "SimpleSet", "SimpleGet": "SimpleGet", + "SimpleList": "SimpleList", + "SimpleClear": "SimpleClear", } WEB_DIRECTORY = "./web" diff --git a/web/simplevariables.js b/web/simplevariables.js index ed40966..f5f4b11 100644 --- a/web/simplevariables.js +++ b/web/simplevariables.js @@ -1,120 +1,63 @@ import { app } from "/scripts/app.js"; +import { ComfyWidgets } from "/scripts/widgets.js"; -// Override Python nodes with virtual node behavior +// Display text on nodes when they execute app.registerExtension({ 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); + beforeRegisterNodeDef(nodeType, nodeData, appInstance) { + if (["SimpleSet", "SimpleGet", "SimpleList", "SimpleClear"].includes(nodeData.name)) { - const node = this; - this.isVirtualNode = true; + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function(message) { + if (onExecuted) onExecuted.apply(this, arguments); - // 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"; - }; - } + if (message?.text) { + // Find or create display widget + let widget = this.widgets?.find(w => w.name === "_display"); - // Update slot types when connection changes - this.onConnectionsChange = function(slotType, slot, isConnect, linkInfo) { - 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; - if (node.outputs?.[0]) { - node.outputs[0].type = type; - node.outputs[0].name = type; - } - } - } - } else { - node.inputs[0].type = "*"; - node.inputs[0].name = "value"; - if (node.outputs?.[0]) { - node.outputs[0].type = "*"; - node.outputs[0].name = "value"; - } - } + if (!widget) { + widget = ComfyWidgets.STRING(this, "_display", [ + "STRING", { multiline: true } + ], appInstance).widget; + widget.inputEl.readOnly = true; + widget.inputEl.style.opacity = "0.7"; + widget.inputEl.style.fontFamily = "monospace"; + widget.inputEl.style.fontSize = "12px"; } - }; + + const text = Array.isArray(message.text) ? message.text[0] : message.text; + widget.value = text || ""; + + // Resize node + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) sz[0] = this.size[0]; + if (sz[1] < this.size[1]) sz[1] = this.size[1]; + this.onResize?.(sz); + appInstance.graph?.setDirtyCanvas(true, false); + }); + } }; - } - if (nodeData.name === "SimpleGet") { + // Update title based on name const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { if (onNodeCreated) onNodeCreated.apply(this, arguments); const node = this; - this.isVirtualNode = true; - - // 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()]; - } - }; - const origCallback = nameWidget.callback; nameWidget.callback = function(value) { if (origCallback) origCallback.apply(this, arguments); - node.title = value ? `Get: ${value}` : "SimpleGet"; - node.syncWithSetter(); + if (nodeData.name === "SimpleSet") { + node.title = value ? `Set: ${value}` : "SimpleSet"; + } else if (nodeData.name === "SimpleGet") { + node.title = value ? `Get: ${value}` : "SimpleGet"; + } }; } - - 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; - }; }; } }