From dde4f8b7e890a64e88cd4eb6c446f2fc9c7e00a6 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Thu, 18 Apr 2024 13:26:01 -0700 Subject: [PATCH 1/8] Add augments to the typings --- src_web/typings/litegraph.d.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src_web/typings/litegraph.d.ts b/src_web/typings/litegraph.d.ts index 121f048d..d4403092 100644 --- a/src_web/typings/litegraph.d.ts +++ b/src_web/typings/litegraph.d.ts @@ -144,6 +144,9 @@ export interface IWidget { serializeValue?(serializedNode: SerializedLGraphNode, widgetIndex: number): TValue; // @rgthree - Checked in LGraphCanvas.prototype.processNodeWidgets, and figured I'd use it too. width?: number; + + // @rgthree + doModeChange?(force?: boolean, skipOtherNodeCheck?: boolean): void; } export interface IButtonWidget extends IWidget { type: "button"; @@ -688,6 +691,8 @@ export declare class LGraphNode { findInputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number findOutputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number + isVirtualNode?: boolean; + // end @rgthree added static title_color?: string; @@ -1210,7 +1215,8 @@ export declare class LGraphGroup { size: Vector2; // @rgthree - apparently it is available? pos: Vector2; - + // @rgthree + _rgthreeHasAnyActiveNode?: boolean; configure(o: SerializedLGraphGroup): void; serialize(): SerializedLGraphGroup; From 1c534e0fbe402ecb4bb1e0496383c0da2c4ee916 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Thu, 18 Apr 2024 13:30:16 -0700 Subject: [PATCH 2/8] Extract widget from fast_groups_muter --- src_web/comfyui/fast_groups_muter.ts | 121 ++------------------------- src_web/comfyui/utils_widgets.ts | 119 ++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 113 deletions(-) diff --git a/src_web/comfyui/fast_groups_muter.ts b/src_web/comfyui/fast_groups_muter.ts index a205b15b..5b7a8813 100644 --- a/src_web/comfyui/fast_groups_muter.ts +++ b/src_web/comfyui/fast_groups_muter.ts @@ -14,6 +14,7 @@ import { } from "typings/litegraph.js"; import {SERVICE as FAST_GROUPS_SERVICE} from "./fast_groups_service.js"; import { drawNodeWidget, fitString } from "./utils_canvas.js"; +import { RgthreeToggleNavWidget } from "./utils_widgets.js"; import { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js"; declare const LGraphCanvas: typeof TLGraphCanvas; @@ -94,6 +95,10 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { FAST_GROUPS_SERVICE.removeFastGroupNode(this); } + get showNav() { + return this.properties?.[PROPERTY_SHOW_NAV] !== false; + } + refreshWidgets() { const canvas = app.canvas as TLGraphCanvas; let sort = this.properties?.[PROPERTY_SORT] || "position"; @@ -192,119 +197,9 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { // When we add a widget, litegraph is going to mess up the size, so we // store it so we can retrieve it in computeSize. Hacky.. this.tempSize = [...this.size]; - widget = this.addCustomWidget>({ - name: "RGTHREE_TOGGLE_AND_NAV", - label: "", - value: false, - disabled: false, - options: { on: "yes", off: "no" }, - draw: function ( - ctx: CanvasRenderingContext2D, - node: LGraphNode, - width: number, - posY: number, - height: number, - ) { - - const widgetData = drawNodeWidget(ctx, { - width, - height, - posY, - }); - - const showNav = node.properties?.[PROPERTY_SHOW_NAV] !== false; - - // Render from right to left, since the text on left will take available space. - // `currentX` markes the current x position moving backwards. - let currentX = widgetData.width - widgetData.margin; - - // The nav arrow - if (!widgetData.lowQuality && showNav) { - currentX -= 7; // Arrow space margin - const midY = widgetData.posY + widgetData.height * 0.5; - ctx.fillStyle = ctx.strokeStyle = "#89A"; - ctx.lineJoin = "round"; - ctx.lineCap = "round"; - const arrow = new Path2D(`M${currentX} ${midY} l -7 6 v -3 h -7 v -6 h 7 v -3 z`); - ctx.fill(arrow); - ctx.stroke(arrow); - currentX -= 14; - - currentX -= 7; - ctx.strokeStyle = widgetData.colorOutline; - ctx.stroke(new Path2D(`M ${currentX} ${widgetData.posY} v ${widgetData.height}`)); - } else if (widgetData.lowQuality && showNav) { - currentX -= 28; - } - - // The toggle itself. - currentX -= 7; - ctx.fillStyle = this.value ? "#89A" : "#333"; - ctx.beginPath(); - const toggleRadius = height * 0.36; - ctx.arc(currentX - toggleRadius, posY + height * 0.5, toggleRadius, 0, Math.PI * 2); - ctx.fill(); - currentX -= toggleRadius * 2; - - if (!widgetData.lowQuality) { - currentX -= 4; - ctx.textAlign = "right"; - ctx.fillStyle = this.value ? widgetData.colorText : widgetData.colorTextSecondary; - const label = this.label || this.name; - const toggleLabelOn = this.options.on || "true"; - const toggleLabelOff = this.options.off || "false"; - ctx.fillText( - this.value ? toggleLabelOn : toggleLabelOff, - currentX, - posY + height * 0.7, - ); - currentX -= Math.max( - ctx.measureText(toggleLabelOn).width, - ctx.measureText(toggleLabelOff).width, - ); - - currentX -= 7; - ctx.textAlign = "left"; - let maxLabelWidth = widgetData.width - widgetData.margin - 10 - (widgetData.width - currentX); - if (label != null) { - ctx.fillText(fitString(ctx, label, maxLabelWidth), widgetData.margin + 10, posY + height * 0.7); - } - } - }, - serializeValue(serializedNode: SerializedLGraphNode, widgetIndex: number) { - return this.value; - }, - mouse(event: PointerEvent, pos: Vector2, node: LGraphNode) { - if (event.type == "pointerdown") { - if ( - node.properties?.[PROPERTY_SHOW_NAV] !== false && - pos[0] >= node.size[0] - 15 - 28 - 1 - ) { - const canvas = app.canvas as TLGraphCanvas; - const lowQuality = (canvas.ds?.scale || 1) <= 0.5; - if (!lowQuality) { - // Clicked on right half with nav arrow, go to the group, center on group and set - // zoom to see it all. - canvas.centerOnNode(group); - const zoomCurrent = canvas.ds?.scale || 1; - const zoomX = canvas.canvas.width / group._size[0] - 0.02; - const zoomY = canvas.canvas.height / group._size[1] - 0.02; - canvas.setZoom(Math.min(zoomCurrent, zoomX, zoomY), [ - canvas.canvas.width / 2, - canvas.canvas.height / 2, - ]); - canvas.setDirty(true, true); - } - } else { - this.value = !this.value; - setTimeout(() => { - this.callback?.(this.value, app.canvas, node, pos, event); - }, 20); - } - } - return true; - }, - }); + widget = this.addCustomWidget( + new RgthreeToggleNavWidget(group, () => this.showNav), + ); (widget as any).doModeChange = (force?: boolean, skipOtherNodeCheck?: boolean) => { group.recomputeInsideNodes(); const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); diff --git a/src_web/comfyui/utils_widgets.ts b/src_web/comfyui/utils_widgets.ts index 28223992..66a00437 100644 --- a/src_web/comfyui/utils_widgets.ts +++ b/src_web/comfyui/utils_widgets.ts @@ -8,6 +8,8 @@ import type { Vector2, AdjustedMouseEvent, Vector4, + WidgetCallback, + SerializedLGraphNode, } from "../typings/litegraph.js"; import { drawNodeWidget, drawRoundedRectangle, fitString, isLowQuality } from "./utils_canvas.js"; @@ -404,3 +406,120 @@ export class RgthreeLabelWidget implements IWidget { return true; } } + +export class RgthreeToggleNavWidget implements IWidget { + name = "RGTHREE_TOGGLE_AND_NAV"; + label = ""; + value = false; + disabled = false; + options = { on: "yes", off: "no" }; + callback?: WidgetCallback>; + + constructor( + private readonly node: { pos: Vector2; size: Vector2 }, + private readonly showNav: () => boolean, + ) {} + + draw( + ctx: CanvasRenderingContext2D, + node: LGraphNode, + width: number, + posY: number, + height: number, + ) { + const widgetData = drawNodeWidget(ctx, { + width, + height, + posY, + }); + + // Render from right to left, since the text on left will take available space. + // `currentX` markes the current x position moving backwards. + let currentX = widgetData.width - widgetData.margin; + + // The nav arrow + if (!widgetData.lowQuality && this.showNav()) { + currentX -= 7; // Arrow space margin + const midY = widgetData.posY + widgetData.height * 0.5; + ctx.fillStyle = ctx.strokeStyle = "#89A"; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + const arrow = new Path2D(`M${currentX} ${midY} l -7 6 v -3 h -7 v -6 h 7 v -3 z`); + ctx.fill(arrow); + ctx.stroke(arrow); + currentX -= 14; + + currentX -= 7; + ctx.strokeStyle = widgetData.colorOutline; + ctx.stroke(new Path2D(`M ${currentX} ${widgetData.posY} v ${widgetData.height}`)); + } else if (widgetData.lowQuality && this.showNav()) { + currentX -= 28; + } + + // The toggle itself. + currentX -= 7; + ctx.fillStyle = this.value ? "#89A" : "#333"; + ctx.beginPath(); + const toggleRadius = height * 0.36; + ctx.arc(currentX - toggleRadius, posY + height * 0.5, toggleRadius, 0, Math.PI * 2); + ctx.fill(); + currentX -= toggleRadius * 2; + + if (!widgetData.lowQuality) { + currentX -= 4; + ctx.textAlign = "right"; + ctx.fillStyle = this.value ? widgetData.colorText : widgetData.colorTextSecondary; + const label = this.label || this.name; + const toggleLabelOn = this.options.on || "true"; + const toggleLabelOff = this.options.off || "false"; + ctx.fillText(this.value ? toggleLabelOn : toggleLabelOff, currentX, posY + height * 0.7); + currentX -= Math.max( + ctx.measureText(toggleLabelOn).width, + ctx.measureText(toggleLabelOff).width, + ); + + currentX -= 7; + ctx.textAlign = "left"; + let maxLabelWidth = widgetData.width - widgetData.margin - 10 - (widgetData.width - currentX); + if (label != null) { + ctx.fillText( + fitString(ctx, label, maxLabelWidth), + widgetData.margin + 10, + posY + height * 0.7, + ); + } + } + } + + serializeValue(serializedNode: SerializedLGraphNode, widgetIndex: number) { + return this.value; + } + + mouse(event: PointerEvent, pos: Vector2, selfNode: LGraphNode) { + if (event.type == "pointerdown") { + if (this.showNav() && pos[0] >= selfNode.size[0] - 15 - 28 - 1) { + const canvas = app.canvas as TLGraphCanvas; + const lowQuality = (canvas.ds?.scale || 1) <= 0.5; + if (!lowQuality) { + // Clicked on right half with nav arrow, go to the group, center on group and set + // zoom to see it all. + canvas.centerOnNode(this.node); + const zoomCurrent = canvas.ds?.scale || 1; + const zoomX = canvas.canvas.width / this.node.size[0] - 0.02; + const zoomY = canvas.canvas.height / this.node.size[1] - 0.02; + canvas.setZoom(Math.min(zoomCurrent, zoomX, zoomY), [ + canvas.canvas.width / 2, + canvas.canvas.height / 2, + ]); + canvas.setDirty(true, true); + } + } else { + this.value = !this.value; + setTimeout(() => { + this.callback?.(this.value, app.canvas, selfNode, pos, event); + }, 20); + } + } + return true; + } +} From b4410783d7e7f600f012002f0b7e44a663f1c83b Mon Sep 17 00:00:00 2001 From: DrJKL Date: Thu, 18 Apr 2024 13:48:55 -0700 Subject: [PATCH 3/8] groupHasActiveNode --- src_web/comfyui/fast_groups_muter.ts | 6 +- src_web/comfyui/fast_groups_service.ts | 401 +++++++-------- src_web/comfyui/feature_group_fast_toggle.ts | 507 ++++++++++--------- src_web/comfyui/utils_fast.ts | 14 + web/comfyui/fast_groups_muter.js | 100 +--- web/comfyui/fast_groups_service.js | 3 +- web/comfyui/feature_group_fast_toggle.js | 3 +- web/comfyui/utils_fast.js | 4 + web/comfyui/utils_widgets.js | 90 ++++ 9 files changed, 579 insertions(+), 549 deletions(-) create mode 100644 src_web/comfyui/utils_fast.ts create mode 100644 web/comfyui/utils_fast.js diff --git a/src_web/comfyui/fast_groups_muter.ts b/src_web/comfyui/fast_groups_muter.ts index 5b7a8813..9228cc98 100644 --- a/src_web/comfyui/fast_groups_muter.ts +++ b/src_web/comfyui/fast_groups_muter.ts @@ -15,6 +15,9 @@ import { import {SERVICE as FAST_GROUPS_SERVICE} from "./fast_groups_service.js"; import { drawNodeWidget, fitString } from "./utils_canvas.js"; import { RgthreeToggleNavWidget } from "./utils_widgets.js"; +import { + groupHasActiveNode, +} from "./utils_fast.js"; import { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js"; declare const LGraphCanvas: typeof TLGraphCanvas; @@ -202,7 +205,7 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { ); (widget as any).doModeChange = (force?: boolean, skipOtherNodeCheck?: boolean) => { group.recomputeInsideNodes(); - const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); + const hasAnyActiveNodes = groupHasActiveNode(group); let newValue = force != null ? force : !hasAnyActiveNodes; if (skipOtherNodeCheck !== true) { if (newValue && this.properties?.[PROPERTY_RESTRICTION]?.includes(" one")) { @@ -216,7 +219,6 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { for (const node of group._nodes) { node.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4; } - (group as any)._rgthreeHasAnyActiveNode = newValue; widget!.value = newValue; app.graph.setDirtyCanvas(true, false); }; diff --git a/src_web/comfyui/fast_groups_service.ts b/src_web/comfyui/fast_groups_service.ts index bf3c367d..a5a54b30 100644 --- a/src_web/comfyui/fast_groups_service.ts +++ b/src_web/comfyui/fast_groups_service.ts @@ -1,200 +1,201 @@ -// / -// @ts-ignore -import { app } from "../../scripts/app.js"; -import type {BaseFastGroupsModeChanger} from './fast_groups_muter.js'; -import { - type LiteGraph as TLiteGraph, - type LGraph as TLGraph, - type LGraphCanvas as TLGraphCanvas, - LGraphGroup, - Vector4, -} from "typings/litegraph.js"; - -declare const LiteGraph: typeof TLiteGraph; - -/** - * A service that keeps global state that can be shared by multiple FastGroupsMuter or - * FastGroupsBypasser nodes rather than calculate it on it's own. - */ -class FastGroupsService { - private msThreshold = 400; - private msLastUnsorted = 0; - private msLastAlpha = 0; - private msLastPosition = 0; - - private groupsUnsorted: LGraphGroup[] = []; - private groupsSortedAlpha: LGraphGroup[] = []; - private groupsSortedPosition: LGraphGroup[] = []; - - private readonly fastGroupNodes: BaseFastGroupsModeChanger[] = []; - - private runScheduledForMs: number | null = null; - private runScheduleTimeout: number | null = null; - private runScheduleAnimation: number | null = null; - - private cachedNodeBoundings: { [key: number]: Vector4 } | null = null; - - constructor() { - // Don't need to do anything, wait until a signal. - } - - addFastGroupNode(node: BaseFastGroupsModeChanger) { - this.fastGroupNodes.push(node); - // Schedule it because the node may not be ready to refreshWidgets (like, when added it may - // not have cloned properties to filter against, etc.). - this.scheduleRun(8); - } - - removeFastGroupNode(node: BaseFastGroupsModeChanger) { - const index = this.fastGroupNodes.indexOf(node); - if (index > -1) { - this.fastGroupNodes.splice(index, 1); - } - // If we have no more group nodes, then clear out data; it could be because of a canvas clear. - if (!this.fastGroupNodes?.length) { - this.clearScheduledRun(); - this.groupsUnsorted = []; - this.groupsSortedAlpha = []; - this.groupsSortedPosition = []; - } - } - - private run() { - // We only run if we're scheduled, so if we're not, then bail. - if (!this.runScheduledForMs) { - return; - } - for (const node of this.fastGroupNodes) { - node.refreshWidgets(); - } - this.clearScheduledRun(); - this.scheduleRun(); - } - - private scheduleRun(ms = 500) { - // If we got a request for an immediate schedule and already have on scheduled for longer, then - // cancel the long one to expediate a fast one. - if (this.runScheduledForMs && ms < this.runScheduledForMs) { - this.clearScheduledRun(); - } - if (!this.runScheduledForMs && this.fastGroupNodes.length) { - this.runScheduledForMs = ms; - this.runScheduleTimeout = setTimeout(() => { - this.runScheduleAnimation = requestAnimationFrame(() => this.run()); - }, ms); - } - } - - private clearScheduledRun() { - this.runScheduleTimeout && clearTimeout(this.runScheduleTimeout); - this.runScheduleAnimation && cancelAnimationFrame(this.runScheduleAnimation); - this.runScheduleTimeout = null; - this.runScheduleAnimation = null; - this.runScheduledForMs = null; - } - - /** - * Returns the boundings for all nodes on the graph, then clears it after a short delay. This is - * to increase efficiency by caching the nodes' boundings when multiple groups are on the page. - */ - getBoundingsForAllNodes() { - if (!this.cachedNodeBoundings) { - this.cachedNodeBoundings = {}; - for (const node of app.graph._nodes) { - this.cachedNodeBoundings[node.id] = node.getBounding(); - } - setTimeout(() => { - this.cachedNodeBoundings = null; - }, 50); - } - return this.cachedNodeBoundings; - } - - /** - * This overrides `LGraphGroup.prototype.recomputeInsideNodes` to be much more efficient when - * calculating for many groups at once (only compute all nodes once in `getBoundingsForAllNodes`). - */ - recomputeInsideNodesForGroup(group: LGraphGroup) { - const cachedBoundings = this.getBoundingsForAllNodes(); - const nodes = group.graph._nodes; - group._nodes.length = 0; - - for (const node of nodes) { - const node_bounding = cachedBoundings[node.id]; - if (!node_bounding || !LiteGraph.overlapBounding(group._bounding, node_bounding)) { - continue; - } - group._nodes.push(node); - } - } - - /** - * Everything goes through getGroupsUnsorted, so we only get groups once. However, LiteGraph's - * `recomputeInsideNodes` is inefficient when calling multiple groups (it iterates over all nodes - * each time). So, we'll do our own dang thing, once. - */ - private getGroupsUnsorted(now: number) { - const canvas = app.canvas as TLGraphCanvas; - const graph = app.graph as TLGraph; - - if ( - // Don't recalculate nodes if we're moving a group (added by ComfyUI in app.js) - !canvas.selected_group_moving && - (!this.groupsUnsorted.length || now - this.msLastUnsorted > this.msThreshold) - ) { - this.groupsUnsorted = [...graph._groups]; - for (const group of this.groupsUnsorted) { - this.recomputeInsideNodesForGroup(group); - (group as any)._rgthreeHasAnyActiveNode = group._nodes.some( - (n) => n.mode === LiteGraph.ALWAYS, - ); - } - this.msLastUnsorted = now; - } - return this.groupsUnsorted; - } - - private getGroupsAlpha(now: number) { - const graph = app.graph as TLGraph; - if (!this.groupsSortedAlpha.length || now - this.msLastAlpha > this.msThreshold) { - this.groupsSortedAlpha = [...this.getGroupsUnsorted(now)].sort((a, b) => { - return a.title.localeCompare(b.title); - }); - this.msLastAlpha = now; - } - return this.groupsSortedAlpha; - } - - private getGroupsPosition(now: number) { - const graph = app.graph as TLGraph; - if (!this.groupsSortedPosition.length || now - this.msLastPosition > this.msThreshold) { - this.groupsSortedPosition = [...this.getGroupsUnsorted(now)].sort((a, b) => { - // Sort by y, then x, clamped to 30. - const aY = Math.floor(a._pos[1] / 30); - const bY = Math.floor(b._pos[1] / 30); - if (aY == bY) { - const aX = Math.floor(a._pos[0] / 30); - const bX = Math.floor(b._pos[0] / 30); - return aX - bX; - } - return aY - bY; - }); - this.msLastPosition = now; - } - return this.groupsSortedPosition; - } - - getGroups(sort?: string) { - const now = +new Date(); - if (sort === "alphanumeric") { - return this.getGroupsAlpha(now); - } - if (sort === "position") { - return this.getGroupsPosition(now); - } - return this.getGroupsUnsorted(now); - } -} - -/** The FastGroupsService singleton. */ -export const SERVICE = new FastGroupsService(); +// / +// @ts-ignore +import { app } from "../../scripts/app.js"; +import type {BaseFastGroupsModeChanger} from './fast_groups_muter.js'; +import { + type LiteGraph as TLiteGraph, + type LGraph as TLGraph, + type LGraphCanvas as TLGraphCanvas, + LGraphGroup, + Vector4, +} from "typings/litegraph.js"; +import { + groupHasActiveNode, + } from "./utils_fast.js"; + +declare const LiteGraph: typeof TLiteGraph; + +/** + * A service that keeps global state that can be shared by multiple FastGroupsMuter or + * FastGroupsBypasser nodes rather than calculate it on it's own. + */ +class FastGroupsService { + private msThreshold = 400; + private msLastUnsorted = 0; + private msLastAlpha = 0; + private msLastPosition = 0; + + private groupsUnsorted: LGraphGroup[] = []; + private groupsSortedAlpha: LGraphGroup[] = []; + private groupsSortedPosition: LGraphGroup[] = []; + + private readonly fastGroupNodes: BaseFastGroupsModeChanger[] = []; + + private runScheduledForMs: number | null = null; + private runScheduleTimeout: number | null = null; + private runScheduleAnimation: number | null = null; + + private cachedNodeBoundings: { [key: number]: Vector4 } | null = null; + + constructor() { + // Don't need to do anything, wait until a signal. + } + + addFastGroupNode(node: BaseFastGroupsModeChanger) { + this.fastGroupNodes.push(node); + // Schedule it because the node may not be ready to refreshWidgets (like, when added it may + // not have cloned properties to filter against, etc.). + this.scheduleRun(8); + } + + removeFastGroupNode(node: BaseFastGroupsModeChanger) { + const index = this.fastGroupNodes.indexOf(node); + if (index > -1) { + this.fastGroupNodes.splice(index, 1); + } + // If we have no more group nodes, then clear out data; it could be because of a canvas clear. + if (!this.fastGroupNodes?.length) { + this.clearScheduledRun(); + this.groupsUnsorted = []; + this.groupsSortedAlpha = []; + this.groupsSortedPosition = []; + } + } + + private run() { + // We only run if we're scheduled, so if we're not, then bail. + if (!this.runScheduledForMs) { + return; + } + for (const node of this.fastGroupNodes) { + node.refreshWidgets(); + } + this.clearScheduledRun(); + this.scheduleRun(); + } + + private scheduleRun(ms = 500) { + // If we got a request for an immediate schedule and already have on scheduled for longer, then + // cancel the long one to expediate a fast one. + if (this.runScheduledForMs && ms < this.runScheduledForMs) { + this.clearScheduledRun(); + } + if (!this.runScheduledForMs && this.fastGroupNodes.length) { + this.runScheduledForMs = ms; + this.runScheduleTimeout = setTimeout(() => { + this.runScheduleAnimation = requestAnimationFrame(() => this.run()); + }, ms); + } + } + + private clearScheduledRun() { + this.runScheduleTimeout && clearTimeout(this.runScheduleTimeout); + this.runScheduleAnimation && cancelAnimationFrame(this.runScheduleAnimation); + this.runScheduleTimeout = null; + this.runScheduleAnimation = null; + this.runScheduledForMs = null; + } + + /** + * Returns the boundings for all nodes on the graph, then clears it after a short delay. This is + * to increase efficiency by caching the nodes' boundings when multiple groups are on the page. + */ + getBoundingsForAllNodes() { + if (!this.cachedNodeBoundings) { + this.cachedNodeBoundings = {}; + for (const node of app.graph._nodes) { + this.cachedNodeBoundings[node.id] = node.getBounding(); + } + setTimeout(() => { + this.cachedNodeBoundings = null; + }, 50); + } + return this.cachedNodeBoundings; + } + + /** + * This overrides `LGraphGroup.prototype.recomputeInsideNodes` to be much more efficient when + * calculating for many groups at once (only compute all nodes once in `getBoundingsForAllNodes`). + */ + recomputeInsideNodesForGroup(group: LGraphGroup) { + const cachedBoundings = this.getBoundingsForAllNodes(); + const nodes = group.graph._nodes; + group._nodes.length = 0; + + for (const node of nodes) { + const node_bounding = cachedBoundings[node.id]; + if (!node_bounding || !LiteGraph.overlapBounding(group._bounding, node_bounding)) { + continue; + } + group._nodes.push(node); + } + } + + /** + * Everything goes through getGroupsUnsorted, so we only get groups once. However, LiteGraph's + * `recomputeInsideNodes` is inefficient when calling multiple groups (it iterates over all nodes + * each time). So, we'll do our own dang thing, once. + */ + private getGroupsUnsorted(now: number) { + const canvas = app.canvas as TLGraphCanvas; + const graph = app.graph as TLGraph; + + if ( + // Don't recalculate nodes if we're moving a group (added by ComfyUI in app.js) + !canvas.selected_group_moving && + (!this.groupsUnsorted.length || now - this.msLastUnsorted > this.msThreshold) + ) { + this.groupsUnsorted = [...graph._groups]; + for (const group of this.groupsUnsorted) { + this.recomputeInsideNodesForGroup(group); + const _ = groupHasActiveNode(group); + } + this.msLastUnsorted = now; + } + return this.groupsUnsorted; + } + + private getGroupsAlpha(now: number) { + const graph = app.graph as TLGraph; + if (!this.groupsSortedAlpha.length || now - this.msLastAlpha > this.msThreshold) { + this.groupsSortedAlpha = [...this.getGroupsUnsorted(now)].sort((a, b) => { + return a.title.localeCompare(b.title); + }); + this.msLastAlpha = now; + } + return this.groupsSortedAlpha; + } + + private getGroupsPosition(now: number) { + const graph = app.graph as TLGraph; + if (!this.groupsSortedPosition.length || now - this.msLastPosition > this.msThreshold) { + this.groupsSortedPosition = [...this.getGroupsUnsorted(now)].sort((a, b) => { + // Sort by y, then x, clamped to 30. + const aY = Math.floor(a._pos[1] / 30); + const bY = Math.floor(b._pos[1] / 30); + if (aY == bY) { + const aX = Math.floor(a._pos[0] / 30); + const bX = Math.floor(b._pos[0] / 30); + return aX - bX; + } + return aY - bY; + }); + this.msLastPosition = now; + } + return this.groupsSortedPosition; + } + + getGroups(sort?: string) { + const now = +new Date(); + if (sort === "alphanumeric") { + return this.getGroupsAlpha(now); + } + if (sort === "position") { + return this.getGroupsPosition(now); + } + return this.getGroupsUnsorted(now); + } +} + +/** The FastGroupsService singleton. */ +export const SERVICE = new FastGroupsService(); diff --git a/src_web/comfyui/feature_group_fast_toggle.ts b/src_web/comfyui/feature_group_fast_toggle.ts index 1240f0f8..c06f4a4d 100644 --- a/src_web/comfyui/feature_group_fast_toggle.ts +++ b/src_web/comfyui/feature_group_fast_toggle.ts @@ -1,253 +1,254 @@ -import type { - LGraphCanvas as TLGraphCanvas, - LGraphGroup as TLGraphGroup, - LiteGraph as TLiteGraph, - LGraph as TLGraph, - AdjustedMouseEvent, - Vector2, -} from "typings/litegraph.js"; -import type { AdjustedMouseCustomEvent } from "typings/rgthree.js"; - -// @ts-ignore -import { app } from "../../scripts/app.js"; -import { rgthree } from "./rgthree.js"; -import { SERVICE as CONFIG_SERVICE } from "./config_service.js"; - -declare const LiteGraph: typeof TLiteGraph; -declare const LGraphCanvas: typeof TLGraphCanvas; - -const BTN_SIZE = 20; -const BTN_MARGIN: Vector2 = [6, 4]; -const BTN_SPACING = 8; -const BTN_GRID = BTN_SIZE / 8; - -const TOGGLE_TO_MODE = new Map([ - ["MUTE", LiteGraph.NEVER], - ["BYPASS", 4], -]); - -/** - * Determines if the user clicked on an fast header icon. - */ -function clickedOnToggleButton(e: AdjustedMouseEvent, group: TLGraphGroup): string | null { - const toggles = CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.toggles"); - const pos = group.pos; - const size = group.size; - for (let i = 0; i < toggles.length; i++) { - const toggle = toggles[i]; - if ( - LiteGraph.isInsideRectangle( - e.canvasX, - e.canvasY, - pos[0] + size[0] - (BTN_SIZE + BTN_MARGIN[0]) * (i + 1), - pos[1] + BTN_MARGIN[1], - BTN_SIZE, - BTN_SIZE, - ) - ) { - return toggle; - } - } - return null; -} - -/** - * Registers the GroupHeaderToggles which places a mute and/or bypass icons in groups headers for - * quick, single-click ability to mute/bypass. - */ -app.registerExtension({ - name: "rgthree.GroupHeaderToggles", - async setup() { - /** - * Handles a click on the icon area if the user has the extension enable from settings. - * Hooks into the already overriden mouse down processor from rgthree. - */ - rgthree.addEventListener("on-process-mouse-down", ((e: AdjustedMouseCustomEvent) => { - if (!CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled")) return; - - const canvas = app.canvas as TLGraphCanvas; - if (canvas.selected_group) { - const originalEvent = e.detail.originalEvent; - const group = canvas.selected_group; - const clickedOnToggle = clickedOnToggleButton(originalEvent, group) || ""; - const toggleMode = TOGGLE_TO_MODE.get(clickedOnToggle?.toLocaleUpperCase()); - if (toggleMode) { - group.recomputeInsideNodes(); - const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); - const isAllMuted = - !hasAnyActiveNodes && group._nodes.every((n) => n.mode === LiteGraph.NEVER); - const isAllBypassed = - !hasAnyActiveNodes && !isAllMuted && group._nodes.every((n) => n.mode === 4); - - let newMode: 0 | 1 | 2 | 3 | 4 = LiteGraph.ALWAYS; - if (toggleMode === LiteGraph.NEVER) { - newMode = isAllMuted ? LiteGraph.ALWAYS : LiteGraph.NEVER; - } else { - newMode = isAllBypassed ? LiteGraph.ALWAYS : 4; - } - for (const node of group._nodes) { - node.mode = newMode; - } - // Make it such that we're not then moving the group on drag. - canvas.selected_group = null; - canvas.dragging_canvas = false; - } - } - }) as EventListener); - - /** - * Overrides LiteGraph's Canvas method for drawingGroups and, after calling the original, checks - * that the user has enabled fast toggles and draws them on the top-right of the app.. - */ - const drawGroups = LGraphCanvas.prototype.drawGroups; - LGraphCanvas.prototype.drawGroups = function ( - canvasEl: HTMLCanvasElement, - ctx: CanvasRenderingContext2D, - ) { - drawGroups.apply(this, [...arguments] as any); - - if ( - !CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") || - !rgthree.lastAdjustedMouseEvent - ) { - return; - } - - const graph = app.graph as TLGraph; - - let groups: TLGraphGroup[]; - // Default to hover if not always. - if (CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always") { - const hoverGroup = graph.getGroupOnPos( - rgthree.lastAdjustedMouseEvent.canvasX, - rgthree.lastAdjustedMouseEvent.canvasY, - ); - groups = hoverGroup ? [hoverGroup] : []; - } else { - groups = graph._groups || []; - } - - if (!groups.length) { - return; - } - - const toggles = CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.toggles"); - - ctx.save(); - for (const group of groups || []) { - let anyActive = false; - let allMuted = !!group._nodes.length; - let allBypassed = allMuted; - - // Find the current state of the group's nodes. - for (const node of group._nodes) { - anyActive = anyActive || node.mode === LiteGraph.ALWAYS; - allMuted = allMuted && node.mode === LiteGraph.NEVER; - allBypassed = allBypassed && node.mode === 4; - if (anyActive || (!allMuted && !allBypassed)) { - break; - } - } - - // Display each toggle. - for (let i = 0; i < toggles.length; i++) { - const toggle = toggles[i]; - const on = toggle === "bypass" ? allBypassed : allMuted; - const pos = group._pos; - const size = group._size; - - ctx.fillStyle = ctx.strokeStyle = group.color || "#335"; - const x = pos[0] + size[0] - BTN_MARGIN[0] - BTN_SIZE - (BTN_SPACING + BTN_SIZE) * i; - const y = pos[1] + BTN_MARGIN[1]; - const midX = x + BTN_SIZE / 2; - const midY = y + BTN_SIZE / 2; - ctx.beginPath(); - ctx.lineJoin = "round"; - ctx.rect(x, y, BTN_SIZE, BTN_SIZE); - - ctx.lineWidth = 2; - if (toggle === "mute") { - ctx.lineJoin = "round"; - ctx.lineCap = "round"; - - if (on) { - ctx.stroke( - new Path2D(` - ${eyeFrame(midX, midY)} - ${eyeLashes(midX, midY)} - `), - ); - } else { - const radius = BTN_GRID * 1.5; - - // Eyeball fill - ctx.fill( - new Path2D(` - ${eyeFrame(midX, midY)} - ${eyeFrame(midX, midY, -1)} - ${circlePath(midX, midY, radius)} - ${circlePath(midX + BTN_GRID / 2, midY - BTN_GRID / 2, BTN_GRID * 0.375)} - `), - "evenodd", - ); - - // Eye Outline Stroke - ctx.stroke(new Path2D(`${eyeFrame(midX, midY)} ${eyeFrame(midX, midY, -1)}`)); - - // Eye lashes (faded) - ctx.globalAlpha = this.editor_alpha * 0.5; - ctx.stroke(new Path2D(`${eyeLashes(midX, midY)} ${eyeLashes(midX, midY, -1)}`)); - ctx.globalAlpha = this.editor_alpha; - } - } else { - const lineChanges = on - ? `a ${BTN_GRID * 3}, ${BTN_GRID * 3} 0 1, 1 ${BTN_GRID * 3 * 2},0 - l ${BTN_GRID * 2.0} 0` - : `l ${BTN_GRID * 8} 0`; - - ctx.stroke( - new Path2D(` - M ${x} ${midY} - ${lineChanges} - M ${x + BTN_SIZE} ${midY} l -2 2 - M ${x + BTN_SIZE} ${midY} l -2 -2 - `), - ); - ctx.fill(new Path2D(`${circlePath(x + BTN_GRID * 3, midY, BTN_GRID * 1.8)}`)); - } - } - } - ctx.restore(); - }; - }, -}); - -function eyeFrame(midX: number, midY: number, yFlip = 1) { - return ` - M ${midX - BTN_SIZE / 2} ${midY} - c ${BTN_GRID * 1.5} ${yFlip * BTN_GRID * 2.5}, ${BTN_GRID * (8 - 1.5)} ${ - yFlip * BTN_GRID * 2.5 - }, ${BTN_GRID * 8} 0 - `; -} - -function eyeLashes(midX: number, midY: number, yFlip = 1) { - return ` - M ${midX - BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l -1.15 ${1.25 * yFlip} - M ${midX - BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l -0.90 ${1.5 * yFlip} - M ${midX - BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l -0.50 ${1.75 * yFlip} - M ${midX + BTN_GRID * 0.0} ${midY + yFlip * BTN_GRID * 2.0} l 0.00 ${2.0 * yFlip} - M ${midX + BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l 0.50 ${1.75 * yFlip} - M ${midX + BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l 0.90 ${1.5 * yFlip} - M ${midX + BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l 1.15 ${1.25 * yFlip} -`; -} - -function circlePath(cx: number, cy: number, radius: number) { - return ` - M ${cx} ${cy} - m ${radius}, 0 - a ${radius},${radius} 0 1, 1 -${radius * 2},0 - a ${radius},${radius} 0 1, 1 ${radius * 2},0 - `; -} +import type { + LGraphCanvas as TLGraphCanvas, + LGraphGroup as TLGraphGroup, + LiteGraph as TLiteGraph, + LGraph as TLGraph, + AdjustedMouseEvent, + Vector2, +} from "typings/litegraph.js"; +import type { AdjustedMouseCustomEvent } from "typings/rgthree.js"; + +// @ts-ignore +import { app } from "../../scripts/app.js"; +import { rgthree } from "./rgthree.js"; +import { SERVICE as CONFIG_SERVICE } from "./config_service.js"; +import { groupHasActiveNode } from "./utils_fast.js"; + +declare const LiteGraph: typeof TLiteGraph; +declare const LGraphCanvas: typeof TLGraphCanvas; + +const BTN_SIZE = 20; +const BTN_MARGIN: Vector2 = [6, 4]; +const BTN_SPACING = 8; +const BTN_GRID = BTN_SIZE / 8; + +const TOGGLE_TO_MODE = new Map([ + ["MUTE", LiteGraph.NEVER], + ["BYPASS", 4], +]); + +/** + * Determines if the user clicked on an fast header icon. + */ +function clickedOnToggleButton(e: AdjustedMouseEvent, group: TLGraphGroup): string | null { + const toggles = CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.toggles"); + const pos = group.pos; + const size = group.size; + for (let i = 0; i < toggles.length; i++) { + const toggle = toggles[i]; + if ( + LiteGraph.isInsideRectangle( + e.canvasX, + e.canvasY, + pos[0] + size[0] - (BTN_SIZE + BTN_MARGIN[0]) * (i + 1), + pos[1] + BTN_MARGIN[1], + BTN_SIZE, + BTN_SIZE, + ) + ) { + return toggle; + } + } + return null; +} + +/** + * Registers the GroupHeaderToggles which places a mute and/or bypass icons in groups headers for + * quick, single-click ability to mute/bypass. + */ +app.registerExtension({ + name: "rgthree.GroupHeaderToggles", + async setup() { + /** + * Handles a click on the icon area if the user has the extension enable from settings. + * Hooks into the already overriden mouse down processor from rgthree. + */ + rgthree.addEventListener("on-process-mouse-down", ((e: AdjustedMouseCustomEvent) => { + if (!CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled")) return; + + const canvas = app.canvas as TLGraphCanvas; + if (canvas.selected_group) { + const originalEvent = e.detail.originalEvent; + const group = canvas.selected_group; + const clickedOnToggle = clickedOnToggleButton(originalEvent, group) || ""; + const toggleMode = TOGGLE_TO_MODE.get(clickedOnToggle?.toLocaleUpperCase()); + if (toggleMode) { + group.recomputeInsideNodes(); + const hasAnyActiveNodes = groupHasActiveNode(group); + const isAllMuted = + !hasAnyActiveNodes && group._nodes.every((n) => n.mode === LiteGraph.NEVER); + const isAllBypassed = + !hasAnyActiveNodes && !isAllMuted && group._nodes.every((n) => n.mode === 4); + + let newMode: 0 | 1 | 2 | 3 | 4 = LiteGraph.ALWAYS; + if (toggleMode === LiteGraph.NEVER) { + newMode = isAllMuted ? LiteGraph.ALWAYS : LiteGraph.NEVER; + } else { + newMode = isAllBypassed ? LiteGraph.ALWAYS : 4; + } + for (const node of group._nodes) { + node.mode = newMode; + } + // Make it such that we're not then moving the group on drag. + canvas.selected_group = null; + canvas.dragging_canvas = false; + } + } + }) as EventListener); + + /** + * Overrides LiteGraph's Canvas method for drawingGroups and, after calling the original, checks + * that the user has enabled fast toggles and draws them on the top-right of the app.. + */ + const drawGroups = LGraphCanvas.prototype.drawGroups; + LGraphCanvas.prototype.drawGroups = function ( + canvasEl: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + ) { + drawGroups.apply(this, [...arguments] as any); + + if ( + !CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") || + !rgthree.lastAdjustedMouseEvent + ) { + return; + } + + const graph = app.graph as TLGraph; + + let groups: TLGraphGroup[]; + // Default to hover if not always. + if (CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always") { + const hoverGroup = graph.getGroupOnPos( + rgthree.lastAdjustedMouseEvent.canvasX, + rgthree.lastAdjustedMouseEvent.canvasY, + ); + groups = hoverGroup ? [hoverGroup] : []; + } else { + groups = graph._groups || []; + } + + if (!groups.length) { + return; + } + + const toggles = CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.toggles"); + + ctx.save(); + for (const group of groups || []) { + let anyActive = false; + let allMuted = !!group._nodes.length; + let allBypassed = allMuted; + + // Find the current state of the group's nodes. + for (const node of group._nodes) { + anyActive = anyActive || node.mode === LiteGraph.ALWAYS; + allMuted = allMuted && node.mode === LiteGraph.NEVER; + allBypassed = allBypassed && node.mode === 4; + if (anyActive || (!allMuted && !allBypassed)) { + break; + } + } + + // Display each toggle. + for (let i = 0; i < toggles.length; i++) { + const toggle = toggles[i]; + const on = toggle === "bypass" ? allBypassed : allMuted; + const pos = group._pos; + const size = group._size; + + ctx.fillStyle = ctx.strokeStyle = group.color || "#335"; + const x = pos[0] + size[0] - BTN_MARGIN[0] - BTN_SIZE - (BTN_SPACING + BTN_SIZE) * i; + const y = pos[1] + BTN_MARGIN[1]; + const midX = x + BTN_SIZE / 2; + const midY = y + BTN_SIZE / 2; + ctx.beginPath(); + ctx.lineJoin = "round"; + ctx.rect(x, y, BTN_SIZE, BTN_SIZE); + + ctx.lineWidth = 2; + if (toggle === "mute") { + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + + if (on) { + ctx.stroke( + new Path2D(` + ${eyeFrame(midX, midY)} + ${eyeLashes(midX, midY)} + `), + ); + } else { + const radius = BTN_GRID * 1.5; + + // Eyeball fill + ctx.fill( + new Path2D(` + ${eyeFrame(midX, midY)} + ${eyeFrame(midX, midY, -1)} + ${circlePath(midX, midY, radius)} + ${circlePath(midX + BTN_GRID / 2, midY - BTN_GRID / 2, BTN_GRID * 0.375)} + `), + "evenodd", + ); + + // Eye Outline Stroke + ctx.stroke(new Path2D(`${eyeFrame(midX, midY)} ${eyeFrame(midX, midY, -1)}`)); + + // Eye lashes (faded) + ctx.globalAlpha = this.editor_alpha * 0.5; + ctx.stroke(new Path2D(`${eyeLashes(midX, midY)} ${eyeLashes(midX, midY, -1)}`)); + ctx.globalAlpha = this.editor_alpha; + } + } else { + const lineChanges = on + ? `a ${BTN_GRID * 3}, ${BTN_GRID * 3} 0 1, 1 ${BTN_GRID * 3 * 2},0 + l ${BTN_GRID * 2.0} 0` + : `l ${BTN_GRID * 8} 0`; + + ctx.stroke( + new Path2D(` + M ${x} ${midY} + ${lineChanges} + M ${x + BTN_SIZE} ${midY} l -2 2 + M ${x + BTN_SIZE} ${midY} l -2 -2 + `), + ); + ctx.fill(new Path2D(`${circlePath(x + BTN_GRID * 3, midY, BTN_GRID * 1.8)}`)); + } + } + } + ctx.restore(); + }; + }, +}); + +function eyeFrame(midX: number, midY: number, yFlip = 1) { + return ` + M ${midX - BTN_SIZE / 2} ${midY} + c ${BTN_GRID * 1.5} ${yFlip * BTN_GRID * 2.5}, ${BTN_GRID * (8 - 1.5)} ${ + yFlip * BTN_GRID * 2.5 + }, ${BTN_GRID * 8} 0 + `; +} + +function eyeLashes(midX: number, midY: number, yFlip = 1) { + return ` + M ${midX - BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l -1.15 ${1.25 * yFlip} + M ${midX - BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l -0.90 ${1.5 * yFlip} + M ${midX - BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l -0.50 ${1.75 * yFlip} + M ${midX + BTN_GRID * 0.0} ${midY + yFlip * BTN_GRID * 2.0} l 0.00 ${2.0 * yFlip} + M ${midX + BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l 0.50 ${1.75 * yFlip} + M ${midX + BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l 0.90 ${1.5 * yFlip} + M ${midX + BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l 1.15 ${1.25 * yFlip} +`; +} + +function circlePath(cx: number, cy: number, radius: number) { + return ` + M ${cx} ${cy} + m ${radius}, 0 + a ${radius},${radius} 0 1, 1 -${radius * 2},0 + a ${radius},${radius} 0 1, 1 ${radius * 2},0 + `; +} diff --git a/src_web/comfyui/utils_fast.ts b/src_web/comfyui/utils_fast.ts new file mode 100644 index 00000000..e08e30cf --- /dev/null +++ b/src_web/comfyui/utils_fast.ts @@ -0,0 +1,14 @@ +import { type LGraphGroup, type LiteGraph as TLiteGraph } from "typings/litegraph.js"; + +declare const LiteGraph: typeof TLiteGraph; + +/** + * Adds `_rgthreeHasAnyActiveNode` augment to group to cache state + * + * @param group Group to check + * @returns true if any nodes are set to ALWAYS in the group + */ +export function groupHasActiveNode(group: LGraphGroup): boolean { + group._rgthreeHasAnyActiveNode = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); + return group._rgthreeHasAnyActiveNode; +} diff --git a/web/comfyui/fast_groups_muter.js b/web/comfyui/fast_groups_muter.js index 80bebaa7..0ed814c5 100644 --- a/web/comfyui/fast_groups_muter.js +++ b/web/comfyui/fast_groups_muter.js @@ -2,7 +2,8 @@ import { app } from "../../scripts/app.js"; import { RgthreeBaseVirtualNode } from "./base_node.js"; import { NodeTypesString } from "./constants.js"; import { SERVICE as FAST_GROUPS_SERVICE } from "./fast_groups_service.js"; -import { drawNodeWidget, fitString } from "./utils_canvas.js"; +import { RgthreeToggleNavWidget } from "./utils_widgets.js"; +import { groupHasActiveNode, } from "./utils_fast.js"; const PROPERTY_SORT = "sort"; const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet"; const PROPERTY_MATCH_COLORS = "matchColors"; @@ -42,6 +43,10 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { onRemoved() { FAST_GROUPS_SERVICE.removeFastGroupNode(this); } + get showNav() { + var _a; + return ((_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_SHOW_NAV]) !== false; + } refreshWidgets() { var _a, _b, _c, _d, _e, _f, _g; const canvas = app.canvas; @@ -130,99 +135,11 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { let widget = this.widgets.find((w) => w.name === widgetName); if (!widget) { this.tempSize = [...this.size]; - widget = this.addCustomWidget({ - name: "RGTHREE_TOGGLE_AND_NAV", - label: "", - value: false, - disabled: false, - options: { on: "yes", off: "no" }, - draw: function (ctx, node, width, posY, height) { - var _a; - const widgetData = drawNodeWidget(ctx, { - width, - height, - posY, - }); - const showNav = ((_a = node.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_SHOW_NAV]) !== false; - let currentX = widgetData.width - widgetData.margin; - if (!widgetData.lowQuality && showNav) { - currentX -= 7; - const midY = widgetData.posY + widgetData.height * 0.5; - ctx.fillStyle = ctx.strokeStyle = "#89A"; - ctx.lineJoin = "round"; - ctx.lineCap = "round"; - const arrow = new Path2D(`M${currentX} ${midY} l -7 6 v -3 h -7 v -6 h 7 v -3 z`); - ctx.fill(arrow); - ctx.stroke(arrow); - currentX -= 14; - currentX -= 7; - ctx.strokeStyle = widgetData.colorOutline; - ctx.stroke(new Path2D(`M ${currentX} ${widgetData.posY} v ${widgetData.height}`)); - } - else if (widgetData.lowQuality && showNav) { - currentX -= 28; - } - currentX -= 7; - ctx.fillStyle = this.value ? "#89A" : "#333"; - ctx.beginPath(); - const toggleRadius = height * 0.36; - ctx.arc(currentX - toggleRadius, posY + height * 0.5, toggleRadius, 0, Math.PI * 2); - ctx.fill(); - currentX -= toggleRadius * 2; - if (!widgetData.lowQuality) { - currentX -= 4; - ctx.textAlign = "right"; - ctx.fillStyle = this.value ? widgetData.colorText : widgetData.colorTextSecondary; - const label = this.label || this.name; - const toggleLabelOn = this.options.on || "true"; - const toggleLabelOff = this.options.off || "false"; - ctx.fillText(this.value ? toggleLabelOn : toggleLabelOff, currentX, posY + height * 0.7); - currentX -= Math.max(ctx.measureText(toggleLabelOn).width, ctx.measureText(toggleLabelOff).width); - currentX -= 7; - ctx.textAlign = "left"; - let maxLabelWidth = widgetData.width - widgetData.margin - 10 - (widgetData.width - currentX); - if (label != null) { - ctx.fillText(fitString(ctx, label, maxLabelWidth), widgetData.margin + 10, posY + height * 0.7); - } - } - }, - serializeValue(serializedNode, widgetIndex) { - return this.value; - }, - mouse(event, pos, node) { - var _a, _b, _c; - if (event.type == "pointerdown") { - if (((_a = node.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_SHOW_NAV]) !== false && - pos[0] >= node.size[0] - 15 - 28 - 1) { - const canvas = app.canvas; - const lowQuality = (((_b = canvas.ds) === null || _b === void 0 ? void 0 : _b.scale) || 1) <= 0.5; - if (!lowQuality) { - canvas.centerOnNode(group); - const zoomCurrent = ((_c = canvas.ds) === null || _c === void 0 ? void 0 : _c.scale) || 1; - const zoomX = canvas.canvas.width / group._size[0] - 0.02; - const zoomY = canvas.canvas.height / group._size[1] - 0.02; - canvas.setZoom(Math.min(zoomCurrent, zoomX, zoomY), [ - canvas.canvas.width / 2, - canvas.canvas.height / 2, - ]); - canvas.setDirty(true, true); - } - } - else { - this.value = !this.value; - setTimeout(() => { - var _a; - (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, this.value, app.canvas, node, pos, event); - }, 20); - } - } - return true; - }, - }); + widget = this.addCustomWidget(new RgthreeToggleNavWidget(group, () => this.showNav)); widget.doModeChange = (force, skipOtherNodeCheck) => { var _a, _b, _c; group.recomputeInsideNodes(); - const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); + const hasAnyActiveNodes = groupHasActiveNode(group); let newValue = force != null ? force : !hasAnyActiveNodes; if (skipOtherNodeCheck !== true) { if (newValue && ((_b = (_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_RESTRICTION]) === null || _b === void 0 ? void 0 : _b.includes(" one"))) { @@ -237,7 +154,6 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { for (const node of group._nodes) { node.mode = (newValue ? this.modeOn : this.modeOff); } - group._rgthreeHasAnyActiveNode = newValue; widget.value = newValue; app.graph.setDirtyCanvas(true, false); }; diff --git a/web/comfyui/fast_groups_service.js b/web/comfyui/fast_groups_service.js index 89e64de9..345e471d 100644 --- a/web/comfyui/fast_groups_service.js +++ b/web/comfyui/fast_groups_service.js @@ -1,4 +1,5 @@ import { app } from "../../scripts/app.js"; +import { groupHasActiveNode, } from "./utils_fast.js"; class FastGroupsService { constructor() { this.msThreshold = 400; @@ -91,7 +92,7 @@ class FastGroupsService { this.groupsUnsorted = [...graph._groups]; for (const group of this.groupsUnsorted) { this.recomputeInsideNodesForGroup(group); - group._rgthreeHasAnyActiveNode = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); + const _ = groupHasActiveNode(group); } this.msLastUnsorted = now; } diff --git a/web/comfyui/feature_group_fast_toggle.js b/web/comfyui/feature_group_fast_toggle.js index 5cd358ab..b77955be 100644 --- a/web/comfyui/feature_group_fast_toggle.js +++ b/web/comfyui/feature_group_fast_toggle.js @@ -1,6 +1,7 @@ import { app } from "../../scripts/app.js"; import { rgthree } from "./rgthree.js"; import { SERVICE as CONFIG_SERVICE } from "./config_service.js"; +import { groupHasActiveNode } from "./utils_fast.js"; const BTN_SIZE = 20; const BTN_MARGIN = [6, 4]; const BTN_SPACING = 8; @@ -35,7 +36,7 @@ app.registerExtension({ const toggleMode = TOGGLE_TO_MODE.get(clickedOnToggle === null || clickedOnToggle === void 0 ? void 0 : clickedOnToggle.toLocaleUpperCase()); if (toggleMode) { group.recomputeInsideNodes(); - const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); + const hasAnyActiveNodes = groupHasActiveNode(group); const isAllMuted = !hasAnyActiveNodes && group._nodes.every((n) => n.mode === LiteGraph.NEVER); const isAllBypassed = !hasAnyActiveNodes && !isAllMuted && group._nodes.every((n) => n.mode === 4); let newMode = LiteGraph.ALWAYS; diff --git a/web/comfyui/utils_fast.js b/web/comfyui/utils_fast.js new file mode 100644 index 00000000..c78a55cf --- /dev/null +++ b/web/comfyui/utils_fast.js @@ -0,0 +1,4 @@ +export function groupHasActiveNode(group) { + group._rgthreeHasAnyActiveNode = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); + return group._rgthreeHasAnyActiveNode; +} diff --git a/web/comfyui/utils_widgets.js b/web/comfyui/utils_widgets.js index 70faea7b..21f79079 100644 --- a/web/comfyui/utils_widgets.js +++ b/web/comfyui/utils_widgets.js @@ -247,3 +247,93 @@ export class RgthreeLabelWidget { return true; } } +export class RgthreeToggleNavWidget { + constructor(node, showNav) { + this.node = node; + this.showNav = showNav; + this.name = "RGTHREE_TOGGLE_AND_NAV"; + this.label = ""; + this.value = false; + this.disabled = false; + this.options = { on: "yes", off: "no" }; + } + draw(ctx, node, width, posY, height) { + const widgetData = drawNodeWidget(ctx, { + width, + height, + posY, + }); + let currentX = widgetData.width - widgetData.margin; + if (!widgetData.lowQuality && this.showNav()) { + currentX -= 7; + const midY = widgetData.posY + widgetData.height * 0.5; + ctx.fillStyle = ctx.strokeStyle = "#89A"; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + const arrow = new Path2D(`M${currentX} ${midY} l -7 6 v -3 h -7 v -6 h 7 v -3 z`); + ctx.fill(arrow); + ctx.stroke(arrow); + currentX -= 14; + currentX -= 7; + ctx.strokeStyle = widgetData.colorOutline; + ctx.stroke(new Path2D(`M ${currentX} ${widgetData.posY} v ${widgetData.height}`)); + } + else if (widgetData.lowQuality && this.showNav()) { + currentX -= 28; + } + currentX -= 7; + ctx.fillStyle = this.value ? "#89A" : "#333"; + ctx.beginPath(); + const toggleRadius = height * 0.36; + ctx.arc(currentX - toggleRadius, posY + height * 0.5, toggleRadius, 0, Math.PI * 2); + ctx.fill(); + currentX -= toggleRadius * 2; + if (!widgetData.lowQuality) { + currentX -= 4; + ctx.textAlign = "right"; + ctx.fillStyle = this.value ? widgetData.colorText : widgetData.colorTextSecondary; + const label = this.label || this.name; + const toggleLabelOn = this.options.on || "true"; + const toggleLabelOff = this.options.off || "false"; + ctx.fillText(this.value ? toggleLabelOn : toggleLabelOff, currentX, posY + height * 0.7); + currentX -= Math.max(ctx.measureText(toggleLabelOn).width, ctx.measureText(toggleLabelOff).width); + currentX -= 7; + ctx.textAlign = "left"; + let maxLabelWidth = widgetData.width - widgetData.margin - 10 - (widgetData.width - currentX); + if (label != null) { + ctx.fillText(fitString(ctx, label, maxLabelWidth), widgetData.margin + 10, posY + height * 0.7); + } + } + } + serializeValue(serializedNode, widgetIndex) { + return this.value; + } + mouse(event, pos, selfNode) { + var _a, _b; + if (event.type == "pointerdown") { + if (this.showNav() && pos[0] >= selfNode.size[0] - 15 - 28 - 1) { + const canvas = app.canvas; + const lowQuality = (((_a = canvas.ds) === null || _a === void 0 ? void 0 : _a.scale) || 1) <= 0.5; + if (!lowQuality) { + canvas.centerOnNode(this.node); + const zoomCurrent = ((_b = canvas.ds) === null || _b === void 0 ? void 0 : _b.scale) || 1; + const zoomX = canvas.canvas.width / this.node.size[0] - 0.02; + const zoomY = canvas.canvas.height / this.node.size[1] - 0.02; + canvas.setZoom(Math.min(zoomCurrent, zoomX, zoomY), [ + canvas.canvas.width / 2, + canvas.canvas.height / 2, + ]); + canvas.setDirty(true, true); + } + } + else { + this.value = !this.value; + setTimeout(() => { + var _a; + (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, this.value, app.canvas, selfNode, pos, event); + }, 20); + } + } + return true; + } +} From 1d7624baf5602f12570bafb21df0cf11dda11d86 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Thu, 18 Apr 2024 14:00:16 -0700 Subject: [PATCH 4/8] as any cleanup --- src_web/comfyui/base_node_mode_changer.ts | 12 ++++++------ src_web/comfyui/fast_groups_muter.ts | 21 ++++++++++----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src_web/comfyui/base_node_mode_changer.ts b/src_web/comfyui/base_node_mode_changer.ts index f2477046..37a336be 100644 --- a/src_web/comfyui/base_node_mode_changer.ts +++ b/src_web/comfyui/base_node_mode_changer.ts @@ -72,12 +72,12 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { widget.name = `Enable ${linkedNode.title}`; widget.options = {'on': 'yes', 'off': 'no'} widget.value = value; - (widget as any).doModeChange = (forceValue?: boolean, skipOtherNodeCheck?: boolean) => { + widget.doModeChange = (forceValue?: boolean, skipOtherNodeCheck?: boolean) => { let newValue = forceValue == null ? linkedNode.mode === this.modeOff : forceValue; if (skipOtherNodeCheck !== true) { if (newValue && this.properties?.['toggleRestriction']?.includes(' one')) { for (const widget of this.widgets) { - (widget as any).doModeChange(false, true); + widget.doModeChange?.(false, true); } } else if (!newValue && this.properties?.['toggleRestriction'] === 'always one') { newValue = this.widgets.every(w => !w.value || w === widget); @@ -87,7 +87,7 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { widget.value = newValue; } widget.callback = () => { - (widget as any).doModeChange(); + widget.doModeChange?.(); } if (forceValue != null) { linkedNode.mode = (forceValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4; @@ -95,13 +95,13 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { } forceWidgetOff(widget: IWidget, skipOtherNodeCheck?: boolean) { - (widget as any).doModeChange(false, skipOtherNodeCheck); + widget.doModeChange?.(false, skipOtherNodeCheck); } forceWidgetOn(widget: IWidget, skipOtherNodeCheck?: boolean) { - (widget as any).doModeChange(true, skipOtherNodeCheck); + widget.doModeChange?.(true, skipOtherNodeCheck); } forceWidgetToggle(widget: IWidget, skipOtherNodeCheck?: boolean) { - (widget as any).doModeChange(!widget.value, skipOtherNodeCheck); + widget.doModeChange?.(!widget.value, skipOtherNodeCheck); } diff --git a/src_web/comfyui/fast_groups_muter.ts b/src_web/comfyui/fast_groups_muter.ts index 9228cc98..e86e2c99 100644 --- a/src_web/comfyui/fast_groups_muter.ts +++ b/src_web/comfyui/fast_groups_muter.ts @@ -200,17 +200,16 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { // When we add a widget, litegraph is going to mess up the size, so we // store it so we can retrieve it in computeSize. Hacky.. this.tempSize = [...this.size]; - widget = this.addCustomWidget( - new RgthreeToggleNavWidget(group, () => this.showNav), - ); - (widget as any).doModeChange = (force?: boolean, skipOtherNodeCheck?: boolean) => { + widget = this.addCustomWidget(new RgthreeToggleNavWidget(group, () => this.showNav)); + + widget.doModeChange = (force?: boolean, skipOtherNodeCheck?: boolean) => { group.recomputeInsideNodes(); const hasAnyActiveNodes = groupHasActiveNode(group); let newValue = force != null ? force : !hasAnyActiveNodes; if (skipOtherNodeCheck !== true) { if (newValue && this.properties?.[PROPERTY_RESTRICTION]?.includes(" one")) { for (const widget of this.widgets) { - (widget as any).doModeChange(false, true); + widget.doModeChange?.(false, true); } } else if (!newValue && this.properties?.[PROPERTY_RESTRICTION] === "always one") { newValue = this.widgets.every((w) => !w.value || w === widget); @@ -223,7 +222,7 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { app.graph.setDirtyCanvas(true, false); }; widget.callback = () => { - (widget as any).doModeChange(); + widget?.doModeChange?.(); }; this.setSize(this.computeSize()); @@ -232,8 +231,8 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { widget.name = widgetName; this.setDirtyCanvas(true, false); } - if (widget.value != (group as any)._rgthreeHasAnyActiveNode) { - widget.value = (group as any)._rgthreeHasAnyActiveNode; + if (widget.value != group._rgthreeHasAnyActiveNode) { + widget.value = group._rgthreeHasAnyActiveNode; this.setDirtyCanvas(true, false); } if (this.widgets[index] !== widget) { @@ -271,12 +270,12 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { if (action === "Mute all" || action === "Bypass all") { const alwaysOne = this.properties?.[PROPERTY_RESTRICTION] === "always one"; for (const [index, widget] of this.widgets.entries()) { - (widget as any)?.doModeChange(alwaysOne && !index ? true : false, true); + widget.doModeChange?.(alwaysOne && !index ? true : false, true); } } else if (action === "Enable all") { const onlyOne = this.properties?.[PROPERTY_RESTRICTION].includes(" one"); for (const [index, widget] of this.widgets.entries()) { - (widget as any)?.doModeChange(onlyOne && index > 0 ? false : true, true); + widget.doModeChange?.(onlyOne && index > 0 ? false : true, true); } } else if (action === "Toggle all") { const onlyOne = this.properties?.[PROPERTY_RESTRICTION].includes(" one"); @@ -285,7 +284,7 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { // If you have only one, then we'll stop at the first. let newValue: boolean = onlyOne && foundOne ? false : !widget.value; foundOne = foundOne || newValue; - (widget as any)?.doModeChange(newValue, true); + widget.doModeChange?.(newValue, true); } // And if you have always one, then we'll flip the last if (!foundOne && this.properties?.[PROPERTY_RESTRICTION] === "always one") { From 6caf2c8d017b96b5fec245e5d4e875041663b01a Mon Sep 17 00:00:00 2001 From: DrJKL Date: Fri, 19 Apr 2024 00:36:32 -0700 Subject: [PATCH 5/8] Big refactor and adding the fast node muter --- src_web/comfyui/constants.ts | 1 + src_web/comfyui/fast_groups_muter.ts | 181 +++++++++---------------- src_web/comfyui/fast_groups_service.ts | 35 ++--- src_web/comfyui/fast_node_muter.ts | 147 ++++++++++++++++++++ src_web/comfyui/utils_fast.ts | 138 ++++++++++++++++++- src_web/comfyui/utils_widgets.ts | 16 ++- web/comfyui/base_node_mode_changer.js | 18 ++- web/comfyui/constants.js | 1 + web/comfyui/fast_groups_muter.js | 149 +++++++------------- web/comfyui/fast_groups_service.js | 32 ++--- web/comfyui/fast_node_muter.js | 108 +++++++++++++++ web/comfyui/utils_fast.js | 94 +++++++++++++ web/comfyui/utils_widgets.js | 6 +- 13 files changed, 650 insertions(+), 276 deletions(-) create mode 100644 src_web/comfyui/fast_node_muter.ts create mode 100644 web/comfyui/fast_node_muter.js diff --git a/src_web/comfyui/constants.ts b/src_web/comfyui/constants.ts index d3960855..3e3329d9 100644 --- a/src_web/comfyui/constants.ts +++ b/src_web/comfyui/constants.ts @@ -13,6 +13,7 @@ export const NodeTypesString = { NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'), FAST_MUTER: addRgthree('Fast Muter'), FAST_BYPASSER: addRgthree('Fast Bypasser'), + FAST_NODE_MUTER: addRgthree('Fast Node Muter'), FAST_GROUPS_MUTER: addRgthree('Fast Groups Muter'), FAST_GROUPS_BYPASSER: addRgthree('Fast Groups Bypasser'), FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'), diff --git a/src_web/comfyui/fast_groups_muter.ts b/src_web/comfyui/fast_groups_muter.ts index e86e2c99..3e1d0656 100644 --- a/src_web/comfyui/fast_groups_muter.ts +++ b/src_web/comfyui/fast_groups_muter.ts @@ -9,18 +9,19 @@ import { type LiteGraph as TLiteGraph, LGraphCanvas as TLGraphCanvas, Vector2, - SerializedLGraphNode, - IWidget, + type LGraphGroup, } from "typings/litegraph.js"; -import {SERVICE as FAST_GROUPS_SERVICE} from "./fast_groups_service.js"; -import { drawNodeWidget, fitString } from "./utils_canvas.js"; +import { SERVICE as FAST_GROUPS_SERVICE } from "./fast_groups_service.js"; import { RgthreeToggleNavWidget } from "./utils_widgets.js"; import { + filterByColor, + filterByTitle, groupHasActiveNode, + sortBy, + type SortType, } from "./utils_fast.js"; import { RgthreeBaseVirtualNodeConstructor } from "typings/rgthree.js"; -declare const LGraphCanvas: typeof TLGraphCanvas; declare const LiteGraph: typeof TLiteGraph; const PROPERTY_SORT = "sort"; @@ -103,136 +104,38 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { } refreshWidgets() { - const canvas = app.canvas as TLGraphCanvas; - let sort = this.properties?.[PROPERTY_SORT] || "position"; - let customAlphabet: string[] | null = null; - if (sort === "custom alphabet") { - const customAlphaStr = this.properties?.[PROPERTY_SORT_CUSTOM_ALPHA]?.replace(/\n/g, ""); - if (customAlphaStr && customAlphaStr.trim()) { - customAlphabet = customAlphaStr.includes(",") - ? customAlphaStr.toLocaleLowerCase().split(",") - : customAlphaStr.toLocaleLowerCase().trim().split(""); - } - if (!customAlphabet?.length) { - sort = "alphanumeric"; - customAlphabet = null; - } - } - + let sort: SortType = this.properties?.[PROPERTY_SORT] || "position"; const groups = [...FAST_GROUPS_SERVICE.getGroups(sort)]; // The service will return pre-sorted groups for alphanumeric and position. If this node has a // custom sort, then we need to sort it manually. - if (customAlphabet?.length) { - groups.sort((a, b) => { - let aIndex = -1; - let bIndex = -1; - // Loop and find indexes. As we're finding multiple, a single for loop is more efficient. - for (const [index, alpha] of customAlphabet!.entries()) { - aIndex = - aIndex < 0 ? (a.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : aIndex; - bIndex = - bIndex < 0 ? (b.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : bIndex; - if (aIndex > -1 && bIndex > -1) { - break; - } - } - // Now compare. - if (aIndex > -1 && bIndex > -1) { - const ret = aIndex - bIndex; - if (ret === 0) { - return a.title.localeCompare(b.title); - } - return ret; - } else if (aIndex > -1) { - return -1; - } else if (bIndex > -1) { - return 1; - } - return a.title.localeCompare(b.title); - }); - } + const alphaSorted = sortBy(groups, { + customAlphabet: this.properties?.[PROPERTY_SORT_CUSTOM_ALPHA]?.replace(/\n/g, ""), + sort, + }); // See if we're filtering by colors, and match against the built-in keywords and actuial hex // values. - let filterColors = ( - (this.properties?.[PROPERTY_MATCH_COLORS] as string)?.split(",") || [] - ).filter((c) => c.trim()); - if (filterColors.length) { - filterColors = filterColors.map((color) => { - color = color.trim().toLocaleLowerCase(); - if (LGraphCanvas.node_colors[color]) { - color = LGraphCanvas.node_colors[color]!.groupcolor; - } - color = color.replace("#", "").toLocaleLowerCase(); - if (color.length === 3) { - color = color.replace(/(.)(.)(.)/, "$1$1$2$2$3$3"); - } - return `#${color}`; - }); - } + const colorFiltered = filterByColor(alphaSorted, { + matchColors: this.properties?.[PROPERTY_MATCH_COLORS], + nodeColorOption: "groupcolor", + }); + + const titleFiltered = filterByTitle(colorFiltered, { + matchTitle: this.properties?.[PROPERTY_MATCH_TITLE]?.trim(), + }); // Go over the groups let index = 0; - for (const group of groups) { - if (filterColors.length) { - let groupColor = group.color.replace("#", "").trim().toLocaleLowerCase(); - if (groupColor.length === 3) { - groupColor = groupColor.replace(/(.)(.)(.)/, "$1$1$2$2$3$3"); - } - groupColor = `#${groupColor}`; - if (!filterColors.includes(groupColor)) { - continue; - } - } - if (this.properties?.[PROPERTY_MATCH_TITLE]?.trim()) { - try { - if (!new RegExp(this.properties[PROPERTY_MATCH_TITLE], "i").exec(group.title)) { - continue; - } - } catch (e) { - console.error(e); - continue; - } - } + for (const group of titleFiltered) { + this.widgets = this.widgets || []; const widgetName = `Enable ${group.title}`; - let widget = this.widgets.find((w) => w.name === widgetName); - if (!widget) { - // When we add a widget, litegraph is going to mess up the size, so we - // store it so we can retrieve it in computeSize. Hacky.. - this.tempSize = [...this.size]; - widget = this.addCustomWidget(new RgthreeToggleNavWidget(group, () => this.showNav)); - - widget.doModeChange = (force?: boolean, skipOtherNodeCheck?: boolean) => { - group.recomputeInsideNodes(); - const hasAnyActiveNodes = groupHasActiveNode(group); - let newValue = force != null ? force : !hasAnyActiveNodes; - if (skipOtherNodeCheck !== true) { - if (newValue && this.properties?.[PROPERTY_RESTRICTION]?.includes(" one")) { - for (const widget of this.widgets) { - widget.doModeChange?.(false, true); - } - } else if (!newValue && this.properties?.[PROPERTY_RESTRICTION] === "always one") { - newValue = this.widgets.every((w) => !w.value || w === widget); - } - } - for (const node of group._nodes) { - node.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4; - } - widget!.value = newValue; - app.graph.setDirtyCanvas(true, false); - }; - widget.callback = () => { - widget?.doModeChange?.(); - }; - - this.setSize(this.computeSize()); - } + const widget = this.getOrCreateToggleWidget(group, widgetName); if (widget.name != widgetName) { widget.name = widgetName; this.setDirtyCanvas(true, false); } if (widget.value != group._rgthreeHasAnyActiveNode) { - widget.value = group._rgthreeHasAnyActiveNode; + widget.value = group._rgthreeHasAnyActiveNode ?? false; this.setDirtyCanvas(true, false); } if (this.widgets[index] !== widget) { @@ -249,6 +152,44 @@ export abstract class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { } } + private getOrCreateToggleWidget(group: LGraphGroup, widgetName: string): RgthreeToggleNavWidget { + const existingWidget = this.widgets.find((w) => w.name === widgetName); + if (existingWidget && existingWidget instanceof RgthreeToggleNavWidget) { + return existingWidget; + } + // When we add a widget, litegraph is going to mess up the size, so we + // store it so we can retrieve it in computeSize. Hacky.. + this.tempSize = [...this.size]; + const widget = this.addCustomWidget( + new RgthreeToggleNavWidget( + group, + () => this.showNav, + (force?: boolean, skipOtherNodeCheck?: boolean) => { + group.recomputeInsideNodes(); + const hasAnyActiveNodes = groupHasActiveNode(group); + let newValue = force != null ? force : !hasAnyActiveNodes; + if (skipOtherNodeCheck !== true) { + if (newValue && this.properties?.[PROPERTY_RESTRICTION]?.includes(" one")) { + for (const widget of this.widgets) { + widget.doModeChange?.(false, true); + } + } else if (!newValue && this.properties?.[PROPERTY_RESTRICTION] === "always one") { + newValue = this.widgets.every((w) => !w.value || w === widget); + } + } + for (const node of group._nodes) { + node.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4; + } + widget!.value = newValue; + app.graph.setDirtyCanvas(true, false); + }, + ), + ); + + this.setSize(this.computeSize()); + return widget; + } + override computeSize(out?: Vector2) { let size = super.computeSize(out); if (this.tempSize) { diff --git a/src_web/comfyui/fast_groups_service.ts b/src_web/comfyui/fast_groups_service.ts index a5a54b30..0987b0f1 100644 --- a/src_web/comfyui/fast_groups_service.ts +++ b/src_web/comfyui/fast_groups_service.ts @@ -9,9 +9,7 @@ import { LGraphGroup, Vector4, } from "typings/litegraph.js"; -import { - groupHasActiveNode, - } from "./utils_fast.js"; +import { groupHasActiveNode, sortBy } from "./utils_fast.js"; declare const LiteGraph: typeof TLiteGraph; @@ -156,30 +154,16 @@ class FastGroupsService { } private getGroupsAlpha(now: number) { - const graph = app.graph as TLGraph; if (!this.groupsSortedAlpha.length || now - this.msLastAlpha > this.msThreshold) { - this.groupsSortedAlpha = [...this.getGroupsUnsorted(now)].sort((a, b) => { - return a.title.localeCompare(b.title); - }); + this.groupsSortedAlpha = sortBy(this.getGroupsUnsorted(now), { sort: "alphanumeric" }); this.msLastAlpha = now; } return this.groupsSortedAlpha; } private getGroupsPosition(now: number) { - const graph = app.graph as TLGraph; if (!this.groupsSortedPosition.length || now - this.msLastPosition > this.msThreshold) { - this.groupsSortedPosition = [...this.getGroupsUnsorted(now)].sort((a, b) => { - // Sort by y, then x, clamped to 30. - const aY = Math.floor(a._pos[1] / 30); - const bY = Math.floor(b._pos[1] / 30); - if (aY == bY) { - const aX = Math.floor(a._pos[0] / 30); - const bX = Math.floor(b._pos[0] / 30); - return aX - bX; - } - return aY - bY; - }); + this.groupsSortedPosition = sortBy(this.getGroupsUnsorted(now), { sort: "position" }); this.msLastPosition = now; } return this.groupsSortedPosition; @@ -187,13 +171,14 @@ class FastGroupsService { getGroups(sort?: string) { const now = +new Date(); - if (sort === "alphanumeric") { - return this.getGroupsAlpha(now); - } - if (sort === "position") { - return this.getGroupsPosition(now); + switch (sort) { + case "alphanumeric": + return this.getGroupsAlpha(now); + case "position": + return this.getGroupsPosition(now); + default: + return this.getGroupsUnsorted(now); } - return this.getGroupsUnsorted(now); } } diff --git a/src_web/comfyui/fast_node_muter.ts b/src_web/comfyui/fast_node_muter.ts new file mode 100644 index 00000000..e0a83969 --- /dev/null +++ b/src_web/comfyui/fast_node_muter.ts @@ -0,0 +1,147 @@ +import { LGraph, SerializedLGraphNode } from "typings/litegraph.js"; +// @ts-ignore +import { app } from "../../scripts/app.js"; +import { RgthreeBaseNode } from "./base_node.js"; +import { NodeTypesString } from "./constants.js"; +import { FastGroupsMuter } from "./fast_groups_muter.js"; +import { + type LGraphNode, + type LiteGraph as TLiteGraph, + LGraphCanvas as TLGraphCanvas, +} from "typings/litegraph.js"; +import { RgthreeToggleNavWidget } from "./utils_widgets.js"; +import { filterByColor, filterByTitle, sortBy, type SortType } from "./utils_fast.js"; + +declare const LiteGraph: typeof TLiteGraph; +declare const LGraphCanvas: typeof TLGraphCanvas; + +const PROPERTY_SORT = "sort"; +const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet"; +const PROPERTY_MATCH_COLORS = "matchColors"; +const PROPERTY_MATCH_TITLE = "matchTitle"; +const PROPERTY_RESTRICTION = "toggleRestriction"; + +/** + * Fast Muter implementation that looks for nodes in the workflow and adds toggles to mute them. + */ +export class FastNodeMuter extends FastGroupsMuter { + static override type = NodeTypesString.FAST_NODE_MUTER; + static override title = NodeTypesString.FAST_NODE_MUTER; + + override readonly modeOn = LiteGraph.ALWAYS; + override readonly modeOff = LiteGraph.NEVER; + + constructor(title = FastNodeMuter.title) { + super(title); + } + + static override setUp(clazz: new (title?: string) => T) { + LiteGraph.registerNodeType((clazz as any).type, clazz); + (clazz as any).category = (clazz as any)._category; + } + + override refreshWidgets() { + const graph: LGraph = app.graph; + let sort: SortType = this.properties?.[PROPERTY_SORT] || "position"; + const nodes: LGraphNode[] = [...(graph._nodes ?? [])].filter((n) => !n.isVirtualNode); + // The service will return pre-sorted groups for alphanumeric and position. If this node has a + // custom sort, then we need to sort it manually. + const alphaSorted = sortBy(nodes, { + customAlphabet: this.properties?.[PROPERTY_SORT_CUSTOM_ALPHA]?.replace(/\n/g, ""), + sort, + }); + + // See if we're filtering by colors, and match against the built-in keywords and actuial hex + // values. + const colorFiltered = filterByColor(alphaSorted, { + matchColors: this.properties?.[PROPERTY_MATCH_COLORS], + nodeColorOption: "groupcolor", + }); + + const titleFiltered = filterByTitle(colorFiltered, { + matchTitle: this.properties?.[PROPERTY_MATCH_TITLE]?.trim(), + }); + + // Go over the nodes + let index = 0; + for (const node of titleFiltered) { + this.widgets = this.widgets || []; + const nodeTitle = nodeWithIndex(nodes, node); + const widgetName = `Enable ${nodeTitle}`; + let widget = this.getOrCreateWidget(node, widgetName); + if (widget.name != widgetName) { + widget.name = widgetName; + this.setDirtyCanvas(true, false); + } + const nodeActive = node.mode === LiteGraph.ALWAYS; + if (widget.value != nodeActive) { + widget.value = nodeActive; + this.setDirtyCanvas(true, false); + } + if (this.widgets[index] !== widget) { + const oldIndex = this.widgets.findIndex((w) => w === widget); + this.widgets.splice(index, 0, this.widgets.splice(oldIndex, 1)[0]!); + this.setDirtyCanvas(true, false); + } + index++; + } + + // Everything should now be in order, so let's remove all remaining widgets. + while ((this.widgets || [])[index]) { + this.removeWidget(index++); + } + } + + private getOrCreateWidget(node: LGraphNode, widgetName: string): RgthreeToggleNavWidget { + const existingWidget = this.widgets.find((w) => w.name === widgetName); + if (existingWidget && existingWidget instanceof RgthreeToggleNavWidget) { + return existingWidget; + } + // When we add a widget, litegraph is going to mess up the size, so we + // store it so we can retrieve it in computeSize. Hacky.. + this.tempSize = [...this.size]; + const widget = this.addCustomWidget( + new RgthreeToggleNavWidget( + node, + () => this.showNav, + (force?: boolean, skipOtherNodeCheck?: boolean) => { + const hasAnyActiveNodes = node.mode === LiteGraph.ALWAYS; + let newValue = force != null ? force : !hasAnyActiveNodes; + if (skipOtherNodeCheck !== true) { + if (newValue && this.properties?.[PROPERTY_RESTRICTION]?.includes(" one")) { + for (const widget of this.widgets) { + widget.doModeChange?.(false, true); + } + } else if (!newValue && this.properties?.[PROPERTY_RESTRICTION] === "always one") { + newValue = this.widgets.every((w) => !w.value || w === widget); + } + } + node.mode = (newValue ? this.modeOn : this.modeOff) as 1 | 2 | 3 | 4; + widget!.value = newValue; + app.graph.setDirtyCanvas(true, false); + }, + ), + ); + + this.setSize(this.computeSize()); + return widget; + } +} + +app.registerExtension({ + name: "rgthree.FastNodeMuter", + registerCustomNodes() { + FastNodeMuter.setUp(FastNodeMuter); + }, + loadedGraphNode(node: LGraphNode) { + if (node.type == FastNodeMuter.title) { + (node as FastNodeMuter).tempSize = [...node.size]; + } + }, +}); + +function nodeWithIndex(nodes: LGraphNode[], node: LGraphNode): string { + const { title } = node; + const sameNameNodes = nodes.filter((n) => n.title === title); + return `${title} ${sameNameNodes.indexOf(node)}`; +} diff --git a/src_web/comfyui/utils_fast.ts b/src_web/comfyui/utils_fast.ts index e08e30cf..6098f72a 100644 --- a/src_web/comfyui/utils_fast.ts +++ b/src_web/comfyui/utils_fast.ts @@ -1,7 +1,143 @@ -import { type LGraphGroup, type LiteGraph as TLiteGraph } from "typings/litegraph.js"; +import { + type LiteGraph as TLiteGraph, + type LGraphGroup, + type Vector2, + type LGraphCanvas as TLGraphCanvas, +} from "typings/litegraph.js"; +declare const LGraphCanvas: typeof TLGraphCanvas; declare const LiteGraph: typeof TLiteGraph; +type HasTitle = { title: string }; +type HasPos = { pos: Vector2 }; +type HasColor = { color: string }; // Possible to extend to bgcolor and boxcolor + +type PosSortOptions = { sort: "position" }; +type TitleSortOptions = { sort: "custom alphabet" | "alphanumeric"; customAlphabet?: string }; +type SortOptions = PosSortOptions | TitleSortOptions; + +type GraphCanvasColors = keyof (typeof LGraphCanvas.node_colors)[string]; +type ColorFilterOptions = { matchColors?: string; nodeColorOption: GraphCanvasColors }; +type TitleFilterOptions = { matchTitle?: string }; + +export type SortType = "custom alphabet" | "alphanumeric" | "position"; + +export function sortBy(items: T[], options: SortOptions): T[] { + const { sort } = options; + + if (sort === "position") { + // Assumes sorted + return sortByPosition(items); + } + + const { customAlphabet: customAlphaStr } = options; + if (!customAlphaStr || sort === "alphanumeric") { + return [...items].sort((a, b) => a.title.localeCompare(b.title)); + } + + let customAlphabet: string[] = customAlphaStr.includes(",") + ? customAlphaStr.toLocaleLowerCase().split(",") + : customAlphaStr.toLocaleLowerCase().trim().split(""); + + if (!customAlphabet.length) { + return items; + } + + items.sort((a, b) => { + let aIndex = -1; + let bIndex = -1; + // Loop and find indexes. As we're finding multiple, a single for loop is more efficient. + for (const [index, alpha] of customAlphabet!.entries()) { + aIndex = aIndex < 0 ? (a.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : aIndex; + bIndex = bIndex < 0 ? (b.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : bIndex; + if (aIndex > -1 && bIndex > -1) { + break; + } + } + // Now compare. + if (aIndex > -1 && bIndex > -1) { + const ret = aIndex - bIndex; + if (ret !== 0) { + return ret; + } + return a.title.localeCompare(b.title); + } + if (aIndex > -1) { + return -1; + } + if (bIndex > -1) { + return 1; + } + return a.title.localeCompare(b.title); + }); + return items; +} + +function sortByPosition(items: T[]): T[] { + return items.sort((a, b) => { + // Sort by y, then x, clamped to 30. + const aY = Math.floor(a.pos[1] / 30); + const bY = Math.floor(b.pos[1] / 30); + if (aY != bY) { + return aY - bY; + } + const aX = Math.floor(a.pos[0] / 30); + const bX = Math.floor(b.pos[0] / 30); + return aX - bX; + }); +} + +function normalizeColor(color: string): string { + const trimmed = color.replace("#", "").trim().toLocaleLowerCase(); + const fullHex = trimmed.length === 3 ? trimmed.replace(/(.)(.)(.)/, "$1$1$2$2$3$3") : trimmed; + return `#${fullHex}`; +} + +export function filterByColor( + items: T[], + options: ColorFilterOptions = { nodeColorOption: "groupcolor" }, +): T[] { + const { matchColors, nodeColorOption } = options; + if (!matchColors) { + return items; + } + + const filterColors = (matchColors.split(",") ?? []) + .filter((c) => c.trim()) + .map((color) => color.trim().toLocaleLowerCase()) + .map((color) => LGraphCanvas.node_colors[color]?.[nodeColorOption] ?? color) + .map((color) => normalizeColor(color)); + + if (!filterColors.length) { + return items; + } + + return items.filter((item) => { + let color = normalizeColor(item.color); + return filterColors.includes(color); + }); +} + +export function filterByTitle( + items: T[], + options: TitleFilterOptions = {}, +): T[] { + const { matchTitle } = options; + if (!matchTitle) { + return items; + } + + const matchPattern = new RegExp(matchTitle, "i"); + return items.filter((item) => { + try { + return matchPattern.exec(item.title); + } catch (e) { + console.error(e); + } + return true; // Default to include on failure + }); +} + /** * Adds `_rgthreeHasAnyActiveNode` augment to group to cache state * diff --git a/src_web/comfyui/utils_widgets.ts b/src_web/comfyui/utils_widgets.ts index 66a00437..763c2ed0 100644 --- a/src_web/comfyui/utils_widgets.ts +++ b/src_web/comfyui/utils_widgets.ts @@ -8,8 +8,8 @@ import type { Vector2, AdjustedMouseEvent, Vector4, - WidgetCallback, SerializedLGraphNode, + LGraphCanvas, } from "../typings/litegraph.js"; import { drawNodeWidget, drawRoundedRectangle, fitString, isLowQuality } from "./utils_canvas.js"; @@ -412,14 +412,24 @@ export class RgthreeToggleNavWidget implements IWidget { label = ""; value = false; disabled = false; - options = { on: "yes", off: "no" }; - callback?: WidgetCallback>; + readonly options = { on: "yes", off: "no" }; constructor( private readonly node: { pos: Vector2; size: Vector2 }, private readonly showNav: () => boolean, + readonly doModeChange: (force?: boolean, skipOtherNodeCheck?: boolean) => void, ) {} + callback( + value: boolean, + graphCanvas: LGraphCanvas, + node: LGraphNode, + pos: Vector2, + event?: MouseEvent, + ) { + this.doModeChange(); + } + draw( ctx: CanvasRenderingContext2D, node: LGraphNode, diff --git a/web/comfyui/base_node_mode_changer.js b/web/comfyui/base_node_mode_changer.js index afec9ced..9bd701a2 100644 --- a/web/comfyui/base_node_mode_changer.js +++ b/web/comfyui/base_node_mode_changer.js @@ -45,15 +45,15 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { widget.options = { 'on': 'yes', 'off': 'no' }; widget.value = value; widget.doModeChange = (forceValue, skipOtherNodeCheck) => { - var _a, _b, _c; + var _a, _b, _c, _d; let newValue = forceValue == null ? linkedNode.mode === this.modeOff : forceValue; if (skipOtherNodeCheck !== true) { if (newValue && ((_b = (_a = this.properties) === null || _a === void 0 ? void 0 : _a['toggleRestriction']) === null || _b === void 0 ? void 0 : _b.includes(' one'))) { for (const widget of this.widgets) { - widget.doModeChange(false, true); + (_c = widget.doModeChange) === null || _c === void 0 ? void 0 : _c.call(widget, false, true); } } - else if (!newValue && ((_c = this.properties) === null || _c === void 0 ? void 0 : _c['toggleRestriction']) === 'always one') { + else if (!newValue && ((_d = this.properties) === null || _d === void 0 ? void 0 : _d['toggleRestriction']) === 'always one') { newValue = this.widgets.every(w => !w.value || w === widget); } } @@ -61,20 +61,24 @@ export class BaseNodeModeChanger extends BaseAnyInputConnectedNode { widget.value = newValue; }; widget.callback = () => { - widget.doModeChange(); + var _a; + (_a = widget.doModeChange) === null || _a === void 0 ? void 0 : _a.call(widget); }; if (forceValue != null) { linkedNode.mode = (forceValue ? this.modeOn : this.modeOff); } } forceWidgetOff(widget, skipOtherNodeCheck) { - widget.doModeChange(false, skipOtherNodeCheck); + var _a; + (_a = widget.doModeChange) === null || _a === void 0 ? void 0 : _a.call(widget, false, skipOtherNodeCheck); } forceWidgetOn(widget, skipOtherNodeCheck) { - widget.doModeChange(true, skipOtherNodeCheck); + var _a; + (_a = widget.doModeChange) === null || _a === void 0 ? void 0 : _a.call(widget, true, skipOtherNodeCheck); } forceWidgetToggle(widget, skipOtherNodeCheck) { - widget.doModeChange(!widget.value, skipOtherNodeCheck); + var _a; + (_a = widget.doModeChange) === null || _a === void 0 ? void 0 : _a.call(widget, !widget.value, skipOtherNodeCheck); } static setUp(clazz) { BaseAnyInputConnectedNode.setUp(clazz); diff --git a/web/comfyui/constants.js b/web/comfyui/constants.js index 6679dac3..86495b24 100644 --- a/web/comfyui/constants.js +++ b/web/comfyui/constants.js @@ -10,6 +10,7 @@ export const NodeTypesString = { NODE_MODE_REPEATER: addRgthree('Mute / Bypass Repeater'), FAST_MUTER: addRgthree('Fast Muter'), FAST_BYPASSER: addRgthree('Fast Bypasser'), + FAST_NODE_MUTER: addRgthree('Fast Node Muter'), FAST_GROUPS_MUTER: addRgthree('Fast Groups Muter'), FAST_GROUPS_BYPASSER: addRgthree('Fast Groups Bypasser'), FAST_ACTIONS_BUTTON: addRgthree('Fast Actions Button'), diff --git a/web/comfyui/fast_groups_muter.js b/web/comfyui/fast_groups_muter.js index 0ed814c5..3f94e52e 100644 --- a/web/comfyui/fast_groups_muter.js +++ b/web/comfyui/fast_groups_muter.js @@ -3,7 +3,7 @@ import { RgthreeBaseVirtualNode } from "./base_node.js"; import { NodeTypesString } from "./constants.js"; import { SERVICE as FAST_GROUPS_SERVICE } from "./fast_groups_service.js"; import { RgthreeToggleNavWidget } from "./utils_widgets.js"; -import { groupHasActiveNode, } from "./utils_fast.js"; +import { filterByColor, filterByTitle, groupHasActiveNode, sortBy, } from "./utils_fast.js"; const PROPERTY_SORT = "sort"; const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet"; const PROPERTY_MATCH_COLORS = "matchColors"; @@ -49,65 +49,19 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { } refreshWidgets() { var _a, _b, _c, _d, _e, _f, _g; - const canvas = app.canvas; let sort = ((_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_SORT]) || "position"; - let customAlphabet = null; - if (sort === "custom alphabet") { - const customAlphaStr = (_c = (_b = this.properties) === null || _b === void 0 ? void 0 : _b[PROPERTY_SORT_CUSTOM_ALPHA]) === null || _c === void 0 ? void 0 : _c.replace(/\n/g, ""); - if (customAlphaStr && customAlphaStr.trim()) { - customAlphabet = customAlphaStr.includes(",") - ? customAlphaStr.toLocaleLowerCase().split(",") - : customAlphaStr.toLocaleLowerCase().trim().split(""); - } - if (!(customAlphabet === null || customAlphabet === void 0 ? void 0 : customAlphabet.length)) { - sort = "alphanumeric"; - customAlphabet = null; - } - } const groups = [...FAST_GROUPS_SERVICE.getGroups(sort)]; - if (customAlphabet === null || customAlphabet === void 0 ? void 0 : customAlphabet.length) { - groups.sort((a, b) => { - let aIndex = -1; - let bIndex = -1; - for (const [index, alpha] of customAlphabet.entries()) { - aIndex = - aIndex < 0 ? (a.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : aIndex; - bIndex = - bIndex < 0 ? (b.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : bIndex; - if (aIndex > -1 && bIndex > -1) { - break; - } - } - if (aIndex > -1 && bIndex > -1) { - const ret = aIndex - bIndex; - if (ret === 0) { - return a.title.localeCompare(b.title); - } - return ret; - } - else if (aIndex > -1) { - return -1; - } - else if (bIndex > -1) { - return 1; - } - return a.title.localeCompare(b.title); - }); - } - let filterColors = (((_e = (_d = this.properties) === null || _d === void 0 ? void 0 : _d[PROPERTY_MATCH_COLORS]) === null || _e === void 0 ? void 0 : _e.split(",")) || []).filter((c) => c.trim()); - if (filterColors.length) { - filterColors = filterColors.map((color) => { - color = color.trim().toLocaleLowerCase(); - if (LGraphCanvas.node_colors[color]) { - color = LGraphCanvas.node_colors[color].groupcolor; - } - color = color.replace("#", "").toLocaleLowerCase(); - if (color.length === 3) { - color = color.replace(/(.)(.)(.)/, "$1$1$2$2$3$3"); - } - return `#${color}`; - }); - } + const alphaSorted = sortBy(groups, { + customAlphabet: (_c = (_b = this.properties) === null || _b === void 0 ? void 0 : _b[PROPERTY_SORT_CUSTOM_ALPHA]) === null || _c === void 0 ? void 0 : _c.replace(/\n/g, ""), + sort, + }); + const colorFiltered = filterByColor(alphaSorted, { + matchColors: (_d = this.properties) === null || _d === void 0 ? void 0 : _d[PROPERTY_MATCH_COLORS], + nodeColorOption: "groupcolor", + }); + const titleFiltered = filterByTitle(colorFiltered, { + matchTitle: (_f = (_e = this.properties) === null || _e === void 0 ? void 0 : _e[PROPERTY_MATCH_TITLE]) === null || _f === void 0 ? void 0 : _f.trim(), + }); let index = 0; for (const group of groups) { if (filterColors.length) { @@ -132,42 +86,13 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { } } const widgetName = `Enable ${group.title}`; - let widget = this.widgets.find((w) => w.name === widgetName); - if (!widget) { - this.tempSize = [...this.size]; - widget = this.addCustomWidget(new RgthreeToggleNavWidget(group, () => this.showNav)); - widget.doModeChange = (force, skipOtherNodeCheck) => { - var _a, _b, _c; - group.recomputeInsideNodes(); - const hasAnyActiveNodes = groupHasActiveNode(group); - let newValue = force != null ? force : !hasAnyActiveNodes; - if (skipOtherNodeCheck !== true) { - if (newValue && ((_b = (_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_RESTRICTION]) === null || _b === void 0 ? void 0 : _b.includes(" one"))) { - for (const widget of this.widgets) { - widget.doModeChange(false, true); - } - } - else if (!newValue && ((_c = this.properties) === null || _c === void 0 ? void 0 : _c[PROPERTY_RESTRICTION]) === "always one") { - newValue = this.widgets.every((w) => !w.value || w === widget); - } - } - for (const node of group._nodes) { - node.mode = (newValue ? this.modeOn : this.modeOff); - } - widget.value = newValue; - app.graph.setDirtyCanvas(true, false); - }; - widget.callback = () => { - widget.doModeChange(); - }; - this.setSize(this.computeSize()); - } + const widget = this.getOrCreateToggleWidget(group, widgetName); if (widget.name != widgetName) { widget.name = widgetName; this.setDirtyCanvas(true, false); } if (widget.value != group._rgthreeHasAnyActiveNode) { - widget.value = group._rgthreeHasAnyActiveNode; + widget.value = (_g = group._rgthreeHasAnyActiveNode) !== null && _g !== void 0 ? _g : false; this.setDirtyCanvas(true, false); } if (this.widgets[index] !== widget) { @@ -181,6 +106,36 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { this.removeWidget(index++); } } + getOrCreateToggleWidget(group, widgetName) { + const existingWidget = this.widgets.find((w) => w.name === widgetName); + if (existingWidget && existingWidget instanceof RgthreeToggleNavWidget) { + return existingWidget; + } + this.tempSize = [...this.size]; + const widget = this.addCustomWidget(new RgthreeToggleNavWidget(group, () => this.showNav, (force, skipOtherNodeCheck) => { + var _a, _b, _c, _d; + group.recomputeInsideNodes(); + const hasAnyActiveNodes = groupHasActiveNode(group); + let newValue = force != null ? force : !hasAnyActiveNodes; + if (skipOtherNodeCheck !== true) { + if (newValue && ((_b = (_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_RESTRICTION]) === null || _b === void 0 ? void 0 : _b.includes(" one"))) { + for (const widget of this.widgets) { + (_c = widget.doModeChange) === null || _c === void 0 ? void 0 : _c.call(widget, false, true); + } + } + else if (!newValue && ((_d = this.properties) === null || _d === void 0 ? void 0 : _d[PROPERTY_RESTRICTION]) === "always one") { + newValue = this.widgets.every((w) => !w.value || w === widget); + } + } + for (const node of group._nodes) { + node.mode = (newValue ? this.modeOn : this.modeOff); + } + widget.value = newValue; + app.graph.setDirtyCanvas(true, false); + })); + this.setSize(this.computeSize()); + return widget; + } computeSize(out) { let size = super.computeSize(out); if (this.tempSize) { @@ -197,29 +152,29 @@ export class BaseFastGroupsModeChanger extends RgthreeBaseVirtualNode { return size; } async handleAction(action) { - var _a, _b, _c, _d, _e; + var _a, _b, _c, _d, _e, _f, _g, _h; if (action === "Mute all" || action === "Bypass all") { const alwaysOne = ((_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_RESTRICTION]) === "always one"; for (const [index, widget] of this.widgets.entries()) { - widget === null || widget === void 0 ? void 0 : widget.doModeChange(alwaysOne && !index ? true : false, true); + (_b = widget.doModeChange) === null || _b === void 0 ? void 0 : _b.call(widget, alwaysOne && !index ? true : false, true); } } else if (action === "Enable all") { - const onlyOne = (_b = this.properties) === null || _b === void 0 ? void 0 : _b[PROPERTY_RESTRICTION].includes(" one"); + const onlyOne = (_c = this.properties) === null || _c === void 0 ? void 0 : _c[PROPERTY_RESTRICTION].includes(" one"); for (const [index, widget] of this.widgets.entries()) { - widget === null || widget === void 0 ? void 0 : widget.doModeChange(onlyOne && index > 0 ? false : true, true); + (_d = widget.doModeChange) === null || _d === void 0 ? void 0 : _d.call(widget, onlyOne && index > 0 ? false : true, true); } } else if (action === "Toggle all") { - const onlyOne = (_c = this.properties) === null || _c === void 0 ? void 0 : _c[PROPERTY_RESTRICTION].includes(" one"); + const onlyOne = (_e = this.properties) === null || _e === void 0 ? void 0 : _e[PROPERTY_RESTRICTION].includes(" one"); let foundOne = false; for (const [index, widget] of this.widgets.entries()) { let newValue = onlyOne && foundOne ? false : !widget.value; foundOne = foundOne || newValue; - widget === null || widget === void 0 ? void 0 : widget.doModeChange(newValue, true); + (_f = widget.doModeChange) === null || _f === void 0 ? void 0 : _f.call(widget, newValue, true); } - if (!foundOne && ((_d = this.properties) === null || _d === void 0 ? void 0 : _d[PROPERTY_RESTRICTION]) === "always one") { - (_e = this.widgets[this.widgets.length - 1]) === null || _e === void 0 ? void 0 : _e.doModeChange(true, true); + if (!foundOne && ((_g = this.properties) === null || _g === void 0 ? void 0 : _g[PROPERTY_RESTRICTION]) === "always one") { + (_h = this.widgets[this.widgets.length - 1]) === null || _h === void 0 ? void 0 : _h.doModeChange(true, true); } } } diff --git a/web/comfyui/fast_groups_service.js b/web/comfyui/fast_groups_service.js index 345e471d..e4accc28 100644 --- a/web/comfyui/fast_groups_service.js +++ b/web/comfyui/fast_groups_service.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { groupHasActiveNode, } from "./utils_fast.js"; +import { groupHasActiveNode, sortBy } from "./utils_fast.js"; class FastGroupsService { constructor() { this.msThreshold = 400; @@ -99,41 +99,29 @@ class FastGroupsService { return this.groupsUnsorted; } getGroupsAlpha(now) { - const graph = app.graph; if (!this.groupsSortedAlpha.length || now - this.msLastAlpha > this.msThreshold) { - this.groupsSortedAlpha = [...this.getGroupsUnsorted(now)].sort((a, b) => { - return a.title.localeCompare(b.title); - }); + this.groupsSortedAlpha = sortBy(this.getGroupsUnsorted(now), { sort: "alphanumeric" }); this.msLastAlpha = now; } return this.groupsSortedAlpha; } getGroupsPosition(now) { - const graph = app.graph; if (!this.groupsSortedPosition.length || now - this.msLastPosition > this.msThreshold) { - this.groupsSortedPosition = [...this.getGroupsUnsorted(now)].sort((a, b) => { - const aY = Math.floor(a._pos[1] / 30); - const bY = Math.floor(b._pos[1] / 30); - if (aY == bY) { - const aX = Math.floor(a._pos[0] / 30); - const bX = Math.floor(b._pos[0] / 30); - return aX - bX; - } - return aY - bY; - }); + this.groupsSortedPosition = sortBy(this.getGroupsUnsorted(now), { sort: "position" }); this.msLastPosition = now; } return this.groupsSortedPosition; } getGroups(sort) { const now = +new Date(); - if (sort === "alphanumeric") { - return this.getGroupsAlpha(now); - } - if (sort === "position") { - return this.getGroupsPosition(now); + switch (sort) { + case "alphanumeric": + return this.getGroupsAlpha(now); + case "position": + return this.getGroupsPosition(now); + default: + return this.getGroupsUnsorted(now); } - return this.getGroupsUnsorted(now); } } export const SERVICE = new FastGroupsService(); diff --git a/web/comfyui/fast_node_muter.js b/web/comfyui/fast_node_muter.js new file mode 100644 index 00000000..dd3da816 --- /dev/null +++ b/web/comfyui/fast_node_muter.js @@ -0,0 +1,108 @@ +import { app } from "../../scripts/app.js"; +import { NodeTypesString } from "./constants.js"; +import { FastGroupsMuter } from "./fast_groups_muter.js"; +import { RgthreeToggleNavWidget } from "./utils_widgets.js"; +import { filterByColor, filterByTitle, sortBy } from "./utils_fast.js"; +const PROPERTY_SORT = "sort"; +const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet"; +const PROPERTY_MATCH_COLORS = "matchColors"; +const PROPERTY_MATCH_TITLE = "matchTitle"; +const PROPERTY_RESTRICTION = "toggleRestriction"; +export class FastNodeMuter extends FastGroupsMuter { + constructor(title = FastNodeMuter.title) { + super(title); + this.modeOn = LiteGraph.ALWAYS; + this.modeOff = LiteGraph.NEVER; + } + static setUp(clazz) { + LiteGraph.registerNodeType(clazz.type, clazz); + clazz.category = clazz._category; + } + refreshWidgets() { + var _a, _b, _c, _d, _e, _f, _g; + const graph = app.graph; + let sort = ((_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_SORT]) || "position"; + const nodes = [...((_b = graph._nodes) !== null && _b !== void 0 ? _b : [])].filter((n) => !n.isVirtualNode); + const alphaSorted = sortBy(nodes, { + customAlphabet: (_d = (_c = this.properties) === null || _c === void 0 ? void 0 : _c[PROPERTY_SORT_CUSTOM_ALPHA]) === null || _d === void 0 ? void 0 : _d.replace(/\n/g, ""), + sort, + }); + const colorFiltered = filterByColor(alphaSorted, { + matchColors: (_e = this.properties) === null || _e === void 0 ? void 0 : _e[PROPERTY_MATCH_COLORS], + nodeColorOption: "groupcolor", + }); + const titleFiltered = filterByTitle(colorFiltered, { + matchTitle: (_g = (_f = this.properties) === null || _f === void 0 ? void 0 : _f[PROPERTY_MATCH_TITLE]) === null || _g === void 0 ? void 0 : _g.trim(), + }); + let index = 0; + for (const node of titleFiltered) { + this.widgets = this.widgets || []; + const nodeTitle = nodeWithIndex(nodes, node); + const widgetName = `Enable ${nodeTitle}`; + let widget = this.getOrCreateWidget(node, widgetName); + if (widget.name != widgetName) { + widget.name = widgetName; + this.setDirtyCanvas(true, false); + } + const nodeActive = node.mode === LiteGraph.ALWAYS; + if (widget.value != nodeActive) { + widget.value = nodeActive; + this.setDirtyCanvas(true, false); + } + if (this.widgets[index] !== widget) { + const oldIndex = this.widgets.findIndex((w) => w === widget); + this.widgets.splice(index, 0, this.widgets.splice(oldIndex, 1)[0]); + this.setDirtyCanvas(true, false); + } + index++; + } + while ((this.widgets || [])[index]) { + this.removeWidget(index++); + } + } + getOrCreateWidget(node, widgetName) { + const existingWidget = this.widgets.find((w) => w.name === widgetName); + if (existingWidget && existingWidget instanceof RgthreeToggleNavWidget) { + return existingWidget; + } + this.tempSize = [...this.size]; + const widget = this.addCustomWidget(new RgthreeToggleNavWidget(node, () => this.showNav, (force, skipOtherNodeCheck) => { + var _a, _b, _c, _d; + const hasAnyActiveNodes = node.mode === LiteGraph.ALWAYS; + let newValue = force != null ? force : !hasAnyActiveNodes; + if (skipOtherNodeCheck !== true) { + if (newValue && ((_b = (_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_RESTRICTION]) === null || _b === void 0 ? void 0 : _b.includes(" one"))) { + for (const widget of this.widgets) { + (_c = widget.doModeChange) === null || _c === void 0 ? void 0 : _c.call(widget, false, true); + } + } + else if (!newValue && ((_d = this.properties) === null || _d === void 0 ? void 0 : _d[PROPERTY_RESTRICTION]) === "always one") { + newValue = this.widgets.every((w) => !w.value || w === widget); + } + } + node.mode = (newValue ? this.modeOn : this.modeOff); + widget.value = newValue; + app.graph.setDirtyCanvas(true, false); + })); + this.setSize(this.computeSize()); + return widget; + } +} +FastNodeMuter.type = NodeTypesString.FAST_NODE_MUTER; +FastNodeMuter.title = NodeTypesString.FAST_NODE_MUTER; +app.registerExtension({ + name: "rgthree.FastNodeMuter", + registerCustomNodes() { + FastNodeMuter.setUp(FastNodeMuter); + }, + loadedGraphNode(node) { + if (node.type == FastNodeMuter.title) { + node.tempSize = [...node.size]; + } + }, +}); +function nodeWithIndex(nodes, node) { + const { title } = node; + const sameNameNodes = nodes.filter((n) => n.title === title); + return `${title} ${sameNameNodes.indexOf(node)}`; +} diff --git a/web/comfyui/utils_fast.js b/web/comfyui/utils_fast.js index c78a55cf..c99befef 100644 --- a/web/comfyui/utils_fast.js +++ b/web/comfyui/utils_fast.js @@ -1,3 +1,97 @@ +export function sortBy(items, options) { + const { sort } = options; + if (sort === "position") { + return sortByPosition(items); + } + const { customAlphabet: customAlphaStr } = options; + if (!customAlphaStr || sort === "alphanumeric") { + return [...items].sort((a, b) => a.title.localeCompare(b.title)); + } + let customAlphabet = customAlphaStr.includes(",") + ? customAlphaStr.toLocaleLowerCase().split(",") + : customAlphaStr.toLocaleLowerCase().trim().split(""); + if (!customAlphabet.length) { + return items; + } + items.sort((a, b) => { + let aIndex = -1; + let bIndex = -1; + for (const [index, alpha] of customAlphabet.entries()) { + aIndex = aIndex < 0 ? (a.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : aIndex; + bIndex = bIndex < 0 ? (b.title.toLocaleLowerCase().startsWith(alpha) ? index : -1) : bIndex; + if (aIndex > -1 && bIndex > -1) { + break; + } + } + if (aIndex > -1 && bIndex > -1) { + const ret = aIndex - bIndex; + if (ret !== 0) { + return ret; + } + return a.title.localeCompare(b.title); + } + if (aIndex > -1) { + return -1; + } + if (bIndex > -1) { + return 1; + } + return a.title.localeCompare(b.title); + }); + return items; +} +function sortByPosition(items) { + return items.sort((a, b) => { + const aY = Math.floor(a.pos[1] / 30); + const bY = Math.floor(b.pos[1] / 30); + if (aY != bY) { + return aY - bY; + } + const aX = Math.floor(a.pos[0] / 30); + const bX = Math.floor(b.pos[0] / 30); + return aX - bX; + }); +} +function normalizeColor(color) { + const trimmed = color.replace("#", "").trim().toLocaleLowerCase(); + const fullHex = trimmed.length === 3 ? trimmed.replace(/(.)(.)(.)/, "$1$1$2$2$3$3") : trimmed; + return `#${fullHex}`; +} +export function filterByColor(items, options = { nodeColorOption: "groupcolor" }) { + var _a; + const { matchColors, nodeColorOption } = options; + if (!matchColors) { + return items; + } + const filterColors = ((_a = matchColors.split(",")) !== null && _a !== void 0 ? _a : []) + .filter((c) => c.trim()) + .map((color) => color.trim().toLocaleLowerCase()) + .map((color) => { var _a, _b; return (_b = (_a = LGraphCanvas.node_colors[color]) === null || _a === void 0 ? void 0 : _a[nodeColorOption]) !== null && _b !== void 0 ? _b : color; }) + .map((color) => normalizeColor(color)); + if (!filterColors.length) { + return items; + } + return items.filter((item) => { + let color = normalizeColor(item.color); + return filterColors.includes(color); + }); +} +export function filterByTitle(items, options = {}) { + const { matchTitle } = options; + if (!matchTitle) { + return items; + } + const matchPattern = new RegExp(matchTitle, "i"); + return items.filter((item) => { + try { + return matchPattern.exec(item.title); + } + catch (e) { + console.error(e); + } + return true; + }); +} export function groupHasActiveNode(group) { group._rgthreeHasAnyActiveNode = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); return group._rgthreeHasAnyActiveNode; diff --git a/web/comfyui/utils_widgets.js b/web/comfyui/utils_widgets.js index 21f79079..16757618 100644 --- a/web/comfyui/utils_widgets.js +++ b/web/comfyui/utils_widgets.js @@ -248,15 +248,19 @@ export class RgthreeLabelWidget { } } export class RgthreeToggleNavWidget { - constructor(node, showNav) { + constructor(node, showNav, doModeChange) { this.node = node; this.showNav = showNav; + this.doModeChange = doModeChange; this.name = "RGTHREE_TOGGLE_AND_NAV"; this.label = ""; this.value = false; this.disabled = false; this.options = { on: "yes", off: "no" }; } + callback(value, graphCanvas, node, pos, event) { + this.doModeChange(); + } draw(ctx, node, width, posY, height) { const widgetData = drawNodeWidget(ctx, { width, From 37048d88d45a366167012c8d9111606e0ba02052 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Fri, 19 Apr 2024 13:39:21 -0700 Subject: [PATCH 6/8] Fix color matching for nodes --- src_web/comfyui/fast_node_muter.ts | 2 +- src_web/comfyui/utils_fast.ts | 3 +++ web/comfyui/fast_node_muter.js | 2 +- web/comfyui/utils_fast.js | 3 +++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src_web/comfyui/fast_node_muter.ts b/src_web/comfyui/fast_node_muter.ts index e0a83969..deb7b370 100644 --- a/src_web/comfyui/fast_node_muter.ts +++ b/src_web/comfyui/fast_node_muter.ts @@ -55,7 +55,7 @@ export class FastNodeMuter extends FastGroupsMuter { // values. const colorFiltered = filterByColor(alphaSorted, { matchColors: this.properties?.[PROPERTY_MATCH_COLORS], - nodeColorOption: "groupcolor", + nodeColorOption: "color", }); const titleFiltered = filterByTitle(colorFiltered, { diff --git a/src_web/comfyui/utils_fast.ts b/src_web/comfyui/utils_fast.ts index 6098f72a..61143e14 100644 --- a/src_web/comfyui/utils_fast.ts +++ b/src_web/comfyui/utils_fast.ts @@ -113,6 +113,9 @@ export function filterByColor( } return items.filter((item) => { + if (!item.color) { + return false; + } let color = normalizeColor(item.color); return filterColors.includes(color); }); diff --git a/web/comfyui/fast_node_muter.js b/web/comfyui/fast_node_muter.js index dd3da816..8636a3e8 100644 --- a/web/comfyui/fast_node_muter.js +++ b/web/comfyui/fast_node_muter.js @@ -29,7 +29,7 @@ export class FastNodeMuter extends FastGroupsMuter { }); const colorFiltered = filterByColor(alphaSorted, { matchColors: (_e = this.properties) === null || _e === void 0 ? void 0 : _e[PROPERTY_MATCH_COLORS], - nodeColorOption: "groupcolor", + nodeColorOption: "color", }); const titleFiltered = filterByTitle(colorFiltered, { matchTitle: (_g = (_f = this.properties) === null || _f === void 0 ? void 0 : _f[PROPERTY_MATCH_TITLE]) === null || _g === void 0 ? void 0 : _g.trim(), diff --git a/web/comfyui/utils_fast.js b/web/comfyui/utils_fast.js index c99befef..a94e26b0 100644 --- a/web/comfyui/utils_fast.js +++ b/web/comfyui/utils_fast.js @@ -72,6 +72,9 @@ export function filterByColor(items, options = { nodeColorOption: "groupcolor" } return items; } return items.filter((item) => { + if (!item.color) { + return false; + } let color = normalizeColor(item.color); return filterColors.includes(color); }); From c9ad0f31629b1ff209cc1991741cbead9f578529 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Fri, 19 Apr 2024 13:59:09 -0700 Subject: [PATCH 7/8] Node titles for unique/repeated --- src_web/comfyui/fast_node_muter.ts | 5 ++++- web/comfyui/fast_node_muter.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src_web/comfyui/fast_node_muter.ts b/src_web/comfyui/fast_node_muter.ts index deb7b370..e0a47b63 100644 --- a/src_web/comfyui/fast_node_muter.ts +++ b/src_web/comfyui/fast_node_muter.ts @@ -143,5 +143,8 @@ app.registerExtension({ function nodeWithIndex(nodes: LGraphNode[], node: LGraphNode): string { const { title } = node; const sameNameNodes = nodes.filter((n) => n.title === title); - return `${title} ${sameNameNodes.indexOf(node)}`; + if (sameNameNodes.length === 1) { + return title; + } + return `${title} ${sameNameNodes.indexOf(node) + 1}/${sameNameNodes.length}`; } diff --git a/web/comfyui/fast_node_muter.js b/web/comfyui/fast_node_muter.js index 8636a3e8..3abfcb39 100644 --- a/web/comfyui/fast_node_muter.js +++ b/web/comfyui/fast_node_muter.js @@ -104,5 +104,8 @@ app.registerExtension({ function nodeWithIndex(nodes, node) { const { title } = node; const sameNameNodes = nodes.filter((n) => n.title === title); - return `${title} ${sameNameNodes.indexOf(node)}`; + if (sameNameNodes.length === 1) { + return title; + } + return `${title} ${sameNameNodes.indexOf(node) + 1}/${sameNameNodes.length}`; } From fdad22579c7fd6dfd428fc5173c98ed3eb7151cc Mon Sep 17 00:00:00 2001 From: DrJKL Date: Fri, 19 Apr 2024 15:44:58 -0700 Subject: [PATCH 8/8] Group filtering (initial) --- src_web/comfyui/fast_node_muter.ts | 21 +++++++++++++++++++-- web/comfyui/fast_node_muter.js | 27 +++++++++++++++++++++------ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src_web/comfyui/fast_node_muter.ts b/src_web/comfyui/fast_node_muter.ts index e0a47b63..ce36a721 100644 --- a/src_web/comfyui/fast_node_muter.ts +++ b/src_web/comfyui/fast_node_muter.ts @@ -11,6 +11,7 @@ import { } from "typings/litegraph.js"; import { RgthreeToggleNavWidget } from "./utils_widgets.js"; import { filterByColor, filterByTitle, sortBy, type SortType } from "./utils_fast.js"; +import { SERVICE as FAST_GROUPS_SERVICE } from "./fast_groups_service.js"; declare const LiteGraph: typeof TLiteGraph; declare const LGraphCanvas: typeof TLGraphCanvas; @@ -19,6 +20,7 @@ const PROPERTY_SORT = "sort"; const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet"; const PROPERTY_MATCH_COLORS = "matchColors"; const PROPERTY_MATCH_TITLE = "matchTitle"; +const PROPERTY_MATCH_TITLE_GROUP = "matchGroupTitle"; const PROPERTY_RESTRICTION = "toggleRestriction"; /** @@ -31,8 +33,11 @@ export class FastNodeMuter extends FastGroupsMuter { override readonly modeOn = LiteGraph.ALWAYS; override readonly modeOff = LiteGraph.NEVER; + static "@matchTitleGroup" = { type: "string" }; + constructor(title = FastNodeMuter.title) { super(title); + this.properties[PROPERTY_MATCH_TITLE_GROUP] = ""; } static override setUp(clazz: new (title?: string) => T) { @@ -40,10 +45,22 @@ export class FastNodeMuter extends FastGroupsMuter { (clazz as any).category = (clazz as any)._category; } + private getNodes(sort: SortType): LGraphNode[] { + const matchGroup = this.properties?.[PROPERTY_MATCH_TITLE_GROUP]; + if (!matchGroup) { + const graph: LGraph = app.graph; + return [...(graph._nodes ?? [])].filter((n) => !n.isVirtualNode); + } + const pattern = new RegExp(matchGroup, "i"); + const allGroups = FAST_GROUPS_SERVICE.getGroups(sort); + const filteredGroups = allGroups.filter((group) => pattern.exec(group.title)); + return Array.from(new Set(filteredGroups.map((group) => group._nodes).flat())); + } + override refreshWidgets() { - const graph: LGraph = app.graph; let sort: SortType = this.properties?.[PROPERTY_SORT] || "position"; - const nodes: LGraphNode[] = [...(graph._nodes ?? [])].filter((n) => !n.isVirtualNode); + const nodes = this.getNodes(sort); + // The service will return pre-sorted groups for alphanumeric and position. If this node has a // custom sort, then we need to sort it manually. const alphaSorted = sortBy(nodes, { diff --git a/web/comfyui/fast_node_muter.js b/web/comfyui/fast_node_muter.js index 3abfcb39..6da37806 100644 --- a/web/comfyui/fast_node_muter.js +++ b/web/comfyui/fast_node_muter.js @@ -3,36 +3,50 @@ import { NodeTypesString } from "./constants.js"; import { FastGroupsMuter } from "./fast_groups_muter.js"; import { RgthreeToggleNavWidget } from "./utils_widgets.js"; import { filterByColor, filterByTitle, sortBy } from "./utils_fast.js"; +import { SERVICE as FAST_GROUPS_SERVICE } from "./fast_groups_service.js"; const PROPERTY_SORT = "sort"; const PROPERTY_SORT_CUSTOM_ALPHA = "customSortAlphabet"; const PROPERTY_MATCH_COLORS = "matchColors"; const PROPERTY_MATCH_TITLE = "matchTitle"; +const PROPERTY_MATCH_TITLE_GROUP = "matchGroupTitle"; const PROPERTY_RESTRICTION = "toggleRestriction"; export class FastNodeMuter extends FastGroupsMuter { constructor(title = FastNodeMuter.title) { super(title); this.modeOn = LiteGraph.ALWAYS; this.modeOff = LiteGraph.NEVER; + this.properties[PROPERTY_MATCH_TITLE_GROUP] = ""; } static setUp(clazz) { LiteGraph.registerNodeType(clazz.type, clazz); clazz.category = clazz._category; } + getNodes(sort) { + var _a, _b; + const matchGroup = (_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_MATCH_TITLE_GROUP]; + if (!matchGroup) { + const graph = app.graph; + return [...((_b = graph._nodes) !== null && _b !== void 0 ? _b : [])].filter((n) => !n.isVirtualNode); + } + const pattern = new RegExp(matchGroup, "i"); + const allGroups = FAST_GROUPS_SERVICE.getGroups(sort); + const filteredGroups = allGroups.filter((group) => pattern.exec(group.title)); + return Array.from(new Set(filteredGroups.map((group) => group._nodes).flat())); + } refreshWidgets() { - var _a, _b, _c, _d, _e, _f, _g; - const graph = app.graph; + var _a, _b, _c, _d, _e, _f; let sort = ((_a = this.properties) === null || _a === void 0 ? void 0 : _a[PROPERTY_SORT]) || "position"; - const nodes = [...((_b = graph._nodes) !== null && _b !== void 0 ? _b : [])].filter((n) => !n.isVirtualNode); + const nodes = this.getNodes(sort); const alphaSorted = sortBy(nodes, { - customAlphabet: (_d = (_c = this.properties) === null || _c === void 0 ? void 0 : _c[PROPERTY_SORT_CUSTOM_ALPHA]) === null || _d === void 0 ? void 0 : _d.replace(/\n/g, ""), + customAlphabet: (_c = (_b = this.properties) === null || _b === void 0 ? void 0 : _b[PROPERTY_SORT_CUSTOM_ALPHA]) === null || _c === void 0 ? void 0 : _c.replace(/\n/g, ""), sort, }); const colorFiltered = filterByColor(alphaSorted, { - matchColors: (_e = this.properties) === null || _e === void 0 ? void 0 : _e[PROPERTY_MATCH_COLORS], + matchColors: (_d = this.properties) === null || _d === void 0 ? void 0 : _d[PROPERTY_MATCH_COLORS], nodeColorOption: "color", }); const titleFiltered = filterByTitle(colorFiltered, { - matchTitle: (_g = (_f = this.properties) === null || _f === void 0 ? void 0 : _f[PROPERTY_MATCH_TITLE]) === null || _g === void 0 ? void 0 : _g.trim(), + matchTitle: (_f = (_e = this.properties) === null || _e === void 0 ? void 0 : _e[PROPERTY_MATCH_TITLE]) === null || _f === void 0 ? void 0 : _f.trim(), }); let index = 0; for (const node of titleFiltered) { @@ -90,6 +104,7 @@ export class FastNodeMuter extends FastGroupsMuter { } FastNodeMuter.type = NodeTypesString.FAST_NODE_MUTER; FastNodeMuter.title = NodeTypesString.FAST_NODE_MUTER; +FastNodeMuter["@matchTitleGroup"] = { type: "string" }; app.registerExtension({ name: "rgthree.FastNodeMuter", registerCustomNodes() {