diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..5cd93d1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +from .debugnode import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +WEB_DIRECTORY = "js" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/debugnode.py b/debugnode.py new file mode 100644 index 0000000..c889658 --- /dev/null +++ b/debugnode.py @@ -0,0 +1,82 @@ +"""A simple debug node used to quickly check output of nodes in ComfyUI""" + +# AnyType was ruthlessly pilfered from https://github.com/pythongosssss/ComfyUI-Custom-Scripts +class AnyType(str): + def __ne__(self, __value: object) -> bool: + return False + +ANY = AnyType("*") + +class WTFDebugNode: + """What's That Field? Displays string representation of node output to ease debugging + + Displays some common attributes, if available: + - len() + - .shape + - if the input was an iterable, the type of the first child item + + Displays str(Exception) on for unhandled exceptions""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "anything": (ANY,), + } + } + + OUTPUT_NODE = True + + INPUT_IS_LIST = True + # OUTPUT_IS_LIST = (False,) + RETURN_TYPES = () + FUNCTION = "execute" + + CATEGORY = "debug" + + def execute(self, anything=None): + out = [] + + for item in anything: + info = { + "type": str(type(item)), + } + + try: + info["len"] = len(item) + except TypeError: + pass + except Exception as e: + info["len"] = str(e) + + if not isinstance(item, str): + try: + first_iter_item = next(iter(item)) + info["firstIterItem"] = str(type(first_iter_item)) + except TypeError: + pass + # info["firstIterItem"] = ["Wasn't a list"] + except StopIteration: + info["firstIterItem"] = "List had no items" + except Exception as e: + info["firstIterItem"] = f"Exception [firstIterItem]: {str(e)}" + + try: + info["shape"] = str(item.shape) + except AttributeError: + pass + except Exception as e: + info["shape"] = str(e) + + info["value"] = str(item) if item != None else None + out.append(info) + + return { "ui": { "items": out, "length": len(anything) } } + +NODE_CLASS_MAPPINGS = { + "WTFDebugNode": WTFDebugNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "WTFDebugNode": "🐜 WTF?", +} diff --git a/js/debugnode.js b/js/debugnode.js new file mode 100644 index 0000000..8a33752 --- /dev/null +++ b/js/debugnode.js @@ -0,0 +1,117 @@ +import { app } from "../../scripts/app.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; + +const hardLimit = 100 + +// Does most of the regular textarea cleanup, but leaks a ref in domWidget.js -> elementWidgets until the node is removed +function removeWidget(node, widget) { + widget.onRemove?.() + const index = node.widgets.indexOf(widget) + if (index !== -1) + node.widgets.splice(index, 1) +} + +function removeAllWidgets(node) { + for (const widget of node.widgets) { + widget.onRemove?.() + } + node.widgets.length = 0 +} + +// Main logic +app.registerExtension({ + name: "WTFDebugNode", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "WTFDebugNode") { + + const origOnExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + const r = origOnExecuted + ? origOnExecuted?.apply(this, arguments) + : undefined + + // Because debug + console.log(message) + + // Sanity init + this.widgets ??= [] + this.serialize_widgets = false + const baseOpacity = app.canvas.editor_alpha + + // Clear if invalid + const itemsLength = parseInt(message?.items?.length) + if (isNaN(itemsLength)) { + removeAllWidgets(this) + return r + } + + const isMultiItem = itemsLength > 1 + const wasMultiItem = this.debugNodeLength !== 1 + + // Clean up existing widgets if required + if (isMultiItem !== wasMultiItem) { + removeAllWidgets(this) + } + // Remove only widgets we can't reuse + else if (this.debugNodeLength > itemsLength) { + for (let i = itemsLength; i <= this.debugNodeLength; i++) { + for (const widget of this.widgets.filter(x => x.name.endsWith(` ${i}`))) { + removeWidget(this, widget) + } + } + } + + this.debugNodeLength = itemsLength + const maxItems = Math.min(itemsLength, hardLimit) + + // Create / update widget values for each item + for (let i = 0; i < maxItems; i++) { + const item = message.items[i] + const type = item.type + const len = item.len + const shape = item.shape + const firstIterItem = item.firstIterItem + const value = item.value + + function processWidget(node, name, value, isMultiline = false) { + const numberedName = isMultiItem + ? `${name} ${i}` + : name + const comfyType = isMultiline + ? ["STRING", { multiline: true }] + : "STRING" + + const current = node.widgets.find(x => x.name === numberedName) + const widget = current ?? ComfyWidgets.STRING(node, numberedName, comfyType, app).widget + + if (!widget.options) + widget.options = {} + widget.options.serialize = false + widget.value = value + widget.disabled = value === null || value === undefined + if (isMultiline) { + widget.element.style.opacity = widget.disabled + ? baseOpacity * 0.5 + : baseOpacity + } + } + + processWidget(this, "type", type) + processWidget(this, "len()", len) + processWidget(this, "shape", shape) + processWidget(this, "type of first iter() item", firstIterItem) + processWidget(this, "value", value, true) + } + + requestAnimationFrame(() => { + const size = this.computeSize(); + for (const widget of this.widgets.filter(x => x.element)) { + widget.onResize?.(size) + } + }) + + return r + } + } + }, +});