diff --git a/__init__.py b/__init__.py index d558042..00c607a 100644 --- a/__init__.py +++ b/__init__.py @@ -7,7 +7,7 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "UINode": "UINode [GroxicTinch]", + "UINode": "Mirror node [Do not add manually!]", } WEB_DIRECTORY = "./web/comfyui" diff --git a/web/comfyui/easyui.js b/web/comfyui/easyui.js index 18226aa..82f7bb5 100644 --- a/web/comfyui/easyui.js +++ b/web/comfyui/easyui.js @@ -1,9 +1,7 @@ import { app } from "../../scripts/app.js"; -// [FIXME] Delete the UINode if the original node is deleted // [FIXME] Order of widgets doesnt persist properly when refreshing page // [FIXME] elements arent working (preview image, customtext) -// [FIXME] adding multiple mirrors will cause callback restoring to break // [TODO] Add a way to get taken to the original node from the UINode // [TODO] Combine many nodes into one UINode @@ -27,6 +25,9 @@ function createMirror(node) { uiNode.inputs = []; uiNode.outputs = []; + node.properties._easyui_mirrorNodeIds = node.properties._easyui_mirrorNodeIds || []; + node.properties._easyui_mirrorNodeIds.push(uiNode.id); + app.canvas.setDirty(true); app.canvas.draw(true); @@ -38,7 +39,7 @@ app.registerExtension({ beforeRegisterNodeDef(nodeTypeInfo) { const proto = nodeTypeInfo.prototype; - const original = proto.getExtraMenuOptions; + const original = proto.getExtraMenuOptions; // hide the UINode from the node list if (nodeTypeInfo.comfyClass === 'UINode') { @@ -50,14 +51,16 @@ app.registerExtension({ if (original) { original.apply(this, arguments); } - + options.push(null); // inserts a divider // 2) if it's a UINode, Don't add “Create Mirror Copy” if (this.type !== 'UINode') { - options.push({ - content: '🖼️ EasyUI – Create Mirror Copy', - callback: () => createMirror(this) - }); + if(this.widgets.length > 0) { + options.push({ + content: '🖼️ EasyUI – Create Mirror Copy', + callback: () => createMirror(this) + }); + } options.push({ content: '🖼️ EasyUI [DEBUG]', callback: () => { @@ -82,15 +85,15 @@ app.registerExtension({ // Add configure override for UINode to restore widgets on load if (nodeTypeInfo.comfyClass === 'UINode') { const originalOnAdded = proto.onAdded; - const originalOnRemoved = proto.onRemoved; const originalConfigure = proto.configure; + const originalOnRemoved = proto.onRemoved; + const originalDrawTitleBox = proto.drawTitleBox; proto.onAdded = function() { // call any existing onAdded first if (originalOnAdded) originalOnAdded.apply(this, arguments); - - // if this UINode was NOT created via createMirror (no original_node_id), - // add a label or change its title + + // if this UINode was NOT created via createMirror (no original_node_id), change title setTimeout(() => { // Need timeout or else this.properties.original_node_id isnt set in time for the mirrored uinodes if (!this.properties?.original_node_id) { @@ -100,8 +103,8 @@ app.registerExtension({ element.innerText = 'This UINode is not linked. It should not be created manually. Please right click on a node and select "🖼️ EasyUI – Create Mirror Copy"'; this.addDOMWidget('Notice', 'customtext', element); - this.width = 355; - this.height = 122; + this.size[0] = 355; + this.size[1] = 122; // force a redraw so the new widget/title shows up immediately app.canvas.setDirty(true); @@ -129,23 +132,49 @@ app.registerExtension({ }; proto.onRemoved = function() { - // 1) restore any original callbacks const originalNodeId = this.properties.original_node_id; const originalNode = app.canvas.graph.getNodeById(originalNodeId); if (originalNode && originalNode.widgets) { originalNode.widgets.forEach(w => { - if (w._easyui_originalCallback) { - w.callback = w._easyui_originalCallback; - delete w._easyui_originalCallback; + if (w._easyui_mirrorCallbacks && w._easyui_mirrorCallbacks.has(this.id)) { + w._easyui_mirrorCallbacks.delete(this.id); + if (w._easyui_mirrorCallbacks.size === 0) { + if (w._easyui_originalCallback) { + w.callback = w._easyui_originalCallback; + delete w._easyui_originalCallback; + } + delete w._easyui_mirrorCallbacks; + } } }); } - - // 2) call the original onRemoved if there was one + + // Call the original onRemoved if there was one if (originalOnRemoved) { originalOnRemoved.apply(this, arguments); } } + + proto.drawTitleBox = function(ctx) { + const originalNodeId = this.properties.original_node_id; + if (originalNodeId) { + const originalNode = app.canvas.graph.getNodeById(originalNodeId); + + if (!originalNode) { + console.warn(`Original node with ID ${originalNodeId} not found.`); + this.title = '❌ Orphaned UINode'; + this.properties.original_node_id = null; + this.widgets = []; + const element = document.createElement('span'); + element.innerText = 'This UINode is not linked. The original Node was deleted'; + this.addDOMWidget('Notice', 'customtext', element); + } + } + + if (originalDrawTitleBox) { + originalDrawTitleBox.apply(this, arguments); + } + } } } }); @@ -316,7 +345,7 @@ function cloneWidgets(originalNode, uiNode) { uiNode.properties.hiddenWidgets = uiNode.properties.hiddenWidgets || []; uiNode.properties.widgetOrder = uiNode.properties.widgetOrder || []; - if(originalNode && originalNode.widgets) { + if (originalNode && originalNode.widgets) { // Create a map of original widgets for easier lookup const widgetMap = new Map(originalNode.widgets.map(widget => [widget.name, widget])); @@ -324,12 +353,12 @@ function cloneWidgets(originalNode, uiNode) { const orderedWidgets = uiNode.properties.widgetOrder .map(name => widgetMap.get(name)) .filter(widget => widget !== undefined); - + // Add any remaining widgets that weren't in the saved order const remainingWidgets = originalNode.widgets.filter( widget => !uiNode.properties.widgetOrder.includes(widget.name) ); - + // Combine ordered and remaining widgets const allWidgets = [...orderedWidgets, ...remainingWidgets]; @@ -337,7 +366,7 @@ function cloneWidgets(originalNode, uiNode) { const type = origW.type || 'string'; const options = origW.options || {}; try { - // Example: Mark some widgets to be hidden initially + // Do not show nodes that are currently inputs const isHidden = uiNode.properties.hiddenWidgets.includes(origW.name); if (origW.computedDisabled || isHidden || ignoredWidgets.includes(type)) { if (!isHidden) { @@ -345,11 +374,15 @@ function cloneWidgets(originalNode, uiNode) { } return; } - - const oldCallback = origW.callback; - origW._easyui_originalCallback = oldCallback; + + // Store original callback and initialize mirror callbacks + if (!origW._easyui_originalCallback) { + origW._easyui_originalCallback = origW.callback; + origW._easyui_mirrorCallbacks = new Map(); + } + var newW; - + switch (type) { case 'custom': // [TODO] @@ -358,29 +391,36 @@ function cloneWidgets(originalNode, uiNode) { const element = origW.element.cloneNode(true); newW = uiNode.addDOMWidget(origW.name, type, element); newW.inputEl = element; - - // initialize value + newW.isSyncing = false; + + // Initialize value newW.value = origW.value; - // 2) Listen on the cloned textarea → push back to original + // Mirror → Original element.addEventListener('input', e => { if (newW.isSyncing) return; newW.isSyncing = true; origW.element.value = e.target.value; origW.value = e.target.value; + if (typeof origW.callback === 'function') { + origW.callback.call(origW, e.target.value); + } app.canvas.setDirty(true); newW.isSyncing = false; }); - // 3) Listen on the original textarea → push forward to clone - origW.element.addEventListener('input', e => { - if (newW.isSyncing) return; - newW.isSyncing = true; - element.value = e.target.value; - newW.value = e.target.value; - app.canvas.setDirty(true); - newW.isSyncing = false; - }); + // Original → Mirror + if (!origW._easyui_mirrorCallbacks.has(uiNode.id)) { + origW._easyui_mirrorCallbacks.set(uiNode.id, function(v) { + if (!newW.isSyncing) { + newW.isSyncing = true; + element.value = v; + newW.value = v; + newW.isSyncing = false; + } + }); + } + // Note: Original event listener should be set once elsewhere break; default: newW = uiNode.addWidget( @@ -388,24 +428,43 @@ function cloneWidgets(originalNode, uiNode) { origW.name, origW.value, function(...args) { + if (newW.isSyncing) return; + newW.isSyncing = true; const v = args[0]; origW.value = v; - if (typeof oldCallback === 'function') { - oldCallback.call(origW, ...args); + if (typeof origW.callback === 'function') { + origW.callback.call(origW, v); } app.canvas.setDirty(true); + newW.isSyncing = false; }, options ); + newW.isSyncing = false; - origW.callback = function(...args) { - const v = args[0]; - if (typeof oldCallback === 'function') { - oldCallback.call(this, ...args); - } - newW.value = v; - app.canvas.setDirty(true); - }; + if (origW._easyui_mirrorCallbacks.size === 0) { + origW.callback = function(...args) { + for (const [key, value] of origW._easyui_mirrorCallbacks) { + if (typeof value === 'function') { + value.call(this, args[0]); + } + } + if (typeof origW._easyui_originalCallback === 'function') { + origW._easyui_originalCallback.call(this, ...args); + } + app.canvas.setDirty(true); + }; + } + + if (!origW._easyui_mirrorCallbacks.has(uiNode.id)) { + origW._easyui_mirrorCallbacks.set(uiNode.id, function(v) { + if (!newW.isSyncing) { + newW.isSyncing = true; + newW.value = v; + newW.isSyncing = false; + } + }); + } break; } } catch (err) {