Fix native 'queue selected nodes'; add random seed range in node properties; fix groups collecting nodes when moving; and other small fixes. Fixes #578 #536 #551 #576

This commit is contained in:
rgthree
2025-09-06 22:12:40 -04:00
parent dbc5fa5e89
commit 26988df02c
24 changed files with 319 additions and 112 deletions
+1
View File
@@ -9,6 +9,7 @@ class RgthreeImageComparer(PreviewImage):
NAME = get_name('Image Comparer')
CATEGORY = get_category()
FUNCTION = "compare_images"
DESCRIPTION = "Compares two images with a hover slider, or click from properties."
@classmethod
def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
+26 -12
View File
@@ -1,7 +1,6 @@
import os
import re
import json
import requests
from .utils import set_dict_value
@@ -45,14 +44,29 @@ LOGO_URL: str = _DATA['tool']['comfy']['Icon']
if not LOGO_URL.endswith('.svg'):
raise ValueError('Bad logo url.')
# Fetch the logo so we have any updated markup.
try:
LOGO_SVG = requests.get(
LOGO_URL,
headers={"user-agent": f"rgthree-comfy/{VERSION}"},
timeout=10
).text
LOGO_SVG = re.sub(r'(id="bg".*fill=)"[^\"]+"', r'\1"{bg}"', LOGO_SVG)
LOGO_SVG = re.sub(r'(id="fg".*fill=)"[^\"]+"', r'\1"{fg}"', LOGO_SVG)
except Exception:
LOGO_SVG = '<svg></svg>'
LOGO_SVG = None
async def get_logo_svg():
import aiohttp
global LOGO_SVG
if LOGO_SVG is not None:
return LOGO_SVG
# Fetch the logo so we have any updated markup.
try:
async with aiohttp.ClientSession(
trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=True)
) as session:
headers = {
"user-agent": f"rgthree-comfy/{VERSION}",
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Expires': '0'
}
async with session.get(LOGO_URL, headers=headers) as resp:
LOGO_SVG = await resp.text()
LOGO_SVG = '<svg></svg>'
LOGO_SVG = re.sub(r'(id="bg".*fill=)"[^\"]+"', r'\1"{bg}"', LOGO_SVG)
LOGO_SVG = re.sub(r'(id="fg".*fill=)"[^\"]+"', r'\1"{fg}"', LOGO_SVG)
except Exception:
LOGO_SVG = '<svg></svg>'
print(LOGO_SVG)
return LOGO_SVG
+24 -3
View File
@@ -4,12 +4,13 @@ from aiohttp import web
from server import PromptServer
from ..pyproject import LOGO_SVG
from ..pyproject import get_logo_svg
from .utils_server import is_param_truthy, get_param
from ..config import get_config, set_user_config, refresh_config
routes = PromptServer.instance.routes
@routes.get('/rgthree/config.js')
def api_get_user_config_file(request):
""" Returns the user configuration as a javascript file. """
@@ -36,9 +37,29 @@ async def api_set_user_config(request):
@routes.get('/rgthree/logo.svg')
async def get_logo(request):
async def get_logo(request, as_markup=False):
""" Returns the rgthree logo with color config. """
bg = get_param(request, 'bg', 'transparent')
fg = get_param(request, 'fg', '#111111')
resp = LOGO_SVG.format(bg=bg, fg=fg)
w = get_param(request, 'w')
h = get_param(request, 'h')
css_class = get_param(request, 'cssClass')
svg = await get_logo_svg()
resp = svg.format(bg=bg, fg=fg)
if w is not None:
resp = re.sub(r'(<svg[^\>]*?)width="[^\"]+"', r'\1', resp)
resp = re.sub(r'<svg', f'<svg width="{w}"', resp)
if h is not None:
resp = re.sub(r'(<svg[^\>]*?)height="[^\"]+"', r'\1', resp)
resp = re.sub(r'<svg', f'<svg height="{h}"', resp)
if css_class is not None:
resp = re.sub(r'<svg', f'<svg class="{css_class}"', resp)
if as_markup:
resp = re.sub(r'^.*?<svg', r'<svg', resp, flags=re.DOTALL)
return web.Response(text=resp, content_type='image/svg+xml')
@routes.get('/rgthree/logo_markup.svg')
async def get_logo_markup(request):
""" Returns the rgthree logo svg markup (no doctype) with options. """
return await get_logo(request, as_markup=True)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "rgthree-comfy"
description = "Making ComfyUI more comfortable."
version = "1.0.2508241658"
version = "1.0.2509062212"
license = { file = "LICENSE" }
dependencies = []
+13 -13
View File
@@ -1,12 +1,12 @@
import {app} from "scripts/app.js";
import { ComfyButtonGroup } from "scripts/ui/components/buttonGroup.js";
import { ComfyButton } from "scripts/ui/components/button.js";
import { iconGear, iconStarFilled, logoRgthree } from "rgthree/common/media/svgs.js";
import { createElement, empty, query } from "rgthree/common/utils_dom.js";
import { SERVICE as BOOKMARKS_SERVICE } from "./services/bookmarks_services.js";
import { SERVICE as CONFIG_SERVICE } from "./services/config_service.js";
import { ComfyPopup } from "scripts/ui/components/popup.js";
import { RgthreeConfigDialog } from "./config.js";
import {ComfyButtonGroup} from "scripts/ui/components/buttonGroup.js";
import {ComfyButton} from "scripts/ui/components/button.js";
import {iconGear, iconStarFilled, logoRgthreeAsync} from "rgthree/common/media/svgs.js";
import {createElement, empty, query} from "rgthree/common/utils_dom.js";
import {SERVICE as BOOKMARKS_SERVICE} from "./services/bookmarks_services.js";
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
import {ComfyPopup} from "scripts/ui/components/popup.js";
import {RgthreeConfigDialog} from "./config.js";
let rgthreeButtonGroup: ComfyButtonGroup | null = null;
@@ -33,10 +33,10 @@ function addRgthreeTopBarButtons() {
});
buttons.push(rgthreeButton);
rgthreeButton.iconElement.style.width = "1.2rem";
rgthreeButton.iconElement.innerHTML = logoRgthree;
logoRgthreeAsync().then((t) => { rgthreeButton.iconElement.innerHTML = t;});
rgthreeButton.withPopup(
new ComfyPopup(
{ target: rgthreeButton.element, classList: "rgthree-top-menu" },
{target: rgthreeButton.element, classList: "rgthree-top-menu"},
createElement("menu", {
children: [
createElement("li", {
@@ -61,7 +61,7 @@ function addRgthreeTopBarButtons() {
const bookmarksListEl = createElement("menu");
bookmarksListEl.appendChild(
createElement("li.rgthree-message", {
child: createElement("span", { text: "No bookmarks in current workflow." }),
child: createElement("span", {text: "No bookmarks in current workflow."}),
}),
);
const bookmarksButton = new ComfyButton({
@@ -70,7 +70,7 @@ function addRgthreeTopBarButtons() {
app,
});
const bookmarksPopup = new ComfyPopup(
{ target: bookmarksButton.element, classList: "rgthree-top-menu" },
{target: bookmarksButton.element, classList: "rgthree-top-menu"},
bookmarksListEl,
);
bookmarksPopup.addEventListener("open", () => {
@@ -92,7 +92,7 @@ function addRgthreeTopBarButtons() {
} else {
bookmarksListEl.appendChild(
createElement("li.rgthree-message", {
child: createElement("span", { text: "No bookmarks in current workflow." }),
child: createElement("span", {text: "No bookmarks in current workflow."}),
}),
);
}
+1 -1
View File
@@ -97,7 +97,7 @@ app.registerExtension({
message: "No output nodes for group!",
});
} else {
rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
rgthree.queueOutputNodes(outputNodes);
}
} else {
const toggleMode = TOGGLE_TO_MODE.get(toggleAction);
+2 -2
View File
@@ -21,7 +21,7 @@ function showQueueNodesMenuIfOutputNodesAreSelected(
content: `Queue Selected Output Nodes (rgthree) &nbsp;`,
className: "rgthree-contextmenu-item",
callback: () => {
rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
rgthree.queueOutputNodes(outputNodes);
},
disabled: !outputNodes.length,
};
@@ -50,7 +50,7 @@ function showQueueGroupNodesMenuIfGroupIsSelected(
content: `Queue Group Output Nodes (rgthree) &nbsp;`,
className: "rgthree-contextmenu-item",
callback: () => {
outputNodes && rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
outputNodes && rgthree.queueOutputNodes(outputNodes);
},
disabled: !outputNodes?.length,
};
+19 -3
View File
@@ -255,6 +255,8 @@ class Rgthree extends EventTarget {
// Comfy/LiteGraph states so nodes and tell what the hell is going on.
canvasCurrentlyCopyingToClipboard = false;
canvasCurrentlyCopyingToClipboardWithMultipleNodes = false;
canvasCurrentlyPastingFromClipboard = false;
canvasCurrentlyPastingFromClipboardWithMultipleNodes = false;
initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff: any = null;
private readonly isMac: boolean = !!(
@@ -419,6 +421,14 @@ class Rgthree extends EventTarget {
rgthree.canvasCurrentlyCopyingToClipboardWithMultipleNodes = false;
};
// [🤮] Pasting from clipboard.
const pasteFromClipboard = LGraphCanvas.prototype.pasteFromClipboard;
LGraphCanvas.prototype.pasteFromClipboard = function (...args: any[]) {
rgthree.canvasCurrentlyPastingFromClipboard = true;
pasteFromClipboard.apply(this, [...arguments] as any);
rgthree.canvasCurrentlyPastingFromClipboard = false;
};
// [⭐] Make it so when we add a group, we get to name it immediately.
const onGroupAdd = LGraphCanvas.onGroupAdd;
LGraphCanvas.onGroupAdd = function (...args: any[]) {
@@ -648,7 +658,13 @@ class Rgthree extends EventTarget {
* Wraps an `app.queuePrompt` call setting a specific node id that we will inspect and change the
* serialized graph right before being sent (below, in our `api.queuePrompt` override).
*/
async queueOutputNodes(nodeIds: NodeId[]) {
async queueOutputNodes(nodes: LGraphNode[]) {
// We can use just these next two lines when
// https://github.com/ltdrdata/ComfyUI-Inspire-Pack/pull/258 is pulled. Until then, we'll keep
// our custom logic as ComfyUI-Inspire-Pack would cause it not to work.
// app.canvas.selectItems(nodes);
// app.extensionManager.command.execute('Comfy.QueueSelectedOutputNodes');
const nodeIds = nodes.map(n => n.id);
try {
this.queueNodeIds = nodeIds;
await app.queuePrompt(0);
@@ -729,7 +745,7 @@ class Rgthree extends EventTarget {
// then we only want to queue those nodes, by rewriting the api format (prompt 'output' field)
// so only those are evaluated.
const apiQueuePrompt = api.queuePrompt as Function;
api.queuePrompt = async function (index: number, prompt: ComfyApiPrompt) {
api.queuePrompt = async function (index: number, prompt: ComfyApiPrompt, ...args: any[]) {
if (rgthree.queueNodeIds?.length && prompt.output) {
const oldOutput = prompt.output;
let newOutput = {};
@@ -742,7 +758,7 @@ class Rgthree extends EventTarget {
workflow: prompt.workflow,
output: prompt.output,
});
const response = apiQueuePrompt.apply(app, [index, prompt]);
const response = apiQueuePrompt.apply(app, [index, prompt, ...args]);
rgthree.dispatchCustomEvent("comfy-api-queue-prompt-end");
return response;
};
+31 -21
View File
@@ -41,30 +41,40 @@ class RgthreeSeed extends RgthreeBaseServerNode {
static override exposedActions = ["Randomize Each Time", "Use Last Queued Seed"];
static "@randomMax" = {type: "number"};
static "@randomMin" = {type: "number"};
lastSeed?: number = undefined;
serializedCtx: SeedSerializedCtx = {};
seedWidget!: IWidget;
lastSeedButton!: IWidget;
lastSeedValue: IWidget | null = null;
randMax = 1125899906842624;
// We can have a full range of seeds, including negative. But, for the randomRange we'll
// only generate positives, since that's what folks assume.
// const min = Math.max(-1125899906842624, this.seedWidget.options.min);
randMin = 0;
randomRange = 1125899906842624;
private handleApiHijackingBound = this.handleApiHijacking.bind(this);
constructor(title = RgthreeSeed.title) {
super(title);
this.properties["randomMax"] = 1125899906842624;
// We can have a full range of seeds, including negative. But, for the randomRange we'll
// only generate positives, since that's what folks assume.
this.properties["randomMin"] = 0;
rgthree.addEventListener(
"comfy-api-queue-prompt-before",
this.handleApiHijackingBound as EventListener,
);
}
override onPropertyChanged(prop: string, value: unknown, prevValue?: unknown): boolean {
if (prop === 'randomMax') {
this.properties["randomMax"] = Math.min(1125899906842624, Number(value as number));
} else if (prop === 'randomMin') {
this.properties["randomMin"] = Math.max(-1125899906842624, Number(value as number));
}
return true;
}
override onRemoved() {
rgthree.addEventListener(
"comfy-api-queue-prompt-before",
@@ -101,14 +111,6 @@ class RgthreeSeed extends RgthreeBaseServerNode {
}
}
// Update random values in case seed comes down with different options.
let step = this.seedWidget.options.step || 1;
this.randMax = Math.min(1125899906842624, this.seedWidget.options.max ?? 0);
// We can have a full range of seeds, including negative. But, for the randomRange we'll
// only generate positives, since that's what folks assume.
this.randMin = Math.max(0, this.seedWidget.options.min ?? 0);
this.randomRange = (this.randMax - Math.max(0, this.randMin)) / (step / 10);
this.addWidget(
"button",
"🎲 Randomize Each Time",
@@ -124,8 +126,7 @@ class RgthreeSeed extends RgthreeBaseServerNode {
"🎲 New Fixed Random",
"",
() => {
this.seedWidget.value =
Math.floor(Math.random() * this.randomRange) * (step / 10) + this.randMin;
this.seedWidget.value = this.generateRandomSeed();
},
{serialize: false},
);
@@ -144,6 +145,18 @@ class RgthreeSeed extends RgthreeBaseServerNode {
this.lastSeedButton.disabled = true;
}
generateRandomSeed() {
let step = this.seedWidget.options.step || 1;
const randomMin = Number(this.properties['randomMin'] || 0);
const randomMax = Number(this.properties['randomMax'] || 1125899906842624);
const randomRange = (randomMax - randomMin) / (step / 10);
let seed = Math.floor(Math.random() * randomRange) * (step / 10) + randomMin;
if (SPECIAL_SEEDS.includes(seed)) {
seed = 0;
}
return seed;
}
override getExtraMenuOptions(canvas: LGraphCanvas, options: IContextMenuValue[]) {
super.getExtraMenuOptions?.apply(this, [...arguments] as any);
options.splice(options.length - 1, 0, {
@@ -263,10 +276,7 @@ class RgthreeSeed extends RgthreeBaseServerNode {
// If we don't have a seed to use, or it's special seed (like we incremented into one), then
// we randomize.
if (seedToUse == null || SPECIAL_SEEDS.includes(seedToUse)) {
seedToUse =
Math.floor(Math.random() * this.randomRange) *
((this.seedWidget.options.step || 1) / 10) +
this.randMin;
seedToUse = this.generateRandomSeed();
}
}
@@ -1,4 +1,4 @@
import type {LGraphGroup} from "@comfyorg/frontend";
import type {LGraphGroup as TLGraphGroup} from "@comfyorg/frontend";
import type {BaseFastGroupsModeChanger} from "../fast_groups_muter.js";
import {app} from "scripts/app.js";
@@ -6,6 +6,8 @@ import {getGraphDependantNodeKey, getGroupNodes, reduceNodesDepthFirst} from "..
type Vector4 = [number, number, number, number];
/**
* A service that keeps global state that can be shared by multiple FastGroupsMuter or
* FastGroupsBypasser nodes rather than calculate it on it's own.
@@ -16,9 +18,9 @@ class FastGroupsService {
private msLastAlpha = 0;
private msLastPosition = 0;
private groupsUnsorted: LGraphGroup[] = [];
private groupsSortedAlpha: LGraphGroup[] = [];
private groupsSortedPosition: LGraphGroup[] = [];
private groupsUnsorted: TLGraphGroup[] = [];
private groupsSortedAlpha: TLGraphGroup[] = [];
private groupsSortedPosition: TLGraphGroup[] = [];
private readonly fastGroupNodes: BaseFastGroupsModeChanger[] = [];
@@ -118,10 +120,13 @@ class FastGroupsService {
}
/**
* This overrides `LGraphGroup.prototype.recomputeInsideNodes` to be much more efficient when
* This overrides `TLGraphGroup.prototype.recomputeInsideNodes` to be much more efficient when
* calculating for many groups at once (only compute all nodes once in `getBoundingsForAllNodes`).
*/
recomputeInsideNodesForGroup(group: LGraphGroup) {
recomputeInsideNodesForGroup(group: TLGraphGroup) {
// If the canvas is currently being dragged (includes if a group is being dragged around) then
// don't recompute anything.
if (app.canvas.isDragging) return;
const cachedBoundings = this.getBoundingsForAllNodes();
const nodes = group.graph!.nodes;
group._children.clear();
+32 -8
View File
@@ -1,13 +1,37 @@
import { createElement as $el } from "../utils_dom.js";
import {createElement as $el} from "../utils_dom.js";
// Some svg repo : https://www.svgrepo.com/svg/326731/open-outline
export const logoRgthree = `<svg viewBox="0 0 256 256" fill="currentColor" class="rgthree-logo">
<path d="M88.503,158.997 L152.731,196.103 L152.738,196.092 L152.762,196.103 L152.769,196.106 L152.771,196.103 L183.922,142.084 L174.153,136.437 L148.611,180.676 L101.512,153.484 L132.193,30.415 L156.124,71.869 L165.896,66.225 L128.002,0.59 "></path>
<path d="M55.586,148.581l13.44,47.521l0.014,0.051l0.168-0.051l10.689-3.022l-6.589-23.313l45.609,26.335l0.087,0.051l0.027-0.051 l5.617-9.718l-42.648-24.622l35.771-143.45L33.232,164.729l9.77,5.645L55.586,148.581z M87.394,93.484l-16.708,67.018l-5.018-17.747 l-8.028,2.27L87.394,93.484z"></path>
<path d="M189.85,107.717 L137.892,137.718 L143.532,147.49 L185.723,123.133 L231.109,201.746 L24.895,201.746 L37.363,180.146 L27.592,174.505 L5.347,213.03 L250.653,213.03 "></path>
<path d="M5.347,247.299v8.111h245.307v-8.111l-41.94-0.003c-1.336,0-2.404-1.065-2.441-2.396v-12.14 c0.037-1.315,1.089-2.368,2.41-2.385h41.972v-8.11H5.347v8.11h41.951c1.338,0.017,2.427,1.104,2.427,2.449v12.01 c0,1.365-1.105,2.462-2.457,2.462L5.347,247.299z M139.438,247.296c-1.334,0-2.406-1.065-2.439-2.396v-12.14 c0.033-1.315,1.085-2.368,2.41-2.385h46.415c1.335,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.459,2.462H139.438z M70.193,247.296c-1.339,0-2.408-1.065-2.441-2.396v-12.14c0.033-1.315,1.086-2.368,2.407-2.385h46.418 c1.336,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.458,2.462H70.193z"></path>
</svg>`;
export let logoRgthree: string = "";
export async function logoRgthreeAsync(): Promise<string> {
if (logoRgthree) return logoRgthree;
let baseUrl = null;
if (window.location.pathname.includes("/rgthree/")) {
// Try to find how many relatives paths we need to go back to hit ./rgthree/api
const parts = window.location.pathname.split("/rgthree/")[1]?.split("/");
if (parts && parts.length) {
baseUrl = parts.map(() => "../").join("") + "rgthree";
}
}
baseUrl = baseUrl || "./rgthree";
return fetch(`${baseUrl}/logo_markup.svg?fg=currentColor&cssClass=rgthree-logo&w=auto&h=auto`)
.then((r) => r.text())
.then((t) => {
if (t.length < 100) {
t = `<svg viewBox="0 0 256 256" fill="currentColor" class="rgthree-logo">
<path d="M88.503,158.997 L152.731,196.103 L152.738,196.092 L152.762,196.103 L152.769,196.106 L152.771,196.103 L183.922,142.084 L174.153,136.437 L148.611,180.676 L101.512,153.484 L132.193,30.415 L156.124,71.869 L165.896,66.225 L128.002,0.59 "></path>
<path d="M55.586,148.581l13.44,47.521l0.014,0.051l0.168-0.051l10.689-3.022l-6.589-23.313l45.609,26.335l0.087,0.051l0.027-0.051 l5.617-9.718l-42.648-24.622l35.771-143.45L33.232,164.729l9.77,5.645L55.586,148.581z M87.394,93.484l-16.708,67.018l-5.018-17.747 l-8.028,2.27L87.394,93.484z"></path>
<path d="M189.85,107.717 L137.892,137.718 L143.532,147.49 L185.723,123.133 L231.109,201.746 L24.895,201.746 L37.363,180.146 L27.592,174.505 L5.347,213.03 L250.653,213.03 "></path>
<path d="M5.347,247.299v8.111h245.307v-8.111l-41.94-0.003c-1.336,0-2.404-1.065-2.441-2.396v-12.14 c0.037-1.315,1.089-2.368,2.41-2.385h41.972v-8.11H5.347v8.11h41.951c1.338,0.017,2.427,1.104,2.427,2.449v12.01 c0,1.365-1.105,2.462-2.457,2.462L5.347,247.299z M139.438,247.296c-1.334,0-2.406-1.065-2.439-2.396v-12.14 c0.033-1.315,1.085-2.368,2.41-2.385h46.415c1.335,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.459,2.462H139.438z M70.193,247.296c-1.339,0-2.408-1.065-2.441-2.396v-12.14c0.033-1.315,1.086-2.368,2.407-2.385h46.418 c1.336,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.458,2.462H70.193z"></path>
</svg>`;
}
logoRgthree = t;
return t;
});
}
// Kick it off to cache upfront.
logoRgthreeAsync();
export const github = `<svg viewBox="0 0 16 16" fill="currentColor" class="github-logo">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
@@ -178,7 +202,7 @@ export const gearColored = `<svg viewBox="0 0 128 128" preserveAspectRatio="xMid
<path d="M25.24 65.87c-.01-22.03 15.9-40.19 38.13-41.05c.68-.03 2.45 0 3.55.99c1.01.91 1.38 2.51.79 3.82c-.95 2.11-2.85 2.07-3.36 2.09c-18.51.66-34.18 15.73-34.19 33.95c0 .29-.05.58-.15.84l-.1.25c-.76 1.98-3.52 2.09-4.43.18c-.15-.34-.24-.7-.24-1.07z" fill="#94d1e0"></path>
</svg>`;
export function $svg(markup: string, attrs: { [key: string]: string }) {
export function $svg(markup: string, attrs: {[key: string]: string}) {
if (!markup.match(/^\s*<svg/)) {
throw new Error("Cannot call $svg with non-svg markup.");
}
+1 -1
View File
@@ -146,7 +146,7 @@ class PromptService extends EventTarget {
// Patch the queuePrompt method so we can capture new data going through.
const queuePrompt = api.queuePrompt;
api.queuePrompt = async function (num: number, prompt: ComfyApiPrompt) {
api.queuePrompt = async function (num: number, prompt: ComfyApiPrompt, ...args: any[]) {
let response;
try {
response = await queuePrompt.apply(api, [...arguments]);
+31 -6
View File
@@ -254,7 +254,7 @@ export function areDataViewsEqual(a: DataView, b: DataView) {
* A cheap check if the source looks like base64.
*/
function looksLikeBase64(source: string) {
return source.length > 500 || source.startsWith("data:");
return source.length > 500 || source.startsWith("data:") || source.includes(";base64,");
}
/**
@@ -267,18 +267,40 @@ export function areArrayBuffersEqual(a?: ArrayBuffer | null, b?: ArrayBuffer | n
return areDataViewsEqual(new DataView(a), new DataView(b));
}
export function newCanvas(
widthOrPtOrImage: number | {width: number; height: number} | HTMLImageElement,
height?: number,
) {
let width: number;
if (typeof widthOrPtOrImage !== "number") {
width = widthOrPtOrImage.width;
height = widthOrPtOrImage.height;
} else {
width = widthOrPtOrImage;
height = height;
}
if (height == null) {
throw new Error("Invalid height supplied when creating new canvas object.");
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
if (widthOrPtOrImage instanceof HTMLImageElement) {
const ctx = canvas.getContext("2d")!;
ctx.drawImage(widthOrPtOrImage, 0, 0, width, height);
}
return canvas;
}
/**
* Returns canvas image data for an HTML Image.
*/
export function getCanvasImageData(
image: HTMLImageElement,
): [HTMLCanvasElement, CanvasRenderingContext2D, ImageData] {
const canvas = document.createElement("canvas");
const canvas = newCanvas(image);
const ctx = canvas.getContext("2d")!;
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return [canvas, ctx, imageData];
}
@@ -301,6 +323,9 @@ export async function convertToBase64(
return convertToBase64(await loadImage(source));
}
if (source instanceof HTMLImageElement) {
if (looksLikeBase64(source.src)) {
return source.src;
}
const [canvas, ctx, imageData] = getCanvasImageData(source);
return convertToBase64(canvas);
}
+12
View File
@@ -356,6 +356,18 @@ export function empty(element: HTMLElement) {
return element;
}
export function remove(element: HTMLElement) {
while (element.parentElement) {
element.parentElement.removeChild(element);
}
return element;
}
export function replaceChild(oldChildNode: Node, newNode: Node) {
oldChildNode.parentNode!.replaceChild(newNode, oldChildNode);
return newNode;
}
type ChildType = HTMLElement | DocumentFragment | Text | string | null;
export function appendChildren(el: HTMLElement, children: ChildType | ChildType[]) {
children = !Array.isArray(children) ? [children] : children;
+2 -2
View File
@@ -1,7 +1,7 @@
import { app } from "../../scripts/app.js";
import { ComfyButtonGroup } from "../../scripts/ui/components/buttonGroup.js";
import { ComfyButton } from "../../scripts/ui/components/button.js";
import { iconGear, iconStarFilled, logoRgthree } from "../../rgthree/common/media/svgs.js";
import { iconGear, iconStarFilled, logoRgthreeAsync } from "../../rgthree/common/media/svgs.js";
import { createElement, empty } from "../../rgthree/common/utils_dom.js";
import { SERVICE as BOOKMARKS_SERVICE } from "./services/bookmarks_services.js";
import { SERVICE as CONFIG_SERVICE } from "./services/config_service.js";
@@ -30,7 +30,7 @@ function addRgthreeTopBarButtons() {
});
buttons.push(rgthreeButton);
rgthreeButton.iconElement.style.width = "1.2rem";
rgthreeButton.iconElement.innerHTML = logoRgthree;
logoRgthreeAsync().then((t) => { rgthreeButton.iconElement.innerHTML = t; });
rgthreeButton.withPopup(new ComfyPopup({ target: rgthreeButton.element, classList: "rgthree-top-menu" }, createElement("menu", {
children: [
createElement("li", {
+1 -1
View File
@@ -57,7 +57,7 @@ app.registerExtension({
});
}
else {
rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
rgthree.queueOutputNodes(outputNodes);
}
}
else {
+2 -2
View File
@@ -11,7 +11,7 @@ function showQueueNodesMenuIfOutputNodesAreSelected(existingOptions) {
content: `Queue Selected Output Nodes (rgthree) &nbsp;`,
className: "rgthree-contextmenu-item",
callback: () => {
rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
rgthree.queueOutputNodes(outputNodes);
},
disabled: !outputNodes.length,
};
@@ -31,7 +31,7 @@ function showQueueGroupNodesMenuIfGroupIsSelected(existingOptions) {
content: `Queue Group Output Nodes (rgthree) &nbsp;`,
className: "rgthree-contextmenu-item",
callback: () => {
outputNodes && rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
outputNodes && rgthree.queueOutputNodes(outputNodes);
},
disabled: !(outputNodes === null || outputNodes === void 0 ? void 0 : outputNodes.length),
};
+12 -3
View File
@@ -143,6 +143,8 @@ class Rgthree extends EventTarget {
this.lastCanvasMouseEvent = null;
this.canvasCurrentlyCopyingToClipboard = false;
this.canvasCurrentlyCopyingToClipboardWithMultipleNodes = false;
this.canvasCurrentlyPastingFromClipboard = false;
this.canvasCurrentlyPastingFromClipboardWithMultipleNodes = false;
this.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff = null;
this.isMac = !!(((_a = navigator.platform) === null || _a === void 0 ? void 0 : _a.toLocaleUpperCase().startsWith("MAC")) ||
((_c = (_b = navigator.userAgentData) === null || _b === void 0 ? void 0 : _b.platform) === null || _c === void 0 ? void 0 : _c.toLocaleUpperCase().startsWith("MAC")));
@@ -262,6 +264,12 @@ class Rgthree extends EventTarget {
rgthree.canvasCurrentlyCopyingToClipboard = false;
rgthree.canvasCurrentlyCopyingToClipboardWithMultipleNodes = false;
};
const pasteFromClipboard = LGraphCanvas.prototype.pasteFromClipboard;
LGraphCanvas.prototype.pasteFromClipboard = function (...args) {
rgthree.canvasCurrentlyPastingFromClipboard = true;
pasteFromClipboard.apply(this, [...arguments]);
rgthree.canvasCurrentlyPastingFromClipboard = false;
};
const onGroupAdd = LGraphCanvas.onGroupAdd;
LGraphCanvas.onGroupAdd = function (...args) {
const graph = app.canvas.getCurrentGraph();
@@ -431,8 +439,9 @@ class Rgthree extends EventTarget {
},
];
}
async queueOutputNodes(nodeIds) {
async queueOutputNodes(nodes) {
var _a;
const nodeIds = nodes.map(n => n.id);
try {
this.queueNodeIds = nodeIds;
await app.queuePrompt(0);
@@ -491,7 +500,7 @@ class Rgthree extends EventTarget {
return promise;
};
const apiQueuePrompt = api.queuePrompt;
api.queuePrompt = async function (index, prompt) {
api.queuePrompt = async function (index, prompt, ...args) {
var _a;
if (((_a = rgthree.queueNodeIds) === null || _a === void 0 ? void 0 : _a.length) && prompt.output) {
const oldOutput = prompt.output;
@@ -505,7 +514,7 @@ class Rgthree extends EventTarget {
workflow: prompt.workflow,
output: prompt.output,
});
const response = apiQueuePrompt.apply(app, [index, prompt]);
const response = apiQueuePrompt.apply(app, [index, prompt, ...args]);
rgthree.dispatchCustomEvent("comfy-api-queue-prompt-end");
return response;
};
+27 -14
View File
@@ -17,12 +17,20 @@ class RgthreeSeed extends RgthreeBaseServerNode {
this.lastSeed = undefined;
this.serializedCtx = {};
this.lastSeedValue = null;
this.randMax = 1125899906842624;
this.randMin = 0;
this.randomRange = 1125899906842624;
this.handleApiHijackingBound = this.handleApiHijacking.bind(this);
this.properties["randomMax"] = 1125899906842624;
this.properties["randomMin"] = 0;
rgthree.addEventListener("comfy-api-queue-prompt-before", this.handleApiHijackingBound);
}
onPropertyChanged(prop, value, prevValue) {
if (prop === 'randomMax') {
this.properties["randomMax"] = Math.min(1125899906842624, Number(value));
}
else if (prop === 'randomMin') {
this.properties["randomMin"] = Math.max(-1125899906842624, Number(value));
}
return true;
}
onRemoved() {
rgthree.addEventListener("comfy-api-queue-prompt-before", this.handleApiHijackingBound);
}
@@ -44,7 +52,7 @@ class RgthreeSeed extends RgthreeBaseServerNode {
}
}
onNodeCreated() {
var _a, _b, _c;
var _a;
(_a = super.onNodeCreated) === null || _a === void 0 ? void 0 : _a.call(this);
for (const [i, w] of this.widgets.entries()) {
if (w.name === "seed") {
@@ -55,16 +63,11 @@ class RgthreeSeed extends RgthreeBaseServerNode {
this.widgets.splice(i, 1);
}
}
let step = this.seedWidget.options.step || 1;
this.randMax = Math.min(1125899906842624, (_b = this.seedWidget.options.max) !== null && _b !== void 0 ? _b : 0);
this.randMin = Math.max(0, (_c = this.seedWidget.options.min) !== null && _c !== void 0 ? _c : 0);
this.randomRange = (this.randMax - Math.max(0, this.randMin)) / (step / 10);
this.addWidget("button", "🎲 Randomize Each Time", "", () => {
this.seedWidget.value = SPECIAL_SEED_RANDOM;
}, { serialize: false });
this.addWidget("button", "🎲 New Fixed Random", "", () => {
this.seedWidget.value =
Math.floor(Math.random() * this.randomRange) * (step / 10) + this.randMin;
this.seedWidget.value = this.generateRandomSeed();
}, { serialize: false });
this.lastSeedButton = this.addWidget("button", LAST_SEED_BUTTON_LABEL, "", () => {
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value;
@@ -73,6 +76,17 @@ class RgthreeSeed extends RgthreeBaseServerNode {
}, { width: 50, serialize: false });
this.lastSeedButton.disabled = true;
}
generateRandomSeed() {
let step = this.seedWidget.options.step || 1;
const randomMin = Number(this.properties['randomMin'] || 0);
const randomMax = Number(this.properties['randomMax'] || 1125899906842624);
const randomRange = (randomMax - randomMin) / (step / 10);
let seed = Math.floor(Math.random() * randomRange) * (step / 10) + randomMin;
if (SPECIAL_SEEDS.includes(seed)) {
seed = 0;
}
return seed;
}
getExtraMenuOptions(canvas, options) {
var _a;
(_a = super.getExtraMenuOptions) === null || _a === void 0 ? void 0 : _a.apply(this, [...arguments]);
@@ -154,10 +168,7 @@ class RgthreeSeed extends RgthreeBaseServerNode {
}
}
if (seedToUse == null || SPECIAL_SEEDS.includes(seedToUse)) {
seedToUse =
Math.floor(Math.random() * this.randomRange) *
((this.seedWidget.options.step || 1) / 10) +
this.randMin;
seedToUse = this.generateRandomSeed();
}
}
return seedToUse !== null && seedToUse !== void 0 ? seedToUse : inputSeed;
@@ -179,6 +190,8 @@ RgthreeSeed.title = NodeTypesString.SEED;
RgthreeSeed.type = NodeTypesString.SEED;
RgthreeSeed.comfyClass = NodeTypesString.SEED;
RgthreeSeed.exposedActions = ["Randomize Each Time", "Use Last Queued Seed"];
RgthreeSeed["@randomMax"] = { type: "number" };
RgthreeSeed["@randomMin"] = { type: "number" };
app.registerExtension({
name: "rgthree.Seed",
async beforeRegisterNodeDef(nodeType, nodeData) {
@@ -81,6 +81,8 @@ class FastGroupsService {
return this.cachedNodeBoundings;
}
recomputeInsideNodesForGroup(group) {
if (app.canvas.isDragging)
return;
const cachedBoundings = this.getBoundingsForAllNodes();
const nodes = group.graph.nodes;
group._children.clear();
+29 -6
View File
@@ -1,10 +1,33 @@
import { createElement as $el } from "../utils_dom.js";
export const logoRgthree = `<svg viewBox="0 0 256 256" fill="currentColor" class="rgthree-logo">
<path d="M88.503,158.997 L152.731,196.103 L152.738,196.092 L152.762,196.103 L152.769,196.106 L152.771,196.103 L183.922,142.084 L174.153,136.437 L148.611,180.676 L101.512,153.484 L132.193,30.415 L156.124,71.869 L165.896,66.225 L128.002,0.59 "></path>
<path d="M55.586,148.581l13.44,47.521l0.014,0.051l0.168-0.051l10.689-3.022l-6.589-23.313l45.609,26.335l0.087,0.051l0.027-0.051 l5.617-9.718l-42.648-24.622l35.771-143.45L33.232,164.729l9.77,5.645L55.586,148.581z M87.394,93.484l-16.708,67.018l-5.018-17.747 l-8.028,2.27L87.394,93.484z"></path>
<path d="M189.85,107.717 L137.892,137.718 L143.532,147.49 L185.723,123.133 L231.109,201.746 L24.895,201.746 L37.363,180.146 L27.592,174.505 L5.347,213.03 L250.653,213.03 "></path>
<path d="M5.347,247.299v8.111h245.307v-8.111l-41.94-0.003c-1.336,0-2.404-1.065-2.441-2.396v-12.14 c0.037-1.315,1.089-2.368,2.41-2.385h41.972v-8.11H5.347v8.11h41.951c1.338,0.017,2.427,1.104,2.427,2.449v12.01 c0,1.365-1.105,2.462-2.457,2.462L5.347,247.299z M139.438,247.296c-1.334,0-2.406-1.065-2.439-2.396v-12.14 c0.033-1.315,1.085-2.368,2.41-2.385h46.415c1.335,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.459,2.462H139.438z M70.193,247.296c-1.339,0-2.408-1.065-2.441-2.396v-12.14c0.033-1.315,1.086-2.368,2.407-2.385h46.418 c1.336,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.458,2.462H70.193z"></path>
</svg>`;
export let logoRgthree = "";
export async function logoRgthreeAsync() {
var _a;
if (logoRgthree)
return logoRgthree;
let baseUrl = null;
if (window.location.pathname.includes("/rgthree/")) {
const parts = (_a = window.location.pathname.split("/rgthree/")[1]) === null || _a === void 0 ? void 0 : _a.split("/");
if (parts && parts.length) {
baseUrl = parts.map(() => "../").join("") + "rgthree";
}
}
baseUrl = baseUrl || "./rgthree";
return fetch(`${baseUrl}/logo_markup.svg?fg=currentColor&cssClass=rgthree-logo&w=auto&h=auto`)
.then((r) => r.text())
.then((t) => {
if (t.length < 100) {
t = `<svg viewBox="0 0 256 256" fill="currentColor" class="rgthree-logo">
<path d="M88.503,158.997 L152.731,196.103 L152.738,196.092 L152.762,196.103 L152.769,196.106 L152.771,196.103 L183.922,142.084 L174.153,136.437 L148.611,180.676 L101.512,153.484 L132.193,30.415 L156.124,71.869 L165.896,66.225 L128.002,0.59 "></path>
<path d="M55.586,148.581l13.44,47.521l0.014,0.051l0.168-0.051l10.689-3.022l-6.589-23.313l45.609,26.335l0.087,0.051l0.027-0.051 l5.617-9.718l-42.648-24.622l35.771-143.45L33.232,164.729l9.77,5.645L55.586,148.581z M87.394,93.484l-16.708,67.018l-5.018-17.747 l-8.028,2.27L87.394,93.484z"></path>
<path d="M189.85,107.717 L137.892,137.718 L143.532,147.49 L185.723,123.133 L231.109,201.746 L24.895,201.746 L37.363,180.146 L27.592,174.505 L5.347,213.03 L250.653,213.03 "></path>
<path d="M5.347,247.299v8.111h245.307v-8.111l-41.94-0.003c-1.336,0-2.404-1.065-2.441-2.396v-12.14 c0.037-1.315,1.089-2.368,2.41-2.385h41.972v-8.11H5.347v8.11h41.951c1.338,0.017,2.427,1.104,2.427,2.449v12.01 c0,1.365-1.105,2.462-2.457,2.462L5.347,247.299z M139.438,247.296c-1.334,0-2.406-1.065-2.439-2.396v-12.14 c0.033-1.315,1.085-2.368,2.41-2.385h46.415c1.335,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.459,2.462H139.438z M70.193,247.296c-1.339,0-2.408-1.065-2.441-2.396v-12.14c0.033-1.315,1.086-2.368,2.407-2.385h46.418 c1.336,0.017,2.425,1.104,2.425,2.449v12.01c0,1.365-1.103,2.462-2.458,2.462H70.193z"></path>
</svg>`;
}
logoRgthree = t;
return t;
});
}
logoRgthreeAsync();
export const github = `<svg viewBox="0 0 16 16" fill="currentColor" class="github-logo">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
</svg>`;
+1 -1
View File
@@ -82,7 +82,7 @@ class PromptService extends EventTarget {
this.lastQueueRemaining = 0;
const that = this;
const queuePrompt = api.queuePrompt;
api.queuePrompt = async function (num, prompt) {
api.queuePrompt = async function (num, prompt, ...args) {
let response;
try {
response = await queuePrompt.apply(api, [...arguments]);
+28 -6
View File
@@ -168,7 +168,7 @@ export function areDataViewsEqual(a, b) {
return true;
}
function looksLikeBase64(source) {
return source.length > 500 || source.startsWith("data:");
return source.length > 500 || source.startsWith("data:") || source.includes(";base64,");
}
export function areArrayBuffersEqual(a, b) {
if (a == b || !a || !b) {
@@ -176,13 +176,32 @@ export function areArrayBuffersEqual(a, b) {
}
return areDataViewsEqual(new DataView(a), new DataView(b));
}
export function getCanvasImageData(image) {
export function newCanvas(widthOrPtOrImage, height) {
let width;
if (typeof widthOrPtOrImage !== "number") {
width = widthOrPtOrImage.width;
height = widthOrPtOrImage.height;
}
else {
width = widthOrPtOrImage;
height = height;
}
if (height == null) {
throw new Error("Invalid height supplied when creating new canvas object.");
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
if (widthOrPtOrImage instanceof HTMLImageElement) {
const ctx = canvas.getContext("2d");
ctx.drawImage(widthOrPtOrImage, 0, 0, width, height);
}
return canvas;
}
export function getCanvasImageData(image) {
const canvas = newCanvas(image);
const ctx = canvas.getContext("2d");
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return [canvas, ctx, imageData];
}
export async function convertToBase64(source) {
@@ -196,6 +215,9 @@ export async function convertToBase64(source) {
return convertToBase64(await loadImage(source));
}
if (source instanceof HTMLImageElement) {
if (looksLikeBase64(source.src)) {
return source.src;
}
const [canvas, ctx, imageData] = getCanvasImageData(source);
return convertToBase64(canvas);
}
+10
View File
@@ -299,6 +299,16 @@ export function empty(element) {
}
return element;
}
export function remove(element) {
while (element.parentElement) {
element.parentElement.removeChild(element);
}
return element;
}
export function replaceChild(oldChildNode, newNode) {
oldChildNode.parentNode.replaceChild(newNode, oldChildNode);
return newNode;
}
export function appendChildren(el, children) {
children = !Array.isArray(children) ? [children] : children;
for (let child of children) {