From 793d864971c727c717ca9100d0f27b557703196b Mon Sep 17 00:00:00 2001 From: heloess Date: Sun, 29 Mar 2026 03:22:33 +0300 Subject: [PATCH] feat: add option to hide bypassed groups from canvas in FastGroupsBypasser and FastGroupsMuter nodes --- src_web/comfyui/fast_groups_bypasser.ts | 6 +- src_web/comfyui/fast_groups_muter.ts | 178 +++++++++++++++++++++++- web/comfyui/fast_groups_bypasser.js | 2 + web/comfyui/fast_groups_muter.js | 140 ++++++++++++++++++- 4 files changed, 317 insertions(+), 9 deletions(-) diff --git a/src_web/comfyui/fast_groups_bypasser.ts b/src_web/comfyui/fast_groups_bypasser.ts index 4e18b07f..24789a88 100644 --- a/src_web/comfyui/fast_groups_bypasser.ts +++ b/src_web/comfyui/fast_groups_bypasser.ts @@ -5,7 +5,8 @@ import {NodeTypesString} from "./constants.js"; import {BaseFastGroupsModeChanger} from "./fast_groups_muter.js"; /** - * Fast Bypasser implementation that looks for groups in the workflow and adds toggles to mute them. + * Fast Bypasser implementation that looks for groups in the workflow and adds toggles to bypass them. + * Includes an option to completely hide bypassed groups from the canvas. */ export class FastGroupsBypasser extends BaseFastGroupsModeChanger { static override type = NodeTypesString.FAST_GROUPS_BYPASSER; @@ -19,8 +20,11 @@ export class FastGroupsBypasser extends BaseFastGroupsModeChanger { override readonly modeOn = LiteGraph.ALWAYS; override readonly modeOff = 4; // Used by Comfy for "bypass" + static "@hideBypassedGroups" = {type: "boolean"}; + constructor(title = FastGroupsBypasser.title) { super(title); + this.properties["hideBypassedGroups"] = false; this.onConstructed(); } } diff --git a/src_web/comfyui/fast_groups_muter.ts b/src_web/comfyui/fast_groups_muter.ts index 4eee7f8c..4081d449 100644 --- a/src_web/comfyui/fast_groups_muter.ts +++ b/src_web/comfyui/fast_groups_muter.ts @@ -24,6 +24,7 @@ const PROPERTY_MATCH_TITLE = "matchTitle"; const PROPERTY_SHOW_NAV = "showNav"; const PROPERTY_SHOW_ALL_GRAPHS = "showAllGraphs"; const PROPERTY_RESTRICTION = "toggleRestriction"; +const PROPERTY_HIDE_BYPASSED = "hideBypassedGroups"; /** * Fast Muter implementation that looks for groups in the workflow and adds toggles to mute them. @@ -167,7 +168,9 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { let index = 0; for (const group of groups) { if (filterColors.length) { - let groupColor = group.color?.replace("#", "").trim().toLocaleLowerCase(); + // Use original color if group is hidden (its visible color is "transparent"). + const rawColor = (group as any).rgthree_hidden ? (group as any).rgthree_origColor : group.color; + let groupColor = rawColor?.replace("#", "").trim().toLocaleLowerCase(); if (!groupColor) { continue; } @@ -232,6 +235,19 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { while ((this.widgets || [])[index]) { this.removeWidget(index++); } + + // Sync group visibility for bypasser nodes with hideBypassedGroups enabled. + const shouldHide = this.properties?.[PROPERTY_HIDE_BYPASSED] === true; + for (const widget of this.widgets) { + if (widget instanceof FastGroupsToggleRowWidget) { + if (shouldHide && !widget.toggled) { + setGroupVisibility(widget.group, false); + } else if (!shouldHide && (widget.group as any).rgthree_hidden) { + // Property was turned off, restore hidden groups. + setGroupVisibility(widget.group, true); + } + } + } } override computeSize(out?: Vector2) { @@ -374,6 +390,133 @@ export class FastGroupsMuter extends BaseFastGroupsModeChanger { } } +/** + * Sets the visibility of a group and all its nodes. When `visible` is false, the group + * and all its contained nodes will be hidden from the canvas. When `visible` is true, + * they are restored. + */ +// Patch LGraphCanvas.drawNode once to skip hidden nodes. +// Patch LGraph.getNodeOnPos once to prevent mouse interaction with hidden nodes. +let _drawNodePatched = false; +function ensureDrawNodePatch() { + if (_drawNodePatched) return; + _drawNodePatched = true; + + const origDrawNode = LGraphCanvas.prototype.drawNode; + LGraphCanvas.prototype.drawNode = function(this: any, node: any, ctx: CanvasRenderingContext2D) { + if (node.rgthree_bypasser_hidden) return; + return origDrawNode.apply(this, arguments as any); + }; + + const origGetGroupOnPos = LGraph.prototype.getGroupOnPos; + if (origGetGroupOnPos) { + LGraph.prototype.getGroupOnPos = function(this: any, x: number, y: number) { + const group = origGetGroupOnPos.apply(this, arguments as any); + if (group && (group as any).rgthree_hidden) return null; + return group; + }; + } + + // Also hide links connecting to hidden nodes + function hookLink(methodName: string) { + const origMethod = (LGraphCanvas.prototype as any)[methodName]; + if (origMethod) { + (LGraphCanvas.prototype as any)[methodName] = function(this: any) { + let linkObj = Array.from(arguments).find((arg: any) => arg && arg.origin_id !== undefined && arg.target_id !== undefined) as any; + if (linkObj) { + let n1 = this.graph.getNodeById(linkObj.origin_id); + let n2 = this.graph.getNodeById(linkObj.target_id); + if ((n1 && n1.rgthree_bypasser_hidden) || (n2 && n2.rgthree_bypasser_hidden)) { + return; // do not draw link + } + } + return origMethod.apply(this, arguments as any); + }; + } + } + hookLink('renderLink'); + hookLink('drawLink'); + + const origGetNodeOnPos = LGraph.prototype.getNodeOnPos; + LGraph.prototype.getNodeOnPos = function(this: any, x: number, y: number, nodes_list?: any[], margin?: number) { + let list = nodes_list || this._nodes_in_order; + if (list) { + list = list.filter((n: any) => !n.rgthree_bypasser_hidden); + } + return origGetNodeOnPos.call(this, x, y, list, margin); + }; +} + +function setGroupVisibility(group: LGraphGroup, visible: boolean) { + ensureDrawNodePatch(); + if (!visible && !(group as any).rgthree_hidden) { + (group as any).rgthree_hidden = true; + const nodes = getGroupNodes(group); + (group as any).rgthree_hiddenNodes = nodes; + + for (const node of nodes as any[]) { + node.rgthree_bypasser_hidden = true; + + // Hook serialize to safely hide without affecting workflow saves + if (!node.rgthree_origSerialize) { + node.rgthree_origSerialize = node.serialize; + node.serialize = function(this: any) { + let data = this.rgthree_origSerialize.apply(this, arguments as any); + if (this.rgthree_bypasser_hidden) { + if (this.rgthree_origFlags) data.flags = Object.assign({}, this.rgthree_origFlags); + } + return data; + }; + } + + // Store original state + node.rgthree_origFlags = Object.assign({}, node.flags); + + // Apply native hiding strategies + node.flags = node.flags || {}; + node.flags.collapsed = true; + } + + // Hide Group Box + (group as any).rgthree_origColor = group.color; + (group as any).rgthree_origFontSize = group.font_size; + (group as any).rgthree_origSize = [...group._size]; + group.color = "transparent"; + group.font_size = 0; + group._size = [0, 0] as any; + + // Override group draw just in case + (group as any).rgthree_origDraw = group.draw; + group.draw = function() {}; + + } else if (visible && (group as any).rgthree_hidden) { + (group as any).rgthree_hidden = false; + + // Restore Group Box + if ((group as any).rgthree_origColor !== undefined) group.color = (group as any).rgthree_origColor; + if ((group as any).rgthree_origFontSize !== undefined) group.font_size = (group as any).rgthree_origFontSize; + if ((group as any).rgthree_origSize !== undefined) group._size = (group as any).rgthree_origSize; + if ((group as any).rgthree_origDraw) { + group.draw = (group as any).rgthree_origDraw; + delete (group as any).rgthree_origDraw; + } else { + delete (group as any).draw; + } + + // Restore nodes + const nodes = ((group as any).rgthree_hiddenNodes || []); + for (const node of nodes) { + delete node.rgthree_bypasser_hidden; + + if (node.rgthree_origFlags) { + node.flags = node.rgthree_origFlags; + delete node.rgthree_origFlags; + } + } + delete (group as any).rgthree_hiddenNodes; + } +} + /** * The PowerLoraLoaderHeaderWidget that renders a toggle all switch, as well as some title info * (more necessary for the double model & clip strengths to label them). @@ -394,9 +537,21 @@ class FastGroupsToggleRowWidget extends RgthreeBaseWidget<{toggled: boolean}> { } doModeChange(force?: boolean, skipOtherNodeCheck?: boolean) { - this.group.recomputeInsideNodes(); - const hasAnyActiveNodes = getGroupNodes(this.group).some((n) => n.mode === LiteGraph.ALWAYS); + // If the group is currently hidden, we need to check stored state instead. + const isHidden = (this.group as any).rgthree_hidden === true; + const shouldHide = this.node.properties?.[PROPERTY_HIDE_BYPASSED] === true; + + // Determine new value based on current state. + let hasAnyActiveNodes: boolean; + if (isHidden) { + // When hidden, all nodes are off-screen so we use the toggled state. + hasAnyActiveNodes = this.toggled; + } else { + this.group.recomputeInsideNodes(); + hasAnyActiveNodes = getGroupNodes(this.group).some((n) => n.mode === LiteGraph.ALWAYS); + } let newValue = force != null ? force : !hasAnyActiveNodes; + if (skipOtherNodeCheck !== true) { // TODO: This work should probably live in BaseFastGroupsModeChanger instead of the widgets. if (newValue && this.node.properties?.[PROPERTY_RESTRICTION]?.includes(" one")) { @@ -409,9 +564,26 @@ class FastGroupsToggleRowWidget extends RgthreeBaseWidget<{toggled: boolean}> { newValue = this.node.widgets.every((w) => !w.value || w === this); } } + + // If enabling and group is hidden, restore visibility first so nodes can be found. + if (newValue && isHidden) { + setGroupVisibility(this.group, true); + this.group.recomputeInsideNodes(); + } + + // Change the mode of the group's nodes. changeModeOfNodes(getGroupNodes(this.group), (newValue ? this.node.modeOn : this.node.modeOff)); this.group.rgthree_hasAnyActiveNode = newValue; this.toggled = newValue; + + // If bypassing and hide is enabled, hide the group after setting modes. + if (shouldHide && !newValue) { + setGroupVisibility(this.group, false); + } else if (!shouldHide && (this.group as any).rgthree_hidden) { + // Property was turned off; restore any hidden group. + setGroupVisibility(this.group, true); + } + this.group.graph?.setDirtyCanvas(true, false); } diff --git a/web/comfyui/fast_groups_bypasser.js b/web/comfyui/fast_groups_bypasser.js index 80ee9910..126b1dc4 100644 --- a/web/comfyui/fast_groups_bypasser.js +++ b/web/comfyui/fast_groups_bypasser.js @@ -8,12 +8,14 @@ export class FastGroupsBypasser extends BaseFastGroupsModeChanger { this.helpActions = "bypass and enable"; this.modeOn = LiteGraph.ALWAYS; this.modeOff = 4; + this.properties["hideBypassedGroups"] = false; this.onConstructed(); } } FastGroupsBypasser.type = NodeTypesString.FAST_GROUPS_BYPASSER; FastGroupsBypasser.title = NodeTypesString.FAST_GROUPS_BYPASSER; FastGroupsBypasser.exposedActions = ["Bypass all", "Enable all", "Toggle all"]; +FastGroupsBypasser["@hideBypassedGroups"] = { type: "boolean" }; app.registerExtension({ name: "rgthree.FastGroupsBypasser", registerCustomNodes() { diff --git a/web/comfyui/fast_groups_muter.js b/web/comfyui/fast_groups_muter.js index 11ee58f0..4ac6677e 100644 --- a/web/comfyui/fast_groups_muter.js +++ b/web/comfyui/fast_groups_muter.js @@ -12,6 +12,7 @@ const PROPERTY_MATCH_TITLE = "matchTitle"; const PROPERTY_SHOW_NAV = "showNav"; const PROPERTY_SHOW_ALL_GRAPHS = "showAllGraphs"; const PROPERTY_RESTRICTION = "toggleRestriction"; +const PROPERTY_HIDE_BYPASSED = "hideBypassedGroups"; export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { constructor(title = FastGroupsMuter.title) { super(title); @@ -40,7 +41,7 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { FAST_GROUPS_SERVICE.removeFastGroupNode(this); } refreshWidgets() { - var _a, _b, _c, _d, _e, _f, _g, _h, _j; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const canvas = app.canvas; let sort = ((_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_SORT]) || "position"; let customAlphabet = null; @@ -161,6 +162,16 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { while ((this.widgets || [])[index]) { this.removeWidget(index++); } + const shouldHideGroups = !!((_k = this.properties) === null || _k === void 0 ? void 0 : _k[PROPERTY_HIDE_BYPASSED]); + for (const w of this.widgets) { + if (w instanceof FastGroupsToggleRowWidget) { + if (shouldHideGroups && !w.toggled) { + setGroupVisibility(w.group, false); + } else if (!shouldHideGroups && w.group.rgthree_hidden) { + setGroupVisibility(w.group, true); + } + } + } } computeSize(out) { let size = super.computeSize(out); @@ -292,6 +303,117 @@ BaseFastGroupsModeChanger["@toggleRestriction"] = { type: "combo", values: ["default", "max one", "always one"], }; +// Patch LGraphCanvas.drawNode once to skip hidden nodes. +// Patch LGraph.getNodeOnPos once to prevent mouse interaction with hidden nodes. +// Patch LGraph.getGroupOnPos once to prevent dragging hidden groups. +let _drawNodePatched = false; +function ensureDrawNodePatch() { + if (_drawNodePatched) return; + _drawNodePatched = true; + + const origDrawNode = LGraphCanvas.prototype.drawNode; + LGraphCanvas.prototype.drawNode = function(node, ctx) { + if (node.rgthree_bypasser_hidden) return; + return origDrawNode.apply(this, arguments); + }; + + const origGetNodeOnPos = LGraph.prototype.getNodeOnPos; + LGraph.prototype.getNodeOnPos = function(x, y, nodes_list, margin) { + let list = nodes_list || this._nodes_in_order; + if (list) { + list = list.filter(n => !n.rgthree_bypasser_hidden); + } + return origGetNodeOnPos.call(this, x, y, list, margin); + }; + + const origGetGroupOnPos = LGraph.prototype.getGroupOnPos; + if (origGetGroupOnPos) { + LGraph.prototype.getGroupOnPos = function(x, y) { + const group = origGetGroupOnPos.apply(this, arguments); + if (group && group.rgthree_hidden) return null; + return group; + }; + } + + // Also hide links connecting to hidden nodes + function hookLink(methodName) { + const origMethod = LGraphCanvas.prototype[methodName]; + if (origMethod) { + LGraphCanvas.prototype[methodName] = function() { + let linkObj = Array.from(arguments).find(arg => arg && arg.origin_id !== undefined && arg.target_id !== undefined); + if (linkObj) { + let n1 = this.graph.getNodeById(linkObj.origin_id); + let n2 = this.graph.getNodeById(linkObj.target_id); + if ((n1 && n1.rgthree_bypasser_hidden) || (n2 && n2.rgthree_bypasser_hidden)) { + return; // do not draw link + } + } + return origMethod.apply(this, arguments); + }; + } + } + hookLink('renderLink'); + hookLink('drawLink'); +} + +function setGroupVisibility(group, visible) { + ensureDrawNodePatch(); + if (!visible && !group.rgthree_hidden) { + group.rgthree_hidden = true; + const nodes = getGroupNodes(group); + group.rgthree_hiddenNodes = nodes; + + for (const node of nodes) { + node.rgthree_bypasser_hidden = true; + + // Hook serialize to safely hide without affecting workflow saves + if (!node.rgthree_origSerialize) { + node.rgthree_origSerialize = node.serialize; + node.serialize = function() { + let data = this.rgthree_origSerialize.apply(this, arguments); + if (this.rgthree_bypasser_hidden) { + if (this.rgthree_origFlags) data.flags = Object.assign({}, this.rgthree_origFlags); + } + return data; + }; + } + + // Store original state + node.rgthree_origFlags = Object.assign({}, node.flags); + + // Apply native hiding strategies (DOM widgets automatically hide when collapsed) + node.flags = node.flags || {}; + node.flags.collapsed = true; + } + + // Hide Group visually via overriding Draw. + // Not touching dimensions allows internal LiteGraph loops to continue recognizing the Group's nodes! + group.rgthree_origDraw = group.draw; + group.draw = function() {}; + + } else if (visible && group.rgthree_hidden) { + group.rgthree_hidden = false; + + // Restore Group Box + if (group.rgthree_origDraw) { + group.draw = group.rgthree_origDraw; + delete group.rgthree_origDraw; + } else { + delete group.draw; + } + + // Restore nodes + const nodes = (group.rgthree_hiddenNodes || []); + for (const node of nodes) { + delete node.rgthree_bypasser_hidden; + if (node.rgthree_origFlags) { + node.flags = node.rgthree_origFlags; + delete node.rgthree_origFlags; + } + } + delete group.rgthree_hiddenNodes; + } +} export class FastGroupsMuter extends BaseFastGroupsModeChanger { constructor(title = FastGroupsMuter.title) { super(title); @@ -316,26 +438,34 @@ class FastGroupsToggleRowWidget extends RgthreeBaseWidget { this.node = node; } doModeChange(force, skipOtherNodeCheck) { - var _a, _b, _c, _d; + var _a, _b, _c, _d, _e; + const isHidden = this.group.rgthree_hidden === true; + const shouldHide = !!((_a = this.node.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_HIDE_BYPASSED]); this.group.recomputeInsideNodes(); const hasAnyActiveNodes = getGroupNodes(this.group).some((n) => n.mode === LiteGraph.ALWAYS); let newValue = force != null ? force : !hasAnyActiveNodes; if (skipOtherNodeCheck !== true) { - if (newValue && ((_b = (_a = this.node.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_RESTRICTION]) === null || _b === void 0 ? void 0 : _b.includes(" one"))) { + if (newValue && ((_c = (_b = this.node.properties) === null || _b === void 0 ? void 0 : _b[PROPERTY_RESTRICTION]) === null || _c === void 0 ? void 0 : _c.includes(" one"))) { for (const widget of this.node.widgets) { if (widget instanceof FastGroupsToggleRowWidget) { widget.doModeChange(false, true); } } } - else if (!newValue && ((_c = this.node.properties) === null || _c === void 0 ? void 0 : _c[PROPERTY_RESTRICTION]) === "always one") { + else if (!newValue && ((_d = this.node.properties) === null || _d === void 0 ? void 0 : _d[PROPERTY_RESTRICTION]) === "always one") { newValue = this.node.widgets.every((w) => !w.value || w === this); } } + if (isHidden) { + setGroupVisibility(this.group, true); + } changeModeOfNodes(getGroupNodes(this.group), (newValue ? this.node.modeOn : this.node.modeOff)); this.group.rgthree_hasAnyActiveNode = newValue; this.toggled = newValue; - (_d = this.group.graph) === null || _d === void 0 ? void 0 : _d.setDirtyCanvas(true, false); + if (shouldHide && !newValue) { + setGroupVisibility(this.group, false); + } + (_e = this.group.graph) === null || _e === void 0 ? void 0 : _e.setDirtyCanvas(true, false); } get toggled() { return this.value.toggled;