Upload 71 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- ComfyUI/web/extensions/core/clipspace.js +166 -0
- ComfyUI/web/extensions/core/colorPalette.js +785 -0
- ComfyUI/web/extensions/core/contextMenuFilter.js +148 -0
- ComfyUI/web/extensions/core/dynamicPrompts.js +48 -0
- ComfyUI/web/extensions/core/editAttention.js +144 -0
- ComfyUI/web/extensions/core/groupNode.js +1281 -0
- ComfyUI/web/extensions/core/groupNodeManage.css +149 -0
- ComfyUI/web/extensions/core/groupNodeManage.js +422 -0
- ComfyUI/web/extensions/core/groupOptions.js +259 -0
- ComfyUI/web/extensions/core/invertMenuScrolling.js +36 -0
- ComfyUI/web/extensions/core/keybinds.js +69 -0
- ComfyUI/web/extensions/core/linkRenderMode.js +25 -0
- ComfyUI/web/extensions/core/maskeditor.js +967 -0
- ComfyUI/web/extensions/core/nodeTemplates.js +412 -0
- ComfyUI/web/extensions/core/noteNode.js +41 -0
- ComfyUI/web/extensions/core/rerouteNode.js +274 -0
- ComfyUI/web/extensions/core/saveImageExtraOutput.js +35 -0
- ComfyUI/web/extensions/core/simpleTouchSupport.js +102 -0
- ComfyUI/web/extensions/core/slotDefaults.js +91 -0
- ComfyUI/web/extensions/core/snapToGrid.js +171 -0
- ComfyUI/web/extensions/core/uploadAudio.js +186 -0
- ComfyUI/web/extensions/core/uploadImage.js +12 -0
- ComfyUI/web/extensions/core/webcamCapture.js +126 -0
- ComfyUI/web/extensions/core/widgetInputs.js +800 -0
- ComfyUI/web/extensions/logging.js.example +55 -0
- ComfyUI/web/fonts/materialdesignicons-webfont.woff2 +0 -0
- ComfyUI/web/index.html +49 -0
- ComfyUI/web/jsconfig.json +12 -0
- ComfyUI/web/lib/litegraph.core.js +0 -0
- ComfyUI/web/lib/litegraph.css +693 -0
- ComfyUI/web/lib/litegraph.extensions.js +21 -0
- ComfyUI/web/lib/materialdesignicons.min.css +0 -0
- ComfyUI/web/scripts/api.js +482 -0
- ComfyUI/web/scripts/app.js +2459 -0
- ComfyUI/web/scripts/changeTracker.js +255 -0
- ComfyUI/web/scripts/defaultGraph.js +119 -0
- ComfyUI/web/scripts/domWidget.js +329 -0
- ComfyUI/web/scripts/logging.js +370 -0
- ComfyUI/web/scripts/pnginfo.js +506 -0
- ComfyUI/web/scripts/ui.js +660 -0
- ComfyUI/web/scripts/ui/components/asyncDialog.js +64 -0
- ComfyUI/web/scripts/ui/components/button.js +163 -0
- ComfyUI/web/scripts/ui/components/buttonGroup.js +45 -0
- ComfyUI/web/scripts/ui/components/popup.js +128 -0
- ComfyUI/web/scripts/ui/components/splitButton.js +43 -0
- ComfyUI/web/scripts/ui/dialog.js +38 -0
- ComfyUI/web/scripts/ui/draggableList.js +287 -0
- ComfyUI/web/scripts/ui/imagePreview.js +97 -0
- ComfyUI/web/scripts/ui/menu/index.js +302 -0
- ComfyUI/web/scripts/ui/menu/interruptButton.js +23 -0
ComfyUI/web/extensions/core/clipspace.js
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
3 |
+
import { ComfyApp } from "../../scripts/app.js";
|
4 |
+
|
5 |
+
export class ClipspaceDialog extends ComfyDialog {
|
6 |
+
static items = [];
|
7 |
+
static instance = null;
|
8 |
+
|
9 |
+
static registerButton(name, contextPredicate, callback) {
|
10 |
+
const item =
|
11 |
+
$el("button", {
|
12 |
+
type: "button",
|
13 |
+
textContent: name,
|
14 |
+
contextPredicate: contextPredicate,
|
15 |
+
onclick: callback
|
16 |
+
})
|
17 |
+
|
18 |
+
ClipspaceDialog.items.push(item);
|
19 |
+
}
|
20 |
+
|
21 |
+
static invalidatePreview() {
|
22 |
+
if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) {
|
23 |
+
const img_preview = document.getElementById("clipspace_preview");
|
24 |
+
if(img_preview) {
|
25 |
+
img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
|
26 |
+
img_preview.style.maxHeight = "100%";
|
27 |
+
img_preview.style.maxWidth = "100%";
|
28 |
+
}
|
29 |
+
}
|
30 |
+
}
|
31 |
+
|
32 |
+
static invalidate() {
|
33 |
+
if(ClipspaceDialog.instance) {
|
34 |
+
const self = ClipspaceDialog.instance;
|
35 |
+
// allow reconstruct controls when copying from non-image to image content.
|
36 |
+
const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]);
|
37 |
+
|
38 |
+
if(self.element) {
|
39 |
+
// update
|
40 |
+
self.element.removeChild(self.element.firstChild);
|
41 |
+
self.element.appendChild(children);
|
42 |
+
}
|
43 |
+
else {
|
44 |
+
// new
|
45 |
+
self.element = $el("div.comfy-modal", { parent: document.body }, [children,]);
|
46 |
+
}
|
47 |
+
|
48 |
+
if(self.element.children[0].children.length <= 1) {
|
49 |
+
self.element.children[0].appendChild($el("p", {}, ["Unable to find the features to edit content of a format stored in the current Clipspace."]));
|
50 |
+
}
|
51 |
+
|
52 |
+
ClipspaceDialog.invalidatePreview();
|
53 |
+
}
|
54 |
+
}
|
55 |
+
|
56 |
+
constructor() {
|
57 |
+
super();
|
58 |
+
}
|
59 |
+
|
60 |
+
createButtons(self) {
|
61 |
+
const buttons = [];
|
62 |
+
|
63 |
+
for(let idx in ClipspaceDialog.items) {
|
64 |
+
const item = ClipspaceDialog.items[idx];
|
65 |
+
if(!item.contextPredicate || item.contextPredicate())
|
66 |
+
buttons.push(ClipspaceDialog.items[idx]);
|
67 |
+
}
|
68 |
+
|
69 |
+
buttons.push(
|
70 |
+
$el("button", {
|
71 |
+
type: "button",
|
72 |
+
textContent: "Close",
|
73 |
+
onclick: () => { this.close(); }
|
74 |
+
})
|
75 |
+
);
|
76 |
+
|
77 |
+
return buttons;
|
78 |
+
}
|
79 |
+
|
80 |
+
createImgSettings() {
|
81 |
+
if(ComfyApp.clipspace.imgs) {
|
82 |
+
const combo_items = [];
|
83 |
+
const imgs = ComfyApp.clipspace.imgs;
|
84 |
+
|
85 |
+
for(let i=0; i < imgs.length; i++) {
|
86 |
+
combo_items.push($el("option", {value:i}, [`${i}`]));
|
87 |
+
}
|
88 |
+
|
89 |
+
const combo1 = $el("select",
|
90 |
+
{id:"clipspace_img_selector", onchange:(event) => {
|
91 |
+
ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex;
|
92 |
+
ClipspaceDialog.invalidatePreview();
|
93 |
+
} }, combo_items);
|
94 |
+
|
95 |
+
const row1 =
|
96 |
+
$el("tr", {},
|
97 |
+
[
|
98 |
+
$el("td", {}, [$el("font", {color:"white"}, ["Select Image"])]),
|
99 |
+
$el("td", {}, [combo1])
|
100 |
+
]);
|
101 |
+
|
102 |
+
|
103 |
+
const combo2 = $el("select",
|
104 |
+
{id:"clipspace_img_paste_mode", onchange:(event) => {
|
105 |
+
ComfyApp.clipspace['img_paste_mode'] = event.target.value;
|
106 |
+
} },
|
107 |
+
[
|
108 |
+
$el("option", {value:'selected'}, 'selected'),
|
109 |
+
$el("option", {value:'all'}, 'all')
|
110 |
+
]);
|
111 |
+
combo2.value = ComfyApp.clipspace['img_paste_mode'];
|
112 |
+
|
113 |
+
const row2 =
|
114 |
+
$el("tr", {},
|
115 |
+
[
|
116 |
+
$el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]),
|
117 |
+
$el("td", {}, [combo2])
|
118 |
+
]);
|
119 |
+
|
120 |
+
const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'},
|
121 |
+
[ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]);
|
122 |
+
|
123 |
+
const row3 =
|
124 |
+
$el("tr", {}, [td]);
|
125 |
+
|
126 |
+
return $el("table", {}, [row1, row2, row3]);
|
127 |
+
}
|
128 |
+
else {
|
129 |
+
return [];
|
130 |
+
}
|
131 |
+
}
|
132 |
+
|
133 |
+
createImgPreview() {
|
134 |
+
if(ComfyApp.clipspace.imgs) {
|
135 |
+
return $el("img",{id:"clipspace_preview", ondragstart:() => false});
|
136 |
+
}
|
137 |
+
else
|
138 |
+
return [];
|
139 |
+
}
|
140 |
+
|
141 |
+
show() {
|
142 |
+
const img_preview = document.getElementById("clipspace_preview");
|
143 |
+
ClipspaceDialog.invalidate();
|
144 |
+
|
145 |
+
this.element.style.display = "block";
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
app.registerExtension({
|
150 |
+
name: "Comfy.Clipspace",
|
151 |
+
init(app) {
|
152 |
+
app.openClipspace =
|
153 |
+
function () {
|
154 |
+
if(!ClipspaceDialog.instance) {
|
155 |
+
ClipspaceDialog.instance = new ClipspaceDialog(app);
|
156 |
+
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
|
157 |
+
}
|
158 |
+
|
159 |
+
if(ComfyApp.clipspace) {
|
160 |
+
ClipspaceDialog.instance.show();
|
161 |
+
}
|
162 |
+
else
|
163 |
+
app.ui.dialog.show("Clipspace is Empty!");
|
164 |
+
};
|
165 |
+
}
|
166 |
+
});
|
ComfyUI/web/extensions/core/colorPalette.js
ADDED
@@ -0,0 +1,785 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {app} from "../../scripts/app.js";
|
2 |
+
import {$el} from "../../scripts/ui.js";
|
3 |
+
|
4 |
+
// Manage color palettes
|
5 |
+
|
6 |
+
const colorPalettes = {
|
7 |
+
"dark": {
|
8 |
+
"id": "dark",
|
9 |
+
"name": "Dark (Default)",
|
10 |
+
"colors": {
|
11 |
+
"node_slot": {
|
12 |
+
"CLIP": "#FFD500", // bright yellow
|
13 |
+
"CLIP_VISION": "#A8DADC", // light blue-gray
|
14 |
+
"CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange
|
15 |
+
"CONDITIONING": "#FFA931", // vibrant orange-yellow
|
16 |
+
"CONTROL_NET": "#6EE7B7", // soft mint green
|
17 |
+
"IMAGE": "#64B5F6", // bright sky blue
|
18 |
+
"LATENT": "#FF9CF9", // light pink-purple
|
19 |
+
"MASK": "#81C784", // muted green
|
20 |
+
"MODEL": "#B39DDB", // light lavender-purple
|
21 |
+
"STYLE_MODEL": "#C2FFAE", // light green-yellow
|
22 |
+
"VAE": "#FF6E6E", // bright red
|
23 |
+
"NOISE": "#B0B0B0", // gray
|
24 |
+
"GUIDER": "#66FFFF", // cyan
|
25 |
+
"SAMPLER": "#ECB4B4", // very soft red
|
26 |
+
"SIGMAS": "#CDFFCD", // soft lime green
|
27 |
+
"TAESD": "#DCC274", // cheesecake
|
28 |
+
},
|
29 |
+
"litegraph_base": {
|
30 |
+
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
|
31 |
+
"CLEAR_BACKGROUND_COLOR": "#222",
|
32 |
+
"NODE_TITLE_COLOR": "#999",
|
33 |
+
"NODE_SELECTED_TITLE_COLOR": "#FFF",
|
34 |
+
"NODE_TEXT_SIZE": 14,
|
35 |
+
"NODE_TEXT_COLOR": "#AAA",
|
36 |
+
"NODE_SUBTEXT_SIZE": 12,
|
37 |
+
"NODE_DEFAULT_COLOR": "#333",
|
38 |
+
"NODE_DEFAULT_BGCOLOR": "#353535",
|
39 |
+
"NODE_DEFAULT_BOXCOLOR": "#666",
|
40 |
+
"NODE_DEFAULT_SHAPE": "box",
|
41 |
+
"NODE_BOX_OUTLINE_COLOR": "#FFF",
|
42 |
+
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
43 |
+
"DEFAULT_GROUP_FONT": 24,
|
44 |
+
|
45 |
+
"WIDGET_BGCOLOR": "#222",
|
46 |
+
"WIDGET_OUTLINE_COLOR": "#666",
|
47 |
+
"WIDGET_TEXT_COLOR": "#DDD",
|
48 |
+
"WIDGET_SECONDARY_TEXT_COLOR": "#999",
|
49 |
+
|
50 |
+
"LINK_COLOR": "#9A9",
|
51 |
+
"EVENT_LINK_COLOR": "#A86",
|
52 |
+
"CONNECTING_LINK_COLOR": "#AFA",
|
53 |
+
},
|
54 |
+
"comfy_base": {
|
55 |
+
"fg-color": "#fff",
|
56 |
+
"bg-color": "#202020",
|
57 |
+
"comfy-menu-bg": "#353535",
|
58 |
+
"comfy-input-bg": "#222",
|
59 |
+
"input-text": "#ddd",
|
60 |
+
"descrip-text": "#999",
|
61 |
+
"drag-text": "#ccc",
|
62 |
+
"error-text": "#ff4444",
|
63 |
+
"border-color": "#4e4e4e",
|
64 |
+
"tr-even-bg-color": "#222",
|
65 |
+
"tr-odd-bg-color": "#353535",
|
66 |
+
"content-bg": "#4e4e4e",
|
67 |
+
"content-fg": "#fff",
|
68 |
+
"content-hover-bg": "#222",
|
69 |
+
"content-hover-fg": "#fff"
|
70 |
+
}
|
71 |
+
},
|
72 |
+
},
|
73 |
+
"light": {
|
74 |
+
"id": "light",
|
75 |
+
"name": "Light",
|
76 |
+
"colors": {
|
77 |
+
"node_slot": {
|
78 |
+
"CLIP": "#FFA726", // orange
|
79 |
+
"CLIP_VISION": "#5C6BC0", // indigo
|
80 |
+
"CLIP_VISION_OUTPUT": "#8D6E63", // brown
|
81 |
+
"CONDITIONING": "#EF5350", // red
|
82 |
+
"CONTROL_NET": "#66BB6A", // green
|
83 |
+
"IMAGE": "#42A5F5", // blue
|
84 |
+
"LATENT": "#AB47BC", // purple
|
85 |
+
"MASK": "#9CCC65", // light green
|
86 |
+
"MODEL": "#7E57C2", // deep purple
|
87 |
+
"STYLE_MODEL": "#D4E157", // lime
|
88 |
+
"VAE": "#FF7043", // deep orange
|
89 |
+
},
|
90 |
+
"litegraph_base": {
|
91 |
+
"BACKGROUND_IMAGE": "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==",
|
92 |
+
"CLEAR_BACKGROUND_COLOR": "lightgray",
|
93 |
+
"NODE_TITLE_COLOR": "#222",
|
94 |
+
"NODE_SELECTED_TITLE_COLOR": "#000",
|
95 |
+
"NODE_TEXT_SIZE": 14,
|
96 |
+
"NODE_TEXT_COLOR": "#444",
|
97 |
+
"NODE_SUBTEXT_SIZE": 12,
|
98 |
+
"NODE_DEFAULT_COLOR": "#F7F7F7",
|
99 |
+
"NODE_DEFAULT_BGCOLOR": "#F5F5F5",
|
100 |
+
"NODE_DEFAULT_BOXCOLOR": "#CCC",
|
101 |
+
"NODE_DEFAULT_SHAPE": "box",
|
102 |
+
"NODE_BOX_OUTLINE_COLOR": "#000",
|
103 |
+
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)",
|
104 |
+
"DEFAULT_GROUP_FONT": 24,
|
105 |
+
|
106 |
+
"WIDGET_BGCOLOR": "#D4D4D4",
|
107 |
+
"WIDGET_OUTLINE_COLOR": "#999",
|
108 |
+
"WIDGET_TEXT_COLOR": "#222",
|
109 |
+
"WIDGET_SECONDARY_TEXT_COLOR": "#555",
|
110 |
+
|
111 |
+
"LINK_COLOR": "#4CAF50",
|
112 |
+
"EVENT_LINK_COLOR": "#FF9800",
|
113 |
+
"CONNECTING_LINK_COLOR": "#2196F3",
|
114 |
+
},
|
115 |
+
"comfy_base": {
|
116 |
+
"fg-color": "#222",
|
117 |
+
"bg-color": "#DDD",
|
118 |
+
"comfy-menu-bg": "#F5F5F5",
|
119 |
+
"comfy-input-bg": "#C9C9C9",
|
120 |
+
"input-text": "#222",
|
121 |
+
"descrip-text": "#444",
|
122 |
+
"drag-text": "#555",
|
123 |
+
"error-text": "#F44336",
|
124 |
+
"border-color": "#888",
|
125 |
+
"tr-even-bg-color": "#f9f9f9",
|
126 |
+
"tr-odd-bg-color": "#fff",
|
127 |
+
"content-bg": "#e0e0e0",
|
128 |
+
"content-fg": "#222",
|
129 |
+
"content-hover-bg": "#adadad",
|
130 |
+
"content-hover-fg": "#222"
|
131 |
+
}
|
132 |
+
},
|
133 |
+
},
|
134 |
+
"solarized": {
|
135 |
+
"id": "solarized",
|
136 |
+
"name": "Solarized",
|
137 |
+
"colors": {
|
138 |
+
"node_slot": {
|
139 |
+
"CLIP": "#2AB7CA", // light blue
|
140 |
+
"CLIP_VISION": "#6c71c4", // blue violet
|
141 |
+
"CLIP_VISION_OUTPUT": "#859900", // olive green
|
142 |
+
"CONDITIONING": "#d33682", // magenta
|
143 |
+
"CONTROL_NET": "#d1ffd7", // light mint green
|
144 |
+
"IMAGE": "#5940bb", // deep blue violet
|
145 |
+
"LATENT": "#268bd2", // blue
|
146 |
+
"MASK": "#CCC9E7", // light purple-gray
|
147 |
+
"MODEL": "#dc322f", // red
|
148 |
+
"STYLE_MODEL": "#1a998a", // teal
|
149 |
+
"UPSCALE_MODEL": "#054A29", // dark green
|
150 |
+
"VAE": "#facfad", // light pink-orange
|
151 |
+
},
|
152 |
+
"litegraph_base": {
|
153 |
+
"NODE_TITLE_COLOR": "#fdf6e3", // Base3
|
154 |
+
"NODE_SELECTED_TITLE_COLOR": "#A9D400",
|
155 |
+
"NODE_TEXT_SIZE": 14,
|
156 |
+
"NODE_TEXT_COLOR": "#657b83", // Base00
|
157 |
+
"NODE_SUBTEXT_SIZE": 12,
|
158 |
+
"NODE_DEFAULT_COLOR": "#094656",
|
159 |
+
"NODE_DEFAULT_BGCOLOR": "#073642", // Base02
|
160 |
+
"NODE_DEFAULT_BOXCOLOR": "#839496", // Base0
|
161 |
+
"NODE_DEFAULT_SHAPE": "box",
|
162 |
+
"NODE_BOX_OUTLINE_COLOR": "#fdf6e3", // Base3
|
163 |
+
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
164 |
+
"DEFAULT_GROUP_FONT": 24,
|
165 |
+
|
166 |
+
"WIDGET_BGCOLOR": "#002b36", // Base03
|
167 |
+
"WIDGET_OUTLINE_COLOR": "#839496", // Base0
|
168 |
+
"WIDGET_TEXT_COLOR": "#fdf6e3", // Base3
|
169 |
+
"WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", // Base1
|
170 |
+
|
171 |
+
"LINK_COLOR": "#2aa198", // Solarized Cyan
|
172 |
+
"EVENT_LINK_COLOR": "#268bd2", // Solarized Blue
|
173 |
+
"CONNECTING_LINK_COLOR": "#859900", // Solarized Green
|
174 |
+
},
|
175 |
+
"comfy_base": {
|
176 |
+
"fg-color": "#fdf6e3", // Base3
|
177 |
+
"bg-color": "#002b36", // Base03
|
178 |
+
"comfy-menu-bg": "#073642", // Base02
|
179 |
+
"comfy-input-bg": "#002b36", // Base03
|
180 |
+
"input-text": "#93a1a1", // Base1
|
181 |
+
"descrip-text": "#586e75", // Base01
|
182 |
+
"drag-text": "#839496", // Base0
|
183 |
+
"error-text": "#dc322f", // Solarized Red
|
184 |
+
"border-color": "#657b83", // Base00
|
185 |
+
"tr-even-bg-color": "#002b36",
|
186 |
+
"tr-odd-bg-color": "#073642",
|
187 |
+
"content-bg": "#657b83",
|
188 |
+
"content-fg": "#fdf6e3",
|
189 |
+
"content-hover-bg": "#002b36",
|
190 |
+
"content-hover-fg": "#fdf6e3"
|
191 |
+
}
|
192 |
+
},
|
193 |
+
},
|
194 |
+
"arc": {
|
195 |
+
"id": "arc",
|
196 |
+
"name": "Arc",
|
197 |
+
"colors": {
|
198 |
+
"node_slot": {
|
199 |
+
"BOOLEAN": "",
|
200 |
+
"CLIP": "#eacb8b",
|
201 |
+
"CLIP_VISION": "#A8DADC",
|
202 |
+
"CLIP_VISION_OUTPUT": "#ad7452",
|
203 |
+
"CONDITIONING": "#cf876f",
|
204 |
+
"CONTROL_NET": "#00d78d",
|
205 |
+
"CONTROL_NET_WEIGHTS": "",
|
206 |
+
"FLOAT": "",
|
207 |
+
"GLIGEN": "",
|
208 |
+
"IMAGE": "#80a1c0",
|
209 |
+
"IMAGEUPLOAD": "",
|
210 |
+
"INT": "",
|
211 |
+
"LATENT": "#b38ead",
|
212 |
+
"LATENT_KEYFRAME": "",
|
213 |
+
"MASK": "#a3bd8d",
|
214 |
+
"MODEL": "#8978a7",
|
215 |
+
"SAMPLER": "",
|
216 |
+
"SIGMAS": "",
|
217 |
+
"STRING": "",
|
218 |
+
"STYLE_MODEL": "#C2FFAE",
|
219 |
+
"T2I_ADAPTER_WEIGHTS": "",
|
220 |
+
"TAESD": "#DCC274",
|
221 |
+
"TIMESTEP_KEYFRAME": "",
|
222 |
+
"UPSCALE_MODEL": "",
|
223 |
+
"VAE": "#be616b"
|
224 |
+
},
|
225 |
+
"litegraph_base": {
|
226 |
+
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==",
|
227 |
+
"CLEAR_BACKGROUND_COLOR": "#2b2f38",
|
228 |
+
"NODE_TITLE_COLOR": "#b2b7bd",
|
229 |
+
"NODE_SELECTED_TITLE_COLOR": "#FFF",
|
230 |
+
"NODE_TEXT_SIZE": 14,
|
231 |
+
"NODE_TEXT_COLOR": "#AAA",
|
232 |
+
"NODE_SUBTEXT_SIZE": 12,
|
233 |
+
"NODE_DEFAULT_COLOR": "#2b2f38",
|
234 |
+
"NODE_DEFAULT_BGCOLOR": "#242730",
|
235 |
+
"NODE_DEFAULT_BOXCOLOR": "#6e7581",
|
236 |
+
"NODE_DEFAULT_SHAPE": "box",
|
237 |
+
"NODE_BOX_OUTLINE_COLOR": "#FFF",
|
238 |
+
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
239 |
+
"DEFAULT_GROUP_FONT": 22,
|
240 |
+
"WIDGET_BGCOLOR": "#2b2f38",
|
241 |
+
"WIDGET_OUTLINE_COLOR": "#6e7581",
|
242 |
+
"WIDGET_TEXT_COLOR": "#DDD",
|
243 |
+
"WIDGET_SECONDARY_TEXT_COLOR": "#b2b7bd",
|
244 |
+
"LINK_COLOR": "#9A9",
|
245 |
+
"EVENT_LINK_COLOR": "#A86",
|
246 |
+
"CONNECTING_LINK_COLOR": "#AFA"
|
247 |
+
},
|
248 |
+
"comfy_base": {
|
249 |
+
"fg-color": "#fff",
|
250 |
+
"bg-color": "#2b2f38",
|
251 |
+
"comfy-menu-bg": "#242730",
|
252 |
+
"comfy-input-bg": "#2b2f38",
|
253 |
+
"input-text": "#ddd",
|
254 |
+
"descrip-text": "#b2b7bd",
|
255 |
+
"drag-text": "#ccc",
|
256 |
+
"error-text": "#ff4444",
|
257 |
+
"border-color": "#6e7581",
|
258 |
+
"tr-even-bg-color": "#2b2f38",
|
259 |
+
"tr-odd-bg-color": "#242730",
|
260 |
+
"content-bg": "#6e7581",
|
261 |
+
"content-fg": "#fff",
|
262 |
+
"content-hover-bg": "#2b2f38",
|
263 |
+
"content-hover-fg": "#fff"
|
264 |
+
}
|
265 |
+
},
|
266 |
+
},
|
267 |
+
"nord": {
|
268 |
+
"id": "nord",
|
269 |
+
"name": "Nord",
|
270 |
+
"colors": {
|
271 |
+
"node_slot": {
|
272 |
+
"BOOLEAN": "",
|
273 |
+
"CLIP": "#eacb8b",
|
274 |
+
"CLIP_VISION": "#A8DADC",
|
275 |
+
"CLIP_VISION_OUTPUT": "#ad7452",
|
276 |
+
"CONDITIONING": "#cf876f",
|
277 |
+
"CONTROL_NET": "#00d78d",
|
278 |
+
"CONTROL_NET_WEIGHTS": "",
|
279 |
+
"FLOAT": "",
|
280 |
+
"GLIGEN": "",
|
281 |
+
"IMAGE": "#80a1c0",
|
282 |
+
"IMAGEUPLOAD": "",
|
283 |
+
"INT": "",
|
284 |
+
"LATENT": "#b38ead",
|
285 |
+
"LATENT_KEYFRAME": "",
|
286 |
+
"MASK": "#a3bd8d",
|
287 |
+
"MODEL": "#8978a7",
|
288 |
+
"SAMPLER": "",
|
289 |
+
"SIGMAS": "",
|
290 |
+
"STRING": "",
|
291 |
+
"STYLE_MODEL": "#C2FFAE",
|
292 |
+
"T2I_ADAPTER_WEIGHTS": "",
|
293 |
+
"TAESD": "#DCC274",
|
294 |
+
"TIMESTEP_KEYFRAME": "",
|
295 |
+
"UPSCALE_MODEL": "",
|
296 |
+
"VAE": "#be616b"
|
297 |
+
},
|
298 |
+
"litegraph_base": {
|
299 |
+
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC",
|
300 |
+
"CLEAR_BACKGROUND_COLOR": "#212732",
|
301 |
+
"NODE_TITLE_COLOR": "#999",
|
302 |
+
"NODE_SELECTED_TITLE_COLOR": "#e5eaf0",
|
303 |
+
"NODE_TEXT_SIZE": 14,
|
304 |
+
"NODE_TEXT_COLOR": "#bcc2c8",
|
305 |
+
"NODE_SUBTEXT_SIZE": 12,
|
306 |
+
"NODE_DEFAULT_COLOR": "#2e3440",
|
307 |
+
"NODE_DEFAULT_BGCOLOR": "#161b22",
|
308 |
+
"NODE_DEFAULT_BOXCOLOR": "#545d70",
|
309 |
+
"NODE_DEFAULT_SHAPE": "box",
|
310 |
+
"NODE_BOX_OUTLINE_COLOR": "#e5eaf0",
|
311 |
+
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
312 |
+
"DEFAULT_GROUP_FONT": 24,
|
313 |
+
"WIDGET_BGCOLOR": "#2e3440",
|
314 |
+
"WIDGET_OUTLINE_COLOR": "#545d70",
|
315 |
+
"WIDGET_TEXT_COLOR": "#bcc2c8",
|
316 |
+
"WIDGET_SECONDARY_TEXT_COLOR": "#999",
|
317 |
+
"LINK_COLOR": "#9A9",
|
318 |
+
"EVENT_LINK_COLOR": "#A86",
|
319 |
+
"CONNECTING_LINK_COLOR": "#AFA"
|
320 |
+
},
|
321 |
+
"comfy_base": {
|
322 |
+
"fg-color": "#e5eaf0",
|
323 |
+
"bg-color": "#2e3440",
|
324 |
+
"comfy-menu-bg": "#161b22",
|
325 |
+
"comfy-input-bg": "#2e3440",
|
326 |
+
"input-text": "#bcc2c8",
|
327 |
+
"descrip-text": "#999",
|
328 |
+
"drag-text": "#ccc",
|
329 |
+
"error-text": "#ff4444",
|
330 |
+
"border-color": "#545d70",
|
331 |
+
"tr-even-bg-color": "#2e3440",
|
332 |
+
"tr-odd-bg-color": "#161b22",
|
333 |
+
"content-bg": "#545d70",
|
334 |
+
"content-fg": "#e5eaf0",
|
335 |
+
"content-hover-bg": "#2e3440",
|
336 |
+
"content-hover-fg": "#e5eaf0"
|
337 |
+
}
|
338 |
+
},
|
339 |
+
},
|
340 |
+
"github": {
|
341 |
+
"id": "github",
|
342 |
+
"name": "Github",
|
343 |
+
"colors": {
|
344 |
+
"node_slot": {
|
345 |
+
"BOOLEAN": "",
|
346 |
+
"CLIP": "#eacb8b",
|
347 |
+
"CLIP_VISION": "#A8DADC",
|
348 |
+
"CLIP_VISION_OUTPUT": "#ad7452",
|
349 |
+
"CONDITIONING": "#cf876f",
|
350 |
+
"CONTROL_NET": "#00d78d",
|
351 |
+
"CONTROL_NET_WEIGHTS": "",
|
352 |
+
"FLOAT": "",
|
353 |
+
"GLIGEN": "",
|
354 |
+
"IMAGE": "#80a1c0",
|
355 |
+
"IMAGEUPLOAD": "",
|
356 |
+
"INT": "",
|
357 |
+
"LATENT": "#b38ead",
|
358 |
+
"LATENT_KEYFRAME": "",
|
359 |
+
"MASK": "#a3bd8d",
|
360 |
+
"MODEL": "#8978a7",
|
361 |
+
"SAMPLER": "",
|
362 |
+
"SIGMAS": "",
|
363 |
+
"STRING": "",
|
364 |
+
"STYLE_MODEL": "#C2FFAE",
|
365 |
+
"T2I_ADAPTER_WEIGHTS": "",
|
366 |
+
"TAESD": "#DCC274",
|
367 |
+
"TIMESTEP_KEYFRAME": "",
|
368 |
+
"UPSCALE_MODEL": "",
|
369 |
+
"VAE": "#be616b"
|
370 |
+
},
|
371 |
+
"litegraph_base": {
|
372 |
+
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==",
|
373 |
+
"CLEAR_BACKGROUND_COLOR": "#040506",
|
374 |
+
"NODE_TITLE_COLOR": "#999",
|
375 |
+
"NODE_SELECTED_TITLE_COLOR": "#e5eaf0",
|
376 |
+
"NODE_TEXT_SIZE": 14,
|
377 |
+
"NODE_TEXT_COLOR": "#bcc2c8",
|
378 |
+
"NODE_SUBTEXT_SIZE": 12,
|
379 |
+
"NODE_DEFAULT_COLOR": "#161b22",
|
380 |
+
"NODE_DEFAULT_BGCOLOR": "#13171d",
|
381 |
+
"NODE_DEFAULT_BOXCOLOR": "#30363d",
|
382 |
+
"NODE_DEFAULT_SHAPE": "box",
|
383 |
+
"NODE_BOX_OUTLINE_COLOR": "#e5eaf0",
|
384 |
+
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
|
385 |
+
"DEFAULT_GROUP_FONT": 24,
|
386 |
+
"WIDGET_BGCOLOR": "#161b22",
|
387 |
+
"WIDGET_OUTLINE_COLOR": "#30363d",
|
388 |
+
"WIDGET_TEXT_COLOR": "#bcc2c8",
|
389 |
+
"WIDGET_SECONDARY_TEXT_COLOR": "#999",
|
390 |
+
"LINK_COLOR": "#9A9",
|
391 |
+
"EVENT_LINK_COLOR": "#A86",
|
392 |
+
"CONNECTING_LINK_COLOR": "#AFA"
|
393 |
+
},
|
394 |
+
"comfy_base": {
|
395 |
+
"fg-color": "#e5eaf0",
|
396 |
+
"bg-color": "#161b22",
|
397 |
+
"comfy-menu-bg": "#13171d",
|
398 |
+
"comfy-input-bg": "#161b22",
|
399 |
+
"input-text": "#bcc2c8",
|
400 |
+
"descrip-text": "#999",
|
401 |
+
"drag-text": "#ccc",
|
402 |
+
"error-text": "#ff4444",
|
403 |
+
"border-color": "#30363d",
|
404 |
+
"tr-even-bg-color": "#161b22",
|
405 |
+
"tr-odd-bg-color": "#13171d",
|
406 |
+
"content-bg": "#30363d",
|
407 |
+
"content-fg": "#e5eaf0",
|
408 |
+
"content-hover-bg": "#161b22",
|
409 |
+
"content-hover-fg": "#e5eaf0"
|
410 |
+
}
|
411 |
+
},
|
412 |
+
}
|
413 |
+
};
|
414 |
+
|
415 |
+
const id = "Comfy.ColorPalette";
|
416 |
+
const idCustomColorPalettes = "Comfy.CustomColorPalettes";
|
417 |
+
const defaultColorPaletteId = "dark";
|
418 |
+
const els = {}
|
419 |
+
// const ctxMenu = LiteGraph.ContextMenu;
|
420 |
+
app.registerExtension({
|
421 |
+
name: id,
|
422 |
+
addCustomNodeDefs(node_defs) {
|
423 |
+
const sortObjectKeys = (unordered) => {
|
424 |
+
return Object.keys(unordered).sort().reduce((obj, key) => {
|
425 |
+
obj[key] = unordered[key];
|
426 |
+
return obj;
|
427 |
+
}, {});
|
428 |
+
};
|
429 |
+
|
430 |
+
function getSlotTypes() {
|
431 |
+
var types = [];
|
432 |
+
|
433 |
+
const defs = node_defs;
|
434 |
+
for (const nodeId in defs) {
|
435 |
+
const nodeData = defs[nodeId];
|
436 |
+
|
437 |
+
var inputs = nodeData["input"]["required"];
|
438 |
+
if (nodeData["input"]["optional"] !== undefined) {
|
439 |
+
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
|
440 |
+
}
|
441 |
+
|
442 |
+
for (const inputName in inputs) {
|
443 |
+
const inputData = inputs[inputName];
|
444 |
+
const type = inputData[0];
|
445 |
+
|
446 |
+
if (!Array.isArray(type)) {
|
447 |
+
types.push(type);
|
448 |
+
}
|
449 |
+
}
|
450 |
+
|
451 |
+
for (const o in nodeData["output"]) {
|
452 |
+
const output = nodeData["output"][o];
|
453 |
+
types.push(output);
|
454 |
+
}
|
455 |
+
}
|
456 |
+
|
457 |
+
return types;
|
458 |
+
}
|
459 |
+
|
460 |
+
function completeColorPalette(colorPalette) {
|
461 |
+
var types = getSlotTypes();
|
462 |
+
|
463 |
+
for (const type of types) {
|
464 |
+
if (!colorPalette.colors.node_slot[type]) {
|
465 |
+
colorPalette.colors.node_slot[type] = "";
|
466 |
+
}
|
467 |
+
}
|
468 |
+
|
469 |
+
colorPalette.colors.node_slot = sortObjectKeys(colorPalette.colors.node_slot);
|
470 |
+
|
471 |
+
return colorPalette;
|
472 |
+
}
|
473 |
+
|
474 |
+
const getColorPaletteTemplate = async () => {
|
475 |
+
let colorPalette = {
|
476 |
+
"id": "my_color_palette_unique_id",
|
477 |
+
"name": "My Color Palette",
|
478 |
+
"colors": {
|
479 |
+
"node_slot": {},
|
480 |
+
"litegraph_base": {},
|
481 |
+
"comfy_base": {}
|
482 |
+
}
|
483 |
+
};
|
484 |
+
|
485 |
+
// Copy over missing keys from default color palette
|
486 |
+
const defaultColorPalette = colorPalettes[defaultColorPaletteId];
|
487 |
+
for (const key in defaultColorPalette.colors.litegraph_base) {
|
488 |
+
if (!colorPalette.colors.litegraph_base[key]) {
|
489 |
+
colorPalette.colors.litegraph_base[key] = "";
|
490 |
+
}
|
491 |
+
}
|
492 |
+
for (const key in defaultColorPalette.colors.comfy_base) {
|
493 |
+
if (!colorPalette.colors.comfy_base[key]) {
|
494 |
+
colorPalette.colors.comfy_base[key] = "";
|
495 |
+
}
|
496 |
+
}
|
497 |
+
|
498 |
+
return completeColorPalette(colorPalette);
|
499 |
+
};
|
500 |
+
|
501 |
+
const getCustomColorPalettes = () => {
|
502 |
+
return app.ui.settings.getSettingValue(idCustomColorPalettes, {});
|
503 |
+
};
|
504 |
+
|
505 |
+
const setCustomColorPalettes = (customColorPalettes) => {
|
506 |
+
return app.ui.settings.setSettingValue(idCustomColorPalettes, customColorPalettes);
|
507 |
+
};
|
508 |
+
|
509 |
+
const addCustomColorPalette = async (colorPalette) => {
|
510 |
+
if (typeof (colorPalette) !== "object") {
|
511 |
+
alert("Invalid color palette.");
|
512 |
+
return;
|
513 |
+
}
|
514 |
+
|
515 |
+
if (!colorPalette.id) {
|
516 |
+
alert("Color palette missing id.");
|
517 |
+
return;
|
518 |
+
}
|
519 |
+
|
520 |
+
if (!colorPalette.name) {
|
521 |
+
alert("Color palette missing name.");
|
522 |
+
return;
|
523 |
+
}
|
524 |
+
|
525 |
+
if (!colorPalette.colors) {
|
526 |
+
alert("Color palette missing colors.");
|
527 |
+
return;
|
528 |
+
}
|
529 |
+
|
530 |
+
if (colorPalette.colors.node_slot && typeof (colorPalette.colors.node_slot) !== "object") {
|
531 |
+
alert("Invalid color palette colors.node_slot.");
|
532 |
+
return;
|
533 |
+
}
|
534 |
+
|
535 |
+
const customColorPalettes = getCustomColorPalettes();
|
536 |
+
customColorPalettes[colorPalette.id] = colorPalette;
|
537 |
+
setCustomColorPalettes(customColorPalettes);
|
538 |
+
|
539 |
+
for (const option of els.select.childNodes) {
|
540 |
+
if (option.value === "custom_" + colorPalette.id) {
|
541 |
+
els.select.removeChild(option);
|
542 |
+
}
|
543 |
+
}
|
544 |
+
|
545 |
+
els.select.append($el("option", {
|
546 |
+
textContent: colorPalette.name + " (custom)",
|
547 |
+
value: "custom_" + colorPalette.id,
|
548 |
+
selected: true
|
549 |
+
}));
|
550 |
+
|
551 |
+
setColorPalette("custom_" + colorPalette.id);
|
552 |
+
await loadColorPalette(colorPalette);
|
553 |
+
};
|
554 |
+
|
555 |
+
const deleteCustomColorPalette = async (colorPaletteId) => {
|
556 |
+
const customColorPalettes = getCustomColorPalettes();
|
557 |
+
delete customColorPalettes[colorPaletteId];
|
558 |
+
setCustomColorPalettes(customColorPalettes);
|
559 |
+
|
560 |
+
for (const option of els.select.childNodes) {
|
561 |
+
if (option.value === defaultColorPaletteId) {
|
562 |
+
option.selected = true;
|
563 |
+
}
|
564 |
+
|
565 |
+
if (option.value === "custom_" + colorPaletteId) {
|
566 |
+
els.select.removeChild(option);
|
567 |
+
}
|
568 |
+
}
|
569 |
+
|
570 |
+
setColorPalette(defaultColorPaletteId);
|
571 |
+
await loadColorPalette(getColorPalette());
|
572 |
+
};
|
573 |
+
|
574 |
+
const loadColorPalette = async (colorPalette) => {
|
575 |
+
colorPalette = await completeColorPalette(colorPalette);
|
576 |
+
if (colorPalette.colors) {
|
577 |
+
// Sets the colors of node slots and links
|
578 |
+
if (colorPalette.colors.node_slot) {
|
579 |
+
Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot);
|
580 |
+
Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot);
|
581 |
+
}
|
582 |
+
// Sets the colors of the LiteGraph objects
|
583 |
+
if (colorPalette.colors.litegraph_base) {
|
584 |
+
// Everything updates correctly in the loop, except the Node Title and Link Color for some reason
|
585 |
+
app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR;
|
586 |
+
app.canvas.default_link_color = colorPalette.colors.litegraph_base.LINK_COLOR;
|
587 |
+
|
588 |
+
for (const key in colorPalette.colors.litegraph_base) {
|
589 |
+
if (colorPalette.colors.litegraph_base.hasOwnProperty(key) && LiteGraph.hasOwnProperty(key)) {
|
590 |
+
LiteGraph[key] = colorPalette.colors.litegraph_base[key];
|
591 |
+
}
|
592 |
+
}
|
593 |
+
}
|
594 |
+
// Sets the color of ComfyUI elements
|
595 |
+
if (colorPalette.colors.comfy_base) {
|
596 |
+
const rootStyle = document.documentElement.style;
|
597 |
+
for (const key in colorPalette.colors.comfy_base) {
|
598 |
+
rootStyle.setProperty('--' + key, colorPalette.colors.comfy_base[key]);
|
599 |
+
}
|
600 |
+
}
|
601 |
+
app.canvas.draw(true, true);
|
602 |
+
}
|
603 |
+
};
|
604 |
+
|
605 |
+
const getColorPalette = (colorPaletteId) => {
|
606 |
+
if (!colorPaletteId) {
|
607 |
+
colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
|
608 |
+
}
|
609 |
+
|
610 |
+
if (colorPaletteId.startsWith("custom_")) {
|
611 |
+
colorPaletteId = colorPaletteId.substr(7);
|
612 |
+
let customColorPalettes = getCustomColorPalettes();
|
613 |
+
if (customColorPalettes[colorPaletteId]) {
|
614 |
+
return customColorPalettes[colorPaletteId];
|
615 |
+
}
|
616 |
+
}
|
617 |
+
|
618 |
+
return colorPalettes[colorPaletteId];
|
619 |
+
};
|
620 |
+
|
621 |
+
const setColorPalette = (colorPaletteId) => {
|
622 |
+
app.ui.settings.setSettingValue(id, colorPaletteId);
|
623 |
+
};
|
624 |
+
|
625 |
+
const fileInput = $el("input", {
|
626 |
+
type: "file",
|
627 |
+
accept: ".json",
|
628 |
+
style: {display: "none"},
|
629 |
+
parent: document.body,
|
630 |
+
onchange: () => {
|
631 |
+
const file = fileInput.files[0];
|
632 |
+
if (file.type === "application/json" || file.name.endsWith(".json")) {
|
633 |
+
const reader = new FileReader();
|
634 |
+
reader.onload = async () => {
|
635 |
+
await addCustomColorPalette(JSON.parse(reader.result));
|
636 |
+
};
|
637 |
+
reader.readAsText(file);
|
638 |
+
}
|
639 |
+
},
|
640 |
+
});
|
641 |
+
|
642 |
+
app.ui.settings.addSetting({
|
643 |
+
id,
|
644 |
+
name: "Color Palette",
|
645 |
+
type: (name, setter, value) => {
|
646 |
+
const options = [
|
647 |
+
...Object.values(colorPalettes).map(c=> $el("option", {
|
648 |
+
textContent: c.name,
|
649 |
+
value: c.id,
|
650 |
+
selected: c.id === value
|
651 |
+
})),
|
652 |
+
...Object.values(getCustomColorPalettes()).map(c=>$el("option", {
|
653 |
+
textContent: `${c.name} (custom)`,
|
654 |
+
value: `custom_${c.id}`,
|
655 |
+
selected: `custom_${c.id}` === value
|
656 |
+
})) ,
|
657 |
+
];
|
658 |
+
|
659 |
+
els.select = $el("select", {
|
660 |
+
style: {
|
661 |
+
marginBottom: "0.15rem",
|
662 |
+
width: "100%",
|
663 |
+
},
|
664 |
+
onchange: (e) => {
|
665 |
+
setter(e.target.value);
|
666 |
+
}
|
667 |
+
}, options)
|
668 |
+
|
669 |
+
return $el("tr", [
|
670 |
+
$el("td", [
|
671 |
+
$el("label", {
|
672 |
+
for: id.replaceAll(".", "-"),
|
673 |
+
textContent: "Color palette",
|
674 |
+
}),
|
675 |
+
]),
|
676 |
+
$el("td", [
|
677 |
+
els.select,
|
678 |
+
$el("div", {
|
679 |
+
style: {
|
680 |
+
display: "grid",
|
681 |
+
gap: "4px",
|
682 |
+
gridAutoFlow: "column",
|
683 |
+
},
|
684 |
+
}, [
|
685 |
+
$el("input", {
|
686 |
+
type: "button",
|
687 |
+
value: "Export",
|
688 |
+
onclick: async () => {
|
689 |
+
const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
|
690 |
+
const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId));
|
691 |
+
const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
|
692 |
+
const blob = new Blob([json], {type: "application/json"});
|
693 |
+
const url = URL.createObjectURL(blob);
|
694 |
+
const a = $el("a", {
|
695 |
+
href: url,
|
696 |
+
download: colorPaletteId + ".json",
|
697 |
+
style: {display: "none"},
|
698 |
+
parent: document.body,
|
699 |
+
});
|
700 |
+
a.click();
|
701 |
+
setTimeout(function () {
|
702 |
+
a.remove();
|
703 |
+
window.URL.revokeObjectURL(url);
|
704 |
+
}, 0);
|
705 |
+
},
|
706 |
+
}),
|
707 |
+
$el("input", {
|
708 |
+
type: "button",
|
709 |
+
value: "Import",
|
710 |
+
onclick: () => {
|
711 |
+
fileInput.click();
|
712 |
+
}
|
713 |
+
}),
|
714 |
+
$el("input", {
|
715 |
+
type: "button",
|
716 |
+
value: "Template",
|
717 |
+
onclick: async () => {
|
718 |
+
const colorPalette = await getColorPaletteTemplate();
|
719 |
+
const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
|
720 |
+
const blob = new Blob([json], {type: "application/json"});
|
721 |
+
const url = URL.createObjectURL(blob);
|
722 |
+
const a = $el("a", {
|
723 |
+
href: url,
|
724 |
+
download: "color_palette.json",
|
725 |
+
style: {display: "none"},
|
726 |
+
parent: document.body,
|
727 |
+
});
|
728 |
+
a.click();
|
729 |
+
setTimeout(function () {
|
730 |
+
a.remove();
|
731 |
+
window.URL.revokeObjectURL(url);
|
732 |
+
}, 0);
|
733 |
+
}
|
734 |
+
}),
|
735 |
+
$el("input", {
|
736 |
+
type: "button",
|
737 |
+
value: "Delete",
|
738 |
+
onclick: async () => {
|
739 |
+
let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
|
740 |
+
|
741 |
+
if (colorPalettes[colorPaletteId]) {
|
742 |
+
alert("You cannot delete a built-in color palette.");
|
743 |
+
return;
|
744 |
+
}
|
745 |
+
|
746 |
+
if (colorPaletteId.startsWith("custom_")) {
|
747 |
+
colorPaletteId = colorPaletteId.substr(7);
|
748 |
+
}
|
749 |
+
|
750 |
+
await deleteCustomColorPalette(colorPaletteId);
|
751 |
+
}
|
752 |
+
}),
|
753 |
+
]),
|
754 |
+
]),
|
755 |
+
])
|
756 |
+
},
|
757 |
+
defaultValue: defaultColorPaletteId,
|
758 |
+
async onChange(value) {
|
759 |
+
if (!value) {
|
760 |
+
return;
|
761 |
+
}
|
762 |
+
|
763 |
+
let palette = colorPalettes[value];
|
764 |
+
if (palette) {
|
765 |
+
await loadColorPalette(palette);
|
766 |
+
} else if (value.startsWith("custom_")) {
|
767 |
+
value = value.substr(7);
|
768 |
+
let customColorPalettes = getCustomColorPalettes();
|
769 |
+
if (customColorPalettes[value]) {
|
770 |
+
palette = customColorPalettes[value];
|
771 |
+
await loadColorPalette(customColorPalettes[value]);
|
772 |
+
}
|
773 |
+
}
|
774 |
+
|
775 |
+
let {BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR} = palette.colors.litegraph_base;
|
776 |
+
if (BACKGROUND_IMAGE === undefined || CLEAR_BACKGROUND_COLOR === undefined) {
|
777 |
+
const base = colorPalettes["dark"].colors.litegraph_base;
|
778 |
+
BACKGROUND_IMAGE = base.BACKGROUND_IMAGE;
|
779 |
+
CLEAR_BACKGROUND_COLOR = base.CLEAR_BACKGROUND_COLOR;
|
780 |
+
}
|
781 |
+
app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR);
|
782 |
+
},
|
783 |
+
});
|
784 |
+
},
|
785 |
+
});
|
ComfyUI/web/extensions/core/contextMenuFilter.js
ADDED
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {app} from "../../scripts/app.js";
|
2 |
+
|
3 |
+
// Adds filtering to combo context menus
|
4 |
+
|
5 |
+
const ext = {
|
6 |
+
name: "Comfy.ContextMenuFilter",
|
7 |
+
init() {
|
8 |
+
const ctxMenu = LiteGraph.ContextMenu;
|
9 |
+
|
10 |
+
LiteGraph.ContextMenu = function (values, options) {
|
11 |
+
const ctx = ctxMenu.call(this, values, options);
|
12 |
+
|
13 |
+
// If we are a dark menu (only used for combo boxes) then add a filter input
|
14 |
+
if (options?.className === "dark" && values?.length > 10) {
|
15 |
+
const filter = document.createElement("input");
|
16 |
+
filter.classList.add("comfy-context-menu-filter");
|
17 |
+
filter.placeholder = "Filter list";
|
18 |
+
this.root.prepend(filter);
|
19 |
+
|
20 |
+
const items = Array.from(this.root.querySelectorAll(".litemenu-entry"));
|
21 |
+
let displayedItems = [...items];
|
22 |
+
let itemCount = displayedItems.length;
|
23 |
+
|
24 |
+
// We must request an animation frame for the current node of the active canvas to update.
|
25 |
+
requestAnimationFrame(() => {
|
26 |
+
const currentNode = LGraphCanvas.active_canvas.current_node;
|
27 |
+
const clickedComboValue = currentNode.widgets
|
28 |
+
?.filter(w => w.type === "combo" && w.options.values.length === values.length)
|
29 |
+
.find(w => w.options.values.every((v, i) => v === values[i]))
|
30 |
+
?.value;
|
31 |
+
|
32 |
+
let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0;
|
33 |
+
if (selectedIndex < 0) {
|
34 |
+
selectedIndex = 0;
|
35 |
+
}
|
36 |
+
let selectedItem = displayedItems[selectedIndex];
|
37 |
+
updateSelected();
|
38 |
+
|
39 |
+
// Apply highlighting to the selected item
|
40 |
+
function updateSelected() {
|
41 |
+
selectedItem?.style.setProperty("background-color", "");
|
42 |
+
selectedItem?.style.setProperty("color", "");
|
43 |
+
selectedItem = displayedItems[selectedIndex];
|
44 |
+
selectedItem?.style.setProperty("background-color", "#ccc", "important");
|
45 |
+
selectedItem?.style.setProperty("color", "#000", "important");
|
46 |
+
}
|
47 |
+
|
48 |
+
const positionList = () => {
|
49 |
+
const rect = this.root.getBoundingClientRect();
|
50 |
+
|
51 |
+
// If the top is off-screen then shift the element with scaling applied
|
52 |
+
if (rect.top < 0) {
|
53 |
+
const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight;
|
54 |
+
const shift = (this.root.clientHeight * scale) / 2;
|
55 |
+
this.root.style.top = -shift + "px";
|
56 |
+
}
|
57 |
+
}
|
58 |
+
|
59 |
+
// Arrow up/down to select items
|
60 |
+
filter.addEventListener("keydown", (event) => {
|
61 |
+
switch (event.key) {
|
62 |
+
case "ArrowUp":
|
63 |
+
event.preventDefault();
|
64 |
+
if (selectedIndex === 0) {
|
65 |
+
selectedIndex = itemCount - 1;
|
66 |
+
} else {
|
67 |
+
selectedIndex--;
|
68 |
+
}
|
69 |
+
updateSelected();
|
70 |
+
break;
|
71 |
+
case "ArrowRight":
|
72 |
+
event.preventDefault();
|
73 |
+
selectedIndex = itemCount - 1;
|
74 |
+
updateSelected();
|
75 |
+
break;
|
76 |
+
case "ArrowDown":
|
77 |
+
event.preventDefault();
|
78 |
+
if (selectedIndex === itemCount - 1) {
|
79 |
+
selectedIndex = 0;
|
80 |
+
} else {
|
81 |
+
selectedIndex++;
|
82 |
+
}
|
83 |
+
updateSelected();
|
84 |
+
break;
|
85 |
+
case "ArrowLeft":
|
86 |
+
event.preventDefault();
|
87 |
+
selectedIndex = 0;
|
88 |
+
updateSelected();
|
89 |
+
break;
|
90 |
+
case "Enter":
|
91 |
+
selectedItem?.click();
|
92 |
+
break;
|
93 |
+
case "Escape":
|
94 |
+
this.close();
|
95 |
+
break;
|
96 |
+
}
|
97 |
+
});
|
98 |
+
|
99 |
+
filter.addEventListener("input", () => {
|
100 |
+
// Hide all items that don't match our filter
|
101 |
+
const term = filter.value.toLocaleLowerCase();
|
102 |
+
// When filtering, recompute which items are visible for arrow up/down and maintain selection.
|
103 |
+
displayedItems = items.filter(item => {
|
104 |
+
const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term);
|
105 |
+
item.style.display = isVisible ? "block" : "none";
|
106 |
+
return isVisible;
|
107 |
+
});
|
108 |
+
|
109 |
+
selectedIndex = 0;
|
110 |
+
if (displayedItems.includes(selectedItem)) {
|
111 |
+
selectedIndex = displayedItems.findIndex(d => d === selectedItem);
|
112 |
+
}
|
113 |
+
itemCount = displayedItems.length;
|
114 |
+
|
115 |
+
updateSelected();
|
116 |
+
|
117 |
+
// If we have an event then we can try and position the list under the source
|
118 |
+
if (options.event) {
|
119 |
+
let top = options.event.clientY - 10;
|
120 |
+
|
121 |
+
const bodyRect = document.body.getBoundingClientRect();
|
122 |
+
const rootRect = this.root.getBoundingClientRect();
|
123 |
+
if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) {
|
124 |
+
top = Math.max(0, bodyRect.height - rootRect.height - 10);
|
125 |
+
}
|
126 |
+
|
127 |
+
this.root.style.top = top + "px";
|
128 |
+
positionList();
|
129 |
+
}
|
130 |
+
});
|
131 |
+
|
132 |
+
requestAnimationFrame(() => {
|
133 |
+
// Focus the filter box when opening
|
134 |
+
filter.focus();
|
135 |
+
|
136 |
+
positionList();
|
137 |
+
});
|
138 |
+
})
|
139 |
+
}
|
140 |
+
|
141 |
+
return ctx;
|
142 |
+
};
|
143 |
+
|
144 |
+
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
145 |
+
},
|
146 |
+
}
|
147 |
+
|
148 |
+
app.registerExtension(ext);
|
ComfyUI/web/extensions/core/dynamicPrompts.js
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
|
3 |
+
// Allows for simple dynamic prompt replacement
|
4 |
+
// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
|
5 |
+
|
6 |
+
/*
|
7 |
+
* Strips C-style line and block comments from a string
|
8 |
+
*/
|
9 |
+
function stripComments(str) {
|
10 |
+
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,'');
|
11 |
+
}
|
12 |
+
|
13 |
+
app.registerExtension({
|
14 |
+
name: "Comfy.DynamicPrompts",
|
15 |
+
nodeCreated(node) {
|
16 |
+
if (node.widgets) {
|
17 |
+
// Locate dynamic prompt text widgets
|
18 |
+
// Include any widgets with dynamicPrompts set to true, and customtext
|
19 |
+
const widgets = node.widgets.filter(
|
20 |
+
(n) => n.dynamicPrompts
|
21 |
+
);
|
22 |
+
for (const widget of widgets) {
|
23 |
+
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
|
24 |
+
widget.serializeValue = (workflowNode, widgetIndex) => {
|
25 |
+
let prompt = stripComments(widget.value);
|
26 |
+
while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) {
|
27 |
+
const startIndex = prompt.replace("\\{", "00").indexOf("{");
|
28 |
+
const endIndex = prompt.replace("\\}", "00").indexOf("}");
|
29 |
+
|
30 |
+
const optionsString = prompt.substring(startIndex + 1, endIndex);
|
31 |
+
const options = optionsString.split("|");
|
32 |
+
|
33 |
+
const randomIndex = Math.floor(Math.random() * options.length);
|
34 |
+
const randomOption = options[randomIndex];
|
35 |
+
|
36 |
+
prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1);
|
37 |
+
}
|
38 |
+
|
39 |
+
// Overwrite the value in the serialized workflow pnginfo
|
40 |
+
if (workflowNode?.widgets_values)
|
41 |
+
workflowNode.widgets_values[widgetIndex] = prompt;
|
42 |
+
|
43 |
+
return prompt;
|
44 |
+
};
|
45 |
+
}
|
46 |
+
}
|
47 |
+
},
|
48 |
+
});
|
ComfyUI/web/extensions/core/editAttention.js
ADDED
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
|
3 |
+
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
4 |
+
|
5 |
+
app.registerExtension({
|
6 |
+
name: "Comfy.EditAttention",
|
7 |
+
init() {
|
8 |
+
const editAttentionDelta = app.ui.settings.addSetting({
|
9 |
+
id: "Comfy.EditAttention.Delta",
|
10 |
+
name: "Ctrl+up/down precision",
|
11 |
+
type: "slider",
|
12 |
+
attrs: {
|
13 |
+
min: 0.01,
|
14 |
+
max: 0.5,
|
15 |
+
step: 0.01,
|
16 |
+
},
|
17 |
+
defaultValue: 0.05,
|
18 |
+
});
|
19 |
+
|
20 |
+
function incrementWeight(weight, delta) {
|
21 |
+
const floatWeight = parseFloat(weight);
|
22 |
+
if (isNaN(floatWeight)) return weight;
|
23 |
+
const newWeight = floatWeight + delta;
|
24 |
+
if (newWeight < 0) return "0";
|
25 |
+
return String(Number(newWeight.toFixed(10)));
|
26 |
+
}
|
27 |
+
|
28 |
+
function findNearestEnclosure(text, cursorPos) {
|
29 |
+
let start = cursorPos, end = cursorPos;
|
30 |
+
let openCount = 0, closeCount = 0;
|
31 |
+
|
32 |
+
// Find opening parenthesis before cursor
|
33 |
+
while (start >= 0) {
|
34 |
+
start--;
|
35 |
+
if (text[start] === "(" && openCount === closeCount) break;
|
36 |
+
if (text[start] === "(") openCount++;
|
37 |
+
if (text[start] === ")") closeCount++;
|
38 |
+
}
|
39 |
+
if (start < 0) return false;
|
40 |
+
|
41 |
+
openCount = 0;
|
42 |
+
closeCount = 0;
|
43 |
+
|
44 |
+
// Find closing parenthesis after cursor
|
45 |
+
while (end < text.length) {
|
46 |
+
if (text[end] === ")" && openCount === closeCount) break;
|
47 |
+
if (text[end] === "(") openCount++;
|
48 |
+
if (text[end] === ")") closeCount++;
|
49 |
+
end++;
|
50 |
+
}
|
51 |
+
if (end === text.length) return false;
|
52 |
+
|
53 |
+
return { start: start + 1, end: end };
|
54 |
+
}
|
55 |
+
|
56 |
+
function addWeightToParentheses(text) {
|
57 |
+
const parenRegex = /^\((.*)\)$/;
|
58 |
+
const parenMatch = text.match(parenRegex);
|
59 |
+
|
60 |
+
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
|
61 |
+
const floatMatch = text.match(floatRegex);
|
62 |
+
|
63 |
+
if (parenMatch && !floatMatch) {
|
64 |
+
return `(${parenMatch[1]}:1.0)`;
|
65 |
+
} else {
|
66 |
+
return text;
|
67 |
+
}
|
68 |
+
};
|
69 |
+
|
70 |
+
function editAttention(event) {
|
71 |
+
const inputField = event.composedPath()[0];
|
72 |
+
const delta = parseFloat(editAttentionDelta.value);
|
73 |
+
|
74 |
+
if (inputField.tagName !== "TEXTAREA") return;
|
75 |
+
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
|
76 |
+
if (!event.ctrlKey && !event.metaKey) return;
|
77 |
+
|
78 |
+
event.preventDefault();
|
79 |
+
|
80 |
+
let start = inputField.selectionStart;
|
81 |
+
let end = inputField.selectionEnd;
|
82 |
+
let selectedText = inputField.value.substring(start, end);
|
83 |
+
|
84 |
+
// If there is no selection, attempt to find the nearest enclosure, or select the current word
|
85 |
+
if (!selectedText) {
|
86 |
+
const nearestEnclosure = findNearestEnclosure(inputField.value, start);
|
87 |
+
if (nearestEnclosure) {
|
88 |
+
start = nearestEnclosure.start;
|
89 |
+
end = nearestEnclosure.end;
|
90 |
+
selectedText = inputField.value.substring(start, end);
|
91 |
+
} else {
|
92 |
+
// Select the current word, find the start and end of the word
|
93 |
+
const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t";
|
94 |
+
|
95 |
+
while (!delimiters.includes(inputField.value[start - 1]) && start > 0) {
|
96 |
+
start--;
|
97 |
+
}
|
98 |
+
|
99 |
+
while (!delimiters.includes(inputField.value[end]) && end < inputField.value.length) {
|
100 |
+
end++;
|
101 |
+
}
|
102 |
+
|
103 |
+
selectedText = inputField.value.substring(start, end);
|
104 |
+
if (!selectedText) return;
|
105 |
+
}
|
106 |
+
}
|
107 |
+
|
108 |
+
// If the selection ends with a space, remove it
|
109 |
+
if (selectedText[selectedText.length - 1] === " ") {
|
110 |
+
selectedText = selectedText.substring(0, selectedText.length - 1);
|
111 |
+
end -= 1;
|
112 |
+
}
|
113 |
+
|
114 |
+
// If there are parentheses left and right of the selection, select them
|
115 |
+
if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") {
|
116 |
+
start -= 1;
|
117 |
+
end += 1;
|
118 |
+
selectedText = inputField.value.substring(start, end);
|
119 |
+
}
|
120 |
+
|
121 |
+
// If the selection is not enclosed in parentheses, add them
|
122 |
+
if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") {
|
123 |
+
selectedText = `(${selectedText})`;
|
124 |
+
}
|
125 |
+
|
126 |
+
// If the selection does not have a weight, add a weight of 1.0
|
127 |
+
selectedText = addWeightToParentheses(selectedText);
|
128 |
+
|
129 |
+
// Increment the weight
|
130 |
+
const weightDelta = event.key === "ArrowUp" ? delta : -delta;
|
131 |
+
const updatedText = selectedText.replace(/\((.*):(\d+(?:\.\d+)?)\)/, (match, text, weight) => {
|
132 |
+
weight = incrementWeight(weight, weightDelta);
|
133 |
+
if (weight == 1) {
|
134 |
+
return text;
|
135 |
+
} else {
|
136 |
+
return `(${text}:${weight})`;
|
137 |
+
}
|
138 |
+
});
|
139 |
+
|
140 |
+
inputField.setRangeText(updatedText, start, end, "select");
|
141 |
+
}
|
142 |
+
window.addEventListener("keydown", editAttention);
|
143 |
+
},
|
144 |
+
});
|
ComfyUI/web/extensions/core/groupNode.js
ADDED
@@ -0,0 +1,1281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { api } from "../../scripts/api.js";
|
3 |
+
import { mergeIfValid } from "./widgetInputs.js";
|
4 |
+
import { ManageGroupDialog } from "./groupNodeManage.js";
|
5 |
+
|
6 |
+
const GROUP = Symbol();
|
7 |
+
|
8 |
+
const Workflow = {
|
9 |
+
InUse: {
|
10 |
+
Free: 0,
|
11 |
+
Registered: 1,
|
12 |
+
InWorkflow: 2,
|
13 |
+
},
|
14 |
+
isInUseGroupNode(name) {
|
15 |
+
const id = `workflow/${name}`;
|
16 |
+
// Check if lready registered/in use in this workflow
|
17 |
+
if (app.graph.extra?.groupNodes?.[name]) {
|
18 |
+
if (app.graph._nodes.find((n) => n.type === id)) {
|
19 |
+
return Workflow.InUse.InWorkflow;
|
20 |
+
} else {
|
21 |
+
return Workflow.InUse.Registered;
|
22 |
+
}
|
23 |
+
}
|
24 |
+
return Workflow.InUse.Free;
|
25 |
+
},
|
26 |
+
storeGroupNode(name, data) {
|
27 |
+
let extra = app.graph.extra;
|
28 |
+
if (!extra) app.graph.extra = extra = {};
|
29 |
+
let groupNodes = extra.groupNodes;
|
30 |
+
if (!groupNodes) extra.groupNodes = groupNodes = {};
|
31 |
+
groupNodes[name] = data;
|
32 |
+
},
|
33 |
+
};
|
34 |
+
|
35 |
+
class GroupNodeBuilder {
|
36 |
+
constructor(nodes) {
|
37 |
+
this.nodes = nodes;
|
38 |
+
}
|
39 |
+
|
40 |
+
build() {
|
41 |
+
const name = this.getName();
|
42 |
+
if (!name) return;
|
43 |
+
|
44 |
+
// Sort the nodes so they are in execution order
|
45 |
+
// this allows for widgets to be in the correct order when reconstructing
|
46 |
+
this.sortNodes();
|
47 |
+
|
48 |
+
this.nodeData = this.getNodeData();
|
49 |
+
Workflow.storeGroupNode(name, this.nodeData);
|
50 |
+
|
51 |
+
return { name, nodeData: this.nodeData };
|
52 |
+
}
|
53 |
+
|
54 |
+
getName() {
|
55 |
+
const name = prompt("Enter group name");
|
56 |
+
if (!name) return;
|
57 |
+
const used = Workflow.isInUseGroupNode(name);
|
58 |
+
switch (used) {
|
59 |
+
case Workflow.InUse.InWorkflow:
|
60 |
+
alert(
|
61 |
+
"An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name."
|
62 |
+
);
|
63 |
+
return;
|
64 |
+
case Workflow.InUse.Registered:
|
65 |
+
if (!confirm("A group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?")) {
|
66 |
+
return;
|
67 |
+
}
|
68 |
+
break;
|
69 |
+
}
|
70 |
+
return name;
|
71 |
+
}
|
72 |
+
|
73 |
+
sortNodes() {
|
74 |
+
// Gets the builders nodes in graph execution order
|
75 |
+
const nodesInOrder = app.graph.computeExecutionOrder(false);
|
76 |
+
this.nodes = this.nodes
|
77 |
+
.map((node) => ({ index: nodesInOrder.indexOf(node), node }))
|
78 |
+
.sort((a, b) => a.index - b.index || a.node.id - b.node.id)
|
79 |
+
.map(({ node }) => node);
|
80 |
+
}
|
81 |
+
|
82 |
+
getNodeData() {
|
83 |
+
const storeLinkTypes = (config) => {
|
84 |
+
// Store link types for dynamically typed nodes e.g. reroutes
|
85 |
+
for (const link of config.links) {
|
86 |
+
const origin = app.graph.getNodeById(link[4]);
|
87 |
+
const type = origin.outputs[link[1]].type;
|
88 |
+
link.push(type);
|
89 |
+
}
|
90 |
+
};
|
91 |
+
|
92 |
+
const storeExternalLinks = (config) => {
|
93 |
+
// Store any external links to the group in the config so when rebuilding we add extra slots
|
94 |
+
config.external = [];
|
95 |
+
for (let i = 0; i < this.nodes.length; i++) {
|
96 |
+
const node = this.nodes[i];
|
97 |
+
if (!node.outputs?.length) continue;
|
98 |
+
for (let slot = 0; slot < node.outputs.length; slot++) {
|
99 |
+
let hasExternal = false;
|
100 |
+
const output = node.outputs[slot];
|
101 |
+
let type = output.type;
|
102 |
+
if (!output.links?.length) continue;
|
103 |
+
for (const l of output.links) {
|
104 |
+
const link = app.graph.links[l];
|
105 |
+
if (!link) continue;
|
106 |
+
if (type === "*") type = link.type;
|
107 |
+
|
108 |
+
if (!app.canvas.selected_nodes[link.target_id]) {
|
109 |
+
hasExternal = true;
|
110 |
+
break;
|
111 |
+
}
|
112 |
+
}
|
113 |
+
if (hasExternal) {
|
114 |
+
config.external.push([i, slot, type]);
|
115 |
+
}
|
116 |
+
}
|
117 |
+
}
|
118 |
+
};
|
119 |
+
|
120 |
+
// Use the built in copyToClipboard function to generate the node data we need
|
121 |
+
const backup = localStorage.getItem("litegrapheditor_clipboard");
|
122 |
+
try {
|
123 |
+
app.canvas.copyToClipboard(this.nodes);
|
124 |
+
const config = JSON.parse(localStorage.getItem("litegrapheditor_clipboard"));
|
125 |
+
|
126 |
+
storeLinkTypes(config);
|
127 |
+
storeExternalLinks(config);
|
128 |
+
|
129 |
+
return config;
|
130 |
+
} finally {
|
131 |
+
localStorage.setItem("litegrapheditor_clipboard", backup);
|
132 |
+
}
|
133 |
+
}
|
134 |
+
}
|
135 |
+
|
136 |
+
export class GroupNodeConfig {
|
137 |
+
constructor(name, nodeData) {
|
138 |
+
this.name = name;
|
139 |
+
this.nodeData = nodeData;
|
140 |
+
this.getLinks();
|
141 |
+
|
142 |
+
this.inputCount = 0;
|
143 |
+
this.oldToNewOutputMap = {};
|
144 |
+
this.newToOldOutputMap = {};
|
145 |
+
this.oldToNewInputMap = {};
|
146 |
+
this.oldToNewWidgetMap = {};
|
147 |
+
this.newToOldWidgetMap = {};
|
148 |
+
this.primitiveDefs = {};
|
149 |
+
this.widgetToPrimitive = {};
|
150 |
+
this.primitiveToWidget = {};
|
151 |
+
this.nodeInputs = {};
|
152 |
+
this.outputVisibility = [];
|
153 |
+
}
|
154 |
+
|
155 |
+
async registerType(source = "workflow") {
|
156 |
+
this.nodeDef = {
|
157 |
+
output: [],
|
158 |
+
output_name: [],
|
159 |
+
output_is_list: [],
|
160 |
+
output_is_hidden: [],
|
161 |
+
name: source + "/" + this.name,
|
162 |
+
display_name: this.name,
|
163 |
+
category: "group nodes" + ("/" + source),
|
164 |
+
input: { required: {} },
|
165 |
+
|
166 |
+
[GROUP]: this,
|
167 |
+
};
|
168 |
+
|
169 |
+
this.inputs = [];
|
170 |
+
const seenInputs = {};
|
171 |
+
const seenOutputs = {};
|
172 |
+
for (let i = 0; i < this.nodeData.nodes.length; i++) {
|
173 |
+
const node = this.nodeData.nodes[i];
|
174 |
+
node.index = i;
|
175 |
+
this.processNode(node, seenInputs, seenOutputs);
|
176 |
+
}
|
177 |
+
|
178 |
+
for (const p of this.#convertedToProcess) {
|
179 |
+
p();
|
180 |
+
}
|
181 |
+
this.#convertedToProcess = null;
|
182 |
+
await app.registerNodeDef("workflow/" + this.name, this.nodeDef);
|
183 |
+
}
|
184 |
+
|
185 |
+
getLinks() {
|
186 |
+
this.linksFrom = {};
|
187 |
+
this.linksTo = {};
|
188 |
+
this.externalFrom = {};
|
189 |
+
|
190 |
+
// Extract links for easy lookup
|
191 |
+
for (const l of this.nodeData.links) {
|
192 |
+
const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l;
|
193 |
+
|
194 |
+
// Skip links outside the copy config
|
195 |
+
if (sourceNodeId == null) continue;
|
196 |
+
|
197 |
+
if (!this.linksFrom[sourceNodeId]) {
|
198 |
+
this.linksFrom[sourceNodeId] = {};
|
199 |
+
}
|
200 |
+
if (!this.linksFrom[sourceNodeId][sourceNodeSlot]) {
|
201 |
+
this.linksFrom[sourceNodeId][sourceNodeSlot] = [];
|
202 |
+
}
|
203 |
+
this.linksFrom[sourceNodeId][sourceNodeSlot].push(l);
|
204 |
+
|
205 |
+
if (!this.linksTo[targetNodeId]) {
|
206 |
+
this.linksTo[targetNodeId] = {};
|
207 |
+
}
|
208 |
+
this.linksTo[targetNodeId][targetNodeSlot] = l;
|
209 |
+
}
|
210 |
+
|
211 |
+
if (this.nodeData.external) {
|
212 |
+
for (const ext of this.nodeData.external) {
|
213 |
+
if (!this.externalFrom[ext[0]]) {
|
214 |
+
this.externalFrom[ext[0]] = { [ext[1]]: ext[2] };
|
215 |
+
} else {
|
216 |
+
this.externalFrom[ext[0]][ext[1]] = ext[2];
|
217 |
+
}
|
218 |
+
}
|
219 |
+
}
|
220 |
+
}
|
221 |
+
|
222 |
+
processNode(node, seenInputs, seenOutputs) {
|
223 |
+
const def = this.getNodeDef(node);
|
224 |
+
if (!def) return;
|
225 |
+
|
226 |
+
const inputs = { ...def.input?.required, ...def.input?.optional };
|
227 |
+
|
228 |
+
this.inputs.push(this.processNodeInputs(node, seenInputs, inputs));
|
229 |
+
if (def.output?.length) this.processNodeOutputs(node, seenOutputs, def);
|
230 |
+
}
|
231 |
+
|
232 |
+
getNodeDef(node) {
|
233 |
+
const def = globalDefs[node.type];
|
234 |
+
if (def) return def;
|
235 |
+
|
236 |
+
const linksFrom = this.linksFrom[node.index];
|
237 |
+
if (node.type === "PrimitiveNode") {
|
238 |
+
// Skip as its not linked
|
239 |
+
if (!linksFrom) return;
|
240 |
+
|
241 |
+
let type = linksFrom["0"][0][5];
|
242 |
+
if (type === "COMBO") {
|
243 |
+
// Use the array items
|
244 |
+
const source = node.outputs[0].widget.name;
|
245 |
+
const fromTypeName = this.nodeData.nodes[linksFrom["0"][0][2]].type;
|
246 |
+
const fromType = globalDefs[fromTypeName];
|
247 |
+
const input = fromType.input.required[source] ?? fromType.input.optional[source];
|
248 |
+
type = input[0];
|
249 |
+
}
|
250 |
+
|
251 |
+
const def = (this.primitiveDefs[node.index] = {
|
252 |
+
input: {
|
253 |
+
required: {
|
254 |
+
value: [type, {}],
|
255 |
+
},
|
256 |
+
},
|
257 |
+
output: [type],
|
258 |
+
output_name: [],
|
259 |
+
output_is_list: [],
|
260 |
+
});
|
261 |
+
return def;
|
262 |
+
} else if (node.type === "Reroute") {
|
263 |
+
const linksTo = this.linksTo[node.index];
|
264 |
+
if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) {
|
265 |
+
// Being used internally
|
266 |
+
return null;
|
267 |
+
}
|
268 |
+
|
269 |
+
let config = {};
|
270 |
+
let rerouteType = "*";
|
271 |
+
if (linksFrom) {
|
272 |
+
for (const [, , id, slot] of linksFrom["0"]) {
|
273 |
+
const node = this.nodeData.nodes[id];
|
274 |
+
const input = node.inputs[slot];
|
275 |
+
if (rerouteType === "*") {
|
276 |
+
rerouteType = input.type;
|
277 |
+
}
|
278 |
+
if (input.widget) {
|
279 |
+
const targetDef = globalDefs[node.type];
|
280 |
+
const targetWidget = targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name];
|
281 |
+
|
282 |
+
const widget = [targetWidget[0], config];
|
283 |
+
const res = mergeIfValid(
|
284 |
+
{
|
285 |
+
widget,
|
286 |
+
},
|
287 |
+
targetWidget,
|
288 |
+
false,
|
289 |
+
null,
|
290 |
+
widget
|
291 |
+
);
|
292 |
+
config = res?.customConfig ?? config;
|
293 |
+
}
|
294 |
+
}
|
295 |
+
} else if (linksTo) {
|
296 |
+
const [id, slot] = linksTo["0"];
|
297 |
+
rerouteType = this.nodeData.nodes[id].outputs[slot].type;
|
298 |
+
} else {
|
299 |
+
// Reroute used as a pipe
|
300 |
+
for (const l of this.nodeData.links) {
|
301 |
+
if (l[2] === node.index) {
|
302 |
+
rerouteType = l[5];
|
303 |
+
break;
|
304 |
+
}
|
305 |
+
}
|
306 |
+
if (rerouteType === "*") {
|
307 |
+
// Check for an external link
|
308 |
+
const t = this.externalFrom[node.index]?.[0];
|
309 |
+
if (t) {
|
310 |
+
rerouteType = t;
|
311 |
+
}
|
312 |
+
}
|
313 |
+
}
|
314 |
+
|
315 |
+
config.forceInput = true;
|
316 |
+
return {
|
317 |
+
input: {
|
318 |
+
required: {
|
319 |
+
[rerouteType]: [rerouteType, config],
|
320 |
+
},
|
321 |
+
},
|
322 |
+
output: [rerouteType],
|
323 |
+
output_name: [],
|
324 |
+
output_is_list: [],
|
325 |
+
};
|
326 |
+
}
|
327 |
+
|
328 |
+
console.warn("Skipping virtual node " + node.type + " when building group node " + this.name);
|
329 |
+
}
|
330 |
+
|
331 |
+
getInputConfig(node, inputName, seenInputs, config, extra) {
|
332 |
+
const customConfig = this.nodeData.config?.[node.index]?.input?.[inputName];
|
333 |
+
let name = customConfig?.name ?? node.inputs?.find((inp) => inp.name === inputName)?.label ?? inputName;
|
334 |
+
let key = name;
|
335 |
+
let prefix = "";
|
336 |
+
// Special handling for primitive to include the title if it is set rather than just "value"
|
337 |
+
if ((node.type === "PrimitiveNode" && node.title) || name in seenInputs) {
|
338 |
+
prefix = `${node.title ?? node.type} `;
|
339 |
+
key = name = `${prefix}${inputName}`;
|
340 |
+
if (name in seenInputs) {
|
341 |
+
name = `${prefix}${seenInputs[name]} ${inputName}`;
|
342 |
+
}
|
343 |
+
}
|
344 |
+
seenInputs[key] = (seenInputs[key] ?? 1) + 1;
|
345 |
+
|
346 |
+
if (inputName === "seed" || inputName === "noise_seed") {
|
347 |
+
if (!extra) extra = {};
|
348 |
+
extra.control_after_generate = `${prefix}control_after_generate`;
|
349 |
+
}
|
350 |
+
if (config[0] === "IMAGEUPLOAD") {
|
351 |
+
if (!extra) extra = {};
|
352 |
+
extra.widget = this.oldToNewWidgetMap[node.index]?.[config[1]?.widget ?? "image"] ?? "image";
|
353 |
+
}
|
354 |
+
|
355 |
+
if (extra) {
|
356 |
+
config = [config[0], { ...config[1], ...extra }];
|
357 |
+
}
|
358 |
+
|
359 |
+
return { name, config, customConfig };
|
360 |
+
}
|
361 |
+
|
362 |
+
processWidgetInputs(inputs, node, inputNames, seenInputs) {
|
363 |
+
const slots = [];
|
364 |
+
const converted = new Map();
|
365 |
+
const widgetMap = (this.oldToNewWidgetMap[node.index] = {});
|
366 |
+
for (const inputName of inputNames) {
|
367 |
+
let widgetType = app.getWidgetType(inputs[inputName], inputName);
|
368 |
+
if (widgetType) {
|
369 |
+
const convertedIndex = node.inputs?.findIndex((inp) => inp.name === inputName && inp.widget?.name === inputName);
|
370 |
+
if (convertedIndex > -1) {
|
371 |
+
// This widget has been converted to a widget
|
372 |
+
// We need to store this in the correct position so link ids line up
|
373 |
+
converted.set(convertedIndex, inputName);
|
374 |
+
widgetMap[inputName] = null;
|
375 |
+
} else {
|
376 |
+
// Normal widget
|
377 |
+
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
|
378 |
+
this.nodeDef.input.required[name] = config;
|
379 |
+
widgetMap[inputName] = name;
|
380 |
+
this.newToOldWidgetMap[name] = { node, inputName };
|
381 |
+
}
|
382 |
+
} else {
|
383 |
+
// Normal input
|
384 |
+
slots.push(inputName);
|
385 |
+
}
|
386 |
+
}
|
387 |
+
return { converted, slots };
|
388 |
+
}
|
389 |
+
|
390 |
+
checkPrimitiveConnection(link, inputName, inputs) {
|
391 |
+
const sourceNode = this.nodeData.nodes[link[0]];
|
392 |
+
if (sourceNode.type === "PrimitiveNode") {
|
393 |
+
// Merge link configurations
|
394 |
+
const [sourceNodeId, _, targetNodeId, __] = link;
|
395 |
+
const primitiveDef = this.primitiveDefs[sourceNodeId];
|
396 |
+
const targetWidget = inputs[inputName];
|
397 |
+
const primitiveConfig = primitiveDef.input.required.value;
|
398 |
+
const output = { widget: primitiveConfig };
|
399 |
+
const config = mergeIfValid(output, targetWidget, false, null, primitiveConfig);
|
400 |
+
primitiveConfig[1] = config?.customConfig ?? inputs[inputName][1] ? { ...inputs[inputName][1] } : {};
|
401 |
+
|
402 |
+
let name = this.oldToNewWidgetMap[sourceNodeId]["value"];
|
403 |
+
name = name.substr(0, name.length - 6);
|
404 |
+
primitiveConfig[1].control_after_generate = true;
|
405 |
+
primitiveConfig[1].control_prefix = name;
|
406 |
+
|
407 |
+
let toPrimitive = this.widgetToPrimitive[targetNodeId];
|
408 |
+
if (!toPrimitive) {
|
409 |
+
toPrimitive = this.widgetToPrimitive[targetNodeId] = {};
|
410 |
+
}
|
411 |
+
if (toPrimitive[inputName]) {
|
412 |
+
toPrimitive[inputName].push(sourceNodeId);
|
413 |
+
}
|
414 |
+
toPrimitive[inputName] = sourceNodeId;
|
415 |
+
|
416 |
+
let toWidget = this.primitiveToWidget[sourceNodeId];
|
417 |
+
if (!toWidget) {
|
418 |
+
toWidget = this.primitiveToWidget[sourceNodeId] = [];
|
419 |
+
}
|
420 |
+
toWidget.push({ nodeId: targetNodeId, inputName });
|
421 |
+
}
|
422 |
+
}
|
423 |
+
|
424 |
+
processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) {
|
425 |
+
this.nodeInputs[node.index] = {};
|
426 |
+
for (let i = 0; i < slots.length; i++) {
|
427 |
+
const inputName = slots[i];
|
428 |
+
if (linksTo[i]) {
|
429 |
+
this.checkPrimitiveConnection(linksTo[i], inputName, inputs);
|
430 |
+
// This input is linked so we can skip it
|
431 |
+
continue;
|
432 |
+
}
|
433 |
+
|
434 |
+
const { name, config, customConfig } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
|
435 |
+
|
436 |
+
this.nodeInputs[node.index][inputName] = name;
|
437 |
+
if(customConfig?.visible === false) continue;
|
438 |
+
|
439 |
+
this.nodeDef.input.required[name] = config;
|
440 |
+
inputMap[i] = this.inputCount++;
|
441 |
+
}
|
442 |
+
}
|
443 |
+
|
444 |
+
processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs) {
|
445 |
+
// Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up
|
446 |
+
const convertedSlots = [...converted.keys()].sort().map((k) => converted.get(k));
|
447 |
+
for (let i = 0; i < convertedSlots.length; i++) {
|
448 |
+
const inputName = convertedSlots[i];
|
449 |
+
if (linksTo[slots.length + i]) {
|
450 |
+
this.checkPrimitiveConnection(linksTo[slots.length + i], inputName, inputs);
|
451 |
+
// This input is linked so we can skip it
|
452 |
+
continue;
|
453 |
+
}
|
454 |
+
|
455 |
+
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], {
|
456 |
+
defaultInput: true,
|
457 |
+
});
|
458 |
+
|
459 |
+
this.nodeDef.input.required[name] = config;
|
460 |
+
this.newToOldWidgetMap[name] = { node, inputName };
|
461 |
+
|
462 |
+
if (!this.oldToNewWidgetMap[node.index]) {
|
463 |
+
this.oldToNewWidgetMap[node.index] = {};
|
464 |
+
}
|
465 |
+
this.oldToNewWidgetMap[node.index][inputName] = name;
|
466 |
+
|
467 |
+
inputMap[slots.length + i] = this.inputCount++;
|
468 |
+
}
|
469 |
+
}
|
470 |
+
|
471 |
+
#convertedToProcess = [];
|
472 |
+
processNodeInputs(node, seenInputs, inputs) {
|
473 |
+
const inputMapping = [];
|
474 |
+
|
475 |
+
const inputNames = Object.keys(inputs);
|
476 |
+
if (!inputNames.length) return;
|
477 |
+
|
478 |
+
const { converted, slots } = this.processWidgetInputs(inputs, node, inputNames, seenInputs);
|
479 |
+
const linksTo = this.linksTo[node.index] ?? {};
|
480 |
+
const inputMap = (this.oldToNewInputMap[node.index] = {});
|
481 |
+
this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs);
|
482 |
+
|
483 |
+
// Converted inputs have to be processed after all other nodes as they'll be at the end of the list
|
484 |
+
this.#convertedToProcess.push(() => this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs));
|
485 |
+
|
486 |
+
return inputMapping;
|
487 |
+
}
|
488 |
+
|
489 |
+
processNodeOutputs(node, seenOutputs, def) {
|
490 |
+
const oldToNew = (this.oldToNewOutputMap[node.index] = {});
|
491 |
+
|
492 |
+
// Add outputs
|
493 |
+
for (let outputId = 0; outputId < def.output.length; outputId++) {
|
494 |
+
const linksFrom = this.linksFrom[node.index];
|
495 |
+
// If this output is linked internally we flag it to hide
|
496 |
+
const hasLink = linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId];
|
497 |
+
const customConfig = this.nodeData.config?.[node.index]?.output?.[outputId];
|
498 |
+
const visible = customConfig?.visible ?? !hasLink;
|
499 |
+
this.outputVisibility.push(visible);
|
500 |
+
if (!visible) {
|
501 |
+
continue;
|
502 |
+
}
|
503 |
+
|
504 |
+
oldToNew[outputId] = this.nodeDef.output.length;
|
505 |
+
this.newToOldOutputMap[this.nodeDef.output.length] = { node, slot: outputId };
|
506 |
+
this.nodeDef.output.push(def.output[outputId]);
|
507 |
+
this.nodeDef.output_is_list.push(def.output_is_list[outputId]);
|
508 |
+
|
509 |
+
let label = customConfig?.name;
|
510 |
+
if (!label) {
|
511 |
+
label = def.output_name?.[outputId] ?? def.output[outputId];
|
512 |
+
const output = node.outputs.find((o) => o.name === label);
|
513 |
+
if (output?.label) {
|
514 |
+
label = output.label;
|
515 |
+
}
|
516 |
+
}
|
517 |
+
|
518 |
+
let name = label;
|
519 |
+
if (name in seenOutputs) {
|
520 |
+
const prefix = `${node.title ?? node.type} `;
|
521 |
+
name = `${prefix}${label}`;
|
522 |
+
if (name in seenOutputs) {
|
523 |
+
name = `${prefix}${node.index} ${label}`;
|
524 |
+
}
|
525 |
+
}
|
526 |
+
seenOutputs[name] = 1;
|
527 |
+
|
528 |
+
this.nodeDef.output_name.push(name);
|
529 |
+
}
|
530 |
+
}
|
531 |
+
|
532 |
+
static async registerFromWorkflow(groupNodes, missingNodeTypes) {
|
533 |
+
const clean = app.clean;
|
534 |
+
app.clean = function () {
|
535 |
+
for (const g in groupNodes) {
|
536 |
+
try {
|
537 |
+
LiteGraph.unregisterNodeType("workflow/" + g);
|
538 |
+
} catch (error) {}
|
539 |
+
}
|
540 |
+
app.clean = clean;
|
541 |
+
};
|
542 |
+
|
543 |
+
for (const g in groupNodes) {
|
544 |
+
const groupData = groupNodes[g];
|
545 |
+
|
546 |
+
let hasMissing = false;
|
547 |
+
for (const n of groupData.nodes) {
|
548 |
+
// Find missing node types
|
549 |
+
if (!(n.type in LiteGraph.registered_node_types)) {
|
550 |
+
missingNodeTypes.push({
|
551 |
+
type: n.type,
|
552 |
+
hint: ` (In group node 'workflow/${g}')`,
|
553 |
+
});
|
554 |
+
|
555 |
+
missingNodeTypes.push({
|
556 |
+
type: "workflow/" + g,
|
557 |
+
action: {
|
558 |
+
text: "Remove from workflow",
|
559 |
+
callback: (e) => {
|
560 |
+
delete groupNodes[g];
|
561 |
+
e.target.textContent = "Removed";
|
562 |
+
e.target.style.pointerEvents = "none";
|
563 |
+
e.target.style.opacity = 0.7;
|
564 |
+
},
|
565 |
+
},
|
566 |
+
});
|
567 |
+
|
568 |
+
hasMissing = true;
|
569 |
+
}
|
570 |
+
}
|
571 |
+
|
572 |
+
if (hasMissing) continue;
|
573 |
+
|
574 |
+
const config = new GroupNodeConfig(g, groupData);
|
575 |
+
await config.registerType();
|
576 |
+
}
|
577 |
+
}
|
578 |
+
}
|
579 |
+
|
580 |
+
export class GroupNodeHandler {
|
581 |
+
node;
|
582 |
+
groupData;
|
583 |
+
|
584 |
+
constructor(node) {
|
585 |
+
this.node = node;
|
586 |
+
this.groupData = node.constructor?.nodeData?.[GROUP];
|
587 |
+
|
588 |
+
this.node.setInnerNodes = (innerNodes) => {
|
589 |
+
this.innerNodes = innerNodes;
|
590 |
+
|
591 |
+
for (let innerNodeIndex = 0; innerNodeIndex < this.innerNodes.length; innerNodeIndex++) {
|
592 |
+
const innerNode = this.innerNodes[innerNodeIndex];
|
593 |
+
|
594 |
+
for (const w of innerNode.widgets ?? []) {
|
595 |
+
if (w.type === "converted-widget") {
|
596 |
+
w.serializeValue = w.origSerializeValue;
|
597 |
+
}
|
598 |
+
}
|
599 |
+
|
600 |
+
innerNode.index = innerNodeIndex;
|
601 |
+
innerNode.getInputNode = (slot) => {
|
602 |
+
// Check if this input is internal or external
|
603 |
+
const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
|
604 |
+
if (externalSlot != null) {
|
605 |
+
return this.node.getInputNode(externalSlot);
|
606 |
+
}
|
607 |
+
|
608 |
+
// Internal link
|
609 |
+
const innerLink = this.groupData.linksTo[innerNode.index]?.[slot];
|
610 |
+
if (!innerLink) return null;
|
611 |
+
|
612 |
+
const inputNode = innerNodes[innerLink[0]];
|
613 |
+
// Primitives will already apply their values
|
614 |
+
if (inputNode.type === "PrimitiveNode") return null;
|
615 |
+
|
616 |
+
return inputNode;
|
617 |
+
};
|
618 |
+
|
619 |
+
innerNode.getInputLink = (slot) => {
|
620 |
+
const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
|
621 |
+
if (externalSlot != null) {
|
622 |
+
// The inner node is connected via the group node inputs
|
623 |
+
const linkId = this.node.inputs[externalSlot].link;
|
624 |
+
let link = app.graph.links[linkId];
|
625 |
+
|
626 |
+
// Use the outer link, but update the target to the inner node
|
627 |
+
link = {
|
628 |
+
...link,
|
629 |
+
target_id: innerNode.id,
|
630 |
+
target_slot: +slot,
|
631 |
+
};
|
632 |
+
return link;
|
633 |
+
}
|
634 |
+
|
635 |
+
let link = this.groupData.linksTo[innerNode.index]?.[slot];
|
636 |
+
if (!link) return null;
|
637 |
+
// Use the inner link, but update the origin node to be inner node id
|
638 |
+
link = {
|
639 |
+
origin_id: innerNodes[link[0]].id,
|
640 |
+
origin_slot: link[1],
|
641 |
+
target_id: innerNode.id,
|
642 |
+
target_slot: +slot,
|
643 |
+
};
|
644 |
+
return link;
|
645 |
+
};
|
646 |
+
}
|
647 |
+
};
|
648 |
+
|
649 |
+
this.node.updateLink = (link) => {
|
650 |
+
// Replace the group node reference with the internal node
|
651 |
+
link = { ...link };
|
652 |
+
const output = this.groupData.newToOldOutputMap[link.origin_slot];
|
653 |
+
let innerNode = this.innerNodes[output.node.index];
|
654 |
+
let l;
|
655 |
+
while (innerNode?.type === "Reroute") {
|
656 |
+
l = innerNode.getInputLink(0);
|
657 |
+
innerNode = innerNode.getInputNode(0);
|
658 |
+
}
|
659 |
+
|
660 |
+
if (!innerNode) {
|
661 |
+
return null;
|
662 |
+
}
|
663 |
+
|
664 |
+
if (l && GroupNodeHandler.isGroupNode(innerNode)) {
|
665 |
+
return innerNode.updateLink(l);
|
666 |
+
}
|
667 |
+
|
668 |
+
link.origin_id = innerNode.id;
|
669 |
+
link.origin_slot = l?.origin_slot ?? output.slot;
|
670 |
+
return link;
|
671 |
+
};
|
672 |
+
|
673 |
+
this.node.getInnerNodes = () => {
|
674 |
+
if (!this.innerNodes) {
|
675 |
+
this.node.setInnerNodes(
|
676 |
+
this.groupData.nodeData.nodes.map((n, i) => {
|
677 |
+
const innerNode = LiteGraph.createNode(n.type);
|
678 |
+
innerNode.configure(n);
|
679 |
+
innerNode.id = `${this.node.id}:${i}`;
|
680 |
+
return innerNode;
|
681 |
+
})
|
682 |
+
);
|
683 |
+
}
|
684 |
+
|
685 |
+
this.updateInnerWidgets();
|
686 |
+
|
687 |
+
return this.innerNodes;
|
688 |
+
};
|
689 |
+
|
690 |
+
this.node.recreate = async () => {
|
691 |
+
const id = this.node.id;
|
692 |
+
const sz = this.node.size;
|
693 |
+
const nodes = this.node.convertToNodes();
|
694 |
+
|
695 |
+
const groupNode = LiteGraph.createNode(this.node.type);
|
696 |
+
groupNode.id = id;
|
697 |
+
|
698 |
+
// Reuse the existing nodes for this instance
|
699 |
+
groupNode.setInnerNodes(nodes);
|
700 |
+
groupNode[GROUP].populateWidgets();
|
701 |
+
app.graph.add(groupNode);
|
702 |
+
groupNode.size = [Math.max(groupNode.size[0], sz[0]), Math.max(groupNode.size[1], sz[1])];
|
703 |
+
|
704 |
+
// Remove all converted nodes and relink them
|
705 |
+
groupNode[GROUP].replaceNodes(nodes);
|
706 |
+
return groupNode;
|
707 |
+
};
|
708 |
+
|
709 |
+
this.node.convertToNodes = () => {
|
710 |
+
const addInnerNodes = () => {
|
711 |
+
const backup = localStorage.getItem("litegrapheditor_clipboard");
|
712 |
+
// Clone the node data so we dont mutate it for other nodes
|
713 |
+
const c = { ...this.groupData.nodeData };
|
714 |
+
c.nodes = [...c.nodes];
|
715 |
+
const innerNodes = this.node.getInnerNodes();
|
716 |
+
let ids = [];
|
717 |
+
for (let i = 0; i < c.nodes.length; i++) {
|
718 |
+
let id = innerNodes?.[i]?.id;
|
719 |
+
// Use existing IDs if they are set on the inner nodes
|
720 |
+
if (id == null || isNaN(id)) {
|
721 |
+
id = undefined;
|
722 |
+
} else {
|
723 |
+
ids.push(id);
|
724 |
+
}
|
725 |
+
c.nodes[i] = { ...c.nodes[i], id };
|
726 |
+
}
|
727 |
+
localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(c));
|
728 |
+
app.canvas.pasteFromClipboard();
|
729 |
+
localStorage.setItem("litegrapheditor_clipboard", backup);
|
730 |
+
|
731 |
+
const [x, y] = this.node.pos;
|
732 |
+
let top;
|
733 |
+
let left;
|
734 |
+
// Configure nodes with current widget data
|
735 |
+
const selectedIds = ids.length ? ids : Object.keys(app.canvas.selected_nodes);
|
736 |
+
const newNodes = [];
|
737 |
+
for (let i = 0; i < selectedIds.length; i++) {
|
738 |
+
const id = selectedIds[i];
|
739 |
+
const newNode = app.graph.getNodeById(id);
|
740 |
+
const innerNode = innerNodes[i];
|
741 |
+
newNodes.push(newNode);
|
742 |
+
|
743 |
+
if (left == null || newNode.pos[0] < left) {
|
744 |
+
left = newNode.pos[0];
|
745 |
+
}
|
746 |
+
if (top == null || newNode.pos[1] < top) {
|
747 |
+
top = newNode.pos[1];
|
748 |
+
}
|
749 |
+
|
750 |
+
if (!newNode.widgets) continue;
|
751 |
+
|
752 |
+
const map = this.groupData.oldToNewWidgetMap[innerNode.index];
|
753 |
+
if (map) {
|
754 |
+
const widgets = Object.keys(map);
|
755 |
+
|
756 |
+
for (const oldName of widgets) {
|
757 |
+
const newName = map[oldName];
|
758 |
+
if (!newName) continue;
|
759 |
+
|
760 |
+
const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName);
|
761 |
+
if (widgetIndex === -1) continue;
|
762 |
+
|
763 |
+
// Populate the main and any linked widgets
|
764 |
+
if (innerNode.type === "PrimitiveNode") {
|
765 |
+
for (let i = 0; i < newNode.widgets.length; i++) {
|
766 |
+
newNode.widgets[i].value = this.node.widgets[widgetIndex + i].value;
|
767 |
+
}
|
768 |
+
} else {
|
769 |
+
const outerWidget = this.node.widgets[widgetIndex];
|
770 |
+
const newWidget = newNode.widgets.find((w) => w.name === oldName);
|
771 |
+
if (!newWidget) continue;
|
772 |
+
|
773 |
+
newWidget.value = outerWidget.value;
|
774 |
+
for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) {
|
775 |
+
newWidget.linkedWidgets[w].value = outerWidget.linkedWidgets[w].value;
|
776 |
+
}
|
777 |
+
}
|
778 |
+
}
|
779 |
+
}
|
780 |
+
}
|
781 |
+
|
782 |
+
// Shift each node
|
783 |
+
for (const newNode of newNodes) {
|
784 |
+
newNode.pos = [newNode.pos[0] - (left - x), newNode.pos[1] - (top - y)];
|
785 |
+
}
|
786 |
+
|
787 |
+
return { newNodes, selectedIds };
|
788 |
+
};
|
789 |
+
|
790 |
+
const reconnectInputs = (selectedIds) => {
|
791 |
+
for (const innerNodeIndex in this.groupData.oldToNewInputMap) {
|
792 |
+
const id = selectedIds[innerNodeIndex];
|
793 |
+
const newNode = app.graph.getNodeById(id);
|
794 |
+
const map = this.groupData.oldToNewInputMap[innerNodeIndex];
|
795 |
+
for (const innerInputId in map) {
|
796 |
+
const groupSlotId = map[innerInputId];
|
797 |
+
if (groupSlotId == null) continue;
|
798 |
+
const slot = node.inputs[groupSlotId];
|
799 |
+
if (slot.link == null) continue;
|
800 |
+
const link = app.graph.links[slot.link];
|
801 |
+
if (!link) continue;
|
802 |
+
// connect this node output to the input of another node
|
803 |
+
const originNode = app.graph.getNodeById(link.origin_id);
|
804 |
+
originNode.connect(link.origin_slot, newNode, +innerInputId);
|
805 |
+
}
|
806 |
+
}
|
807 |
+
};
|
808 |
+
|
809 |
+
const reconnectOutputs = (selectedIds) => {
|
810 |
+
for (let groupOutputId = 0; groupOutputId < node.outputs?.length; groupOutputId++) {
|
811 |
+
const output = node.outputs[groupOutputId];
|
812 |
+
if (!output.links) continue;
|
813 |
+
const links = [...output.links];
|
814 |
+
for (const l of links) {
|
815 |
+
const slot = this.groupData.newToOldOutputMap[groupOutputId];
|
816 |
+
const link = app.graph.links[l];
|
817 |
+
const targetNode = app.graph.getNodeById(link.target_id);
|
818 |
+
const newNode = app.graph.getNodeById(selectedIds[slot.node.index]);
|
819 |
+
newNode.connect(slot.slot, targetNode, link.target_slot);
|
820 |
+
}
|
821 |
+
}
|
822 |
+
};
|
823 |
+
|
824 |
+
const { newNodes, selectedIds } = addInnerNodes();
|
825 |
+
reconnectInputs(selectedIds);
|
826 |
+
reconnectOutputs(selectedIds);
|
827 |
+
app.graph.remove(this.node);
|
828 |
+
|
829 |
+
return newNodes;
|
830 |
+
};
|
831 |
+
|
832 |
+
const getExtraMenuOptions = this.node.getExtraMenuOptions;
|
833 |
+
this.node.getExtraMenuOptions = function (_, options) {
|
834 |
+
getExtraMenuOptions?.apply(this, arguments);
|
835 |
+
|
836 |
+
let optionIndex = options.findIndex((o) => o.content === "Outputs");
|
837 |
+
if (optionIndex === -1) optionIndex = options.length;
|
838 |
+
else optionIndex++;
|
839 |
+
options.splice(
|
840 |
+
optionIndex,
|
841 |
+
0,
|
842 |
+
null,
|
843 |
+
{
|
844 |
+
content: "Convert to nodes",
|
845 |
+
callback: () => {
|
846 |
+
return this.convertToNodes();
|
847 |
+
},
|
848 |
+
},
|
849 |
+
{
|
850 |
+
content: "Manage Group Node",
|
851 |
+
callback: () => {
|
852 |
+
new ManageGroupDialog(app).show(this.type);
|
853 |
+
},
|
854 |
+
}
|
855 |
+
);
|
856 |
+
};
|
857 |
+
|
858 |
+
// Draw custom collapse icon to identity this as a group
|
859 |
+
const onDrawTitleBox = this.node.onDrawTitleBox;
|
860 |
+
this.node.onDrawTitleBox = function (ctx, height, size, scale) {
|
861 |
+
onDrawTitleBox?.apply(this, arguments);
|
862 |
+
|
863 |
+
const fill = ctx.fillStyle;
|
864 |
+
ctx.beginPath();
|
865 |
+
ctx.rect(11, -height + 11, 2, 2);
|
866 |
+
ctx.rect(14, -height + 11, 2, 2);
|
867 |
+
ctx.rect(17, -height + 11, 2, 2);
|
868 |
+
ctx.rect(11, -height + 14, 2, 2);
|
869 |
+
ctx.rect(14, -height + 14, 2, 2);
|
870 |
+
ctx.rect(17, -height + 14, 2, 2);
|
871 |
+
ctx.rect(11, -height + 17, 2, 2);
|
872 |
+
ctx.rect(14, -height + 17, 2, 2);
|
873 |
+
ctx.rect(17, -height + 17, 2, 2);
|
874 |
+
|
875 |
+
ctx.fillStyle = this.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
|
876 |
+
ctx.fill();
|
877 |
+
ctx.fillStyle = fill;
|
878 |
+
};
|
879 |
+
|
880 |
+
// Draw progress label
|
881 |
+
const onDrawForeground = node.onDrawForeground;
|
882 |
+
const groupData = this.groupData.nodeData;
|
883 |
+
node.onDrawForeground = function (ctx) {
|
884 |
+
const r = onDrawForeground?.apply?.(this, arguments);
|
885 |
+
if (+app.runningNodeId === this.id && this.runningInternalNodeId !== null) {
|
886 |
+
const n = groupData.nodes[this.runningInternalNodeId];
|
887 |
+
if(!n) return;
|
888 |
+
const message = `Running ${n.title || n.type} (${this.runningInternalNodeId}/${groupData.nodes.length})`;
|
889 |
+
ctx.save();
|
890 |
+
ctx.font = "12px sans-serif";
|
891 |
+
const sz = ctx.measureText(message);
|
892 |
+
ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
|
893 |
+
ctx.beginPath();
|
894 |
+
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5);
|
895 |
+
ctx.fill();
|
896 |
+
|
897 |
+
ctx.fillStyle = "#fff";
|
898 |
+
ctx.fillText(message, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
|
899 |
+
ctx.restore();
|
900 |
+
}
|
901 |
+
};
|
902 |
+
|
903 |
+
// Flag this node as needing to be reset
|
904 |
+
const onExecutionStart = this.node.onExecutionStart;
|
905 |
+
this.node.onExecutionStart = function () {
|
906 |
+
this.resetExecution = true;
|
907 |
+
return onExecutionStart?.apply(this, arguments);
|
908 |
+
};
|
909 |
+
|
910 |
+
const self = this;
|
911 |
+
const onNodeCreated = this.node.onNodeCreated;
|
912 |
+
this.node.onNodeCreated = function () {
|
913 |
+
if (!this.widgets) {
|
914 |
+
return;
|
915 |
+
}
|
916 |
+
const config = self.groupData.nodeData.config;
|
917 |
+
if (config) {
|
918 |
+
for (const n in config) {
|
919 |
+
const inputs = config[n]?.input;
|
920 |
+
for (const w in inputs) {
|
921 |
+
if (inputs[w].visible !== false) continue;
|
922 |
+
const widgetName = self.groupData.oldToNewWidgetMap[n][w];
|
923 |
+
const widget = this.widgets.find((w) => w.name === widgetName);
|
924 |
+
if (widget) {
|
925 |
+
widget.type = "hidden";
|
926 |
+
widget.computeSize = () => [0, -4];
|
927 |
+
}
|
928 |
+
}
|
929 |
+
}
|
930 |
+
}
|
931 |
+
|
932 |
+
return onNodeCreated?.apply(this, arguments);
|
933 |
+
};
|
934 |
+
|
935 |
+
function handleEvent(type, getId, getEvent) {
|
936 |
+
const handler = ({ detail }) => {
|
937 |
+
const id = getId(detail);
|
938 |
+
if (!id) return;
|
939 |
+
const node = app.graph.getNodeById(id);
|
940 |
+
if (node) return;
|
941 |
+
|
942 |
+
const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id);
|
943 |
+
if (innerNodeIndex > -1) {
|
944 |
+
this.node.runningInternalNodeId = innerNodeIndex;
|
945 |
+
api.dispatchEvent(new CustomEvent(type, { detail: getEvent(detail, this.node.id + "", this.node) }));
|
946 |
+
}
|
947 |
+
};
|
948 |
+
api.addEventListener(type, handler);
|
949 |
+
return handler;
|
950 |
+
}
|
951 |
+
|
952 |
+
const executing = handleEvent.call(
|
953 |
+
this,
|
954 |
+
"executing",
|
955 |
+
(d) => d,
|
956 |
+
(d, id, node) => id
|
957 |
+
);
|
958 |
+
|
959 |
+
const executed = handleEvent.call(
|
960 |
+
this,
|
961 |
+
"executed",
|
962 |
+
(d) => d?.node,
|
963 |
+
(d, id, node) => ({ ...d, node: id, merge: !node.resetExecution })
|
964 |
+
);
|
965 |
+
|
966 |
+
const onRemoved = node.onRemoved;
|
967 |
+
this.node.onRemoved = function () {
|
968 |
+
onRemoved?.apply(this, arguments);
|
969 |
+
api.removeEventListener("executing", executing);
|
970 |
+
api.removeEventListener("executed", executed);
|
971 |
+
};
|
972 |
+
|
973 |
+
this.node.refreshComboInNode = (defs) => {
|
974 |
+
// Update combo widget options
|
975 |
+
for (const widgetName in this.groupData.newToOldWidgetMap) {
|
976 |
+
const widget = this.node.widgets.find((w) => w.name === widgetName);
|
977 |
+
if (widget?.type === "combo") {
|
978 |
+
const old = this.groupData.newToOldWidgetMap[widgetName];
|
979 |
+
const def = defs[old.node.type];
|
980 |
+
const input = def?.input?.required?.[old.inputName] ?? def?.input?.optional?.[old.inputName];
|
981 |
+
if (!input) continue;
|
982 |
+
|
983 |
+
widget.options.values = input[0];
|
984 |
+
|
985 |
+
if (old.inputName !== "image" && !widget.options.values.includes(widget.value)) {
|
986 |
+
widget.value = widget.options.values[0];
|
987 |
+
widget.callback(widget.value);
|
988 |
+
}
|
989 |
+
}
|
990 |
+
}
|
991 |
+
};
|
992 |
+
}
|
993 |
+
|
994 |
+
updateInnerWidgets() {
|
995 |
+
for (const newWidgetName in this.groupData.newToOldWidgetMap) {
|
996 |
+
const newWidget = this.node.widgets.find((w) => w.name === newWidgetName);
|
997 |
+
if (!newWidget) continue;
|
998 |
+
|
999 |
+
const newValue = newWidget.value;
|
1000 |
+
const old = this.groupData.newToOldWidgetMap[newWidgetName];
|
1001 |
+
let innerNode = this.innerNodes[old.node.index];
|
1002 |
+
|
1003 |
+
if (innerNode.type === "PrimitiveNode") {
|
1004 |
+
innerNode.primitiveValue = newValue;
|
1005 |
+
const primitiveLinked = this.groupData.primitiveToWidget[old.node.index];
|
1006 |
+
for (const linked of primitiveLinked ?? []) {
|
1007 |
+
const node = this.innerNodes[linked.nodeId];
|
1008 |
+
const widget = node.widgets.find((w) => w.name === linked.inputName);
|
1009 |
+
|
1010 |
+
if (widget) {
|
1011 |
+
widget.value = newValue;
|
1012 |
+
}
|
1013 |
+
}
|
1014 |
+
continue;
|
1015 |
+
} else if (innerNode.type === "Reroute") {
|
1016 |
+
const rerouteLinks = this.groupData.linksFrom[old.node.index];
|
1017 |
+
if (rerouteLinks) {
|
1018 |
+
for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
|
1019 |
+
const node = this.innerNodes[targetNodeId];
|
1020 |
+
const input = node.inputs[targetSlot];
|
1021 |
+
if (input.widget) {
|
1022 |
+
const widget = node.widgets?.find((w) => w.name === input.widget.name);
|
1023 |
+
if (widget) {
|
1024 |
+
widget.value = newValue;
|
1025 |
+
}
|
1026 |
+
}
|
1027 |
+
}
|
1028 |
+
}
|
1029 |
+
}
|
1030 |
+
|
1031 |
+
const widget = innerNode.widgets?.find((w) => w.name === old.inputName);
|
1032 |
+
if (widget) {
|
1033 |
+
widget.value = newValue;
|
1034 |
+
}
|
1035 |
+
}
|
1036 |
+
}
|
1037 |
+
|
1038 |
+
populatePrimitive(node, nodeId, oldName, i, linkedShift) {
|
1039 |
+
// Converted widget, populate primitive if linked
|
1040 |
+
const primitiveId = this.groupData.widgetToPrimitive[nodeId]?.[oldName];
|
1041 |
+
if (primitiveId == null) return;
|
1042 |
+
const targetWidgetName = this.groupData.oldToNewWidgetMap[primitiveId]["value"];
|
1043 |
+
const targetWidgetIndex = this.node.widgets.findIndex((w) => w.name === targetWidgetName);
|
1044 |
+
if (targetWidgetIndex > -1) {
|
1045 |
+
const primitiveNode = this.innerNodes[primitiveId];
|
1046 |
+
let len = primitiveNode.widgets.length;
|
1047 |
+
if (len - 1 !== this.node.widgets[targetWidgetIndex].linkedWidgets?.length) {
|
1048 |
+
// Fallback handling for if some reason the primitive has a different number of widgets
|
1049 |
+
// we dont want to overwrite random widgets, better to leave blank
|
1050 |
+
len = 1;
|
1051 |
+
}
|
1052 |
+
for (let i = 0; i < len; i++) {
|
1053 |
+
this.node.widgets[targetWidgetIndex + i].value = primitiveNode.widgets[i].value;
|
1054 |
+
}
|
1055 |
+
}
|
1056 |
+
return true;
|
1057 |
+
}
|
1058 |
+
|
1059 |
+
populateReroute(node, nodeId, map) {
|
1060 |
+
if (node.type !== "Reroute") return;
|
1061 |
+
|
1062 |
+
const link = this.groupData.linksFrom[nodeId]?.[0]?.[0];
|
1063 |
+
if (!link) return;
|
1064 |
+
const [, , targetNodeId, targetNodeSlot] = link;
|
1065 |
+
const targetNode = this.groupData.nodeData.nodes[targetNodeId];
|
1066 |
+
const inputs = targetNode.inputs;
|
1067 |
+
const targetWidget = inputs?.[targetNodeSlot]?.widget;
|
1068 |
+
if (!targetWidget) return;
|
1069 |
+
|
1070 |
+
const offset = inputs.length - (targetNode.widgets_values?.length ?? 0);
|
1071 |
+
const v = targetNode.widgets_values?.[targetNodeSlot - offset];
|
1072 |
+
if (v == null) return;
|
1073 |
+
|
1074 |
+
const widgetName = Object.values(map)[0];
|
1075 |
+
const widget = this.node.widgets.find((w) => w.name === widgetName);
|
1076 |
+
if (widget) {
|
1077 |
+
widget.value = v;
|
1078 |
+
}
|
1079 |
+
}
|
1080 |
+
|
1081 |
+
populateWidgets() {
|
1082 |
+
if (!this.node.widgets) return;
|
1083 |
+
|
1084 |
+
for (let nodeId = 0; nodeId < this.groupData.nodeData.nodes.length; nodeId++) {
|
1085 |
+
const node = this.groupData.nodeData.nodes[nodeId];
|
1086 |
+
const map = this.groupData.oldToNewWidgetMap[nodeId] ?? {};
|
1087 |
+
const widgets = Object.keys(map);
|
1088 |
+
|
1089 |
+
if (!node.widgets_values?.length) {
|
1090 |
+
// special handling for populating values into reroutes
|
1091 |
+
// this allows primitives connect to them to pick up the correct value
|
1092 |
+
this.populateReroute(node, nodeId, map);
|
1093 |
+
continue;
|
1094 |
+
}
|
1095 |
+
|
1096 |
+
let linkedShift = 0;
|
1097 |
+
for (let i = 0; i < widgets.length; i++) {
|
1098 |
+
const oldName = widgets[i];
|
1099 |
+
const newName = map[oldName];
|
1100 |
+
const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName);
|
1101 |
+
const mainWidget = this.node.widgets[widgetIndex];
|
1102 |
+
if (this.populatePrimitive(node, nodeId, oldName, i, linkedShift) || widgetIndex === -1) {
|
1103 |
+
// Find the inner widget and shift by the number of linked widgets as they will have been removed too
|
1104 |
+
const innerWidget = this.innerNodes[nodeId].widgets?.find((w) => w.name === oldName);
|
1105 |
+
linkedShift += innerWidget?.linkedWidgets?.length ?? 0;
|
1106 |
+
}
|
1107 |
+
if (widgetIndex === -1) {
|
1108 |
+
continue;
|
1109 |
+
}
|
1110 |
+
|
1111 |
+
// Populate the main and any linked widget
|
1112 |
+
mainWidget.value = node.widgets_values[i + linkedShift];
|
1113 |
+
for (let w = 0; w < mainWidget.linkedWidgets?.length; w++) {
|
1114 |
+
this.node.widgets[widgetIndex + w + 1].value = node.widgets_values[i + ++linkedShift];
|
1115 |
+
}
|
1116 |
+
}
|
1117 |
+
}
|
1118 |
+
}
|
1119 |
+
|
1120 |
+
replaceNodes(nodes) {
|
1121 |
+
let top;
|
1122 |
+
let left;
|
1123 |
+
|
1124 |
+
for (let i = 0; i < nodes.length; i++) {
|
1125 |
+
const node = nodes[i];
|
1126 |
+
if (left == null || node.pos[0] < left) {
|
1127 |
+
left = node.pos[0];
|
1128 |
+
}
|
1129 |
+
if (top == null || node.pos[1] < top) {
|
1130 |
+
top = node.pos[1];
|
1131 |
+
}
|
1132 |
+
|
1133 |
+
this.linkOutputs(node, i);
|
1134 |
+
app.graph.remove(node);
|
1135 |
+
}
|
1136 |
+
|
1137 |
+
this.linkInputs();
|
1138 |
+
this.node.pos = [left, top];
|
1139 |
+
}
|
1140 |
+
|
1141 |
+
linkOutputs(originalNode, nodeId) {
|
1142 |
+
if (!originalNode.outputs) return;
|
1143 |
+
|
1144 |
+
for (const output of originalNode.outputs) {
|
1145 |
+
if (!output.links) continue;
|
1146 |
+
// Clone the links as they'll be changed if we reconnect
|
1147 |
+
const links = [...output.links];
|
1148 |
+
for (const l of links) {
|
1149 |
+
const link = app.graph.links[l];
|
1150 |
+
if (!link) continue;
|
1151 |
+
|
1152 |
+
const targetNode = app.graph.getNodeById(link.target_id);
|
1153 |
+
const newSlot = this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot];
|
1154 |
+
if (newSlot != null) {
|
1155 |
+
this.node.connect(newSlot, targetNode, link.target_slot);
|
1156 |
+
}
|
1157 |
+
}
|
1158 |
+
}
|
1159 |
+
}
|
1160 |
+
|
1161 |
+
linkInputs() {
|
1162 |
+
for (const link of this.groupData.nodeData.links ?? []) {
|
1163 |
+
const [, originSlot, targetId, targetSlot, actualOriginId] = link;
|
1164 |
+
const originNode = app.graph.getNodeById(actualOriginId);
|
1165 |
+
if (!originNode) continue; // this node is in the group
|
1166 |
+
originNode.connect(originSlot, this.node.id, this.groupData.oldToNewInputMap[targetId][targetSlot]);
|
1167 |
+
}
|
1168 |
+
}
|
1169 |
+
|
1170 |
+
static getGroupData(node) {
|
1171 |
+
return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP];
|
1172 |
+
}
|
1173 |
+
|
1174 |
+
static isGroupNode(node) {
|
1175 |
+
return !!node.constructor?.nodeData?.[GROUP];
|
1176 |
+
}
|
1177 |
+
|
1178 |
+
static async fromNodes(nodes) {
|
1179 |
+
// Process the nodes into the stored workflow group node data
|
1180 |
+
const builder = new GroupNodeBuilder(nodes);
|
1181 |
+
const res = builder.build();
|
1182 |
+
if (!res) return;
|
1183 |
+
|
1184 |
+
const { name, nodeData } = res;
|
1185 |
+
|
1186 |
+
// Convert this data into a LG node definition and register it
|
1187 |
+
const config = new GroupNodeConfig(name, nodeData);
|
1188 |
+
await config.registerType();
|
1189 |
+
|
1190 |
+
const groupNode = LiteGraph.createNode(`workflow/${name}`);
|
1191 |
+
// Reuse the existing nodes for this instance
|
1192 |
+
groupNode.setInnerNodes(builder.nodes);
|
1193 |
+
groupNode[GROUP].populateWidgets();
|
1194 |
+
app.graph.add(groupNode);
|
1195 |
+
|
1196 |
+
// Remove all converted nodes and relink them
|
1197 |
+
groupNode[GROUP].replaceNodes(builder.nodes);
|
1198 |
+
return groupNode;
|
1199 |
+
}
|
1200 |
+
}
|
1201 |
+
|
1202 |
+
function addConvertToGroupOptions() {
|
1203 |
+
function addConvertOption(options, index) {
|
1204 |
+
const selected = Object.values(app.canvas.selected_nodes ?? {});
|
1205 |
+
const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n));
|
1206 |
+
options.splice(index + 1, null, {
|
1207 |
+
content: `Convert to Group Node`,
|
1208 |
+
disabled,
|
1209 |
+
callback: async () => {
|
1210 |
+
return await GroupNodeHandler.fromNodes(selected);
|
1211 |
+
},
|
1212 |
+
});
|
1213 |
+
}
|
1214 |
+
|
1215 |
+
function addManageOption(options, index) {
|
1216 |
+
const groups = app.graph.extra?.groupNodes;
|
1217 |
+
const disabled = !groups || !Object.keys(groups).length;
|
1218 |
+
options.splice(index + 1, null, {
|
1219 |
+
content: `Manage Group Nodes`,
|
1220 |
+
disabled,
|
1221 |
+
callback: () => {
|
1222 |
+
new ManageGroupDialog(app).show();
|
1223 |
+
},
|
1224 |
+
});
|
1225 |
+
}
|
1226 |
+
|
1227 |
+
// Add to canvas
|
1228 |
+
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
|
1229 |
+
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
1230 |
+
const options = getCanvasMenuOptions.apply(this, arguments);
|
1231 |
+
const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length;
|
1232 |
+
addConvertOption(options, index);
|
1233 |
+
addManageOption(options, index + 1);
|
1234 |
+
return options;
|
1235 |
+
};
|
1236 |
+
|
1237 |
+
// Add to nodes
|
1238 |
+
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
|
1239 |
+
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
|
1240 |
+
const options = getNodeMenuOptions.apply(this, arguments);
|
1241 |
+
if (!GroupNodeHandler.isGroupNode(node)) {
|
1242 |
+
const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1;
|
1243 |
+
addConvertOption(options, index);
|
1244 |
+
}
|
1245 |
+
return options;
|
1246 |
+
};
|
1247 |
+
}
|
1248 |
+
|
1249 |
+
const id = "Comfy.GroupNode";
|
1250 |
+
let globalDefs;
|
1251 |
+
const ext = {
|
1252 |
+
name: id,
|
1253 |
+
setup() {
|
1254 |
+
addConvertToGroupOptions();
|
1255 |
+
},
|
1256 |
+
async beforeConfigureGraph(graphData, missingNodeTypes) {
|
1257 |
+
const nodes = graphData?.extra?.groupNodes;
|
1258 |
+
if (nodes) {
|
1259 |
+
await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes);
|
1260 |
+
}
|
1261 |
+
},
|
1262 |
+
addCustomNodeDefs(defs) {
|
1263 |
+
// Store this so we can mutate it later with group nodes
|
1264 |
+
globalDefs = defs;
|
1265 |
+
},
|
1266 |
+
nodeCreated(node) {
|
1267 |
+
if (GroupNodeHandler.isGroupNode(node)) {
|
1268 |
+
node[GROUP] = new GroupNodeHandler(node);
|
1269 |
+
}
|
1270 |
+
},
|
1271 |
+
async refreshComboInNodes(defs) {
|
1272 |
+
// Re-register group nodes so new ones are created with the correct options
|
1273 |
+
Object.assign(globalDefs, defs);
|
1274 |
+
const nodes = app.graph.extra?.groupNodes;
|
1275 |
+
if (nodes) {
|
1276 |
+
await GroupNodeConfig.registerFromWorkflow(nodes, {});
|
1277 |
+
}
|
1278 |
+
}
|
1279 |
+
};
|
1280 |
+
|
1281 |
+
app.registerExtension(ext);
|
ComfyUI/web/extensions/core/groupNodeManage.css
ADDED
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.comfy-group-manage {
|
2 |
+
background: var(--bg-color);
|
3 |
+
color: var(--fg-color);
|
4 |
+
padding: 0;
|
5 |
+
font-family: Arial, Helvetica, sans-serif;
|
6 |
+
border-color: black;
|
7 |
+
margin: 20vh auto;
|
8 |
+
max-height: 60vh;
|
9 |
+
}
|
10 |
+
.comfy-group-manage-outer {
|
11 |
+
max-height: 60vh;
|
12 |
+
min-width: 500px;
|
13 |
+
display: flex;
|
14 |
+
flex-direction: column;
|
15 |
+
}
|
16 |
+
.comfy-group-manage-outer > header {
|
17 |
+
display: flex;
|
18 |
+
align-items: center;
|
19 |
+
gap: 10px;
|
20 |
+
justify-content: space-between;
|
21 |
+
background: var(--comfy-menu-bg);
|
22 |
+
padding: 15px 20px;
|
23 |
+
}
|
24 |
+
.comfy-group-manage-outer > header select {
|
25 |
+
background: var(--comfy-input-bg);
|
26 |
+
border: 1px solid var(--border-color);
|
27 |
+
color: var(--input-text);
|
28 |
+
padding: 5px 10px;
|
29 |
+
border-radius: 5px;
|
30 |
+
}
|
31 |
+
.comfy-group-manage h2 {
|
32 |
+
margin: 0;
|
33 |
+
font-weight: normal;
|
34 |
+
}
|
35 |
+
.comfy-group-manage main {
|
36 |
+
display: flex;
|
37 |
+
overflow: hidden;
|
38 |
+
}
|
39 |
+
.comfy-group-manage .drag-handle {
|
40 |
+
font-weight: bold;
|
41 |
+
}
|
42 |
+
.comfy-group-manage-list {
|
43 |
+
border-right: 1px solid var(--comfy-menu-bg);
|
44 |
+
}
|
45 |
+
.comfy-group-manage-list ul {
|
46 |
+
margin: 40px 0 0;
|
47 |
+
padding: 0;
|
48 |
+
list-style: none;
|
49 |
+
}
|
50 |
+
.comfy-group-manage-list-items {
|
51 |
+
max-height: calc(100% - 40px);
|
52 |
+
overflow-y: scroll;
|
53 |
+
overflow-x: hidden;
|
54 |
+
}
|
55 |
+
.comfy-group-manage-list li {
|
56 |
+
display: flex;
|
57 |
+
padding: 10px 20px 10px 10px;
|
58 |
+
cursor: pointer;
|
59 |
+
align-items: center;
|
60 |
+
gap: 5px;
|
61 |
+
}
|
62 |
+
.comfy-group-manage-list div {
|
63 |
+
display: flex;
|
64 |
+
flex-direction: column;
|
65 |
+
}
|
66 |
+
.comfy-group-manage-list li:not(.selected):hover div {
|
67 |
+
text-decoration: underline;
|
68 |
+
}
|
69 |
+
.comfy-group-manage-list li.selected {
|
70 |
+
background: var(--border-color);
|
71 |
+
}
|
72 |
+
.comfy-group-manage-list li span {
|
73 |
+
opacity: 0.7;
|
74 |
+
font-size: smaller;
|
75 |
+
}
|
76 |
+
.comfy-group-manage-node {
|
77 |
+
flex: auto;
|
78 |
+
background: var(--border-color);
|
79 |
+
display: flex;
|
80 |
+
flex-direction: column;
|
81 |
+
}
|
82 |
+
.comfy-group-manage-node > div {
|
83 |
+
overflow: auto;
|
84 |
+
}
|
85 |
+
.comfy-group-manage-node header {
|
86 |
+
display: flex;
|
87 |
+
background: var(--bg-color);
|
88 |
+
height: 40px;
|
89 |
+
}
|
90 |
+
.comfy-group-manage-node header a {
|
91 |
+
text-align: center;
|
92 |
+
flex: auto;
|
93 |
+
border-right: 1px solid var(--comfy-menu-bg);
|
94 |
+
border-bottom: 1px solid var(--comfy-menu-bg);
|
95 |
+
padding: 10px;
|
96 |
+
cursor: pointer;
|
97 |
+
font-size: 15px;
|
98 |
+
}
|
99 |
+
.comfy-group-manage-node header a:last-child {
|
100 |
+
border-right: none;
|
101 |
+
}
|
102 |
+
.comfy-group-manage-node header a:not(.active):hover {
|
103 |
+
text-decoration: underline;
|
104 |
+
}
|
105 |
+
.comfy-group-manage-node header a.active {
|
106 |
+
background: var(--border-color);
|
107 |
+
border-bottom: none;
|
108 |
+
}
|
109 |
+
.comfy-group-manage-node-page {
|
110 |
+
display: none;
|
111 |
+
overflow: auto;
|
112 |
+
}
|
113 |
+
.comfy-group-manage-node-page.active {
|
114 |
+
display: block;
|
115 |
+
}
|
116 |
+
.comfy-group-manage-node-page div {
|
117 |
+
padding: 10px;
|
118 |
+
display: flex;
|
119 |
+
align-items: center;
|
120 |
+
gap: 10px;
|
121 |
+
}
|
122 |
+
.comfy-group-manage-node-page input {
|
123 |
+
border: none;
|
124 |
+
color: var(--input-text);
|
125 |
+
background: var(--comfy-input-bg);
|
126 |
+
padding: 5px 10px;
|
127 |
+
}
|
128 |
+
.comfy-group-manage-node-page input[type="text"] {
|
129 |
+
flex: auto;
|
130 |
+
}
|
131 |
+
.comfy-group-manage-node-page label {
|
132 |
+
display: flex;
|
133 |
+
gap: 5px;
|
134 |
+
align-items: center;
|
135 |
+
}
|
136 |
+
.comfy-group-manage footer {
|
137 |
+
border-top: 1px solid var(--comfy-menu-bg);
|
138 |
+
padding: 10px;
|
139 |
+
display: flex;
|
140 |
+
gap: 10px;
|
141 |
+
}
|
142 |
+
.comfy-group-manage footer button {
|
143 |
+
font-size: 14px;
|
144 |
+
padding: 5px 10px;
|
145 |
+
border-radius: 0;
|
146 |
+
}
|
147 |
+
.comfy-group-manage footer button:first-child {
|
148 |
+
margin-right: auto;
|
149 |
+
}
|
ComfyUI/web/extensions/core/groupNodeManage.js
ADDED
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { $el, ComfyDialog } from "../../scripts/ui.js";
|
2 |
+
import { DraggableList } from "../../scripts/ui/draggableList.js";
|
3 |
+
import { addStylesheet } from "../../scripts/utils.js";
|
4 |
+
import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js";
|
5 |
+
|
6 |
+
addStylesheet(import.meta.url);
|
7 |
+
|
8 |
+
const ORDER = Symbol();
|
9 |
+
|
10 |
+
function merge(target, source) {
|
11 |
+
if (typeof target === "object" && typeof source === "object") {
|
12 |
+
for (const key in source) {
|
13 |
+
const sv = source[key];
|
14 |
+
if (typeof sv === "object") {
|
15 |
+
let tv = target[key];
|
16 |
+
if (!tv) tv = target[key] = {};
|
17 |
+
merge(tv, source[key]);
|
18 |
+
} else {
|
19 |
+
target[key] = sv;
|
20 |
+
}
|
21 |
+
}
|
22 |
+
}
|
23 |
+
|
24 |
+
return target;
|
25 |
+
}
|
26 |
+
|
27 |
+
export class ManageGroupDialog extends ComfyDialog {
|
28 |
+
/** @type { Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}> } */
|
29 |
+
tabs = {};
|
30 |
+
/** @type { number | null | undefined } */
|
31 |
+
selectedNodeIndex;
|
32 |
+
/** @type { keyof ManageGroupDialog["tabs"] } */
|
33 |
+
selectedTab = "Inputs";
|
34 |
+
/** @type { string | undefined } */
|
35 |
+
selectedGroup;
|
36 |
+
|
37 |
+
/** @type { Record<string, Record<string, Record<string, { name?: string | undefined, visible?: boolean | undefined }>>> } */
|
38 |
+
modifications = {};
|
39 |
+
|
40 |
+
get selectedNodeInnerIndex() {
|
41 |
+
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex;
|
42 |
+
}
|
43 |
+
|
44 |
+
constructor(app) {
|
45 |
+
super();
|
46 |
+
this.app = app;
|
47 |
+
this.element = $el("dialog.comfy-group-manage", {
|
48 |
+
parent: document.body,
|
49 |
+
});
|
50 |
+
}
|
51 |
+
|
52 |
+
changeTab(tab) {
|
53 |
+
this.tabs[this.selectedTab].tab.classList.remove("active");
|
54 |
+
this.tabs[this.selectedTab].page.classList.remove("active");
|
55 |
+
this.tabs[tab].tab.classList.add("active");
|
56 |
+
this.tabs[tab].page.classList.add("active");
|
57 |
+
this.selectedTab = tab;
|
58 |
+
}
|
59 |
+
|
60 |
+
changeNode(index, force) {
|
61 |
+
if (!force && this.selectedNodeIndex === index) return;
|
62 |
+
|
63 |
+
if (this.selectedNodeIndex != null) {
|
64 |
+
this.nodeItems[this.selectedNodeIndex].classList.remove("selected");
|
65 |
+
}
|
66 |
+
this.nodeItems[index].classList.add("selected");
|
67 |
+
this.selectedNodeIndex = index;
|
68 |
+
|
69 |
+
if (!this.buildInputsPage() && this.selectedTab === "Inputs") {
|
70 |
+
this.changeTab("Widgets");
|
71 |
+
}
|
72 |
+
if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") {
|
73 |
+
this.changeTab("Outputs");
|
74 |
+
}
|
75 |
+
if (!this.buildOutputsPage() && this.selectedTab === "Outputs") {
|
76 |
+
this.changeTab("Inputs");
|
77 |
+
}
|
78 |
+
|
79 |
+
this.changeTab(this.selectedTab);
|
80 |
+
}
|
81 |
+
|
82 |
+
getGroupData() {
|
83 |
+
this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
|
84 |
+
this.groupNodeDef = this.groupNodeType.nodeData;
|
85 |
+
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
|
86 |
+
}
|
87 |
+
|
88 |
+
changeGroup(group, reset = true) {
|
89 |
+
this.selectedGroup = group;
|
90 |
+
this.getGroupData();
|
91 |
+
|
92 |
+
const nodes = this.groupData.nodeData.nodes;
|
93 |
+
this.nodeItems = nodes.map((n, i) =>
|
94 |
+
$el(
|
95 |
+
"li.draggable-item",
|
96 |
+
{
|
97 |
+
dataset: {
|
98 |
+
nodeindex: n.index + "",
|
99 |
+
},
|
100 |
+
onclick: () => {
|
101 |
+
this.changeNode(i);
|
102 |
+
},
|
103 |
+
},
|
104 |
+
[
|
105 |
+
$el("span.drag-handle"),
|
106 |
+
$el(
|
107 |
+
"div",
|
108 |
+
{
|
109 |
+
textContent: n.title ?? n.type,
|
110 |
+
},
|
111 |
+
n.title
|
112 |
+
? $el("span", {
|
113 |
+
textContent: n.type,
|
114 |
+
})
|
115 |
+
: []
|
116 |
+
),
|
117 |
+
]
|
118 |
+
)
|
119 |
+
);
|
120 |
+
|
121 |
+
this.innerNodesList.replaceChildren(...this.nodeItems);
|
122 |
+
|
123 |
+
if (reset) {
|
124 |
+
this.selectedNodeIndex = null;
|
125 |
+
this.changeNode(0);
|
126 |
+
} else {
|
127 |
+
const items = this.draggable.getAllItems();
|
128 |
+
let index = items.findIndex(item => item.classList.contains("selected"));
|
129 |
+
if(index === -1) index = this.selectedNodeIndex;
|
130 |
+
this.changeNode(index, true);
|
131 |
+
}
|
132 |
+
|
133 |
+
const ordered = [...nodes];
|
134 |
+
this.draggable?.dispose();
|
135 |
+
this.draggable = new DraggableList(this.innerNodesList, "li");
|
136 |
+
this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => {
|
137 |
+
if (oldPosition === newPosition) return;
|
138 |
+
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
|
139 |
+
for (let i = 0; i < ordered.length; i++) {
|
140 |
+
this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i });
|
141 |
+
}
|
142 |
+
});
|
143 |
+
}
|
144 |
+
|
145 |
+
storeModification({ nodeIndex, section, prop, value }) {
|
146 |
+
const groupMod = (this.modifications[this.selectedGroup] ??= {});
|
147 |
+
const nodesMod = (groupMod.nodes ??= {});
|
148 |
+
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {});
|
149 |
+
const typeMod = (nodeMod[section] ??= {});
|
150 |
+
if (typeof value === "object") {
|
151 |
+
const objMod = (typeMod[prop] ??= {});
|
152 |
+
Object.assign(objMod, value);
|
153 |
+
} else {
|
154 |
+
typeMod[prop] = value;
|
155 |
+
}
|
156 |
+
}
|
157 |
+
|
158 |
+
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
159 |
+
if (value === placeholder) value = "";
|
160 |
+
|
161 |
+
const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop];
|
162 |
+
if (mods) {
|
163 |
+
if (mods.name != null) {
|
164 |
+
value = mods.name;
|
165 |
+
}
|
166 |
+
if (mods.visible != null) {
|
167 |
+
checked = mods.visible;
|
168 |
+
}
|
169 |
+
}
|
170 |
+
|
171 |
+
return $el("div", [
|
172 |
+
$el("input", {
|
173 |
+
value,
|
174 |
+
placeholder,
|
175 |
+
type: "text",
|
176 |
+
onchange: (e) => {
|
177 |
+
this.storeModification({ section, prop, value: { name: e.target.value } });
|
178 |
+
},
|
179 |
+
}),
|
180 |
+
$el("label", { textContent: "Visible" }, [
|
181 |
+
$el("input", {
|
182 |
+
type: "checkbox",
|
183 |
+
checked,
|
184 |
+
disabled: !checkable,
|
185 |
+
onchange: (e) => {
|
186 |
+
this.storeModification({ section, prop, value: { visible: !!e.target.checked } });
|
187 |
+
},
|
188 |
+
}),
|
189 |
+
]),
|
190 |
+
]);
|
191 |
+
}
|
192 |
+
|
193 |
+
buildWidgetsPage() {
|
194 |
+
const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
|
195 |
+
const items = Object.keys(widgets ?? {});
|
196 |
+
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
197 |
+
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
198 |
+
this.widgetsPage.replaceChildren(
|
199 |
+
...items.map((oldName) => {
|
200 |
+
return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false);
|
201 |
+
})
|
202 |
+
);
|
203 |
+
return !!items.length;
|
204 |
+
}
|
205 |
+
|
206 |
+
buildInputsPage() {
|
207 |
+
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex];
|
208 |
+
const items = Object.keys(inputs ?? {});
|
209 |
+
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
210 |
+
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
211 |
+
this.inputsPage.replaceChildren(
|
212 |
+
...items
|
213 |
+
.map((oldName) => {
|
214 |
+
let value = inputs[oldName];
|
215 |
+
if (!value) {
|
216 |
+
return;
|
217 |
+
}
|
218 |
+
|
219 |
+
return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false);
|
220 |
+
})
|
221 |
+
.filter(Boolean)
|
222 |
+
);
|
223 |
+
return !!items.length;
|
224 |
+
}
|
225 |
+
|
226 |
+
buildOutputsPage() {
|
227 |
+
const nodes = this.groupData.nodeData.nodes;
|
228 |
+
const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]);
|
229 |
+
const outputs = innerNodeDef?.output ?? [];
|
230 |
+
const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
|
231 |
+
|
232 |
+
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
233 |
+
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
|
234 |
+
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex];
|
235 |
+
const checkable = node.type !== "PrimitiveNode";
|
236 |
+
this.outputsPage.replaceChildren(
|
237 |
+
...outputs
|
238 |
+
.map((type, slot) => {
|
239 |
+
const groupOutputIndex = groupOutputs?.[slot];
|
240 |
+
const oldName = innerNodeDef.output_name?.[slot] ?? type;
|
241 |
+
let value = config?.[slot]?.name;
|
242 |
+
const visible = config?.[slot]?.visible || groupOutputIndex != null;
|
243 |
+
if (!value || value === oldName) {
|
244 |
+
value = "";
|
245 |
+
}
|
246 |
+
return this.getEditElement("output", slot, value, oldName, visible, checkable);
|
247 |
+
})
|
248 |
+
.filter(Boolean)
|
249 |
+
);
|
250 |
+
return !!outputs.length;
|
251 |
+
}
|
252 |
+
|
253 |
+
show(type) {
|
254 |
+
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b));
|
255 |
+
|
256 |
+
this.innerNodesList = $el("ul.comfy-group-manage-list-items");
|
257 |
+
this.widgetsPage = $el("section.comfy-group-manage-node-page");
|
258 |
+
this.inputsPage = $el("section.comfy-group-manage-node-page");
|
259 |
+
this.outputsPage = $el("section.comfy-group-manage-node-page");
|
260 |
+
const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]);
|
261 |
+
|
262 |
+
this.tabs = [
|
263 |
+
["Inputs", this.inputsPage],
|
264 |
+
["Widgets", this.widgetsPage],
|
265 |
+
["Outputs", this.outputsPage],
|
266 |
+
].reduce((p, [name, page]) => {
|
267 |
+
p[name] = {
|
268 |
+
tab: $el("a", {
|
269 |
+
onclick: () => {
|
270 |
+
this.changeTab(name);
|
271 |
+
},
|
272 |
+
textContent: name,
|
273 |
+
}),
|
274 |
+
page,
|
275 |
+
};
|
276 |
+
return p;
|
277 |
+
}, {});
|
278 |
+
|
279 |
+
const outer = $el("div.comfy-group-manage-outer", [
|
280 |
+
$el("header", [
|
281 |
+
$el("h2", "Group Nodes"),
|
282 |
+
$el(
|
283 |
+
"select",
|
284 |
+
{
|
285 |
+
onchange: (e) => {
|
286 |
+
this.changeGroup(e.target.value);
|
287 |
+
},
|
288 |
+
},
|
289 |
+
groupNodes.map((g) =>
|
290 |
+
$el("option", {
|
291 |
+
textContent: g,
|
292 |
+
selected: "workflow/" + g === type,
|
293 |
+
value: g,
|
294 |
+
})
|
295 |
+
)
|
296 |
+
),
|
297 |
+
]),
|
298 |
+
$el("main", [
|
299 |
+
$el("section.comfy-group-manage-list", this.innerNodesList),
|
300 |
+
$el("section.comfy-group-manage-node", [
|
301 |
+
$el(
|
302 |
+
"header",
|
303 |
+
Object.values(this.tabs).map((t) => t.tab)
|
304 |
+
),
|
305 |
+
pages,
|
306 |
+
]),
|
307 |
+
]),
|
308 |
+
$el("footer", [
|
309 |
+
$el(
|
310 |
+
"button.comfy-btn",
|
311 |
+
{
|
312 |
+
onclick: (e) => {
|
313 |
+
const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup);
|
314 |
+
if (node) {
|
315 |
+
alert("This group node is in use in the current workflow, please first remove these.");
|
316 |
+
return;
|
317 |
+
}
|
318 |
+
if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) {
|
319 |
+
delete app.graph.extra.groupNodes[this.selectedGroup];
|
320 |
+
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
|
321 |
+
}
|
322 |
+
this.show();
|
323 |
+
},
|
324 |
+
},
|
325 |
+
"Delete Group Node"
|
326 |
+
),
|
327 |
+
$el(
|
328 |
+
"button.comfy-btn",
|
329 |
+
{
|
330 |
+
onclick: async () => {
|
331 |
+
let nodesByType;
|
332 |
+
let recreateNodes = [];
|
333 |
+
const types = {};
|
334 |
+
for (const g in this.modifications) {
|
335 |
+
const type = app.graph.extra.groupNodes[g];
|
336 |
+
let config = (type.config ??= {});
|
337 |
+
|
338 |
+
let nodeMods = this.modifications[g]?.nodes;
|
339 |
+
if (nodeMods) {
|
340 |
+
const keys = Object.keys(nodeMods);
|
341 |
+
if (nodeMods[keys[0]][ORDER]) {
|
342 |
+
// If any node is reordered, they will all need sequencing
|
343 |
+
const orderedNodes = [];
|
344 |
+
const orderedMods = {};
|
345 |
+
const orderedConfig = {};
|
346 |
+
|
347 |
+
for (const n of keys) {
|
348 |
+
const order = nodeMods[n][ORDER].order;
|
349 |
+
orderedNodes[order] = type.nodes[+n];
|
350 |
+
orderedMods[order] = nodeMods[n];
|
351 |
+
orderedNodes[order].index = order;
|
352 |
+
}
|
353 |
+
|
354 |
+
// Rewrite links
|
355 |
+
for (const l of type.links) {
|
356 |
+
if (l[0] != null) l[0] = type.nodes[l[0]].index;
|
357 |
+
if (l[2] != null) l[2] = type.nodes[l[2]].index;
|
358 |
+
}
|
359 |
+
|
360 |
+
// Rewrite externals
|
361 |
+
if (type.external) {
|
362 |
+
for (const ext of type.external) {
|
363 |
+
ext[0] = type.nodes[ext[0]];
|
364 |
+
}
|
365 |
+
}
|
366 |
+
|
367 |
+
// Rewrite modifications
|
368 |
+
for (const id of keys) {
|
369 |
+
if (config[id]) {
|
370 |
+
orderedConfig[type.nodes[id].index] = config[id];
|
371 |
+
}
|
372 |
+
delete config[id];
|
373 |
+
}
|
374 |
+
|
375 |
+
type.nodes = orderedNodes;
|
376 |
+
nodeMods = orderedMods;
|
377 |
+
type.config = config = orderedConfig;
|
378 |
+
}
|
379 |
+
|
380 |
+
merge(config, nodeMods);
|
381 |
+
}
|
382 |
+
|
383 |
+
types[g] = type;
|
384 |
+
|
385 |
+
if (!nodesByType) {
|
386 |
+
nodesByType = app.graph._nodes.reduce((p, n) => {
|
387 |
+
p[n.type] ??= [];
|
388 |
+
p[n.type].push(n);
|
389 |
+
return p;
|
390 |
+
}, {});
|
391 |
+
}
|
392 |
+
|
393 |
+
const nodes = nodesByType["workflow/" + g];
|
394 |
+
if (nodes) recreateNodes.push(...nodes);
|
395 |
+
}
|
396 |
+
|
397 |
+
await GroupNodeConfig.registerFromWorkflow(types, {});
|
398 |
+
|
399 |
+
for (const node of recreateNodes) {
|
400 |
+
node.recreate();
|
401 |
+
}
|
402 |
+
|
403 |
+
this.modifications = {};
|
404 |
+
this.app.graph.setDirtyCanvas(true, true);
|
405 |
+
this.changeGroup(this.selectedGroup, false);
|
406 |
+
},
|
407 |
+
},
|
408 |
+
"Save"
|
409 |
+
),
|
410 |
+
$el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"),
|
411 |
+
]),
|
412 |
+
]);
|
413 |
+
|
414 |
+
this.element.replaceChildren(outer);
|
415 |
+
this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]);
|
416 |
+
this.element.showModal();
|
417 |
+
|
418 |
+
this.element.addEventListener("close", () => {
|
419 |
+
this.draggable?.dispose();
|
420 |
+
});
|
421 |
+
}
|
422 |
+
}
|
ComfyUI/web/extensions/core/groupOptions.js
ADDED
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {app} from "../../scripts/app.js";
|
2 |
+
|
3 |
+
function setNodeMode(node, mode) {
|
4 |
+
node.mode = mode;
|
5 |
+
node.graph.change();
|
6 |
+
}
|
7 |
+
|
8 |
+
function addNodesToGroup(group, nodes=[]) {
|
9 |
+
var x1, y1, x2, y2;
|
10 |
+
var nx1, ny1, nx2, ny2;
|
11 |
+
var node;
|
12 |
+
|
13 |
+
x1 = y1 = x2 = y2 = -1;
|
14 |
+
nx1 = ny1 = nx2 = ny2 = -1;
|
15 |
+
|
16 |
+
for (var n of [group._nodes, nodes]) {
|
17 |
+
for (var i in n) {
|
18 |
+
node = n[i]
|
19 |
+
|
20 |
+
nx1 = node.pos[0]
|
21 |
+
ny1 = node.pos[1]
|
22 |
+
nx2 = node.pos[0] + node.size[0]
|
23 |
+
ny2 = node.pos[1] + node.size[1]
|
24 |
+
|
25 |
+
if (node.type != "Reroute") {
|
26 |
+
ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
|
27 |
+
}
|
28 |
+
|
29 |
+
if (node.flags?.collapsed) {
|
30 |
+
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
|
31 |
+
|
32 |
+
if (node?._collapsed_width) {
|
33 |
+
nx2 = nx1 + Math.round(node._collapsed_width);
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
if (x1 == -1 || nx1 < x1) {
|
38 |
+
x1 = nx1;
|
39 |
+
}
|
40 |
+
|
41 |
+
if (y1 == -1 || ny1 < y1) {
|
42 |
+
y1 = ny1;
|
43 |
+
}
|
44 |
+
|
45 |
+
if (x2 == -1 || nx2 > x2) {
|
46 |
+
x2 = nx2;
|
47 |
+
}
|
48 |
+
|
49 |
+
if (y2 == -1 || ny2 > y2) {
|
50 |
+
y2 = ny2;
|
51 |
+
}
|
52 |
+
}
|
53 |
+
}
|
54 |
+
|
55 |
+
var padding = 10;
|
56 |
+
|
57 |
+
y1 = y1 - Math.round(group.font_size * 1.4);
|
58 |
+
|
59 |
+
group.pos = [x1 - padding, y1 - padding];
|
60 |
+
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
|
61 |
+
}
|
62 |
+
|
63 |
+
app.registerExtension({
|
64 |
+
name: "Comfy.GroupOptions",
|
65 |
+
setup() {
|
66 |
+
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
67 |
+
// graph_mouse
|
68 |
+
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
69 |
+
const options = orig.apply(this, arguments);
|
70 |
+
const group = this.graph.getGroupOnPos(this.graph_mouse[0], this.graph_mouse[1]);
|
71 |
+
if (!group) {
|
72 |
+
options.push({
|
73 |
+
content: "Add Group For Selected Nodes",
|
74 |
+
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
75 |
+
callback: () => {
|
76 |
+
var group = new LiteGraph.LGraphGroup();
|
77 |
+
addNodesToGroup(group, this.selected_nodes)
|
78 |
+
app.canvas.graph.add(group);
|
79 |
+
this.graph.change();
|
80 |
+
}
|
81 |
+
});
|
82 |
+
|
83 |
+
return options;
|
84 |
+
}
|
85 |
+
|
86 |
+
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
|
87 |
+
group.recomputeInsideNodes();
|
88 |
+
const nodesInGroup = group._nodes;
|
89 |
+
|
90 |
+
options.push({
|
91 |
+
content: "Add Selected Nodes To Group",
|
92 |
+
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
93 |
+
callback: () => {
|
94 |
+
addNodesToGroup(group, this.selected_nodes)
|
95 |
+
this.graph.change();
|
96 |
+
}
|
97 |
+
});
|
98 |
+
|
99 |
+
// No nodes in group, return default options
|
100 |
+
if (nodesInGroup.length === 0) {
|
101 |
+
return options;
|
102 |
+
} else {
|
103 |
+
// Add a separator between the default options and the group options
|
104 |
+
options.push(null);
|
105 |
+
}
|
106 |
+
|
107 |
+
// Check if all nodes are the same mode
|
108 |
+
let allNodesAreSameMode = true;
|
109 |
+
for (let i = 1; i < nodesInGroup.length; i++) {
|
110 |
+
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
|
111 |
+
allNodesAreSameMode = false;
|
112 |
+
break;
|
113 |
+
}
|
114 |
+
}
|
115 |
+
|
116 |
+
options.push({
|
117 |
+
content: "Fit Group To Nodes",
|
118 |
+
callback: () => {
|
119 |
+
addNodesToGroup(group)
|
120 |
+
this.graph.change();
|
121 |
+
}
|
122 |
+
});
|
123 |
+
|
124 |
+
options.push({
|
125 |
+
content: "Select Nodes",
|
126 |
+
callback: () => {
|
127 |
+
this.selectNodes(nodesInGroup);
|
128 |
+
this.graph.change();
|
129 |
+
this.canvas.focus();
|
130 |
+
}
|
131 |
+
});
|
132 |
+
|
133 |
+
// Modes
|
134 |
+
// 0: Always
|
135 |
+
// 1: On Event
|
136 |
+
// 2: Never
|
137 |
+
// 3: On Trigger
|
138 |
+
// 4: Bypass
|
139 |
+
// If all nodes are the same mode, add a menu option to change the mode
|
140 |
+
if (allNodesAreSameMode) {
|
141 |
+
const mode = nodesInGroup[0].mode;
|
142 |
+
switch (mode) {
|
143 |
+
case 0:
|
144 |
+
// All nodes are always, option to disable, and bypass
|
145 |
+
options.push({
|
146 |
+
content: "Set Group Nodes to Never",
|
147 |
+
callback: () => {
|
148 |
+
for (const node of nodesInGroup) {
|
149 |
+
setNodeMode(node, 2);
|
150 |
+
}
|
151 |
+
}
|
152 |
+
});
|
153 |
+
options.push({
|
154 |
+
content: "Bypass Group Nodes",
|
155 |
+
callback: () => {
|
156 |
+
for (const node of nodesInGroup) {
|
157 |
+
setNodeMode(node, 4);
|
158 |
+
}
|
159 |
+
}
|
160 |
+
});
|
161 |
+
break;
|
162 |
+
case 2:
|
163 |
+
// All nodes are never, option to enable, and bypass
|
164 |
+
options.push({
|
165 |
+
content: "Set Group Nodes to Always",
|
166 |
+
callback: () => {
|
167 |
+
for (const node of nodesInGroup) {
|
168 |
+
setNodeMode(node, 0);
|
169 |
+
}
|
170 |
+
}
|
171 |
+
});
|
172 |
+
options.push({
|
173 |
+
content: "Bypass Group Nodes",
|
174 |
+
callback: () => {
|
175 |
+
for (const node of nodesInGroup) {
|
176 |
+
setNodeMode(node, 4);
|
177 |
+
}
|
178 |
+
}
|
179 |
+
});
|
180 |
+
break;
|
181 |
+
case 4:
|
182 |
+
// All nodes are bypass, option to enable, and disable
|
183 |
+
options.push({
|
184 |
+
content: "Set Group Nodes to Always",
|
185 |
+
callback: () => {
|
186 |
+
for (const node of nodesInGroup) {
|
187 |
+
setNodeMode(node, 0);
|
188 |
+
}
|
189 |
+
}
|
190 |
+
});
|
191 |
+
options.push({
|
192 |
+
content: "Set Group Nodes to Never",
|
193 |
+
callback: () => {
|
194 |
+
for (const node of nodesInGroup) {
|
195 |
+
setNodeMode(node, 2);
|
196 |
+
}
|
197 |
+
}
|
198 |
+
});
|
199 |
+
break;
|
200 |
+
default:
|
201 |
+
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
|
202 |
+
options.push({
|
203 |
+
content: "Set Group Nodes to Always",
|
204 |
+
callback: () => {
|
205 |
+
for (const node of nodesInGroup) {
|
206 |
+
setNodeMode(node, 0);
|
207 |
+
}
|
208 |
+
}
|
209 |
+
});
|
210 |
+
options.push({
|
211 |
+
content: "Set Group Nodes to Never",
|
212 |
+
callback: () => {
|
213 |
+
for (const node of nodesInGroup) {
|
214 |
+
setNodeMode(node, 2);
|
215 |
+
}
|
216 |
+
}
|
217 |
+
});
|
218 |
+
options.push({
|
219 |
+
content: "Bypass Group Nodes",
|
220 |
+
callback: () => {
|
221 |
+
for (const node of nodesInGroup) {
|
222 |
+
setNodeMode(node, 4);
|
223 |
+
}
|
224 |
+
}
|
225 |
+
});
|
226 |
+
break;
|
227 |
+
}
|
228 |
+
} else {
|
229 |
+
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
|
230 |
+
options.push({
|
231 |
+
content: "Set Group Nodes to Always",
|
232 |
+
callback: () => {
|
233 |
+
for (const node of nodesInGroup) {
|
234 |
+
setNodeMode(node, 0);
|
235 |
+
}
|
236 |
+
}
|
237 |
+
});
|
238 |
+
options.push({
|
239 |
+
content: "Set Group Nodes to Never",
|
240 |
+
callback: () => {
|
241 |
+
for (const node of nodesInGroup) {
|
242 |
+
setNodeMode(node, 2);
|
243 |
+
}
|
244 |
+
}
|
245 |
+
});
|
246 |
+
options.push({
|
247 |
+
content: "Bypass Group Nodes",
|
248 |
+
callback: () => {
|
249 |
+
for (const node of nodesInGroup) {
|
250 |
+
setNodeMode(node, 4);
|
251 |
+
}
|
252 |
+
}
|
253 |
+
});
|
254 |
+
}
|
255 |
+
|
256 |
+
return options
|
257 |
+
}
|
258 |
+
}
|
259 |
+
});
|
ComfyUI/web/extensions/core/invertMenuScrolling.js
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
|
3 |
+
// Inverts the scrolling of context menus
|
4 |
+
|
5 |
+
const id = "Comfy.InvertMenuScrolling";
|
6 |
+
app.registerExtension({
|
7 |
+
name: id,
|
8 |
+
init() {
|
9 |
+
const ctxMenu = LiteGraph.ContextMenu;
|
10 |
+
const replace = () => {
|
11 |
+
LiteGraph.ContextMenu = function (values, options) {
|
12 |
+
options = options || {};
|
13 |
+
if (options.scroll_speed) {
|
14 |
+
options.scroll_speed *= -1;
|
15 |
+
} else {
|
16 |
+
options.scroll_speed = -0.1;
|
17 |
+
}
|
18 |
+
return ctxMenu.call(this, values, options);
|
19 |
+
};
|
20 |
+
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
21 |
+
};
|
22 |
+
app.ui.settings.addSetting({
|
23 |
+
id,
|
24 |
+
name: "Invert Menu Scrolling",
|
25 |
+
type: "boolean",
|
26 |
+
defaultValue: false,
|
27 |
+
onChange(value) {
|
28 |
+
if (value) {
|
29 |
+
replace();
|
30 |
+
} else {
|
31 |
+
LiteGraph.ContextMenu = ctxMenu;
|
32 |
+
}
|
33 |
+
},
|
34 |
+
});
|
35 |
+
},
|
36 |
+
});
|
ComfyUI/web/extensions/core/keybinds.js
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {app} from "../../scripts/app.js";
|
2 |
+
|
3 |
+
app.registerExtension({
|
4 |
+
name: "Comfy.Keybinds",
|
5 |
+
init() {
|
6 |
+
const keybindListener = function (event) {
|
7 |
+
const modifierPressed = event.ctrlKey || event.metaKey;
|
8 |
+
|
9 |
+
// Queue prompt using ctrl or command + enter
|
10 |
+
if (modifierPressed && event.key === "Enter") {
|
11 |
+
app.queuePrompt(event.shiftKey ? -1 : 0).then();
|
12 |
+
return;
|
13 |
+
}
|
14 |
+
|
15 |
+
const target = event.composedPath()[0];
|
16 |
+
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
|
17 |
+
return;
|
18 |
+
}
|
19 |
+
|
20 |
+
const modifierKeyIdMap = {
|
21 |
+
s: "#comfy-save-button",
|
22 |
+
o: "#comfy-file-input",
|
23 |
+
Backspace: "#comfy-clear-button",
|
24 |
+
d: "#comfy-load-default-button",
|
25 |
+
};
|
26 |
+
|
27 |
+
const modifierKeybindId = modifierKeyIdMap[event.key];
|
28 |
+
if (modifierPressed && modifierKeybindId) {
|
29 |
+
event.preventDefault();
|
30 |
+
|
31 |
+
const elem = document.querySelector(modifierKeybindId);
|
32 |
+
elem.click();
|
33 |
+
return;
|
34 |
+
}
|
35 |
+
|
36 |
+
// Finished Handling all modifier keybinds, now handle the rest
|
37 |
+
if (event.ctrlKey || event.altKey || event.metaKey) {
|
38 |
+
return;
|
39 |
+
}
|
40 |
+
|
41 |
+
// Close out of modals using escape
|
42 |
+
if (event.key === "Escape") {
|
43 |
+
const modals = document.querySelectorAll(".comfy-modal");
|
44 |
+
const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none");
|
45 |
+
if (modal) {
|
46 |
+
modal.style.display = "none";
|
47 |
+
}
|
48 |
+
|
49 |
+
[...document.querySelectorAll("dialog")].forEach(d => {
|
50 |
+
d.close();
|
51 |
+
});
|
52 |
+
}
|
53 |
+
|
54 |
+
const keyIdMap = {
|
55 |
+
q: "#comfy-view-queue-button",
|
56 |
+
h: "#comfy-view-history-button",
|
57 |
+
r: "#comfy-refresh-button",
|
58 |
+
};
|
59 |
+
|
60 |
+
const buttonId = keyIdMap[event.key];
|
61 |
+
if (buttonId) {
|
62 |
+
const button = document.querySelector(buttonId);
|
63 |
+
button.click();
|
64 |
+
}
|
65 |
+
}
|
66 |
+
|
67 |
+
window.addEventListener("keydown", keybindListener, true);
|
68 |
+
}
|
69 |
+
});
|
ComfyUI/web/extensions/core/linkRenderMode.js
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
|
3 |
+
const id = "Comfy.LinkRenderMode";
|
4 |
+
const ext = {
|
5 |
+
name: id,
|
6 |
+
async setup(app) {
|
7 |
+
app.ui.settings.addSetting({
|
8 |
+
id,
|
9 |
+
name: "Link Render Mode",
|
10 |
+
defaultValue: 2,
|
11 |
+
type: "combo",
|
12 |
+
options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
|
13 |
+
value: i,
|
14 |
+
text: m,
|
15 |
+
selected: i == app.canvas.links_render_mode,
|
16 |
+
})),
|
17 |
+
onChange(value) {
|
18 |
+
app.canvas.links_render_mode = +value;
|
19 |
+
app.graph.setDirtyCanvas(true);
|
20 |
+
},
|
21 |
+
});
|
22 |
+
},
|
23 |
+
};
|
24 |
+
|
25 |
+
app.registerExtension(ext);
|
ComfyUI/web/extensions/core/maskeditor.js
ADDED
@@ -0,0 +1,967 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
3 |
+
import { ComfyApp } from "../../scripts/app.js";
|
4 |
+
import { api } from "../../scripts/api.js"
|
5 |
+
import { ClipspaceDialog } from "./clipspace.js";
|
6 |
+
|
7 |
+
// Helper function to convert a data URL to a Blob object
|
8 |
+
function dataURLToBlob(dataURL) {
|
9 |
+
const parts = dataURL.split(';base64,');
|
10 |
+
const contentType = parts[0].split(':')[1];
|
11 |
+
const byteString = atob(parts[1]);
|
12 |
+
const arrayBuffer = new ArrayBuffer(byteString.length);
|
13 |
+
const uint8Array = new Uint8Array(arrayBuffer);
|
14 |
+
for (let i = 0; i < byteString.length; i++) {
|
15 |
+
uint8Array[i] = byteString.charCodeAt(i);
|
16 |
+
}
|
17 |
+
return new Blob([arrayBuffer], { type: contentType });
|
18 |
+
}
|
19 |
+
|
20 |
+
function loadedImageToBlob(image) {
|
21 |
+
const canvas = document.createElement('canvas');
|
22 |
+
|
23 |
+
canvas.width = image.width;
|
24 |
+
canvas.height = image.height;
|
25 |
+
|
26 |
+
const ctx = canvas.getContext('2d');
|
27 |
+
|
28 |
+
ctx.drawImage(image, 0, 0);
|
29 |
+
|
30 |
+
const dataURL = canvas.toDataURL('image/png', 1);
|
31 |
+
const blob = dataURLToBlob(dataURL);
|
32 |
+
|
33 |
+
return blob;
|
34 |
+
}
|
35 |
+
|
36 |
+
function loadImage(imagePath) {
|
37 |
+
return new Promise((resolve, reject) => {
|
38 |
+
const image = new Image();
|
39 |
+
|
40 |
+
image.onload = function() {
|
41 |
+
resolve(image);
|
42 |
+
};
|
43 |
+
|
44 |
+
image.src = imagePath;
|
45 |
+
});
|
46 |
+
}
|
47 |
+
|
48 |
+
async function uploadMask(filepath, formData) {
|
49 |
+
await api.fetchApi('/upload/mask', {
|
50 |
+
method: 'POST',
|
51 |
+
body: formData
|
52 |
+
}).then(response => {}).catch(error => {
|
53 |
+
console.error('Error:', error);
|
54 |
+
});
|
55 |
+
|
56 |
+
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
|
57 |
+
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = api.apiURL("/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam() + app.getRandParam());
|
58 |
+
|
59 |
+
if(ComfyApp.clipspace.images)
|
60 |
+
ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;
|
61 |
+
|
62 |
+
ClipspaceDialog.invalidatePreview();
|
63 |
+
}
|
64 |
+
|
65 |
+
function prepare_mask(image, maskCanvas, maskCtx, maskColor) {
|
66 |
+
// paste mask data into alpha channel
|
67 |
+
maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height);
|
68 |
+
const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);
|
69 |
+
|
70 |
+
// invert mask
|
71 |
+
for (let i = 0; i < maskData.data.length; i += 4) {
|
72 |
+
if(maskData.data[i+3] == 255)
|
73 |
+
maskData.data[i+3] = 0;
|
74 |
+
else
|
75 |
+
maskData.data[i+3] = 255;
|
76 |
+
|
77 |
+
maskData.data[i] = maskColor.r;
|
78 |
+
maskData.data[i+1] = maskColor.g;
|
79 |
+
maskData.data[i+2] = maskColor.b;
|
80 |
+
}
|
81 |
+
|
82 |
+
maskCtx.globalCompositeOperation = 'source-over';
|
83 |
+
maskCtx.putImageData(maskData, 0, 0);
|
84 |
+
}
|
85 |
+
|
86 |
+
class MaskEditorDialog extends ComfyDialog {
|
87 |
+
static instance = null;
|
88 |
+
|
89 |
+
static getInstance() {
|
90 |
+
if(!MaskEditorDialog.instance) {
|
91 |
+
MaskEditorDialog.instance = new MaskEditorDialog(app);
|
92 |
+
}
|
93 |
+
|
94 |
+
return MaskEditorDialog.instance;
|
95 |
+
}
|
96 |
+
|
97 |
+
is_layout_created = false;
|
98 |
+
|
99 |
+
constructor() {
|
100 |
+
super();
|
101 |
+
this.element = $el("div.comfy-modal", { parent: document.body },
|
102 |
+
[ $el("div.comfy-modal-content",
|
103 |
+
[...this.createButtons()]),
|
104 |
+
]);
|
105 |
+
}
|
106 |
+
|
107 |
+
createButtons() {
|
108 |
+
return [];
|
109 |
+
}
|
110 |
+
|
111 |
+
createButton(name, callback) {
|
112 |
+
var button = document.createElement("button");
|
113 |
+
button.style.pointerEvents = "auto";
|
114 |
+
button.innerText = name;
|
115 |
+
button.addEventListener("click", callback);
|
116 |
+
return button;
|
117 |
+
}
|
118 |
+
|
119 |
+
createLeftButton(name, callback) {
|
120 |
+
var button = this.createButton(name, callback);
|
121 |
+
button.style.cssFloat = "left";
|
122 |
+
button.style.marginRight = "4px";
|
123 |
+
return button;
|
124 |
+
}
|
125 |
+
|
126 |
+
createRightButton(name, callback) {
|
127 |
+
var button = this.createButton(name, callback);
|
128 |
+
button.style.cssFloat = "right";
|
129 |
+
button.style.marginLeft = "4px";
|
130 |
+
return button;
|
131 |
+
}
|
132 |
+
|
133 |
+
createLeftSlider(self, name, callback) {
|
134 |
+
const divElement = document.createElement('div');
|
135 |
+
divElement.id = "maskeditor-slider";
|
136 |
+
divElement.style.cssFloat = "left";
|
137 |
+
divElement.style.fontFamily = "sans-serif";
|
138 |
+
divElement.style.marginRight = "4px";
|
139 |
+
divElement.style.color = "var(--input-text)";
|
140 |
+
divElement.style.backgroundColor = "var(--comfy-input-bg)";
|
141 |
+
divElement.style.borderRadius = "8px";
|
142 |
+
divElement.style.borderColor = "var(--border-color)";
|
143 |
+
divElement.style.borderStyle = "solid";
|
144 |
+
divElement.style.fontSize = "15px";
|
145 |
+
divElement.style.height = "21px";
|
146 |
+
divElement.style.padding = "1px 6px";
|
147 |
+
divElement.style.display = "flex";
|
148 |
+
divElement.style.position = "relative";
|
149 |
+
divElement.style.top = "2px";
|
150 |
+
divElement.style.pointerEvents = "auto";
|
151 |
+
self.brush_slider_input = document.createElement('input');
|
152 |
+
self.brush_slider_input.setAttribute('type', 'range');
|
153 |
+
self.brush_slider_input.setAttribute('min', '1');
|
154 |
+
self.brush_slider_input.setAttribute('max', '100');
|
155 |
+
self.brush_slider_input.setAttribute('value', '10');
|
156 |
+
const labelElement = document.createElement("label");
|
157 |
+
labelElement.textContent = name;
|
158 |
+
|
159 |
+
divElement.appendChild(labelElement);
|
160 |
+
divElement.appendChild(self.brush_slider_input);
|
161 |
+
|
162 |
+
self.brush_slider_input.addEventListener("change", callback);
|
163 |
+
|
164 |
+
return divElement;
|
165 |
+
}
|
166 |
+
|
167 |
+
createOpacitySlider(self, name, callback) {
|
168 |
+
const divElement = document.createElement('div');
|
169 |
+
divElement.id = "maskeditor-opacity-slider";
|
170 |
+
divElement.style.cssFloat = "left";
|
171 |
+
divElement.style.fontFamily = "sans-serif";
|
172 |
+
divElement.style.marginRight = "4px";
|
173 |
+
divElement.style.color = "var(--input-text)";
|
174 |
+
divElement.style.backgroundColor = "var(--comfy-input-bg)";
|
175 |
+
divElement.style.borderRadius = "8px";
|
176 |
+
divElement.style.borderColor = "var(--border-color)";
|
177 |
+
divElement.style.borderStyle = "solid";
|
178 |
+
divElement.style.fontSize = "15px";
|
179 |
+
divElement.style.height = "21px";
|
180 |
+
divElement.style.padding = "1px 6px";
|
181 |
+
divElement.style.display = "flex";
|
182 |
+
divElement.style.position = "relative";
|
183 |
+
divElement.style.top = "2px";
|
184 |
+
divElement.style.pointerEvents = "auto";
|
185 |
+
self.opacity_slider_input = document.createElement('input');
|
186 |
+
self.opacity_slider_input.setAttribute('type', 'range');
|
187 |
+
self.opacity_slider_input.setAttribute('min', '0.1');
|
188 |
+
self.opacity_slider_input.setAttribute('max', '1.0');
|
189 |
+
self.opacity_slider_input.setAttribute('step', '0.01')
|
190 |
+
self.opacity_slider_input.setAttribute('value', '0.7');
|
191 |
+
const labelElement = document.createElement("label");
|
192 |
+
labelElement.textContent = name;
|
193 |
+
|
194 |
+
divElement.appendChild(labelElement);
|
195 |
+
divElement.appendChild(self.opacity_slider_input);
|
196 |
+
|
197 |
+
self.opacity_slider_input.addEventListener("input", callback);
|
198 |
+
|
199 |
+
return divElement;
|
200 |
+
}
|
201 |
+
|
202 |
+
setlayout(imgCanvas, maskCanvas) {
|
203 |
+
const self = this;
|
204 |
+
|
205 |
+
// If it is specified as relative, using it only as a hidden placeholder for padding is recommended
|
206 |
+
// to prevent anomalies where it exceeds a certain size and goes outside of the window.
|
207 |
+
var bottom_panel = document.createElement("div");
|
208 |
+
bottom_panel.style.position = "absolute";
|
209 |
+
bottom_panel.style.bottom = "0px";
|
210 |
+
bottom_panel.style.left = "20px";
|
211 |
+
bottom_panel.style.right = "20px";
|
212 |
+
bottom_panel.style.height = "50px";
|
213 |
+
bottom_panel.style.pointerEvents = "none";
|
214 |
+
|
215 |
+
var brush = document.createElement("div");
|
216 |
+
brush.id = "brush";
|
217 |
+
brush.style.backgroundColor = "transparent";
|
218 |
+
brush.style.outline = "1px dashed black";
|
219 |
+
brush.style.boxShadow = "0 0 0 1px white";
|
220 |
+
brush.style.borderRadius = "50%";
|
221 |
+
brush.style.MozBorderRadius = "50%";
|
222 |
+
brush.style.WebkitBorderRadius = "50%";
|
223 |
+
brush.style.position = "absolute";
|
224 |
+
brush.style.zIndex = 8889;
|
225 |
+
brush.style.pointerEvents = "none";
|
226 |
+
this.brush = brush;
|
227 |
+
this.element.appendChild(imgCanvas);
|
228 |
+
this.element.appendChild(maskCanvas);
|
229 |
+
this.element.appendChild(bottom_panel);
|
230 |
+
document.body.appendChild(brush);
|
231 |
+
|
232 |
+
var clearButton = this.createLeftButton("Clear", () => {
|
233 |
+
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
|
234 |
+
});
|
235 |
+
|
236 |
+
this.brush_size_slider = this.createLeftSlider(self, "Thickness", (event) => {
|
237 |
+
self.brush_size = event.target.value;
|
238 |
+
self.updateBrushPreview(self, null, null);
|
239 |
+
});
|
240 |
+
|
241 |
+
this.brush_opacity_slider = this.createOpacitySlider(self, "Opacity", (event) => {
|
242 |
+
self.brush_opacity = event.target.value;
|
243 |
+
if (self.brush_color_mode !== "negative") {
|
244 |
+
self.maskCanvas.style.opacity = self.brush_opacity;
|
245 |
+
}
|
246 |
+
});
|
247 |
+
|
248 |
+
this.colorButton = this.createLeftButton(this.getColorButtonText(), () => {
|
249 |
+
if (self.brush_color_mode === "black") {
|
250 |
+
self.brush_color_mode = "white";
|
251 |
+
}
|
252 |
+
else if (self.brush_color_mode === "white") {
|
253 |
+
self.brush_color_mode = "negative";
|
254 |
+
}
|
255 |
+
else {
|
256 |
+
self.brush_color_mode = "black";
|
257 |
+
}
|
258 |
+
|
259 |
+
self.updateWhenBrushColorModeChanged();
|
260 |
+
});
|
261 |
+
|
262 |
+
var cancelButton = this.createRightButton("Cancel", () => {
|
263 |
+
document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
|
264 |
+
document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
|
265 |
+
self.close();
|
266 |
+
});
|
267 |
+
|
268 |
+
this.saveButton = this.createRightButton("Save", () => {
|
269 |
+
document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
|
270 |
+
document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
|
271 |
+
self.save();
|
272 |
+
});
|
273 |
+
|
274 |
+
this.element.appendChild(imgCanvas);
|
275 |
+
this.element.appendChild(maskCanvas);
|
276 |
+
this.element.appendChild(bottom_panel);
|
277 |
+
|
278 |
+
bottom_panel.appendChild(clearButton);
|
279 |
+
bottom_panel.appendChild(this.saveButton);
|
280 |
+
bottom_panel.appendChild(cancelButton);
|
281 |
+
bottom_panel.appendChild(this.brush_size_slider);
|
282 |
+
bottom_panel.appendChild(this.brush_opacity_slider);
|
283 |
+
bottom_panel.appendChild(this.colorButton);
|
284 |
+
|
285 |
+
imgCanvas.style.position = "absolute";
|
286 |
+
maskCanvas.style.position = "absolute";
|
287 |
+
|
288 |
+
imgCanvas.style.top = "200";
|
289 |
+
imgCanvas.style.left = "0";
|
290 |
+
|
291 |
+
maskCanvas.style.top = imgCanvas.style.top;
|
292 |
+
maskCanvas.style.left = imgCanvas.style.left;
|
293 |
+
|
294 |
+
const maskCanvasStyle = this.getMaskCanvasStyle();
|
295 |
+
maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
|
296 |
+
maskCanvas.style.opacity = maskCanvasStyle.opacity;
|
297 |
+
}
|
298 |
+
|
299 |
+
async show() {
|
300 |
+
this.zoom_ratio = 1.0;
|
301 |
+
this.pan_x = 0;
|
302 |
+
this.pan_y = 0;
|
303 |
+
|
304 |
+
if(!this.is_layout_created) {
|
305 |
+
// layout
|
306 |
+
const imgCanvas = document.createElement('canvas');
|
307 |
+
const maskCanvas = document.createElement('canvas');
|
308 |
+
|
309 |
+
imgCanvas.id = "imageCanvas";
|
310 |
+
maskCanvas.id = "maskCanvas";
|
311 |
+
|
312 |
+
this.setlayout(imgCanvas, maskCanvas);
|
313 |
+
|
314 |
+
// prepare content
|
315 |
+
this.imgCanvas = imgCanvas;
|
316 |
+
this.maskCanvas = maskCanvas;
|
317 |
+
this.maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true });
|
318 |
+
|
319 |
+
this.setEventHandler(maskCanvas);
|
320 |
+
|
321 |
+
this.is_layout_created = true;
|
322 |
+
|
323 |
+
// replacement of onClose hook since close is not real close
|
324 |
+
const self = this;
|
325 |
+
const observer = new MutationObserver(function(mutations) {
|
326 |
+
mutations.forEach(function(mutation) {
|
327 |
+
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
328 |
+
if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') {
|
329 |
+
document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp);
|
330 |
+
self.brush.style.display = "none";
|
331 |
+
ComfyApp.onClipspaceEditorClosed();
|
332 |
+
}
|
333 |
+
|
334 |
+
self.last_display_style = self.element.style.display;
|
335 |
+
}
|
336 |
+
});
|
337 |
+
});
|
338 |
+
|
339 |
+
const config = { attributes: true };
|
340 |
+
observer.observe(this.element, config);
|
341 |
+
}
|
342 |
+
|
343 |
+
// The keydown event needs to be reconfigured when closing the dialog as it gets removed.
|
344 |
+
document.addEventListener('keydown', MaskEditorDialog.handleKeyDown);
|
345 |
+
|
346 |
+
if(ComfyApp.clipspace_return_node) {
|
347 |
+
this.saveButton.innerText = "Save to node";
|
348 |
+
}
|
349 |
+
else {
|
350 |
+
this.saveButton.innerText = "Save";
|
351 |
+
}
|
352 |
+
this.saveButton.disabled = false;
|
353 |
+
|
354 |
+
this.element.style.display = "block";
|
355 |
+
this.element.style.width = "85%";
|
356 |
+
this.element.style.margin = "0 7.5%";
|
357 |
+
this.element.style.height = "100vh";
|
358 |
+
this.element.style.top = "50%";
|
359 |
+
this.element.style.left = "42%";
|
360 |
+
this.element.style.zIndex = 8888; // NOTE: alert dialog must be high priority.
|
361 |
+
|
362 |
+
await this.setImages(this.imgCanvas);
|
363 |
+
|
364 |
+
this.is_visible = true;
|
365 |
+
}
|
366 |
+
|
367 |
+
isOpened() {
|
368 |
+
return this.element.style.display == "block";
|
369 |
+
}
|
370 |
+
|
371 |
+
invalidateCanvas(orig_image, mask_image) {
|
372 |
+
this.imgCanvas.width = orig_image.width;
|
373 |
+
this.imgCanvas.height = orig_image.height;
|
374 |
+
|
375 |
+
this.maskCanvas.width = orig_image.width;
|
376 |
+
this.maskCanvas.height = orig_image.height;
|
377 |
+
|
378 |
+
let imgCtx = this.imgCanvas.getContext('2d', {willReadFrequently: true });
|
379 |
+
let maskCtx = this.maskCanvas.getContext('2d', {willReadFrequently: true });
|
380 |
+
|
381 |
+
imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height);
|
382 |
+
prepare_mask(mask_image, this.maskCanvas, maskCtx, this.getMaskColor());
|
383 |
+
}
|
384 |
+
|
385 |
+
async setImages(imgCanvas) {
|
386 |
+
let self = this;
|
387 |
+
|
388 |
+
const imgCtx = imgCanvas.getContext('2d', {willReadFrequently: true });
|
389 |
+
const maskCtx = this.maskCtx;
|
390 |
+
const maskCanvas = this.maskCanvas;
|
391 |
+
|
392 |
+
imgCtx.clearRect(0,0,this.imgCanvas.width,this.imgCanvas.height);
|
393 |
+
maskCtx.clearRect(0,0,this.maskCanvas.width,this.maskCanvas.height);
|
394 |
+
|
395 |
+
// image load
|
396 |
+
const filepath = ComfyApp.clipspace.images;
|
397 |
+
|
398 |
+
const alpha_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src)
|
399 |
+
alpha_url.searchParams.delete('channel');
|
400 |
+
alpha_url.searchParams.delete('preview');
|
401 |
+
alpha_url.searchParams.set('channel', 'a');
|
402 |
+
let mask_image = await loadImage(alpha_url);
|
403 |
+
|
404 |
+
// original image load
|
405 |
+
const rgb_url = new URL(ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src);
|
406 |
+
rgb_url.searchParams.delete('channel');
|
407 |
+
rgb_url.searchParams.set('channel', 'rgb');
|
408 |
+
this.image = new Image();
|
409 |
+
this.image.onload = function() {
|
410 |
+
maskCanvas.width = self.image.width;
|
411 |
+
maskCanvas.height = self.image.height;
|
412 |
+
|
413 |
+
self.invalidateCanvas(self.image, mask_image);
|
414 |
+
self.initializeCanvasPanZoom();
|
415 |
+
};
|
416 |
+
this.image.src = rgb_url;
|
417 |
+
}
|
418 |
+
|
419 |
+
initializeCanvasPanZoom() {
|
420 |
+
// set initialize
|
421 |
+
let drawWidth = this.image.width;
|
422 |
+
let drawHeight = this.image.height;
|
423 |
+
|
424 |
+
let width = this.element.clientWidth;
|
425 |
+
let height = this.element.clientHeight;
|
426 |
+
|
427 |
+
if (this.image.width > width) {
|
428 |
+
drawWidth = width;
|
429 |
+
drawHeight = (drawWidth / this.image.width) * this.image.height;
|
430 |
+
}
|
431 |
+
|
432 |
+
if (drawHeight > height) {
|
433 |
+
drawHeight = height;
|
434 |
+
drawWidth = (drawHeight / this.image.height) * this.image.width;
|
435 |
+
}
|
436 |
+
|
437 |
+
this.zoom_ratio = drawWidth/this.image.width;
|
438 |
+
|
439 |
+
const canvasX = (width - drawWidth) / 2;
|
440 |
+
const canvasY = (height - drawHeight) / 2;
|
441 |
+
this.pan_x = canvasX;
|
442 |
+
this.pan_y = canvasY;
|
443 |
+
|
444 |
+
this.invalidatePanZoom();
|
445 |
+
}
|
446 |
+
|
447 |
+
|
448 |
+
invalidatePanZoom() {
|
449 |
+
let raw_width = this.image.width * this.zoom_ratio;
|
450 |
+
let raw_height = this.image.height * this.zoom_ratio;
|
451 |
+
|
452 |
+
if(this.pan_x + raw_width < 10) {
|
453 |
+
this.pan_x = 10 - raw_width;
|
454 |
+
}
|
455 |
+
|
456 |
+
if(this.pan_y + raw_height < 10) {
|
457 |
+
this.pan_y = 10 - raw_height;
|
458 |
+
}
|
459 |
+
|
460 |
+
let width = `${raw_width}px`;
|
461 |
+
let height = `${raw_height}px`;
|
462 |
+
|
463 |
+
let left = `${this.pan_x}px`;
|
464 |
+
let top = `${this.pan_y}px`;
|
465 |
+
|
466 |
+
this.maskCanvas.style.width = width;
|
467 |
+
this.maskCanvas.style.height = height;
|
468 |
+
this.maskCanvas.style.left = left;
|
469 |
+
this.maskCanvas.style.top = top;
|
470 |
+
|
471 |
+
this.imgCanvas.style.width = width;
|
472 |
+
this.imgCanvas.style.height = height;
|
473 |
+
this.imgCanvas.style.left = left;
|
474 |
+
this.imgCanvas.style.top = top;
|
475 |
+
}
|
476 |
+
|
477 |
+
|
478 |
+
setEventHandler(maskCanvas) {
|
479 |
+
const self = this;
|
480 |
+
|
481 |
+
if(!this.handler_registered) {
|
482 |
+
maskCanvas.addEventListener("contextmenu", (event) => {
|
483 |
+
event.preventDefault();
|
484 |
+
});
|
485 |
+
|
486 |
+
this.element.addEventListener('wheel', (event) => this.handleWheelEvent(self,event));
|
487 |
+
this.element.addEventListener('pointermove', (event) => this.pointMoveEvent(self,event));
|
488 |
+
this.element.addEventListener('touchmove', (event) => this.pointMoveEvent(self,event));
|
489 |
+
|
490 |
+
this.element.addEventListener('dragstart', (event) => {
|
491 |
+
if(event.ctrlKey) {
|
492 |
+
event.preventDefault();
|
493 |
+
}
|
494 |
+
});
|
495 |
+
|
496 |
+
maskCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event));
|
497 |
+
maskCanvas.addEventListener('pointermove', (event) => this.draw_move(self,event));
|
498 |
+
maskCanvas.addEventListener('touchmove', (event) => this.draw_move(self,event));
|
499 |
+
maskCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; });
|
500 |
+
maskCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; });
|
501 |
+
|
502 |
+
document.addEventListener('pointerup', MaskEditorDialog.handlePointerUp);
|
503 |
+
|
504 |
+
this.handler_registered = true;
|
505 |
+
}
|
506 |
+
}
|
507 |
+
|
508 |
+
getMaskCanvasStyle() {
|
509 |
+
if (this.brush_color_mode === "negative") {
|
510 |
+
return {
|
511 |
+
mixBlendMode: "difference",
|
512 |
+
opacity: "1",
|
513 |
+
};
|
514 |
+
}
|
515 |
+
else {
|
516 |
+
return {
|
517 |
+
mixBlendMode: "initial",
|
518 |
+
opacity: this.brush_opacity,
|
519 |
+
};
|
520 |
+
}
|
521 |
+
}
|
522 |
+
|
523 |
+
getMaskColor() {
|
524 |
+
if (this.brush_color_mode === "black") {
|
525 |
+
return { r: 0, g: 0, b: 0 };
|
526 |
+
}
|
527 |
+
if (this.brush_color_mode === "white") {
|
528 |
+
return { r: 255, g: 255, b: 255 };
|
529 |
+
}
|
530 |
+
if (this.brush_color_mode === "negative") {
|
531 |
+
// negative effect only works with white color
|
532 |
+
return { r: 255, g: 255, b: 255 };
|
533 |
+
}
|
534 |
+
|
535 |
+
return { r: 0, g: 0, b: 0 };
|
536 |
+
}
|
537 |
+
|
538 |
+
getMaskFillStyle() {
|
539 |
+
const maskColor = this.getMaskColor();
|
540 |
+
|
541 |
+
return "rgb(" + maskColor.r + "," + maskColor.g + "," + maskColor.b + ")";
|
542 |
+
}
|
543 |
+
|
544 |
+
getColorButtonText() {
|
545 |
+
let colorCaption = "unknown";
|
546 |
+
|
547 |
+
if (this.brush_color_mode === "black") {
|
548 |
+
colorCaption = "black";
|
549 |
+
}
|
550 |
+
else if (this.brush_color_mode === "white") {
|
551 |
+
colorCaption = "white";
|
552 |
+
}
|
553 |
+
else if (this.brush_color_mode === "negative") {
|
554 |
+
colorCaption = "negative";
|
555 |
+
}
|
556 |
+
|
557 |
+
return "Color: " + colorCaption;
|
558 |
+
}
|
559 |
+
|
560 |
+
updateWhenBrushColorModeChanged() {
|
561 |
+
this.colorButton.innerText = this.getColorButtonText();
|
562 |
+
|
563 |
+
// update mask canvas css styles
|
564 |
+
|
565 |
+
const maskCanvasStyle = this.getMaskCanvasStyle();
|
566 |
+
this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
|
567 |
+
this.maskCanvas.style.opacity = maskCanvasStyle.opacity;
|
568 |
+
|
569 |
+
// update mask canvas rgb colors
|
570 |
+
|
571 |
+
const maskColor = this.getMaskColor();
|
572 |
+
|
573 |
+
const maskData = this.maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
574 |
+
|
575 |
+
for (let i = 0; i < maskData.data.length; i += 4) {
|
576 |
+
maskData.data[i] = maskColor.r;
|
577 |
+
maskData.data[i+1] = maskColor.g;
|
578 |
+
maskData.data[i+2] = maskColor.b;
|
579 |
+
}
|
580 |
+
|
581 |
+
this.maskCtx.putImageData(maskData, 0, 0);
|
582 |
+
}
|
583 |
+
|
584 |
+
brush_opacity = 0.7;
|
585 |
+
brush_size = 10;
|
586 |
+
brush_color_mode = "black";
|
587 |
+
drawing_mode = false;
|
588 |
+
lastx = -1;
|
589 |
+
lasty = -1;
|
590 |
+
lasttime = 0;
|
591 |
+
|
592 |
+
static handleKeyDown(event) {
|
593 |
+
const self = MaskEditorDialog.instance;
|
594 |
+
if (event.key === ']') {
|
595 |
+
self.brush_size = Math.min(self.brush_size+2, 100);
|
596 |
+
self.brush_slider_input.value = self.brush_size;
|
597 |
+
} else if (event.key === '[') {
|
598 |
+
self.brush_size = Math.max(self.brush_size-2, 1);
|
599 |
+
self.brush_slider_input.value = self.brush_size;
|
600 |
+
} else if(event.key === 'Enter') {
|
601 |
+
self.save();
|
602 |
+
}
|
603 |
+
|
604 |
+
self.updateBrushPreview(self);
|
605 |
+
}
|
606 |
+
|
607 |
+
static handlePointerUp(event) {
|
608 |
+
event.preventDefault();
|
609 |
+
|
610 |
+
this.mousedown_x = null;
|
611 |
+
this.mousedown_y = null;
|
612 |
+
|
613 |
+
MaskEditorDialog.instance.drawing_mode = false;
|
614 |
+
}
|
615 |
+
|
616 |
+
updateBrushPreview(self) {
|
617 |
+
const brush = self.brush;
|
618 |
+
|
619 |
+
var centerX = self.cursorX;
|
620 |
+
var centerY = self.cursorY;
|
621 |
+
|
622 |
+
brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px";
|
623 |
+
brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px";
|
624 |
+
brush.style.left = (centerX - self.brush_size * this.zoom_ratio) + "px";
|
625 |
+
brush.style.top = (centerY - self.brush_size * this.zoom_ratio) + "px";
|
626 |
+
}
|
627 |
+
|
628 |
+
handleWheelEvent(self, event) {
|
629 |
+
event.preventDefault();
|
630 |
+
|
631 |
+
if(event.ctrlKey) {
|
632 |
+
// zoom canvas
|
633 |
+
if(event.deltaY < 0) {
|
634 |
+
this.zoom_ratio = Math.min(10.0, this.zoom_ratio+0.2);
|
635 |
+
}
|
636 |
+
else {
|
637 |
+
this.zoom_ratio = Math.max(0.2, this.zoom_ratio-0.2);
|
638 |
+
}
|
639 |
+
|
640 |
+
this.invalidatePanZoom();
|
641 |
+
}
|
642 |
+
else {
|
643 |
+
// adjust brush size
|
644 |
+
if(event.deltaY < 0)
|
645 |
+
this.brush_size = Math.min(this.brush_size+2, 100);
|
646 |
+
else
|
647 |
+
this.brush_size = Math.max(this.brush_size-2, 1);
|
648 |
+
|
649 |
+
this.brush_slider_input.value = this.brush_size;
|
650 |
+
|
651 |
+
this.updateBrushPreview(this);
|
652 |
+
}
|
653 |
+
}
|
654 |
+
|
655 |
+
pointMoveEvent(self, event) {
|
656 |
+
this.cursorX = event.pageX;
|
657 |
+
this.cursorY = event.pageY;
|
658 |
+
|
659 |
+
self.updateBrushPreview(self);
|
660 |
+
|
661 |
+
if(event.ctrlKey) {
|
662 |
+
event.preventDefault();
|
663 |
+
self.pan_move(self, event);
|
664 |
+
}
|
665 |
+
|
666 |
+
let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;
|
667 |
+
|
668 |
+
if(event.shiftKey && left_button_down) {
|
669 |
+
self.drawing_mode = false;
|
670 |
+
|
671 |
+
const y = event.clientY;
|
672 |
+
let delta = (self.zoom_lasty - y)*0.005;
|
673 |
+
self.zoom_ratio = Math.max(Math.min(10.0, self.last_zoom_ratio - delta), 0.2);
|
674 |
+
|
675 |
+
this.invalidatePanZoom();
|
676 |
+
return;
|
677 |
+
}
|
678 |
+
}
|
679 |
+
|
680 |
+
pan_move(self, event) {
|
681 |
+
if(event.buttons == 1) {
|
682 |
+
if(this.mousedown_x) {
|
683 |
+
let deltaX = this.mousedown_x - event.clientX;
|
684 |
+
let deltaY = this.mousedown_y - event.clientY;
|
685 |
+
|
686 |
+
self.pan_x = this.mousedown_pan_x - deltaX;
|
687 |
+
self.pan_y = this.mousedown_pan_y - deltaY;
|
688 |
+
|
689 |
+
self.invalidatePanZoom();
|
690 |
+
}
|
691 |
+
}
|
692 |
+
}
|
693 |
+
|
694 |
+
draw_move(self, event) {
|
695 |
+
if(event.ctrlKey || event.shiftKey) {
|
696 |
+
return;
|
697 |
+
}
|
698 |
+
|
699 |
+
event.preventDefault();
|
700 |
+
|
701 |
+
this.cursorX = event.pageX;
|
702 |
+
this.cursorY = event.pageY;
|
703 |
+
|
704 |
+
self.updateBrushPreview(self);
|
705 |
+
|
706 |
+
let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;
|
707 |
+
let right_button_down = [2, 5, 32].includes(event.buttons);
|
708 |
+
|
709 |
+
if (!event.altKey && left_button_down) {
|
710 |
+
var diff = performance.now() - self.lasttime;
|
711 |
+
|
712 |
+
const maskRect = self.maskCanvas.getBoundingClientRect();
|
713 |
+
|
714 |
+
var x = event.offsetX;
|
715 |
+
var y = event.offsetY
|
716 |
+
|
717 |
+
if(event.offsetX == null) {
|
718 |
+
x = event.targetTouches[0].clientX - maskRect.left;
|
719 |
+
}
|
720 |
+
|
721 |
+
if(event.offsetY == null) {
|
722 |
+
y = event.targetTouches[0].clientY - maskRect.top;
|
723 |
+
}
|
724 |
+
|
725 |
+
x /= self.zoom_ratio;
|
726 |
+
y /= self.zoom_ratio;
|
727 |
+
|
728 |
+
var brush_size = this.brush_size;
|
729 |
+
if(event instanceof PointerEvent && event.pointerType == 'pen') {
|
730 |
+
brush_size *= event.pressure;
|
731 |
+
this.last_pressure = event.pressure;
|
732 |
+
}
|
733 |
+
else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
|
734 |
+
// The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents.
|
735 |
+
brush_size *= this.last_pressure;
|
736 |
+
}
|
737 |
+
else {
|
738 |
+
brush_size = this.brush_size;
|
739 |
+
}
|
740 |
+
|
741 |
+
if(diff > 20 && !this.drawing_mode)
|
742 |
+
requestAnimationFrame(() => {
|
743 |
+
self.maskCtx.beginPath();
|
744 |
+
self.maskCtx.fillStyle = this.getMaskFillStyle();
|
745 |
+
self.maskCtx.globalCompositeOperation = "source-over";
|
746 |
+
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
747 |
+
self.maskCtx.fill();
|
748 |
+
self.lastx = x;
|
749 |
+
self.lasty = y;
|
750 |
+
});
|
751 |
+
else
|
752 |
+
requestAnimationFrame(() => {
|
753 |
+
self.maskCtx.beginPath();
|
754 |
+
self.maskCtx.fillStyle = this.getMaskFillStyle();
|
755 |
+
self.maskCtx.globalCompositeOperation = "source-over";
|
756 |
+
|
757 |
+
var dx = x - self.lastx;
|
758 |
+
var dy = y - self.lasty;
|
759 |
+
|
760 |
+
var distance = Math.sqrt(dx * dx + dy * dy);
|
761 |
+
var directionX = dx / distance;
|
762 |
+
var directionY = dy / distance;
|
763 |
+
|
764 |
+
for (var i = 0; i < distance; i+=5) {
|
765 |
+
var px = self.lastx + (directionX * i);
|
766 |
+
var py = self.lasty + (directionY * i);
|
767 |
+
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
|
768 |
+
self.maskCtx.fill();
|
769 |
+
}
|
770 |
+
self.lastx = x;
|
771 |
+
self.lasty = y;
|
772 |
+
});
|
773 |
+
|
774 |
+
self.lasttime = performance.now();
|
775 |
+
}
|
776 |
+
else if((event.altKey && left_button_down) || right_button_down) {
|
777 |
+
const maskRect = self.maskCanvas.getBoundingClientRect();
|
778 |
+
const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
|
779 |
+
const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
|
780 |
+
|
781 |
+
var brush_size = this.brush_size;
|
782 |
+
if(event instanceof PointerEvent && event.pointerType == 'pen') {
|
783 |
+
brush_size *= event.pressure;
|
784 |
+
this.last_pressure = event.pressure;
|
785 |
+
}
|
786 |
+
else if(window.TouchEvent && event instanceof TouchEvent && diff < 20){
|
787 |
+
brush_size *= this.last_pressure;
|
788 |
+
}
|
789 |
+
else {
|
790 |
+
brush_size = this.brush_size;
|
791 |
+
}
|
792 |
+
|
793 |
+
if(diff > 20 && !drawing_mode) // cannot tracking drawing_mode for touch event
|
794 |
+
requestAnimationFrame(() => {
|
795 |
+
self.maskCtx.beginPath();
|
796 |
+
self.maskCtx.globalCompositeOperation = "destination-out";
|
797 |
+
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
798 |
+
self.maskCtx.fill();
|
799 |
+
self.lastx = x;
|
800 |
+
self.lasty = y;
|
801 |
+
});
|
802 |
+
else
|
803 |
+
requestAnimationFrame(() => {
|
804 |
+
self.maskCtx.beginPath();
|
805 |
+
self.maskCtx.globalCompositeOperation = "destination-out";
|
806 |
+
|
807 |
+
var dx = x - self.lastx;
|
808 |
+
var dy = y - self.lasty;
|
809 |
+
|
810 |
+
var distance = Math.sqrt(dx * dx + dy * dy);
|
811 |
+
var directionX = dx / distance;
|
812 |
+
var directionY = dy / distance;
|
813 |
+
|
814 |
+
for (var i = 0; i < distance; i+=5) {
|
815 |
+
var px = self.lastx + (directionX * i);
|
816 |
+
var py = self.lasty + (directionY * i);
|
817 |
+
self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
|
818 |
+
self.maskCtx.fill();
|
819 |
+
}
|
820 |
+
self.lastx = x;
|
821 |
+
self.lasty = y;
|
822 |
+
});
|
823 |
+
|
824 |
+
self.lasttime = performance.now();
|
825 |
+
}
|
826 |
+
}
|
827 |
+
|
828 |
+
handlePointerDown(self, event) {
|
829 |
+
if(event.ctrlKey) {
|
830 |
+
if (event.buttons == 1) {
|
831 |
+
this.mousedown_x = event.clientX;
|
832 |
+
this.mousedown_y = event.clientY;
|
833 |
+
|
834 |
+
this.mousedown_pan_x = this.pan_x;
|
835 |
+
this.mousedown_pan_y = this.pan_y;
|
836 |
+
}
|
837 |
+
return;
|
838 |
+
}
|
839 |
+
|
840 |
+
var brush_size = this.brush_size;
|
841 |
+
if(event instanceof PointerEvent && event.pointerType == 'pen') {
|
842 |
+
brush_size *= event.pressure;
|
843 |
+
this.last_pressure = event.pressure;
|
844 |
+
}
|
845 |
+
|
846 |
+
if ([0, 2, 5].includes(event.button)) {
|
847 |
+
self.drawing_mode = true;
|
848 |
+
|
849 |
+
event.preventDefault();
|
850 |
+
|
851 |
+
if(event.shiftKey) {
|
852 |
+
self.zoom_lasty = event.clientY;
|
853 |
+
self.last_zoom_ratio = self.zoom_ratio;
|
854 |
+
return;
|
855 |
+
}
|
856 |
+
|
857 |
+
const maskRect = self.maskCanvas.getBoundingClientRect();
|
858 |
+
const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
|
859 |
+
const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
|
860 |
+
|
861 |
+
self.maskCtx.beginPath();
|
862 |
+
if (!event.altKey && event.button == 0) {
|
863 |
+
self.maskCtx.fillStyle = this.getMaskFillStyle();
|
864 |
+
self.maskCtx.globalCompositeOperation = "source-over";
|
865 |
+
} else {
|
866 |
+
self.maskCtx.globalCompositeOperation = "destination-out";
|
867 |
+
}
|
868 |
+
self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
869 |
+
self.maskCtx.fill();
|
870 |
+
self.lastx = x;
|
871 |
+
self.lasty = y;
|
872 |
+
self.lasttime = performance.now();
|
873 |
+
}
|
874 |
+
}
|
875 |
+
|
876 |
+
async save() {
|
877 |
+
const backupCanvas = document.createElement('canvas');
|
878 |
+
const backupCtx = backupCanvas.getContext('2d', {willReadFrequently:true});
|
879 |
+
backupCanvas.width = this.image.width;
|
880 |
+
backupCanvas.height = this.image.height;
|
881 |
+
|
882 |
+
backupCtx.clearRect(0,0, backupCanvas.width, backupCanvas.height);
|
883 |
+
backupCtx.drawImage(this.maskCanvas,
|
884 |
+
0, 0, this.maskCanvas.width, this.maskCanvas.height,
|
885 |
+
0, 0, backupCanvas.width, backupCanvas.height);
|
886 |
+
|
887 |
+
// paste mask data into alpha channel
|
888 |
+
const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height);
|
889 |
+
|
890 |
+
// refine mask image
|
891 |
+
for (let i = 0; i < backupData.data.length; i += 4) {
|
892 |
+
if(backupData.data[i+3] == 255)
|
893 |
+
backupData.data[i+3] = 0;
|
894 |
+
else
|
895 |
+
backupData.data[i+3] = 255;
|
896 |
+
|
897 |
+
backupData.data[i] = 0;
|
898 |
+
backupData.data[i+1] = 0;
|
899 |
+
backupData.data[i+2] = 0;
|
900 |
+
}
|
901 |
+
|
902 |
+
backupCtx.globalCompositeOperation = 'source-over';
|
903 |
+
backupCtx.putImageData(backupData, 0, 0);
|
904 |
+
|
905 |
+
const formData = new FormData();
|
906 |
+
const filename = "clipspace-mask-" + performance.now() + ".png";
|
907 |
+
|
908 |
+
const item =
|
909 |
+
{
|
910 |
+
"filename": filename,
|
911 |
+
"subfolder": "clipspace",
|
912 |
+
"type": "input",
|
913 |
+
};
|
914 |
+
|
915 |
+
if(ComfyApp.clipspace.images)
|
916 |
+
ComfyApp.clipspace.images[0] = item;
|
917 |
+
|
918 |
+
if(ComfyApp.clipspace.widgets) {
|
919 |
+
const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
|
920 |
+
|
921 |
+
if(index >= 0)
|
922 |
+
ComfyApp.clipspace.widgets[index].value = item;
|
923 |
+
}
|
924 |
+
|
925 |
+
const dataURL = backupCanvas.toDataURL();
|
926 |
+
const blob = dataURLToBlob(dataURL);
|
927 |
+
|
928 |
+
let original_url = new URL(this.image.src);
|
929 |
+
|
930 |
+
const original_ref = { filename: original_url.searchParams.get('filename') };
|
931 |
+
|
932 |
+
let original_subfolder = original_url.searchParams.get("subfolder");
|
933 |
+
if(original_subfolder)
|
934 |
+
original_ref.subfolder = original_subfolder;
|
935 |
+
|
936 |
+
let original_type = original_url.searchParams.get("type");
|
937 |
+
if(original_type)
|
938 |
+
original_ref.type = original_type;
|
939 |
+
|
940 |
+
formData.append('image', blob, filename);
|
941 |
+
formData.append('original_ref', JSON.stringify(original_ref));
|
942 |
+
formData.append('type', "input");
|
943 |
+
formData.append('subfolder', "clipspace");
|
944 |
+
|
945 |
+
this.saveButton.innerText = "Saving...";
|
946 |
+
this.saveButton.disabled = true;
|
947 |
+
await uploadMask(item, formData);
|
948 |
+
ComfyApp.onClipspaceEditorSave();
|
949 |
+
this.close();
|
950 |
+
}
|
951 |
+
}
|
952 |
+
|
953 |
+
app.registerExtension({
|
954 |
+
name: "Comfy.MaskEditor",
|
955 |
+
init(app) {
|
956 |
+
ComfyApp.open_maskeditor =
|
957 |
+
function () {
|
958 |
+
const dlg = MaskEditorDialog.getInstance();
|
959 |
+
if(!dlg.isOpened()) {
|
960 |
+
dlg.show();
|
961 |
+
}
|
962 |
+
};
|
963 |
+
|
964 |
+
const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0
|
965 |
+
ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor);
|
966 |
+
}
|
967 |
+
});
|
ComfyUI/web/extensions/core/nodeTemplates.js
ADDED
@@ -0,0 +1,412 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { api } from "../../scripts/api.js";
|
3 |
+
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
4 |
+
import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js";
|
5 |
+
|
6 |
+
// Adds the ability to save and add multiple nodes as a template
|
7 |
+
// To save:
|
8 |
+
// Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes)
|
9 |
+
// Right click the canvas
|
10 |
+
// Save Node Template -> give it a name
|
11 |
+
//
|
12 |
+
// To add:
|
13 |
+
// Right click the canvas
|
14 |
+
// Node templates -> click the one to add
|
15 |
+
//
|
16 |
+
// To delete/rename:
|
17 |
+
// Right click the canvas
|
18 |
+
// Node templates -> Manage
|
19 |
+
//
|
20 |
+
// To rearrange:
|
21 |
+
// Open the manage dialog and Drag and drop elements using the "Name:" label as handle
|
22 |
+
|
23 |
+
const id = "Comfy.NodeTemplates";
|
24 |
+
const file = "comfy.templates.json";
|
25 |
+
|
26 |
+
class ManageTemplates extends ComfyDialog {
|
27 |
+
constructor() {
|
28 |
+
super();
|
29 |
+
this.load().then((v) => {
|
30 |
+
this.templates = v;
|
31 |
+
});
|
32 |
+
|
33 |
+
this.element.classList.add("comfy-manage-templates");
|
34 |
+
this.draggedEl = null;
|
35 |
+
this.saveVisualCue = null;
|
36 |
+
this.emptyImg = new Image();
|
37 |
+
this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
|
38 |
+
|
39 |
+
this.importInput = $el("input", {
|
40 |
+
type: "file",
|
41 |
+
accept: ".json",
|
42 |
+
multiple: true,
|
43 |
+
style: { display: "none" },
|
44 |
+
parent: document.body,
|
45 |
+
onchange: () => this.importAll(),
|
46 |
+
});
|
47 |
+
}
|
48 |
+
|
49 |
+
createButtons() {
|
50 |
+
const btns = super.createButtons();
|
51 |
+
btns[0].textContent = "Close";
|
52 |
+
btns[0].onclick = (e) => {
|
53 |
+
clearTimeout(this.saveVisualCue);
|
54 |
+
this.close();
|
55 |
+
};
|
56 |
+
btns.unshift(
|
57 |
+
$el("button", {
|
58 |
+
type: "button",
|
59 |
+
textContent: "Export",
|
60 |
+
onclick: () => this.exportAll(),
|
61 |
+
})
|
62 |
+
);
|
63 |
+
btns.unshift(
|
64 |
+
$el("button", {
|
65 |
+
type: "button",
|
66 |
+
textContent: "Import",
|
67 |
+
onclick: () => {
|
68 |
+
this.importInput.click();
|
69 |
+
},
|
70 |
+
})
|
71 |
+
);
|
72 |
+
return btns;
|
73 |
+
}
|
74 |
+
|
75 |
+
async load() {
|
76 |
+
let templates = [];
|
77 |
+
if (app.storageLocation === "server") {
|
78 |
+
if (app.isNewUserSession) {
|
79 |
+
// New user so migrate existing templates
|
80 |
+
const json = localStorage.getItem(id);
|
81 |
+
if (json) {
|
82 |
+
templates = JSON.parse(json);
|
83 |
+
}
|
84 |
+
await api.storeUserData(file, json, { stringify: false });
|
85 |
+
} else {
|
86 |
+
const res = await api.getUserData(file);
|
87 |
+
if (res.status === 200) {
|
88 |
+
try {
|
89 |
+
templates = await res.json();
|
90 |
+
} catch (error) {
|
91 |
+
}
|
92 |
+
} else if (res.status !== 404) {
|
93 |
+
console.error(res.status + " " + res.statusText);
|
94 |
+
}
|
95 |
+
}
|
96 |
+
} else {
|
97 |
+
const json = localStorage.getItem(id);
|
98 |
+
if (json) {
|
99 |
+
templates = JSON.parse(json);
|
100 |
+
}
|
101 |
+
}
|
102 |
+
|
103 |
+
return templates ?? [];
|
104 |
+
}
|
105 |
+
|
106 |
+
async store() {
|
107 |
+
if(app.storageLocation === "server") {
|
108 |
+
const templates = JSON.stringify(this.templates, undefined, 4);
|
109 |
+
localStorage.setItem(id, templates); // Backwards compatibility
|
110 |
+
try {
|
111 |
+
await api.storeUserData(file, templates, { stringify: false });
|
112 |
+
} catch (error) {
|
113 |
+
console.error(error);
|
114 |
+
alert(error.message);
|
115 |
+
}
|
116 |
+
} else {
|
117 |
+
localStorage.setItem(id, JSON.stringify(this.templates));
|
118 |
+
}
|
119 |
+
}
|
120 |
+
|
121 |
+
async importAll() {
|
122 |
+
for (const file of this.importInput.files) {
|
123 |
+
if (file.type === "application/json" || file.name.endsWith(".json")) {
|
124 |
+
const reader = new FileReader();
|
125 |
+
reader.onload = async () => {
|
126 |
+
const importFile = JSON.parse(reader.result);
|
127 |
+
if (importFile?.templates) {
|
128 |
+
for (const template of importFile.templates) {
|
129 |
+
if (template?.name && template?.data) {
|
130 |
+
this.templates.push(template);
|
131 |
+
}
|
132 |
+
}
|
133 |
+
await this.store();
|
134 |
+
}
|
135 |
+
};
|
136 |
+
await reader.readAsText(file);
|
137 |
+
}
|
138 |
+
}
|
139 |
+
|
140 |
+
this.importInput.value = null;
|
141 |
+
|
142 |
+
this.close();
|
143 |
+
}
|
144 |
+
|
145 |
+
exportAll() {
|
146 |
+
if (this.templates.length == 0) {
|
147 |
+
alert("No templates to export.");
|
148 |
+
return;
|
149 |
+
}
|
150 |
+
|
151 |
+
const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
|
152 |
+
const blob = new Blob([json], { type: "application/json" });
|
153 |
+
const url = URL.createObjectURL(blob);
|
154 |
+
const a = $el("a", {
|
155 |
+
href: url,
|
156 |
+
download: "node_templates.json",
|
157 |
+
style: { display: "none" },
|
158 |
+
parent: document.body,
|
159 |
+
});
|
160 |
+
a.click();
|
161 |
+
setTimeout(function () {
|
162 |
+
a.remove();
|
163 |
+
window.URL.revokeObjectURL(url);
|
164 |
+
}, 0);
|
165 |
+
}
|
166 |
+
|
167 |
+
show() {
|
168 |
+
// Show list of template names + delete button
|
169 |
+
super.show(
|
170 |
+
$el(
|
171 |
+
"div",
|
172 |
+
{},
|
173 |
+
this.templates.flatMap((t,i) => {
|
174 |
+
let nameInput;
|
175 |
+
return [
|
176 |
+
$el(
|
177 |
+
"div",
|
178 |
+
{
|
179 |
+
dataset: { id: i },
|
180 |
+
className: "tempateManagerRow",
|
181 |
+
style: {
|
182 |
+
display: "grid",
|
183 |
+
gridTemplateColumns: "1fr auto",
|
184 |
+
border: "1px dashed transparent",
|
185 |
+
gap: "5px",
|
186 |
+
backgroundColor: "var(--comfy-menu-bg)"
|
187 |
+
},
|
188 |
+
ondragstart: (e) => {
|
189 |
+
this.draggedEl = e.currentTarget;
|
190 |
+
e.currentTarget.style.opacity = "0.6";
|
191 |
+
e.currentTarget.style.border = "1px dashed yellow";
|
192 |
+
e.dataTransfer.effectAllowed = 'move';
|
193 |
+
e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
|
194 |
+
},
|
195 |
+
ondragend: (e) => {
|
196 |
+
e.target.style.opacity = "1";
|
197 |
+
e.currentTarget.style.border = "1px dashed transparent";
|
198 |
+
e.currentTarget.removeAttribute("draggable");
|
199 |
+
|
200 |
+
// rearrange the elements
|
201 |
+
this.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => {
|
202 |
+
var prev_i = el.dataset.id;
|
203 |
+
|
204 |
+
if ( el == this.draggedEl && prev_i != i ) {
|
205 |
+
this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]);
|
206 |
+
}
|
207 |
+
el.dataset.id = i;
|
208 |
+
});
|
209 |
+
this.store();
|
210 |
+
},
|
211 |
+
ondragover: (e) => {
|
212 |
+
e.preventDefault();
|
213 |
+
if ( e.currentTarget == this.draggedEl )
|
214 |
+
return;
|
215 |
+
|
216 |
+
let rect = e.currentTarget.getBoundingClientRect();
|
217 |
+
if (e.clientY > rect.top + rect.height / 2) {
|
218 |
+
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling);
|
219 |
+
} else {
|
220 |
+
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget);
|
221 |
+
}
|
222 |
+
}
|
223 |
+
},
|
224 |
+
[
|
225 |
+
$el(
|
226 |
+
"label",
|
227 |
+
{
|
228 |
+
textContent: "Name: ",
|
229 |
+
style: {
|
230 |
+
cursor: "grab",
|
231 |
+
},
|
232 |
+
onmousedown: (e) => {
|
233 |
+
// enable dragging only from the label
|
234 |
+
if (e.target.localName == 'label')
|
235 |
+
e.currentTarget.parentNode.draggable = 'true';
|
236 |
+
}
|
237 |
+
},
|
238 |
+
[
|
239 |
+
$el("input", {
|
240 |
+
value: t.name,
|
241 |
+
dataset: { name: t.name },
|
242 |
+
style: {
|
243 |
+
transitionProperty: 'background-color',
|
244 |
+
transitionDuration: '0s',
|
245 |
+
},
|
246 |
+
onchange: (e) => {
|
247 |
+
clearTimeout(this.saveVisualCue);
|
248 |
+
var el = e.target;
|
249 |
+
var row = el.parentNode.parentNode;
|
250 |
+
this.templates[row.dataset.id].name = el.value.trim() || 'untitled';
|
251 |
+
this.store();
|
252 |
+
el.style.backgroundColor = 'rgb(40, 95, 40)';
|
253 |
+
el.style.transitionDuration = '0s';
|
254 |
+
this.saveVisualCue = setTimeout(function () {
|
255 |
+
el.style.transitionDuration = '.7s';
|
256 |
+
el.style.backgroundColor = 'var(--comfy-input-bg)';
|
257 |
+
}, 15);
|
258 |
+
},
|
259 |
+
onkeypress: (e) => {
|
260 |
+
var el = e.target;
|
261 |
+
clearTimeout(this.saveVisualCue);
|
262 |
+
el.style.transitionDuration = '0s';
|
263 |
+
el.style.backgroundColor = 'var(--comfy-input-bg)';
|
264 |
+
},
|
265 |
+
$: (el) => (nameInput = el),
|
266 |
+
})
|
267 |
+
]
|
268 |
+
),
|
269 |
+
$el(
|
270 |
+
"div",
|
271 |
+
{},
|
272 |
+
[
|
273 |
+
$el("button", {
|
274 |
+
textContent: "Export",
|
275 |
+
style: {
|
276 |
+
fontSize: "12px",
|
277 |
+
fontWeight: "normal",
|
278 |
+
},
|
279 |
+
onclick: (e) => {
|
280 |
+
const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string
|
281 |
+
const blob = new Blob([json], {type: "application/json"});
|
282 |
+
const url = URL.createObjectURL(blob);
|
283 |
+
const a = $el("a", {
|
284 |
+
href: url,
|
285 |
+
download: (nameInput.value || t.name) + ".json",
|
286 |
+
style: {display: "none"},
|
287 |
+
parent: document.body,
|
288 |
+
});
|
289 |
+
a.click();
|
290 |
+
setTimeout(function () {
|
291 |
+
a.remove();
|
292 |
+
window.URL.revokeObjectURL(url);
|
293 |
+
}, 0);
|
294 |
+
},
|
295 |
+
}),
|
296 |
+
$el("button", {
|
297 |
+
textContent: "Delete",
|
298 |
+
style: {
|
299 |
+
fontSize: "12px",
|
300 |
+
color: "red",
|
301 |
+
fontWeight: "normal",
|
302 |
+
},
|
303 |
+
onclick: (e) => {
|
304 |
+
const item = e.target.parentNode.parentNode;
|
305 |
+
item.parentNode.removeChild(item);
|
306 |
+
this.templates.splice(item.dataset.id*1, 1);
|
307 |
+
this.store();
|
308 |
+
// update the rows index, setTimeout ensures that the list is updated
|
309 |
+
var that = this;
|
310 |
+
setTimeout(function (){
|
311 |
+
that.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => {
|
312 |
+
el.dataset.id = i;
|
313 |
+
});
|
314 |
+
}, 0);
|
315 |
+
},
|
316 |
+
}),
|
317 |
+
]
|
318 |
+
),
|
319 |
+
]
|
320 |
+
)
|
321 |
+
];
|
322 |
+
})
|
323 |
+
)
|
324 |
+
);
|
325 |
+
}
|
326 |
+
}
|
327 |
+
|
328 |
+
app.registerExtension({
|
329 |
+
name: id,
|
330 |
+
setup() {
|
331 |
+
const manage = new ManageTemplates();
|
332 |
+
|
333 |
+
const clipboardAction = async (cb) => {
|
334 |
+
// We use the clipboard functions but dont want to overwrite the current user clipboard
|
335 |
+
// Restore it after we've run our callback
|
336 |
+
const old = localStorage.getItem("litegrapheditor_clipboard");
|
337 |
+
await cb();
|
338 |
+
localStorage.setItem("litegrapheditor_clipboard", old);
|
339 |
+
};
|
340 |
+
|
341 |
+
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
342 |
+
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
343 |
+
const options = orig.apply(this, arguments);
|
344 |
+
|
345 |
+
options.push(null);
|
346 |
+
options.push({
|
347 |
+
content: `Save Selected as Template`,
|
348 |
+
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
349 |
+
callback: () => {
|
350 |
+
const name = prompt("Enter name");
|
351 |
+
if (!name?.trim()) return;
|
352 |
+
|
353 |
+
clipboardAction(() => {
|
354 |
+
app.canvas.copyToClipboard();
|
355 |
+
let data = localStorage.getItem("litegrapheditor_clipboard");
|
356 |
+
data = JSON.parse(data);
|
357 |
+
const nodeIds = Object.keys(app.canvas.selected_nodes);
|
358 |
+
for (let i = 0; i < nodeIds.length; i++) {
|
359 |
+
const node = app.graph.getNodeById(nodeIds[i]);
|
360 |
+
const nodeData = node?.constructor.nodeData;
|
361 |
+
|
362 |
+
let groupData = GroupNodeHandler.getGroupData(node);
|
363 |
+
if (groupData) {
|
364 |
+
groupData = groupData.nodeData;
|
365 |
+
if (!data.groupNodes) {
|
366 |
+
data.groupNodes = {};
|
367 |
+
}
|
368 |
+
data.groupNodes[nodeData.name] = groupData;
|
369 |
+
data.nodes[i].type = nodeData.name;
|
370 |
+
}
|
371 |
+
}
|
372 |
+
|
373 |
+
manage.templates.push({
|
374 |
+
name,
|
375 |
+
data: JSON.stringify(data),
|
376 |
+
});
|
377 |
+
manage.store();
|
378 |
+
});
|
379 |
+
},
|
380 |
+
});
|
381 |
+
|
382 |
+
// Map each template to a menu item
|
383 |
+
const subItems = manage.templates.map((t) => {
|
384 |
+
return {
|
385 |
+
content: t.name,
|
386 |
+
callback: () => {
|
387 |
+
clipboardAction(async () => {
|
388 |
+
const data = JSON.parse(t.data);
|
389 |
+
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
|
390 |
+
localStorage.setItem("litegrapheditor_clipboard", t.data);
|
391 |
+
app.canvas.pasteFromClipboard();
|
392 |
+
});
|
393 |
+
},
|
394 |
+
};
|
395 |
+
});
|
396 |
+
|
397 |
+
subItems.push(null, {
|
398 |
+
content: "Manage",
|
399 |
+
callback: () => manage.show(),
|
400 |
+
});
|
401 |
+
|
402 |
+
options.push({
|
403 |
+
content: "Node Templates",
|
404 |
+
submenu: {
|
405 |
+
options: subItems,
|
406 |
+
},
|
407 |
+
});
|
408 |
+
|
409 |
+
return options;
|
410 |
+
};
|
411 |
+
},
|
412 |
+
});
|
ComfyUI/web/extensions/core/noteNode.js
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {app} from "../../scripts/app.js";
|
2 |
+
import {ComfyWidgets} from "../../scripts/widgets.js";
|
3 |
+
// Node that add notes to your project
|
4 |
+
|
5 |
+
app.registerExtension({
|
6 |
+
name: "Comfy.NoteNode",
|
7 |
+
registerCustomNodes() {
|
8 |
+
class NoteNode {
|
9 |
+
color=LGraphCanvas.node_colors.yellow.color;
|
10 |
+
bgcolor=LGraphCanvas.node_colors.yellow.bgcolor;
|
11 |
+
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
|
12 |
+
constructor() {
|
13 |
+
if (!this.properties) {
|
14 |
+
this.properties = {};
|
15 |
+
this.properties.text="";
|
16 |
+
}
|
17 |
+
|
18 |
+
ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app)
|
19 |
+
|
20 |
+
this.serialize_widgets = true;
|
21 |
+
this.isVirtualNode = true;
|
22 |
+
|
23 |
+
}
|
24 |
+
|
25 |
+
|
26 |
+
}
|
27 |
+
|
28 |
+
// Load default visibility
|
29 |
+
|
30 |
+
LiteGraph.registerNodeType(
|
31 |
+
"Note",
|
32 |
+
Object.assign(NoteNode, {
|
33 |
+
title_mode: LiteGraph.NORMAL_TITLE,
|
34 |
+
title: "Note",
|
35 |
+
collapsable: true,
|
36 |
+
})
|
37 |
+
);
|
38 |
+
|
39 |
+
NoteNode.category = "utils";
|
40 |
+
},
|
41 |
+
});
|
ComfyUI/web/extensions/core/rerouteNode.js
ADDED
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs.js";
|
3 |
+
|
4 |
+
// Node that allows you to redirect connections for cleaner graphs
|
5 |
+
|
6 |
+
app.registerExtension({
|
7 |
+
name: "Comfy.RerouteNode",
|
8 |
+
registerCustomNodes(app) {
|
9 |
+
class RerouteNode {
|
10 |
+
constructor() {
|
11 |
+
if (!this.properties) {
|
12 |
+
this.properties = {};
|
13 |
+
}
|
14 |
+
this.properties.showOutputText = RerouteNode.defaultVisibility;
|
15 |
+
this.properties.horizontal = false;
|
16 |
+
|
17 |
+
this.addInput("", "*");
|
18 |
+
this.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
19 |
+
|
20 |
+
this.onAfterGraphConfigured = function () {
|
21 |
+
requestAnimationFrame(() => {
|
22 |
+
this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
|
23 |
+
});
|
24 |
+
};
|
25 |
+
|
26 |
+
this.onConnectionsChange = function (type, index, connected, link_info) {
|
27 |
+
this.applyOrientation();
|
28 |
+
|
29 |
+
// Prevent multiple connections to different types when we have no input
|
30 |
+
if (connected && type === LiteGraph.OUTPUT) {
|
31 |
+
// Ignore wildcard nodes as these will be updated to real types
|
32 |
+
const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
|
33 |
+
if (types.size > 1) {
|
34 |
+
const linksToDisconnect = [];
|
35 |
+
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
36 |
+
const linkId = this.outputs[0].links[i];
|
37 |
+
const link = app.graph.links[linkId];
|
38 |
+
linksToDisconnect.push(link);
|
39 |
+
}
|
40 |
+
for (const link of linksToDisconnect) {
|
41 |
+
const node = app.graph.getNodeById(link.target_id);
|
42 |
+
node.disconnectInput(link.target_slot);
|
43 |
+
}
|
44 |
+
}
|
45 |
+
}
|
46 |
+
|
47 |
+
// Find root input
|
48 |
+
let currentNode = this;
|
49 |
+
let updateNodes = [];
|
50 |
+
let inputType = null;
|
51 |
+
let inputNode = null;
|
52 |
+
while (currentNode) {
|
53 |
+
updateNodes.unshift(currentNode);
|
54 |
+
const linkId = currentNode.inputs[0].link;
|
55 |
+
if (linkId !== null) {
|
56 |
+
const link = app.graph.links[linkId];
|
57 |
+
if (!link) return;
|
58 |
+
const node = app.graph.getNodeById(link.origin_id);
|
59 |
+
const type = node.constructor.type;
|
60 |
+
if (type === "Reroute") {
|
61 |
+
if (node === this) {
|
62 |
+
// We've found a circle
|
63 |
+
currentNode.disconnectInput(link.target_slot);
|
64 |
+
currentNode = null;
|
65 |
+
} else {
|
66 |
+
// Move the previous node
|
67 |
+
currentNode = node;
|
68 |
+
}
|
69 |
+
} else {
|
70 |
+
// We've found the end
|
71 |
+
inputNode = currentNode;
|
72 |
+
inputType = node.outputs[link.origin_slot]?.type ?? null;
|
73 |
+
break;
|
74 |
+
}
|
75 |
+
} else {
|
76 |
+
// This path has no input node
|
77 |
+
currentNode = null;
|
78 |
+
break;
|
79 |
+
}
|
80 |
+
}
|
81 |
+
|
82 |
+
// Find all outputs
|
83 |
+
const nodes = [this];
|
84 |
+
let outputType = null;
|
85 |
+
while (nodes.length) {
|
86 |
+
currentNode = nodes.pop();
|
87 |
+
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
|
88 |
+
if (outputs.length) {
|
89 |
+
for (const linkId of outputs) {
|
90 |
+
const link = app.graph.links[linkId];
|
91 |
+
|
92 |
+
// When disconnecting sometimes the link is still registered
|
93 |
+
if (!link) continue;
|
94 |
+
|
95 |
+
const node = app.graph.getNodeById(link.target_id);
|
96 |
+
const type = node.constructor.type;
|
97 |
+
|
98 |
+
if (type === "Reroute") {
|
99 |
+
// Follow reroute nodes
|
100 |
+
nodes.push(node);
|
101 |
+
updateNodes.push(node);
|
102 |
+
} else {
|
103 |
+
// We've found an output
|
104 |
+
const nodeOutType =
|
105 |
+
node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type
|
106 |
+
? node.inputs[link.target_slot].type
|
107 |
+
: null;
|
108 |
+
if (inputType && inputType !== "*" && nodeOutType !== inputType) {
|
109 |
+
// The output doesnt match our input so disconnect it
|
110 |
+
node.disconnectInput(link.target_slot);
|
111 |
+
} else {
|
112 |
+
outputType = nodeOutType;
|
113 |
+
}
|
114 |
+
}
|
115 |
+
}
|
116 |
+
} else {
|
117 |
+
// No more outputs for this path
|
118 |
+
}
|
119 |
+
}
|
120 |
+
|
121 |
+
const displayType = inputType || outputType || "*";
|
122 |
+
const color = LGraphCanvas.link_type_colors[displayType];
|
123 |
+
|
124 |
+
let widgetConfig;
|
125 |
+
let targetWidget;
|
126 |
+
let widgetType;
|
127 |
+
// Update the types of each node
|
128 |
+
for (const node of updateNodes) {
|
129 |
+
// If we dont have an input type we are always wildcard but we'll show the output type
|
130 |
+
// This lets you change the output link to a different type and all nodes will update
|
131 |
+
node.outputs[0].type = inputType || "*";
|
132 |
+
node.__outputType = displayType;
|
133 |
+
node.outputs[0].name = node.properties.showOutputText ? displayType : "";
|
134 |
+
node.size = node.computeSize();
|
135 |
+
node.applyOrientation();
|
136 |
+
|
137 |
+
for (const l of node.outputs[0].links || []) {
|
138 |
+
const link = app.graph.links[l];
|
139 |
+
if (link) {
|
140 |
+
link.color = color;
|
141 |
+
|
142 |
+
if (app.configuringGraph) continue;
|
143 |
+
const targetNode = app.graph.getNodeById(link.target_id);
|
144 |
+
const targetInput = targetNode.inputs?.[link.target_slot];
|
145 |
+
if (targetInput?.widget) {
|
146 |
+
const config = getWidgetConfig(targetInput);
|
147 |
+
if (!widgetConfig) {
|
148 |
+
widgetConfig = config[1] ?? {};
|
149 |
+
widgetType = config[0];
|
150 |
+
}
|
151 |
+
if (!targetWidget) {
|
152 |
+
targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name);
|
153 |
+
}
|
154 |
+
|
155 |
+
const merged = mergeIfValid(targetInput, [config[0], widgetConfig]);
|
156 |
+
if (merged.customConfig) {
|
157 |
+
widgetConfig = merged.customConfig;
|
158 |
+
}
|
159 |
+
}
|
160 |
+
}
|
161 |
+
}
|
162 |
+
}
|
163 |
+
|
164 |
+
for (const node of updateNodes) {
|
165 |
+
if (widgetConfig && outputType) {
|
166 |
+
node.inputs[0].widget = { name: "value" };
|
167 |
+
setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget);
|
168 |
+
} else {
|
169 |
+
setWidgetConfig(node.inputs[0], null);
|
170 |
+
}
|
171 |
+
}
|
172 |
+
|
173 |
+
if (inputNode) {
|
174 |
+
const link = app.graph.links[inputNode.inputs[0].link];
|
175 |
+
if (link) {
|
176 |
+
link.color = color;
|
177 |
+
}
|
178 |
+
}
|
179 |
+
};
|
180 |
+
|
181 |
+
this.clone = function () {
|
182 |
+
const cloned = RerouteNode.prototype.clone.apply(this);
|
183 |
+
cloned.removeOutput(0);
|
184 |
+
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
185 |
+
cloned.size = cloned.computeSize();
|
186 |
+
return cloned;
|
187 |
+
};
|
188 |
+
|
189 |
+
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
190 |
+
this.isVirtualNode = true;
|
191 |
+
}
|
192 |
+
|
193 |
+
getExtraMenuOptions(_, options) {
|
194 |
+
options.unshift(
|
195 |
+
{
|
196 |
+
content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
|
197 |
+
callback: () => {
|
198 |
+
this.properties.showOutputText = !this.properties.showOutputText;
|
199 |
+
if (this.properties.showOutputText) {
|
200 |
+
this.outputs[0].name = this.__outputType || this.outputs[0].type;
|
201 |
+
} else {
|
202 |
+
this.outputs[0].name = "";
|
203 |
+
}
|
204 |
+
this.size = this.computeSize();
|
205 |
+
this.applyOrientation();
|
206 |
+
app.graph.setDirtyCanvas(true, true);
|
207 |
+
},
|
208 |
+
},
|
209 |
+
{
|
210 |
+
content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default",
|
211 |
+
callback: () => {
|
212 |
+
RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility);
|
213 |
+
},
|
214 |
+
},
|
215 |
+
{
|
216 |
+
// naming is inverted with respect to LiteGraphNode.horizontal
|
217 |
+
// LiteGraphNode.horizontal == true means that
|
218 |
+
// each slot in the inputs and outputs are layed out horizontally,
|
219 |
+
// which is the opposite of the visual orientation of the inputs and outputs as a node
|
220 |
+
content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
|
221 |
+
callback: () => {
|
222 |
+
this.properties.horizontal = !this.properties.horizontal;
|
223 |
+
this.applyOrientation();
|
224 |
+
},
|
225 |
+
}
|
226 |
+
);
|
227 |
+
}
|
228 |
+
applyOrientation() {
|
229 |
+
this.horizontal = this.properties.horizontal;
|
230 |
+
if (this.horizontal) {
|
231 |
+
// we correct the input position, because LiteGraphNode.horizontal
|
232 |
+
// doesn't account for title presence
|
233 |
+
// which reroute nodes don't have
|
234 |
+
this.inputs[0].pos = [this.size[0] / 2, 0];
|
235 |
+
} else {
|
236 |
+
delete this.inputs[0].pos;
|
237 |
+
}
|
238 |
+
app.graph.setDirtyCanvas(true, true);
|
239 |
+
}
|
240 |
+
|
241 |
+
computeSize() {
|
242 |
+
return [
|
243 |
+
this.properties.showOutputText && this.outputs && this.outputs.length
|
244 |
+
? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40)
|
245 |
+
: 75,
|
246 |
+
26,
|
247 |
+
];
|
248 |
+
}
|
249 |
+
|
250 |
+
static setDefaultTextVisibility(visible) {
|
251 |
+
RerouteNode.defaultVisibility = visible;
|
252 |
+
if (visible) {
|
253 |
+
localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
|
254 |
+
} else {
|
255 |
+
delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
|
256 |
+
}
|
257 |
+
}
|
258 |
+
}
|
259 |
+
|
260 |
+
// Load default visibility
|
261 |
+
RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]);
|
262 |
+
|
263 |
+
LiteGraph.registerNodeType(
|
264 |
+
"Reroute",
|
265 |
+
Object.assign(RerouteNode, {
|
266 |
+
title_mode: LiteGraph.NO_TITLE,
|
267 |
+
title: "Reroute",
|
268 |
+
collapsable: false,
|
269 |
+
})
|
270 |
+
);
|
271 |
+
|
272 |
+
RerouteNode.category = "utils";
|
273 |
+
},
|
274 |
+
});
|
ComfyUI/web/extensions/core/saveImageExtraOutput.js
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { applyTextReplacements } from "../../scripts/utils.js";
|
3 |
+
// Use widget values and dates in output filenames
|
4 |
+
|
5 |
+
app.registerExtension({
|
6 |
+
name: "Comfy.SaveImageExtraOutput",
|
7 |
+
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
8 |
+
if (nodeData.name === "SaveImage") {
|
9 |
+
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
10 |
+
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
|
11 |
+
nodeType.prototype.onNodeCreated = function () {
|
12 |
+
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
13 |
+
|
14 |
+
const widget = this.widgets.find((w) => w.name === "filename_prefix");
|
15 |
+
widget.serializeValue = () => {
|
16 |
+
return applyTextReplacements(app, widget.value);
|
17 |
+
};
|
18 |
+
|
19 |
+
return r;
|
20 |
+
};
|
21 |
+
} else {
|
22 |
+
// When any other node is created add a property to alias the node
|
23 |
+
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
24 |
+
nodeType.prototype.onNodeCreated = function () {
|
25 |
+
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
26 |
+
|
27 |
+
if (!this.properties || !("Node name for S&R" in this.properties)) {
|
28 |
+
this.addProperty("Node name for S&R", this.constructor.type, "string");
|
29 |
+
}
|
30 |
+
|
31 |
+
return r;
|
32 |
+
};
|
33 |
+
}
|
34 |
+
},
|
35 |
+
});
|
ComfyUI/web/extensions/core/simpleTouchSupport.js
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
|
3 |
+
let touchZooming;
|
4 |
+
let touchCount = 0;
|
5 |
+
|
6 |
+
app.registerExtension({
|
7 |
+
name: "Comfy.SimpleTouchSupport",
|
8 |
+
setup() {
|
9 |
+
let zoomPos;
|
10 |
+
let touchTime;
|
11 |
+
let lastTouch;
|
12 |
+
|
13 |
+
function getMultiTouchPos(e) {
|
14 |
+
return Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
15 |
+
}
|
16 |
+
|
17 |
+
app.canvasEl.addEventListener(
|
18 |
+
"touchstart",
|
19 |
+
(e) => {
|
20 |
+
touchCount++;
|
21 |
+
lastTouch = null;
|
22 |
+
if (e.touches?.length === 1) {
|
23 |
+
// Store start time for press+hold for context menu
|
24 |
+
touchTime = new Date();
|
25 |
+
lastTouch = e.touches[0];
|
26 |
+
} else {
|
27 |
+
touchTime = null;
|
28 |
+
if (e.touches?.length === 2) {
|
29 |
+
// Store center pos for zoom
|
30 |
+
zoomPos = getMultiTouchPos(e);
|
31 |
+
app.canvas.pointer_is_down = false;
|
32 |
+
}
|
33 |
+
}
|
34 |
+
},
|
35 |
+
true
|
36 |
+
);
|
37 |
+
|
38 |
+
app.canvasEl.addEventListener("touchend", (e) => {
|
39 |
+
touchZooming = false;
|
40 |
+
touchCount = e.touches?.length ?? touchCount - 1;
|
41 |
+
if (touchTime && !e.touches?.length) {
|
42 |
+
if (new Date() - touchTime > 600) {
|
43 |
+
try {
|
44 |
+
// hack to get litegraph to use this event
|
45 |
+
e.constructor = CustomEvent;
|
46 |
+
} catch (error) {}
|
47 |
+
e.clientX = lastTouch.clientX;
|
48 |
+
e.clientY = lastTouch.clientY;
|
49 |
+
|
50 |
+
app.canvas.pointer_is_down = true;
|
51 |
+
app.canvas._mousedown_callback(e);
|
52 |
+
}
|
53 |
+
touchTime = null;
|
54 |
+
}
|
55 |
+
});
|
56 |
+
|
57 |
+
app.canvasEl.addEventListener(
|
58 |
+
"touchmove",
|
59 |
+
(e) => {
|
60 |
+
touchTime = null;
|
61 |
+
if (e.touches?.length === 2) {
|
62 |
+
app.canvas.pointer_is_down = false;
|
63 |
+
touchZooming = true;
|
64 |
+
LiteGraph.closeAllContextMenus();
|
65 |
+
app.canvas.search_box?.close();
|
66 |
+
const newZoomPos = getMultiTouchPos(e);
|
67 |
+
|
68 |
+
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
69 |
+
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
70 |
+
|
71 |
+
let scale = app.canvas.ds.scale;
|
72 |
+
const diff = zoomPos - newZoomPos;
|
73 |
+
if (diff > 0.5) {
|
74 |
+
scale *= 1 / 1.07;
|
75 |
+
} else if (diff < -0.5) {
|
76 |
+
scale *= 1.07;
|
77 |
+
}
|
78 |
+
app.canvas.ds.changeScale(scale, [midX, midY]);
|
79 |
+
app.canvas.setDirty(true, true);
|
80 |
+
zoomPos = newZoomPos;
|
81 |
+
}
|
82 |
+
},
|
83 |
+
true
|
84 |
+
);
|
85 |
+
},
|
86 |
+
});
|
87 |
+
|
88 |
+
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
89 |
+
LGraphCanvas.prototype.processMouseDown = function (e) {
|
90 |
+
if (touchZooming || touchCount) {
|
91 |
+
return;
|
92 |
+
}
|
93 |
+
return processMouseDown.apply(this, arguments);
|
94 |
+
};
|
95 |
+
|
96 |
+
const processMouseMove = LGraphCanvas.prototype.processMouseMove;
|
97 |
+
LGraphCanvas.prototype.processMouseMove = function (e) {
|
98 |
+
if (touchZooming || touchCount > 1) {
|
99 |
+
return;
|
100 |
+
}
|
101 |
+
return processMouseMove.apply(this, arguments);
|
102 |
+
};
|
ComfyUI/web/extensions/core/slotDefaults.js
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { ComfyWidgets } from "../../scripts/widgets.js";
|
3 |
+
// Adds defaults for quickly adding nodes with middle click on the input/output
|
4 |
+
|
5 |
+
app.registerExtension({
|
6 |
+
name: "Comfy.SlotDefaults",
|
7 |
+
suggestionsNumber: null,
|
8 |
+
init() {
|
9 |
+
LiteGraph.search_filter_enabled = true;
|
10 |
+
LiteGraph.middle_click_slot_add_default_node = true;
|
11 |
+
this.suggestionsNumber = app.ui.settings.addSetting({
|
12 |
+
id: "Comfy.NodeSuggestions.number",
|
13 |
+
name: "Number of nodes suggestions",
|
14 |
+
type: "slider",
|
15 |
+
attrs: {
|
16 |
+
min: 1,
|
17 |
+
max: 100,
|
18 |
+
step: 1,
|
19 |
+
},
|
20 |
+
defaultValue: 5,
|
21 |
+
onChange: (newVal, oldVal) => {
|
22 |
+
this.setDefaults(newVal);
|
23 |
+
}
|
24 |
+
});
|
25 |
+
},
|
26 |
+
slot_types_default_out: {},
|
27 |
+
slot_types_default_in: {},
|
28 |
+
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
29 |
+
var nodeId = nodeData.name;
|
30 |
+
var inputs = [];
|
31 |
+
inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs
|
32 |
+
for (const inputKey in inputs) {
|
33 |
+
var input = (inputs[inputKey]);
|
34 |
+
if (typeof input[0] !== "string") continue;
|
35 |
+
|
36 |
+
var type = input[0]
|
37 |
+
if (type in ComfyWidgets) {
|
38 |
+
var customProperties = input[1]
|
39 |
+
if (!(customProperties?.forceInput)) continue; //ignore widgets that don't force input
|
40 |
+
}
|
41 |
+
|
42 |
+
if (!(type in this.slot_types_default_out)) {
|
43 |
+
this.slot_types_default_out[type] = ["Reroute"];
|
44 |
+
}
|
45 |
+
if (this.slot_types_default_out[type].includes(nodeId)) continue;
|
46 |
+
this.slot_types_default_out[type].push(nodeId);
|
47 |
+
|
48 |
+
// Input types have to be stored as lower case
|
49 |
+
// Store each node that can handle this input type
|
50 |
+
const lowerType = type.toLocaleLowerCase();
|
51 |
+
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
|
52 |
+
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
|
53 |
+
}
|
54 |
+
LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeType.comfyClass);
|
55 |
+
}
|
56 |
+
|
57 |
+
var outputs = nodeData["output"];
|
58 |
+
for (const key in outputs) {
|
59 |
+
var type = outputs[key];
|
60 |
+
if (!(type in this.slot_types_default_in)) {
|
61 |
+
this.slot_types_default_in[type] = ["Reroute"];// ["Reroute", "Primitive"]; primitive doesn't always work :'()
|
62 |
+
}
|
63 |
+
|
64 |
+
this.slot_types_default_in[type].push(nodeId);
|
65 |
+
|
66 |
+
// Store each node that can handle this output type
|
67 |
+
if (!(type in LiteGraph.registered_slot_out_types)) {
|
68 |
+
LiteGraph.registered_slot_out_types[type] = { nodes: [] };
|
69 |
+
}
|
70 |
+
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
|
71 |
+
|
72 |
+
if(!LiteGraph.slot_types_out.includes(type)) {
|
73 |
+
LiteGraph.slot_types_out.push(type);
|
74 |
+
}
|
75 |
+
}
|
76 |
+
var maxNum = this.suggestionsNumber.value;
|
77 |
+
this.setDefaults(maxNum);
|
78 |
+
},
|
79 |
+
setDefaults(maxNum) {
|
80 |
+
|
81 |
+
LiteGraph.slot_types_default_out = {};
|
82 |
+
LiteGraph.slot_types_default_in = {};
|
83 |
+
|
84 |
+
for (const type in this.slot_types_default_out) {
|
85 |
+
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum);
|
86 |
+
}
|
87 |
+
for (const type in this.slot_types_default_in) {
|
88 |
+
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum);
|
89 |
+
}
|
90 |
+
}
|
91 |
+
});
|
ComfyUI/web/extensions/core/snapToGrid.js
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
|
3 |
+
// Shift + drag/resize to snap to grid
|
4 |
+
|
5 |
+
/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
|
6 |
+
function roundVectorToGrid(vec) {
|
7 |
+
vec[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE);
|
8 |
+
vec[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE);
|
9 |
+
return vec;
|
10 |
+
}
|
11 |
+
|
12 |
+
app.registerExtension({
|
13 |
+
name: "Comfy.SnapToGrid",
|
14 |
+
init() {
|
15 |
+
// Add setting to control grid size
|
16 |
+
app.ui.settings.addSetting({
|
17 |
+
id: "Comfy.SnapToGrid.GridSize",
|
18 |
+
name: "Grid Size",
|
19 |
+
type: "slider",
|
20 |
+
attrs: {
|
21 |
+
min: 1,
|
22 |
+
max: 500,
|
23 |
+
},
|
24 |
+
tooltip:
|
25 |
+
"When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
|
26 |
+
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
27 |
+
onChange(value) {
|
28 |
+
LiteGraph.CANVAS_GRID_SIZE = +value;
|
29 |
+
},
|
30 |
+
});
|
31 |
+
|
32 |
+
// After moving a node, if the shift key is down align it to grid
|
33 |
+
const onNodeMoved = app.canvas.onNodeMoved;
|
34 |
+
app.canvas.onNodeMoved = function (node) {
|
35 |
+
const r = onNodeMoved?.apply(this, arguments);
|
36 |
+
|
37 |
+
if (app.shiftDown) {
|
38 |
+
// Ensure all selected nodes are realigned
|
39 |
+
for (const id in this.selected_nodes) {
|
40 |
+
this.selected_nodes[id].alignToGrid();
|
41 |
+
}
|
42 |
+
}
|
43 |
+
|
44 |
+
return r;
|
45 |
+
};
|
46 |
+
|
47 |
+
// When a node is added, add a resize handler to it so we can fix align the size with the grid
|
48 |
+
const onNodeAdded = app.graph.onNodeAdded;
|
49 |
+
app.graph.onNodeAdded = function (node) {
|
50 |
+
const onResize = node.onResize;
|
51 |
+
node.onResize = function () {
|
52 |
+
if (app.shiftDown) {
|
53 |
+
roundVectorToGrid(node.size);
|
54 |
+
}
|
55 |
+
return onResize?.apply(this, arguments);
|
56 |
+
};
|
57 |
+
return onNodeAdded?.apply(this, arguments);
|
58 |
+
};
|
59 |
+
|
60 |
+
// Draw a preview of where the node will go if holding shift and the node is selected
|
61 |
+
const origDrawNode = LGraphCanvas.prototype.drawNode;
|
62 |
+
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
63 |
+
if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
|
64 |
+
const [x, y] = roundVectorToGrid([...node.pos]);
|
65 |
+
const shiftX = x - node.pos[0];
|
66 |
+
let shiftY = y - node.pos[1];
|
67 |
+
|
68 |
+
let w, h;
|
69 |
+
if (node.flags.collapsed) {
|
70 |
+
w = node._collapsed_width;
|
71 |
+
h = LiteGraph.NODE_TITLE_HEIGHT;
|
72 |
+
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
73 |
+
} else {
|
74 |
+
w = node.size[0];
|
75 |
+
h = node.size[1];
|
76 |
+
let titleMode = node.constructor.title_mode;
|
77 |
+
if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) {
|
78 |
+
h += LiteGraph.NODE_TITLE_HEIGHT;
|
79 |
+
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
80 |
+
}
|
81 |
+
}
|
82 |
+
const f = ctx.fillStyle;
|
83 |
+
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
|
84 |
+
ctx.fillRect(shiftX, shiftY, w, h);
|
85 |
+
ctx.fillStyle = f;
|
86 |
+
}
|
87 |
+
|
88 |
+
return origDrawNode.apply(this, arguments);
|
89 |
+
};
|
90 |
+
|
91 |
+
|
92 |
+
|
93 |
+
/**
|
94 |
+
* The currently moving, selected group only. Set after the `selected_group` has actually started
|
95 |
+
* moving.
|
96 |
+
*/
|
97 |
+
let selectedAndMovingGroup = null;
|
98 |
+
|
99 |
+
/**
|
100 |
+
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
|
101 |
+
* below) as well as handle the last move call from LiteGraph's `processMouseUp`.
|
102 |
+
*/
|
103 |
+
const groupMove = LGraphGroup.prototype.move;
|
104 |
+
LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
|
105 |
+
const v = groupMove.apply(this, arguments);
|
106 |
+
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
|
107 |
+
// too eagerly and we don't want to behave like we're moving until we get a delta.
|
108 |
+
if (!selectedAndMovingGroup && app.canvas.selected_group === this && (deltax || deltay)) {
|
109 |
+
selectedAndMovingGroup = this;
|
110 |
+
}
|
111 |
+
|
112 |
+
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
|
113 |
+
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
|
114 |
+
// has been set to `false`. Essentially, this check here is the equivilant to calling an
|
115 |
+
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
|
116 |
+
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
117 |
+
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
|
118 |
+
// align the group itself.
|
119 |
+
this.recomputeInsideNodes();
|
120 |
+
for (const node of this._nodes) {
|
121 |
+
node.alignToGrid();
|
122 |
+
}
|
123 |
+
LGraphNode.prototype.alignToGrid.apply(this);
|
124 |
+
}
|
125 |
+
return v;
|
126 |
+
};
|
127 |
+
|
128 |
+
/**
|
129 |
+
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
|
130 |
+
* drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
|
131 |
+
* both.
|
132 |
+
*/
|
133 |
+
const drawGroups = LGraphCanvas.prototype.drawGroups;
|
134 |
+
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
|
135 |
+
if (this.selected_group && app.shiftDown) {
|
136 |
+
if (this.selected_group_resizing) {
|
137 |
+
roundVectorToGrid(this.selected_group.size);
|
138 |
+
} else if (selectedAndMovingGroup) {
|
139 |
+
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
|
140 |
+
const f = ctx.fillStyle;
|
141 |
+
const s = ctx.strokeStyle;
|
142 |
+
ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
|
143 |
+
ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
|
144 |
+
ctx.rect(x, y, ...selectedAndMovingGroup.size);
|
145 |
+
ctx.fill();
|
146 |
+
ctx.stroke();
|
147 |
+
ctx.fillStyle = f;
|
148 |
+
ctx.strokeStyle = s;
|
149 |
+
}
|
150 |
+
} else if (!this.selected_group) {
|
151 |
+
selectedAndMovingGroup = null;
|
152 |
+
}
|
153 |
+
return drawGroups.apply(this, arguments);
|
154 |
+
};
|
155 |
+
|
156 |
+
|
157 |
+
/** Handles adding a group in a snapping-enabled state. */
|
158 |
+
const onGroupAdd = LGraphCanvas.onGroupAdd;
|
159 |
+
LGraphCanvas.onGroupAdd = function() {
|
160 |
+
const v = onGroupAdd.apply(app.canvas, arguments);
|
161 |
+
if (app.shiftDown) {
|
162 |
+
const lastGroup = app.graph._groups[app.graph._groups.length - 1];
|
163 |
+
if (lastGroup) {
|
164 |
+
roundVectorToGrid(lastGroup.pos);
|
165 |
+
roundVectorToGrid(lastGroup.size);
|
166 |
+
}
|
167 |
+
}
|
168 |
+
return v;
|
169 |
+
};
|
170 |
+
},
|
171 |
+
});
|
ComfyUI/web/extensions/core/uploadAudio.js
ADDED
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js"
|
2 |
+
import { api } from "../../scripts/api.js"
|
3 |
+
|
4 |
+
function splitFilePath(path) {
|
5 |
+
const folder_separator = path.lastIndexOf("/")
|
6 |
+
if (folder_separator === -1) {
|
7 |
+
return ["", path]
|
8 |
+
}
|
9 |
+
return [
|
10 |
+
path.substring(0, folder_separator),
|
11 |
+
path.substring(folder_separator + 1)
|
12 |
+
]
|
13 |
+
}
|
14 |
+
|
15 |
+
function getResourceURL(subfolder, filename, type = "input") {
|
16 |
+
const params = [
|
17 |
+
"filename=" + encodeURIComponent(filename),
|
18 |
+
"type=" + type,
|
19 |
+
"subfolder=" + subfolder,
|
20 |
+
app.getRandParam().substring(1)
|
21 |
+
].join("&")
|
22 |
+
|
23 |
+
return `/view?${params}`
|
24 |
+
}
|
25 |
+
|
26 |
+
async function uploadFile(
|
27 |
+
audioWidget,
|
28 |
+
audioUIWidget,
|
29 |
+
file,
|
30 |
+
updateNode,
|
31 |
+
pasted = false
|
32 |
+
) {
|
33 |
+
try {
|
34 |
+
// Wrap file in formdata so it includes filename
|
35 |
+
const body = new FormData()
|
36 |
+
body.append("image", file)
|
37 |
+
if (pasted) body.append("subfolder", "pasted")
|
38 |
+
const resp = await api.fetchApi("/upload/image", {
|
39 |
+
method: "POST",
|
40 |
+
body
|
41 |
+
})
|
42 |
+
|
43 |
+
if (resp.status === 200) {
|
44 |
+
const data = await resp.json()
|
45 |
+
// Add the file to the dropdown list and update the widget value
|
46 |
+
let path = data.name
|
47 |
+
if (data.subfolder) path = data.subfolder + "/" + path
|
48 |
+
|
49 |
+
if (!audioWidget.options.values.includes(path)) {
|
50 |
+
audioWidget.options.values.push(path)
|
51 |
+
}
|
52 |
+
|
53 |
+
if (updateNode) {
|
54 |
+
audioUIWidget.element.src = api.apiURL(
|
55 |
+
getResourceURL(...splitFilePath(path))
|
56 |
+
)
|
57 |
+
audioWidget.value = path
|
58 |
+
}
|
59 |
+
} else {
|
60 |
+
alert(resp.status + " - " + resp.statusText)
|
61 |
+
}
|
62 |
+
} catch (error) {
|
63 |
+
alert(error)
|
64 |
+
}
|
65 |
+
}
|
66 |
+
|
67 |
+
// AudioWidget MUST be registered first, as AUDIOUPLOAD depends on AUDIO_UI to be
|
68 |
+
// present.
|
69 |
+
app.registerExtension({
|
70 |
+
name: "Comfy.AudioWidget",
|
71 |
+
async beforeRegisterNodeDef(nodeType, nodeData) {
|
72 |
+
if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) {
|
73 |
+
nodeData.input.required.audioUI = ["AUDIO_UI"]
|
74 |
+
}
|
75 |
+
},
|
76 |
+
getCustomWidgets() {
|
77 |
+
return {
|
78 |
+
AUDIO_UI(node, inputName) {
|
79 |
+
const audio = document.createElement("audio")
|
80 |
+
audio.controls = true
|
81 |
+
audio.classList.add("comfy-audio")
|
82 |
+
audio.setAttribute("name", "media")
|
83 |
+
|
84 |
+
const audioUIWidget = node.addDOMWidget(
|
85 |
+
inputName,
|
86 |
+
/* name=*/ "audioUI",
|
87 |
+
audio
|
88 |
+
)
|
89 |
+
// @ts-ignore
|
90 |
+
// TODO: Sort out the DOMWidget type.
|
91 |
+
audioUIWidget.serialize = false
|
92 |
+
|
93 |
+
const isOutputNode = node.constructor.nodeData.output_node
|
94 |
+
if (isOutputNode) {
|
95 |
+
// Hide the audio widget when there is no audio initially.
|
96 |
+
audioUIWidget.element.classList.add("empty-audio-widget")
|
97 |
+
// Populate the audio widget UI on node execution.
|
98 |
+
const onExecuted = node.onExecuted
|
99 |
+
node.onExecuted = function(message) {
|
100 |
+
onExecuted?.apply(this, arguments)
|
101 |
+
const audios = message.audio
|
102 |
+
if (!audios) return
|
103 |
+
const audio = audios[0]
|
104 |
+
audioUIWidget.element.src = api.apiURL(
|
105 |
+
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
106 |
+
)
|
107 |
+
audioUIWidget.element.classList.remove("empty-audio-widget")
|
108 |
+
}
|
109 |
+
}
|
110 |
+
return { widget: audioUIWidget }
|
111 |
+
}
|
112 |
+
}
|
113 |
+
},
|
114 |
+
onNodeOutputsUpdated(nodeOutputs) {
|
115 |
+
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
|
116 |
+
const node = app.graph.getNodeById(Number.parseInt(nodeId));
|
117 |
+
if ("audio" in output) {
|
118 |
+
const audioUIWidget = node.widgets.find((w) => w.name === "audioUI");
|
119 |
+
const audio = output.audio[0];
|
120 |
+
audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type));
|
121 |
+
audioUIWidget.element.classList.remove("empty-audio-widget");
|
122 |
+
}
|
123 |
+
}
|
124 |
+
},
|
125 |
+
})
|
126 |
+
|
127 |
+
app.registerExtension({
|
128 |
+
name: "Comfy.UploadAudio",
|
129 |
+
async beforeRegisterNodeDef(nodeType, nodeData) {
|
130 |
+
if (nodeData?.input?.required?.audio?.[1]?.audio_upload === true) {
|
131 |
+
nodeData.input.required.upload = ["AUDIOUPLOAD"]
|
132 |
+
}
|
133 |
+
},
|
134 |
+
getCustomWidgets() {
|
135 |
+
return {
|
136 |
+
AUDIOUPLOAD(node, inputName) {
|
137 |
+
// The widget that allows user to select file.
|
138 |
+
const audioWidget = node.widgets.find(w => w.name === "audio")
|
139 |
+
const audioUIWidget = node.widgets.find(w => w.name === "audioUI")
|
140 |
+
|
141 |
+
const onAudioWidgetUpdate = () => {
|
142 |
+
audioUIWidget.element.src = api.apiURL(
|
143 |
+
getResourceURL(...splitFilePath(audioWidget.value))
|
144 |
+
)
|
145 |
+
}
|
146 |
+
// Initially load default audio file to audioUIWidget.
|
147 |
+
if (audioWidget.value) {
|
148 |
+
onAudioWidgetUpdate()
|
149 |
+
}
|
150 |
+
audioWidget.callback = onAudioWidgetUpdate
|
151 |
+
|
152 |
+
// Load saved audio file widget values if restoring from workflow
|
153 |
+
const onGraphConfigured = node.onGraphConfigured;
|
154 |
+
node.onGraphConfigured = function() {
|
155 |
+
onGraphConfigured?.apply(this, arguments)
|
156 |
+
if (audioWidget.value) {
|
157 |
+
onAudioWidgetUpdate()
|
158 |
+
}
|
159 |
+
}
|
160 |
+
|
161 |
+
const fileInput = document.createElement("input")
|
162 |
+
fileInput.type = "file"
|
163 |
+
fileInput.accept = "audio/*"
|
164 |
+
fileInput.style.display = "none"
|
165 |
+
fileInput.onchange = () => {
|
166 |
+
if (fileInput.files.length) {
|
167 |
+
uploadFile(audioWidget, audioUIWidget, fileInput.files[0], true)
|
168 |
+
}
|
169 |
+
}
|
170 |
+
// The widget to pop up the upload dialog.
|
171 |
+
const uploadWidget = node.addWidget(
|
172 |
+
"button",
|
173 |
+
inputName,
|
174 |
+
/* value=*/ "",
|
175 |
+
() => {
|
176 |
+
fileInput.click()
|
177 |
+
}
|
178 |
+
)
|
179 |
+
uploadWidget.label = "choose file to upload"
|
180 |
+
uploadWidget.serialize = false
|
181 |
+
|
182 |
+
return { widget: uploadWidget }
|
183 |
+
}
|
184 |
+
}
|
185 |
+
}
|
186 |
+
})
|
ComfyUI/web/extensions/core/uploadImage.js
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
|
3 |
+
// Adds an upload button to the nodes
|
4 |
+
|
5 |
+
app.registerExtension({
|
6 |
+
name: "Comfy.UploadImage",
|
7 |
+
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
8 |
+
if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
|
9 |
+
nodeData.input.required.upload = ["IMAGEUPLOAD"];
|
10 |
+
}
|
11 |
+
},
|
12 |
+
});
|
ComfyUI/web/extensions/core/webcamCapture.js
ADDED
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../../scripts/app.js";
|
2 |
+
import { api } from "../../scripts/api.js";
|
3 |
+
|
4 |
+
const WEBCAM_READY = Symbol();
|
5 |
+
|
6 |
+
app.registerExtension({
|
7 |
+
name: "Comfy.WebcamCapture",
|
8 |
+
getCustomWidgets(app) {
|
9 |
+
return {
|
10 |
+
WEBCAM(node, inputName) {
|
11 |
+
let res;
|
12 |
+
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve));
|
13 |
+
|
14 |
+
const container = document.createElement("div");
|
15 |
+
container.style.background = "rgba(0,0,0,0.25)";
|
16 |
+
container.style.textAlign = "center";
|
17 |
+
|
18 |
+
const video = document.createElement("video");
|
19 |
+
video.style.height = video.style.width = "100%";
|
20 |
+
|
21 |
+
const loadVideo = async () => {
|
22 |
+
try {
|
23 |
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
24 |
+
container.replaceChildren(video);
|
25 |
+
|
26 |
+
setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes?
|
27 |
+
video.addEventListener("loadedmetadata", () => res(video), false);
|
28 |
+
video.srcObject = stream;
|
29 |
+
video.play();
|
30 |
+
} catch (error) {
|
31 |
+
const label = document.createElement("div");
|
32 |
+
label.style.color = "red";
|
33 |
+
label.style.overflow = "auto";
|
34 |
+
label.style.maxHeight = "100%";
|
35 |
+
label.style.whiteSpace = "pre-wrap";
|
36 |
+
|
37 |
+
if (window.isSecureContext) {
|
38 |
+
label.textContent = "Unable to load webcam, please ensure access is granted:\n" + error.message;
|
39 |
+
} else {
|
40 |
+
label.textContent = "Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" + error.message;
|
41 |
+
}
|
42 |
+
|
43 |
+
container.replaceChildren(label);
|
44 |
+
}
|
45 |
+
};
|
46 |
+
|
47 |
+
loadVideo();
|
48 |
+
|
49 |
+
return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
|
50 |
+
},
|
51 |
+
};
|
52 |
+
},
|
53 |
+
nodeCreated(node) {
|
54 |
+
if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return;
|
55 |
+
|
56 |
+
let video;
|
57 |
+
const camera = node.widgets.find((w) => w.name === "image");
|
58 |
+
const w = node.widgets.find((w) => w.name === "width");
|
59 |
+
const h = node.widgets.find((w) => w.name === "height");
|
60 |
+
const captureOnQueue = node.widgets.find((w) => w.name === "capture_on_queue");
|
61 |
+
|
62 |
+
const canvas = document.createElement("canvas");
|
63 |
+
|
64 |
+
const capture = () => {
|
65 |
+
canvas.width = w.value;
|
66 |
+
canvas.height = h.value;
|
67 |
+
const ctx = canvas.getContext("2d");
|
68 |
+
ctx.drawImage(video, 0, 0, w.value, h.value);
|
69 |
+
const data = canvas.toDataURL("image/png");
|
70 |
+
|
71 |
+
const img = new Image();
|
72 |
+
img.onload = () => {
|
73 |
+
node.imgs = [img];
|
74 |
+
app.graph.setDirtyCanvas(true);
|
75 |
+
requestAnimationFrame(() => {
|
76 |
+
node.setSizeForImage?.();
|
77 |
+
});
|
78 |
+
};
|
79 |
+
img.src = data;
|
80 |
+
};
|
81 |
+
|
82 |
+
const btn = node.addWidget("button", "waiting for camera...", "capture", capture);
|
83 |
+
btn.disabled = true;
|
84 |
+
btn.serializeValue = () => undefined;
|
85 |
+
|
86 |
+
camera.serializeValue = async () => {
|
87 |
+
if (captureOnQueue.value) {
|
88 |
+
capture();
|
89 |
+
} else if (!node.imgs?.length) {
|
90 |
+
const err = `No webcam image captured`;
|
91 |
+
alert(err);
|
92 |
+
throw new Error(err);
|
93 |
+
}
|
94 |
+
|
95 |
+
// Upload image to temp storage
|
96 |
+
const blob = await new Promise((r) => canvas.toBlob(r));
|
97 |
+
const name = `${+new Date()}.png`;
|
98 |
+
const file = new File([blob], name);
|
99 |
+
const body = new FormData();
|
100 |
+
body.append("image", file);
|
101 |
+
body.append("subfolder", "webcam");
|
102 |
+
body.append("type", "temp");
|
103 |
+
const resp = await api.fetchApi("/upload/image", {
|
104 |
+
method: "POST",
|
105 |
+
body,
|
106 |
+
});
|
107 |
+
if (resp.status !== 200) {
|
108 |
+
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
|
109 |
+
alert(err);
|
110 |
+
throw new Error(err);
|
111 |
+
}
|
112 |
+
return `webcam/${name} [temp]`;
|
113 |
+
};
|
114 |
+
|
115 |
+
node[WEBCAM_READY].then((v) => {
|
116 |
+
video = v;
|
117 |
+
// If width isnt specified then use video output resolution
|
118 |
+
if (!w.value) {
|
119 |
+
w.value = video.videoWidth || 640;
|
120 |
+
h.value = video.videoHeight || 480;
|
121 |
+
}
|
122 |
+
btn.disabled = false;
|
123 |
+
btn.label = "capture";
|
124 |
+
});
|
125 |
+
},
|
126 |
+
});
|
ComfyUI/web/extensions/core/widgetInputs.js
ADDED
@@ -0,0 +1,800 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComfyWidgets, addValueControlWidgets } from "../../scripts/widgets.js";
|
2 |
+
import { app } from "../../scripts/app.js";
|
3 |
+
import { applyTextReplacements } from "../../scripts/utils.js";
|
4 |
+
|
5 |
+
const CONVERTED_TYPE = "converted-widget";
|
6 |
+
const VALID_TYPES = ["STRING", "combo", "number", "BOOLEAN"];
|
7 |
+
const CONFIG = Symbol();
|
8 |
+
const GET_CONFIG = Symbol();
|
9 |
+
const TARGET = Symbol(); // Used for reroutes to specify the real target widget
|
10 |
+
|
11 |
+
export function getWidgetConfig(slot) {
|
12 |
+
return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG]();
|
13 |
+
}
|
14 |
+
|
15 |
+
function getConfig(widgetName) {
|
16 |
+
const { nodeData } = this.constructor;
|
17 |
+
return nodeData?.input?.required[widgetName] ?? nodeData?.input?.optional?.[widgetName];
|
18 |
+
}
|
19 |
+
|
20 |
+
function isConvertableWidget(widget, config) {
|
21 |
+
return (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput;
|
22 |
+
}
|
23 |
+
|
24 |
+
function hideWidget(node, widget, suffix = "") {
|
25 |
+
if (widget.type?.startsWith(CONVERTED_TYPE)) return;
|
26 |
+
widget.origType = widget.type;
|
27 |
+
widget.origComputeSize = widget.computeSize;
|
28 |
+
widget.origSerializeValue = widget.serializeValue;
|
29 |
+
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
|
30 |
+
widget.type = CONVERTED_TYPE + suffix;
|
31 |
+
widget.serializeValue = () => {
|
32 |
+
// Prevent serializing the widget if we have no input linked
|
33 |
+
if (!node.inputs) {
|
34 |
+
return undefined;
|
35 |
+
}
|
36 |
+
let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
|
37 |
+
|
38 |
+
if (!node_input || !node_input.link) {
|
39 |
+
return undefined;
|
40 |
+
}
|
41 |
+
return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
42 |
+
};
|
43 |
+
|
44 |
+
// Hide any linked widgets, e.g. seed+seedControl
|
45 |
+
if (widget.linkedWidgets) {
|
46 |
+
for (const w of widget.linkedWidgets) {
|
47 |
+
hideWidget(node, w, ":" + widget.name);
|
48 |
+
}
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
function showWidget(widget) {
|
53 |
+
widget.type = widget.origType;
|
54 |
+
widget.computeSize = widget.origComputeSize;
|
55 |
+
widget.serializeValue = widget.origSerializeValue;
|
56 |
+
|
57 |
+
delete widget.origType;
|
58 |
+
delete widget.origComputeSize;
|
59 |
+
delete widget.origSerializeValue;
|
60 |
+
|
61 |
+
// Hide any linked widgets, e.g. seed+seedControl
|
62 |
+
if (widget.linkedWidgets) {
|
63 |
+
for (const w of widget.linkedWidgets) {
|
64 |
+
showWidget(w);
|
65 |
+
}
|
66 |
+
}
|
67 |
+
}
|
68 |
+
|
69 |
+
function convertToInput(node, widget, config) {
|
70 |
+
hideWidget(node, widget);
|
71 |
+
|
72 |
+
const { type } = getWidgetType(config);
|
73 |
+
|
74 |
+
// Add input and store widget config for creating on primitive node
|
75 |
+
const sz = node.size;
|
76 |
+
node.addInput(widget.name, type, {
|
77 |
+
widget: { name: widget.name, [GET_CONFIG]: () => config },
|
78 |
+
});
|
79 |
+
|
80 |
+
for (const widget of node.widgets) {
|
81 |
+
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
|
82 |
+
}
|
83 |
+
|
84 |
+
// Restore original size but grow if needed
|
85 |
+
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
|
86 |
+
}
|
87 |
+
|
88 |
+
function convertToWidget(node, widget) {
|
89 |
+
showWidget(widget);
|
90 |
+
const sz = node.size;
|
91 |
+
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
|
92 |
+
|
93 |
+
for (const widget of node.widgets) {
|
94 |
+
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT;
|
95 |
+
}
|
96 |
+
|
97 |
+
// Restore original size but grow if needed
|
98 |
+
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
|
99 |
+
}
|
100 |
+
|
101 |
+
function getWidgetType(config) {
|
102 |
+
// Special handling for COMBO so we restrict links based on the entries
|
103 |
+
let type = config[0];
|
104 |
+
if (type instanceof Array) {
|
105 |
+
type = "COMBO";
|
106 |
+
}
|
107 |
+
return { type };
|
108 |
+
}
|
109 |
+
|
110 |
+
function isValidCombo(combo, obj) {
|
111 |
+
// New input isnt a combo
|
112 |
+
if (!(obj instanceof Array)) {
|
113 |
+
console.log(`connection rejected: tried to connect combo to ${obj}`);
|
114 |
+
return false;
|
115 |
+
}
|
116 |
+
// New imput combo has a different size
|
117 |
+
if (combo.length !== obj.length) {
|
118 |
+
console.log(`connection rejected: combo lists dont match`);
|
119 |
+
return false;
|
120 |
+
}
|
121 |
+
// New input combo has different elements
|
122 |
+
if (combo.find((v, i) => obj[i] !== v)) {
|
123 |
+
console.log(`connection rejected: combo lists dont match`);
|
124 |
+
return false;
|
125 |
+
}
|
126 |
+
|
127 |
+
return true;
|
128 |
+
}
|
129 |
+
|
130 |
+
export function setWidgetConfig(slot, config, target) {
|
131 |
+
if (!slot.widget) return;
|
132 |
+
if (config) {
|
133 |
+
slot.widget[GET_CONFIG] = () => config;
|
134 |
+
slot.widget[TARGET] = target;
|
135 |
+
} else {
|
136 |
+
delete slot.widget;
|
137 |
+
}
|
138 |
+
|
139 |
+
if (slot.link) {
|
140 |
+
const link = app.graph.links[slot.link];
|
141 |
+
if (link) {
|
142 |
+
const originNode = app.graph.getNodeById(link.origin_id);
|
143 |
+
if (originNode.type === "PrimitiveNode") {
|
144 |
+
if (config) {
|
145 |
+
originNode.recreateWidget();
|
146 |
+
} else if(!app.configuringGraph) {
|
147 |
+
originNode.disconnectOutput(0);
|
148 |
+
originNode.onLastDisconnect();
|
149 |
+
}
|
150 |
+
}
|
151 |
+
}
|
152 |
+
}
|
153 |
+
}
|
154 |
+
|
155 |
+
export function mergeIfValid(output, config2, forceUpdate, recreateWidget, config1) {
|
156 |
+
if (!config1) {
|
157 |
+
config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG]();
|
158 |
+
}
|
159 |
+
|
160 |
+
if (config1[0] instanceof Array) {
|
161 |
+
if (!isValidCombo(config1[0], config2[0])) return false;
|
162 |
+
} else if (config1[0] !== config2[0]) {
|
163 |
+
// Types dont match
|
164 |
+
console.log(`connection rejected: types dont match`, config1[0], config2[0]);
|
165 |
+
return false;
|
166 |
+
}
|
167 |
+
|
168 |
+
const keys = new Set([...Object.keys(config1[1] ?? {}), ...Object.keys(config2[1] ?? {})]);
|
169 |
+
|
170 |
+
let customConfig;
|
171 |
+
const getCustomConfig = () => {
|
172 |
+
if (!customConfig) {
|
173 |
+
if (typeof structuredClone === "undefined") {
|
174 |
+
customConfig = JSON.parse(JSON.stringify(config1[1] ?? {}));
|
175 |
+
} else {
|
176 |
+
customConfig = structuredClone(config1[1] ?? {});
|
177 |
+
}
|
178 |
+
}
|
179 |
+
return customConfig;
|
180 |
+
};
|
181 |
+
|
182 |
+
const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
|
183 |
+
for (const k of keys.values()) {
|
184 |
+
if (k !== "default" && k !== "forceInput" && k !== "defaultInput" && k !== "control_after_generate" && k !== "multiline") {
|
185 |
+
let v1 = config1[1][k];
|
186 |
+
let v2 = config2[1]?.[k];
|
187 |
+
|
188 |
+
if (v1 === v2 || (!v1 && !v2)) continue;
|
189 |
+
|
190 |
+
if (isNumber) {
|
191 |
+
if (k === "min") {
|
192 |
+
const theirMax = config2[1]?.["max"];
|
193 |
+
if (theirMax != null && v1 > theirMax) {
|
194 |
+
console.log("connection rejected: min > max", v1, theirMax);
|
195 |
+
return false;
|
196 |
+
}
|
197 |
+
getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2);
|
198 |
+
continue;
|
199 |
+
} else if (k === "max") {
|
200 |
+
const theirMin = config2[1]?.["min"];
|
201 |
+
if (theirMin != null && v1 < theirMin) {
|
202 |
+
console.log("connection rejected: max < min", v1, theirMin);
|
203 |
+
return false;
|
204 |
+
}
|
205 |
+
getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2);
|
206 |
+
continue;
|
207 |
+
} else if (k === "step") {
|
208 |
+
let step;
|
209 |
+
if (v1 == null) {
|
210 |
+
// No current step
|
211 |
+
step = v2;
|
212 |
+
} else if (v2 == null) {
|
213 |
+
// No new step
|
214 |
+
step = v1;
|
215 |
+
} else {
|
216 |
+
if (v1 < v2) {
|
217 |
+
// Ensure v1 is larger for the mod
|
218 |
+
const a = v2;
|
219 |
+
v2 = v1;
|
220 |
+
v1 = a;
|
221 |
+
}
|
222 |
+
if (v1 % v2) {
|
223 |
+
console.log("connection rejected: steps not divisible", "current:", v1, "new:", v2);
|
224 |
+
return false;
|
225 |
+
}
|
226 |
+
|
227 |
+
step = v1;
|
228 |
+
}
|
229 |
+
|
230 |
+
getCustomConfig()[k] = step;
|
231 |
+
continue;
|
232 |
+
}
|
233 |
+
}
|
234 |
+
|
235 |
+
console.log(`connection rejected: config ${k} values dont match`, v1, v2);
|
236 |
+
return false;
|
237 |
+
}
|
238 |
+
}
|
239 |
+
|
240 |
+
if (customConfig || forceUpdate) {
|
241 |
+
if (customConfig) {
|
242 |
+
output.widget[CONFIG] = [config1[0], customConfig];
|
243 |
+
}
|
244 |
+
|
245 |
+
const widget = recreateWidget?.call(this);
|
246 |
+
// When deleting a node this can be null
|
247 |
+
if (widget) {
|
248 |
+
const min = widget.options.min;
|
249 |
+
const max = widget.options.max;
|
250 |
+
if (min != null && widget.value < min) widget.value = min;
|
251 |
+
if (max != null && widget.value > max) widget.value = max;
|
252 |
+
widget.callback(widget.value);
|
253 |
+
}
|
254 |
+
}
|
255 |
+
|
256 |
+
return { customConfig };
|
257 |
+
}
|
258 |
+
|
259 |
+
let useConversionSubmenusSetting;
|
260 |
+
app.registerExtension({
|
261 |
+
name: "Comfy.WidgetInputs",
|
262 |
+
init() {
|
263 |
+
useConversionSubmenusSetting = app.ui.settings.addSetting({
|
264 |
+
id: "Comfy.NodeInputConversionSubmenus",
|
265 |
+
name: "Node widget/input conversion sub-menus",
|
266 |
+
tooltip: "In the node context menu, place the entries that convert between input/widget in sub-menus.",
|
267 |
+
type: "boolean",
|
268 |
+
defaultValue: true,
|
269 |
+
});
|
270 |
+
},
|
271 |
+
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
272 |
+
// Add menu options to conver to/from widgets
|
273 |
+
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
274 |
+
nodeType.prototype.convertWidgetToInput = function (widget) {
|
275 |
+
const config = getConfig.call(this, widget.name) ?? [widget.type, widget.options || {}];
|
276 |
+
if (!isConvertableWidget(widget, config)) return false;
|
277 |
+
convertToInput(this, widget, config);
|
278 |
+
return true;
|
279 |
+
};
|
280 |
+
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
281 |
+
const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined;
|
282 |
+
|
283 |
+
if (this.widgets) {
|
284 |
+
let toInput = [];
|
285 |
+
let toWidget = [];
|
286 |
+
for (const w of this.widgets) {
|
287 |
+
if (w.options?.forceInput) {
|
288 |
+
continue;
|
289 |
+
}
|
290 |
+
if (w.type === CONVERTED_TYPE) {
|
291 |
+
toWidget.push({
|
292 |
+
content: `Convert ${w.name} to widget`,
|
293 |
+
callback: () => convertToWidget(this, w),
|
294 |
+
});
|
295 |
+
} else {
|
296 |
+
const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}];
|
297 |
+
if (isConvertableWidget(w, config)) {
|
298 |
+
toInput.push({
|
299 |
+
content: `Convert ${w.name} to input`,
|
300 |
+
callback: () => convertToInput(this, w, config),
|
301 |
+
});
|
302 |
+
}
|
303 |
+
}
|
304 |
+
}
|
305 |
+
|
306 |
+
//Convert.. main menu
|
307 |
+
if (toInput.length) {
|
308 |
+
if (useConversionSubmenusSetting.value) {
|
309 |
+
options.push({
|
310 |
+
content: "Convert Widget to Input",
|
311 |
+
submenu: {
|
312 |
+
options: toInput,
|
313 |
+
},
|
314 |
+
});
|
315 |
+
} else {
|
316 |
+
options.push(...toInput, null);
|
317 |
+
}
|
318 |
+
}
|
319 |
+
if (toWidget.length) {
|
320 |
+
if (useConversionSubmenusSetting.value) {
|
321 |
+
options.push({
|
322 |
+
content: "Convert Input to Widget",
|
323 |
+
submenu: {
|
324 |
+
options: toWidget,
|
325 |
+
},
|
326 |
+
});
|
327 |
+
} else {
|
328 |
+
options.push(...toWidget, null);
|
329 |
+
}
|
330 |
+
}
|
331 |
+
}
|
332 |
+
|
333 |
+
return r;
|
334 |
+
};
|
335 |
+
|
336 |
+
nodeType.prototype.onGraphConfigured = function () {
|
337 |
+
if (!this.inputs) return;
|
338 |
+
|
339 |
+
for (const input of this.inputs) {
|
340 |
+
if (input.widget) {
|
341 |
+
if (!input.widget[GET_CONFIG]) {
|
342 |
+
input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name);
|
343 |
+
}
|
344 |
+
|
345 |
+
// Cleanup old widget config
|
346 |
+
if (input.widget.config) {
|
347 |
+
if (input.widget.config[0] instanceof Array) {
|
348 |
+
// If we are an old converted combo then replace the input type and the stored link data
|
349 |
+
input.type = "COMBO";
|
350 |
+
|
351 |
+
const link = app.graph.links[input.link];
|
352 |
+
if (link) {
|
353 |
+
link.type = input.type;
|
354 |
+
}
|
355 |
+
}
|
356 |
+
delete input.widget.config;
|
357 |
+
}
|
358 |
+
|
359 |
+
const w = this.widgets.find((w) => w.name === input.widget.name);
|
360 |
+
if (w) {
|
361 |
+
hideWidget(this, w);
|
362 |
+
} else {
|
363 |
+
convertToWidget(this, input);
|
364 |
+
}
|
365 |
+
}
|
366 |
+
}
|
367 |
+
};
|
368 |
+
|
369 |
+
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
370 |
+
nodeType.prototype.onNodeCreated = function () {
|
371 |
+
const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : undefined;
|
372 |
+
|
373 |
+
// When node is created, convert any force/default inputs
|
374 |
+
if (!app.configuringGraph && this.widgets) {
|
375 |
+
for (const w of this.widgets) {
|
376 |
+
if (w?.options?.forceInput || w?.options?.defaultInput) {
|
377 |
+
const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}];
|
378 |
+
convertToInput(this, w, config);
|
379 |
+
}
|
380 |
+
}
|
381 |
+
}
|
382 |
+
|
383 |
+
return r;
|
384 |
+
};
|
385 |
+
|
386 |
+
const origOnConfigure = nodeType.prototype.onConfigure;
|
387 |
+
nodeType.prototype.onConfigure = function () {
|
388 |
+
const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
|
389 |
+
if (!app.configuringGraph && this.inputs) {
|
390 |
+
// On copy + paste of nodes, ensure that widget configs are set up
|
391 |
+
for (const input of this.inputs) {
|
392 |
+
if (input.widget && !input.widget[GET_CONFIG]) {
|
393 |
+
input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name);
|
394 |
+
const w = this.widgets.find((w) => w.name === input.widget.name);
|
395 |
+
if (w) {
|
396 |
+
hideWidget(this, w);
|
397 |
+
}
|
398 |
+
}
|
399 |
+
}
|
400 |
+
}
|
401 |
+
|
402 |
+
return r;
|
403 |
+
};
|
404 |
+
|
405 |
+
function isNodeAtPos(pos) {
|
406 |
+
for (const n of app.graph._nodes) {
|
407 |
+
if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
|
408 |
+
return true;
|
409 |
+
}
|
410 |
+
}
|
411 |
+
return false;
|
412 |
+
}
|
413 |
+
|
414 |
+
// Double click a widget input to automatically attach a primitive
|
415 |
+
const origOnInputDblClick = nodeType.prototype.onInputDblClick;
|
416 |
+
const ignoreDblClick = Symbol();
|
417 |
+
nodeType.prototype.onInputDblClick = function (slot) {
|
418 |
+
const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined;
|
419 |
+
|
420 |
+
const input = this.inputs[slot];
|
421 |
+
if (!input.widget || !input[ignoreDblClick]) {
|
422 |
+
// Not a widget input or already handled input
|
423 |
+
if (!(input.type in ComfyWidgets) && !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array)) {
|
424 |
+
return r; //also Not a ComfyWidgets input or combo (do nothing)
|
425 |
+
}
|
426 |
+
}
|
427 |
+
|
428 |
+
// Create a primitive node
|
429 |
+
const node = LiteGraph.createNode("PrimitiveNode");
|
430 |
+
app.graph.add(node);
|
431 |
+
|
432 |
+
// Calculate a position that wont directly overlap another node
|
433 |
+
const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]];
|
434 |
+
while (isNodeAtPos(pos)) {
|
435 |
+
pos[1] += LiteGraph.NODE_TITLE_HEIGHT;
|
436 |
+
}
|
437 |
+
|
438 |
+
node.pos = pos;
|
439 |
+
node.connect(0, this, slot);
|
440 |
+
node.title = input.name;
|
441 |
+
|
442 |
+
// Prevent adding duplicates due to triple clicking
|
443 |
+
input[ignoreDblClick] = true;
|
444 |
+
setTimeout(() => {
|
445 |
+
delete input[ignoreDblClick];
|
446 |
+
}, 300);
|
447 |
+
|
448 |
+
return r;
|
449 |
+
};
|
450 |
+
|
451 |
+
// Prevent connecting COMBO lists to converted inputs that dont match types
|
452 |
+
const onConnectInput = nodeType.prototype.onConnectInput;
|
453 |
+
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
454 |
+
const v = onConnectInput?.(this, arguments);
|
455 |
+
// Not a combo, ignore
|
456 |
+
if (type !== "COMBO") return v;
|
457 |
+
// Primitive output, allow that to handle
|
458 |
+
if (originNode.outputs[originSlot].widget) return v;
|
459 |
+
|
460 |
+
// Ensure target is also a combo
|
461 |
+
const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0];
|
462 |
+
if (!targetCombo || !(targetCombo instanceof Array)) return v;
|
463 |
+
|
464 |
+
// Check they match
|
465 |
+
const originConfig = originNode.constructor?.nodeData?.output?.[originSlot];
|
466 |
+
if (!originConfig || !isValidCombo(targetCombo, originConfig)) {
|
467 |
+
return false;
|
468 |
+
}
|
469 |
+
|
470 |
+
return v;
|
471 |
+
};
|
472 |
+
},
|
473 |
+
registerCustomNodes() {
|
474 |
+
const replacePropertyName = "Run widget replace on values";
|
475 |
+
class PrimitiveNode {
|
476 |
+
constructor() {
|
477 |
+
this.addOutput("connect to widget input", "*");
|
478 |
+
this.serialize_widgets = true;
|
479 |
+
this.isVirtualNode = true;
|
480 |
+
|
481 |
+
if (!this.properties || !(replacePropertyName in this.properties)) {
|
482 |
+
this.addProperty(replacePropertyName, false, "boolean");
|
483 |
+
}
|
484 |
+
}
|
485 |
+
|
486 |
+
applyToGraph(extraLinks = []) {
|
487 |
+
if (!this.outputs[0].links?.length) return;
|
488 |
+
|
489 |
+
function get_links(node) {
|
490 |
+
let links = [];
|
491 |
+
for (const l of node.outputs[0].links) {
|
492 |
+
const linkInfo = app.graph.links[l];
|
493 |
+
const n = node.graph.getNodeById(linkInfo.target_id);
|
494 |
+
if (n.type == "Reroute") {
|
495 |
+
links = links.concat(get_links(n));
|
496 |
+
} else {
|
497 |
+
links.push(l);
|
498 |
+
}
|
499 |
+
}
|
500 |
+
return links;
|
501 |
+
}
|
502 |
+
|
503 |
+
let links = [...get_links(this).map((l) => app.graph.links[l]), ...extraLinks];
|
504 |
+
let v = this.widgets?.[0].value;
|
505 |
+
if(v && this.properties[replacePropertyName]) {
|
506 |
+
v = applyTextReplacements(app, v);
|
507 |
+
}
|
508 |
+
|
509 |
+
// For each output link copy our value over the original widget value
|
510 |
+
for (const linkInfo of links) {
|
511 |
+
const node = this.graph.getNodeById(linkInfo.target_id);
|
512 |
+
const input = node.inputs[linkInfo.target_slot];
|
513 |
+
let widget;
|
514 |
+
if (input.widget[TARGET]) {
|
515 |
+
widget = input.widget[TARGET];
|
516 |
+
} else {
|
517 |
+
const widgetName = input.widget.name;
|
518 |
+
if (widgetName) {
|
519 |
+
widget = node.widgets.find((w) => w.name === widgetName);
|
520 |
+
}
|
521 |
+
}
|
522 |
+
|
523 |
+
if (widget) {
|
524 |
+
widget.value = v;
|
525 |
+
if (widget.callback) {
|
526 |
+
widget.callback(widget.value, app.canvas, node, app.canvas.graph_mouse, {});
|
527 |
+
}
|
528 |
+
}
|
529 |
+
}
|
530 |
+
}
|
531 |
+
|
532 |
+
refreshComboInNode() {
|
533 |
+
const widget = this.widgets?.[0];
|
534 |
+
if (widget?.type === "combo") {
|
535 |
+
widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0];
|
536 |
+
|
537 |
+
if (!widget.options.values.includes(widget.value)) {
|
538 |
+
widget.value = widget.options.values[0];
|
539 |
+
widget.callback(widget.value);
|
540 |
+
}
|
541 |
+
}
|
542 |
+
}
|
543 |
+
|
544 |
+
onAfterGraphConfigured() {
|
545 |
+
if (this.outputs[0].links?.length && !this.widgets?.length) {
|
546 |
+
if (!this.#onFirstConnection()) return;
|
547 |
+
|
548 |
+
// Populate widget values from config data
|
549 |
+
if (this.widgets) {
|
550 |
+
for (let i = 0; i < this.widgets_values.length; i++) {
|
551 |
+
const w = this.widgets[i];
|
552 |
+
if (w) {
|
553 |
+
w.value = this.widgets_values[i];
|
554 |
+
}
|
555 |
+
}
|
556 |
+
}
|
557 |
+
|
558 |
+
// Merge values if required
|
559 |
+
this.#mergeWidgetConfig();
|
560 |
+
}
|
561 |
+
}
|
562 |
+
|
563 |
+
onConnectionsChange(_, index, connected) {
|
564 |
+
if (app.configuringGraph) {
|
565 |
+
// Dont run while the graph is still setting up
|
566 |
+
return;
|
567 |
+
}
|
568 |
+
|
569 |
+
const links = this.outputs[0].links;
|
570 |
+
if (connected) {
|
571 |
+
if (links?.length && !this.widgets?.length) {
|
572 |
+
this.#onFirstConnection();
|
573 |
+
}
|
574 |
+
} else {
|
575 |
+
// We may have removed a link that caused the constraints to change
|
576 |
+
this.#mergeWidgetConfig();
|
577 |
+
|
578 |
+
if (!links?.length) {
|
579 |
+
this.onLastDisconnect();
|
580 |
+
}
|
581 |
+
}
|
582 |
+
}
|
583 |
+
|
584 |
+
onConnectOutput(slot, type, input, target_node, target_slot) {
|
585 |
+
// Fires before the link is made allowing us to reject it if it isn't valid
|
586 |
+
// No widget, we cant connect
|
587 |
+
if (!input.widget) {
|
588 |
+
if (!(input.type in ComfyWidgets)) return false;
|
589 |
+
}
|
590 |
+
|
591 |
+
if (this.outputs[slot].links?.length) {
|
592 |
+
const valid = this.#isValidConnection(input);
|
593 |
+
if (valid) {
|
594 |
+
// On connect of additional outputs, copy our value to their widget
|
595 |
+
this.applyToGraph([{ target_id: target_node.id, target_slot }]);
|
596 |
+
}
|
597 |
+
return valid;
|
598 |
+
}
|
599 |
+
}
|
600 |
+
|
601 |
+
#onFirstConnection(recreating) {
|
602 |
+
// First connection can fire before the graph is ready on initial load so random things can be missing
|
603 |
+
if (!this.outputs[0].links) {
|
604 |
+
this.onLastDisconnect();
|
605 |
+
return;
|
606 |
+
}
|
607 |
+
const linkId = this.outputs[0].links[0];
|
608 |
+
const link = this.graph.links[linkId];
|
609 |
+
if (!link) return;
|
610 |
+
|
611 |
+
const theirNode = this.graph.getNodeById(link.target_id);
|
612 |
+
if (!theirNode || !theirNode.inputs) return;
|
613 |
+
|
614 |
+
const input = theirNode.inputs[link.target_slot];
|
615 |
+
if (!input) return;
|
616 |
+
|
617 |
+
let widget;
|
618 |
+
if (!input.widget) {
|
619 |
+
if (!(input.type in ComfyWidgets)) return;
|
620 |
+
widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] }; //fake widget
|
621 |
+
} else {
|
622 |
+
widget = input.widget;
|
623 |
+
}
|
624 |
+
|
625 |
+
const config = widget[GET_CONFIG]?.();
|
626 |
+
if (!config) return;
|
627 |
+
|
628 |
+
const { type } = getWidgetType(config);
|
629 |
+
// Update our output to restrict to the widget type
|
630 |
+
this.outputs[0].type = type;
|
631 |
+
this.outputs[0].name = type;
|
632 |
+
this.outputs[0].widget = widget;
|
633 |
+
|
634 |
+
this.#createWidget(widget[CONFIG] ?? config, theirNode, widget.name, recreating, widget[TARGET]);
|
635 |
+
}
|
636 |
+
|
637 |
+
#createWidget(inputData, node, widgetName, recreating, targetWidget) {
|
638 |
+
let type = inputData[0];
|
639 |
+
|
640 |
+
if (type instanceof Array) {
|
641 |
+
type = "COMBO";
|
642 |
+
}
|
643 |
+
|
644 |
+
let widget;
|
645 |
+
if (type in ComfyWidgets) {
|
646 |
+
widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget;
|
647 |
+
} else {
|
648 |
+
widget = this.addWidget(type, "value", null, () => {}, {});
|
649 |
+
}
|
650 |
+
|
651 |
+
if (targetWidget) {
|
652 |
+
widget.value = targetWidget.value;
|
653 |
+
} else if (node?.widgets && widget) {
|
654 |
+
const theirWidget = node.widgets.find((w) => w.name === widgetName);
|
655 |
+
if (theirWidget) {
|
656 |
+
widget.value = theirWidget.value;
|
657 |
+
}
|
658 |
+
}
|
659 |
+
|
660 |
+
if (!inputData?.[1]?.control_after_generate && (widget.type === "number" || widget.type === "combo")) {
|
661 |
+
let control_value = this.widgets_values?.[1];
|
662 |
+
if (!control_value) {
|
663 |
+
control_value = "fixed";
|
664 |
+
}
|
665 |
+
addValueControlWidgets(this, widget, control_value, undefined, inputData);
|
666 |
+
let filter = this.widgets_values?.[2];
|
667 |
+
if (filter && this.widgets.length === 3) {
|
668 |
+
this.widgets[2].value = filter;
|
669 |
+
}
|
670 |
+
}
|
671 |
+
|
672 |
+
// Restore any saved control values
|
673 |
+
const controlValues = this.controlValues;
|
674 |
+
if(this.lastType === this.widgets[0].type && controlValues?.length === this.widgets.length - 1) {
|
675 |
+
for(let i = 0; i < controlValues.length; i++) {
|
676 |
+
this.widgets[i + 1].value = controlValues[i];
|
677 |
+
}
|
678 |
+
}
|
679 |
+
|
680 |
+
// When our value changes, update other widgets to reflect our changes
|
681 |
+
// e.g. so LoadImage shows correct image
|
682 |
+
const callback = widget.callback;
|
683 |
+
const self = this;
|
684 |
+
widget.callback = function () {
|
685 |
+
const r = callback ? callback.apply(this, arguments) : undefined;
|
686 |
+
self.applyToGraph();
|
687 |
+
return r;
|
688 |
+
};
|
689 |
+
|
690 |
+
if (!recreating) {
|
691 |
+
// Grow our node if required
|
692 |
+
const sz = this.computeSize();
|
693 |
+
if (this.size[0] < sz[0]) {
|
694 |
+
this.size[0] = sz[0];
|
695 |
+
}
|
696 |
+
if (this.size[1] < sz[1]) {
|
697 |
+
this.size[1] = sz[1];
|
698 |
+
}
|
699 |
+
|
700 |
+
requestAnimationFrame(() => {
|
701 |
+
if (this.onResize) {
|
702 |
+
this.onResize(this.size);
|
703 |
+
}
|
704 |
+
});
|
705 |
+
}
|
706 |
+
}
|
707 |
+
|
708 |
+
recreateWidget() {
|
709 |
+
const values = this.widgets?.map((w) => w.value);
|
710 |
+
this.#removeWidgets();
|
711 |
+
this.#onFirstConnection(true);
|
712 |
+
if (values?.length) {
|
713 |
+
for (let i = 0; i < this.widgets?.length; i++) this.widgets[i].value = values[i];
|
714 |
+
}
|
715 |
+
return this.widgets?.[0];
|
716 |
+
}
|
717 |
+
|
718 |
+
#mergeWidgetConfig() {
|
719 |
+
// Merge widget configs if the node has multiple outputs
|
720 |
+
const output = this.outputs[0];
|
721 |
+
const links = output.links;
|
722 |
+
|
723 |
+
const hasConfig = !!output.widget[CONFIG];
|
724 |
+
if (hasConfig) {
|
725 |
+
delete output.widget[CONFIG];
|
726 |
+
}
|
727 |
+
|
728 |
+
if (links?.length < 2 && hasConfig) {
|
729 |
+
// Copy the widget options from the source
|
730 |
+
if (links.length) {
|
731 |
+
this.recreateWidget();
|
732 |
+
}
|
733 |
+
|
734 |
+
return;
|
735 |
+
}
|
736 |
+
|
737 |
+
const config1 = output.widget[GET_CONFIG]();
|
738 |
+
const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
|
739 |
+
if (!isNumber) return;
|
740 |
+
|
741 |
+
for (const linkId of links) {
|
742 |
+
const link = app.graph.links[linkId];
|
743 |
+
if (!link) continue; // Can be null when removing a node
|
744 |
+
|
745 |
+
const theirNode = app.graph.getNodeById(link.target_id);
|
746 |
+
const theirInput = theirNode.inputs[link.target_slot];
|
747 |
+
|
748 |
+
// Call is valid connection so it can merge the configs when validating
|
749 |
+
this.#isValidConnection(theirInput, hasConfig);
|
750 |
+
}
|
751 |
+
}
|
752 |
+
|
753 |
+
#isValidConnection(input, forceUpdate) {
|
754 |
+
// Only allow connections where the configs match
|
755 |
+
const output = this.outputs[0];
|
756 |
+
const config2 = input.widget[GET_CONFIG]();
|
757 |
+
return !!mergeIfValid.call(this, output, config2, forceUpdate, this.recreateWidget);
|
758 |
+
}
|
759 |
+
|
760 |
+
#removeWidgets() {
|
761 |
+
if (this.widgets) {
|
762 |
+
// Allow widgets to cleanup
|
763 |
+
for (const w of this.widgets) {
|
764 |
+
if (w.onRemove) {
|
765 |
+
w.onRemove();
|
766 |
+
}
|
767 |
+
}
|
768 |
+
|
769 |
+
// Temporarily store the current values in case the node is being recreated
|
770 |
+
// e.g. by group node conversion
|
771 |
+
this.controlValues = [];
|
772 |
+
this.lastType = this.widgets[0]?.type;
|
773 |
+
for(let i = 1; i < this.widgets.length; i++) {
|
774 |
+
this.controlValues.push(this.widgets[i].value);
|
775 |
+
}
|
776 |
+
setTimeout(() => { delete this.lastType; delete this.controlValues }, 15);
|
777 |
+
this.widgets.length = 0;
|
778 |
+
}
|
779 |
+
}
|
780 |
+
|
781 |
+
onLastDisconnect() {
|
782 |
+
// We cant remove + re-add the output here as if you drag a link over the same link
|
783 |
+
// it removes, then re-adds, causing it to break
|
784 |
+
this.outputs[0].type = "*";
|
785 |
+
this.outputs[0].name = "connect to widget input";
|
786 |
+
delete this.outputs[0].widget;
|
787 |
+
|
788 |
+
this.#removeWidgets();
|
789 |
+
}
|
790 |
+
}
|
791 |
+
|
792 |
+
LiteGraph.registerNodeType(
|
793 |
+
"PrimitiveNode",
|
794 |
+
Object.assign(PrimitiveNode, {
|
795 |
+
title: "Primitive",
|
796 |
+
})
|
797 |
+
);
|
798 |
+
PrimitiveNode.category = "utils";
|
799 |
+
},
|
800 |
+
});
|
ComfyUI/web/extensions/logging.js.example
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app } from "../scripts/app.js";
|
2 |
+
|
3 |
+
const ext = {
|
4 |
+
// Unique name for the extension
|
5 |
+
name: "Example.LoggingExtension",
|
6 |
+
async init(app) {
|
7 |
+
// Any initial setup to run as soon as the page loads
|
8 |
+
console.log("[logging]", "extension init");
|
9 |
+
},
|
10 |
+
async setup(app) {
|
11 |
+
// Any setup to run after the app is created
|
12 |
+
console.log("[logging]", "extension setup");
|
13 |
+
},
|
14 |
+
async addCustomNodeDefs(defs, app) {
|
15 |
+
// Add custom node definitions
|
16 |
+
// These definitions will be configured and registered automatically
|
17 |
+
// defs is a lookup core nodes, add yours into this
|
18 |
+
console.log("[logging]", "add custom node definitions", "current nodes:", Object.keys(defs));
|
19 |
+
},
|
20 |
+
async getCustomWidgets(app) {
|
21 |
+
// Return custom widget types
|
22 |
+
// See ComfyWidgets for widget examples
|
23 |
+
console.log("[logging]", "provide custom widgets");
|
24 |
+
},
|
25 |
+
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
26 |
+
// Run custom logic before a node definition is registered with the graph
|
27 |
+
console.log("[logging]", "before register node: ", nodeType, nodeData);
|
28 |
+
|
29 |
+
// This fires for every node definition so only log once
|
30 |
+
delete ext.beforeRegisterNodeDef;
|
31 |
+
},
|
32 |
+
async registerCustomNodes(app) {
|
33 |
+
// Register any custom node implementations here allowing for more flexability than a custom node def
|
34 |
+
console.log("[logging]", "register custom nodes");
|
35 |
+
},
|
36 |
+
loadedGraphNode(node, app) {
|
37 |
+
// Fires for each node when loading/dragging/etc a workflow json or png
|
38 |
+
// If you break something in the backend and want to patch workflows in the frontend
|
39 |
+
// This is the place to do this
|
40 |
+
console.log("[logging]", "loaded graph node: ", node);
|
41 |
+
|
42 |
+
// This fires for every node on each load so only log once
|
43 |
+
delete ext.loadedGraphNode;
|
44 |
+
},
|
45 |
+
nodeCreated(node, app) {
|
46 |
+
// Fires every time a node is constructed
|
47 |
+
// You can modify widgets/add handlers/etc here
|
48 |
+
console.log("[logging]", "node created: ", node);
|
49 |
+
|
50 |
+
// This fires for every node so only log once
|
51 |
+
delete ext.nodeCreated;
|
52 |
+
}
|
53 |
+
};
|
54 |
+
|
55 |
+
app.registerExtension(ext);
|
ComfyUI/web/fonts/materialdesignicons-webfont.woff2
ADDED
Binary file (403 kB). View file
|
|
ComfyUI/web/index.html
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<title>ComfyUI</title>
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
7 |
+
<link rel="stylesheet" type="text/css" href="./lib/litegraph.css" />
|
8 |
+
<link rel="stylesheet" type="text/css" href="./lib/materialdesignicons.min.css" />
|
9 |
+
<link rel="stylesheet" type="text/css" href="./style.css" />
|
10 |
+
<link rel="stylesheet" type="text/css" href="./user.css" />
|
11 |
+
<script type="text/javascript" src="./lib/litegraph.core.js"></script>
|
12 |
+
<script type="text/javascript" src="./lib/litegraph.extensions.js" defer></script>
|
13 |
+
<script type="module">
|
14 |
+
import { app } from "./scripts/app.js";
|
15 |
+
await app.setup();
|
16 |
+
window.app = app;
|
17 |
+
window.graph = app.graph;
|
18 |
+
</script>
|
19 |
+
</head>
|
20 |
+
<body class="litegraph">
|
21 |
+
<div id="comfy-user-selection" class="comfy-user-selection" style="display: none;">
|
22 |
+
<main class="comfy-user-selection-inner">
|
23 |
+
<h1>ComfyUI</h1>
|
24 |
+
<form>
|
25 |
+
<section>
|
26 |
+
<label>New user:
|
27 |
+
<input placeholder="Enter a username" />
|
28 |
+
</label>
|
29 |
+
</section>
|
30 |
+
<div class="comfy-user-existing">
|
31 |
+
<span class="or-separator">OR</span>
|
32 |
+
<section>
|
33 |
+
<label>
|
34 |
+
Existing user:
|
35 |
+
<select>
|
36 |
+
<option hidden disabled selected value> Select a user </option>
|
37 |
+
</select>
|
38 |
+
</label>
|
39 |
+
</section>
|
40 |
+
</div>
|
41 |
+
<footer>
|
42 |
+
<span class="comfy-user-error"> </span>
|
43 |
+
<button class="comfy-btn comfy-user-button-next">Next</button>
|
44 |
+
</footer>
|
45 |
+
</form>
|
46 |
+
</main>
|
47 |
+
</div>
|
48 |
+
</body>
|
49 |
+
</html>
|
ComfyUI/web/jsconfig.json
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"baseUrl": ".",
|
4 |
+
"paths": {
|
5 |
+
"/*": ["./*"]
|
6 |
+
},
|
7 |
+
"lib": ["DOM", "ES2022", "DOM.Iterable"],
|
8 |
+
"target": "ES2015",
|
9 |
+
"module": "es2020"
|
10 |
+
},
|
11 |
+
"include": ["."]
|
12 |
+
}
|
ComfyUI/web/lib/litegraph.core.js
ADDED
The diff for this file is too large to render.
See raw diff
|
|
ComfyUI/web/lib/litegraph.css
ADDED
@@ -0,0 +1,693 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* this CSS contains only the basic CSS needed to run the app and use it */
|
2 |
+
|
3 |
+
.lgraphcanvas {
|
4 |
+
/*cursor: crosshair;*/
|
5 |
+
user-select: none;
|
6 |
+
-moz-user-select: none;
|
7 |
+
-webkit-user-select: none;
|
8 |
+
outline: none;
|
9 |
+
font-family: Tahoma, sans-serif;
|
10 |
+
}
|
11 |
+
|
12 |
+
.lgraphcanvas * {
|
13 |
+
box-sizing: border-box;
|
14 |
+
}
|
15 |
+
|
16 |
+
.litegraph.litecontextmenu {
|
17 |
+
font-family: Tahoma, sans-serif;
|
18 |
+
position: fixed;
|
19 |
+
top: 100px;
|
20 |
+
left: 100px;
|
21 |
+
min-width: 100px;
|
22 |
+
color: #aaf;
|
23 |
+
padding: 0;
|
24 |
+
box-shadow: 0 0 10px black !important;
|
25 |
+
background-color: #2e2e2e !important;
|
26 |
+
z-index: 10;
|
27 |
+
}
|
28 |
+
|
29 |
+
.litegraph.litecontextmenu.dark {
|
30 |
+
background-color: #000 !important;
|
31 |
+
}
|
32 |
+
|
33 |
+
.litegraph.litecontextmenu .litemenu-title img {
|
34 |
+
margin-top: 2px;
|
35 |
+
margin-left: 2px;
|
36 |
+
margin-right: 4px;
|
37 |
+
}
|
38 |
+
|
39 |
+
.litegraph.litecontextmenu .litemenu-entry {
|
40 |
+
margin: 2px;
|
41 |
+
padding: 2px;
|
42 |
+
}
|
43 |
+
|
44 |
+
.litegraph.litecontextmenu .litemenu-entry.submenu {
|
45 |
+
background-color: #2e2e2e !important;
|
46 |
+
}
|
47 |
+
|
48 |
+
.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
|
49 |
+
background-color: #000 !important;
|
50 |
+
}
|
51 |
+
|
52 |
+
.litegraph .litemenubar ul {
|
53 |
+
font-family: Tahoma, sans-serif;
|
54 |
+
margin: 0;
|
55 |
+
padding: 0;
|
56 |
+
}
|
57 |
+
|
58 |
+
.litegraph .litemenubar li {
|
59 |
+
font-size: 14px;
|
60 |
+
color: #999;
|
61 |
+
display: inline-block;
|
62 |
+
min-width: 50px;
|
63 |
+
padding-left: 10px;
|
64 |
+
padding-right: 10px;
|
65 |
+
user-select: none;
|
66 |
+
-moz-user-select: none;
|
67 |
+
-webkit-user-select: none;
|
68 |
+
cursor: pointer;
|
69 |
+
}
|
70 |
+
|
71 |
+
.litegraph .litemenubar li:hover {
|
72 |
+
background-color: #777;
|
73 |
+
color: #eee;
|
74 |
+
}
|
75 |
+
|
76 |
+
.litegraph .litegraph .litemenubar-panel {
|
77 |
+
position: absolute;
|
78 |
+
top: 5px;
|
79 |
+
left: 5px;
|
80 |
+
min-width: 100px;
|
81 |
+
background-color: #444;
|
82 |
+
box-shadow: 0 0 3px black;
|
83 |
+
padding: 4px;
|
84 |
+
border-bottom: 2px solid #aaf;
|
85 |
+
z-index: 10;
|
86 |
+
}
|
87 |
+
|
88 |
+
.litegraph .litemenu-entry,
|
89 |
+
.litemenu-title {
|
90 |
+
font-size: 12px;
|
91 |
+
color: #aaa;
|
92 |
+
padding: 0 0 0 4px;
|
93 |
+
margin: 2px;
|
94 |
+
padding-left: 2px;
|
95 |
+
-moz-user-select: none;
|
96 |
+
-webkit-user-select: none;
|
97 |
+
user-select: none;
|
98 |
+
cursor: pointer;
|
99 |
+
}
|
100 |
+
|
101 |
+
.litegraph .litemenu-entry .icon {
|
102 |
+
display: inline-block;
|
103 |
+
width: 12px;
|
104 |
+
height: 12px;
|
105 |
+
margin: 2px;
|
106 |
+
vertical-align: top;
|
107 |
+
}
|
108 |
+
|
109 |
+
.litegraph .litemenu-entry.checked .icon {
|
110 |
+
background-color: #aaf;
|
111 |
+
}
|
112 |
+
|
113 |
+
.litegraph .litemenu-entry .more {
|
114 |
+
float: right;
|
115 |
+
padding-right: 5px;
|
116 |
+
}
|
117 |
+
|
118 |
+
.litegraph .litemenu-entry.disabled {
|
119 |
+
opacity: 0.5;
|
120 |
+
cursor: default;
|
121 |
+
}
|
122 |
+
|
123 |
+
.litegraph .litemenu-entry.separator {
|
124 |
+
display: block;
|
125 |
+
border-top: 1px solid #333;
|
126 |
+
border-bottom: 1px solid #666;
|
127 |
+
width: 100%;
|
128 |
+
height: 0px;
|
129 |
+
margin: 3px 0 2px 0;
|
130 |
+
background-color: transparent;
|
131 |
+
padding: 0 !important;
|
132 |
+
cursor: default !important;
|
133 |
+
}
|
134 |
+
|
135 |
+
.litegraph .litemenu-entry.has_submenu {
|
136 |
+
border-right: 2px solid cyan;
|
137 |
+
}
|
138 |
+
|
139 |
+
.litegraph .litemenu-title {
|
140 |
+
color: #dde;
|
141 |
+
background-color: #111;
|
142 |
+
margin: 0;
|
143 |
+
padding: 2px;
|
144 |
+
cursor: default;
|
145 |
+
}
|
146 |
+
|
147 |
+
.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) {
|
148 |
+
background-color: #444 !important;
|
149 |
+
color: #eee;
|
150 |
+
transition: all 0.2s;
|
151 |
+
}
|
152 |
+
|
153 |
+
.litegraph .litemenu-entry .property_name {
|
154 |
+
display: inline-block;
|
155 |
+
text-align: left;
|
156 |
+
min-width: 80px;
|
157 |
+
min-height: 1.2em;
|
158 |
+
}
|
159 |
+
|
160 |
+
.litegraph .litemenu-entry .property_value {
|
161 |
+
display: inline-block;
|
162 |
+
background-color: rgba(0, 0, 0, 0.5);
|
163 |
+
text-align: right;
|
164 |
+
min-width: 80px;
|
165 |
+
min-height: 1.2em;
|
166 |
+
vertical-align: middle;
|
167 |
+
padding-right: 10px;
|
168 |
+
}
|
169 |
+
|
170 |
+
.litegraph.litesearchbox {
|
171 |
+
font-family: Tahoma, sans-serif;
|
172 |
+
position: absolute;
|
173 |
+
background-color: rgba(0, 0, 0, 0.5);
|
174 |
+
padding-top: 4px;
|
175 |
+
}
|
176 |
+
|
177 |
+
.litegraph.litesearchbox input,
|
178 |
+
.litegraph.litesearchbox select {
|
179 |
+
margin-top: 3px;
|
180 |
+
min-width: 60px;
|
181 |
+
min-height: 1.5em;
|
182 |
+
background-color: black;
|
183 |
+
border: 0;
|
184 |
+
color: white;
|
185 |
+
padding-left: 10px;
|
186 |
+
margin-right: 5px;
|
187 |
+
max-width: 300px;
|
188 |
+
}
|
189 |
+
|
190 |
+
.litegraph.litesearchbox .name {
|
191 |
+
display: inline-block;
|
192 |
+
min-width: 60px;
|
193 |
+
min-height: 1.5em;
|
194 |
+
padding-left: 10px;
|
195 |
+
}
|
196 |
+
|
197 |
+
.litegraph.litesearchbox .helper {
|
198 |
+
overflow: auto;
|
199 |
+
max-height: 200px;
|
200 |
+
margin-top: 2px;
|
201 |
+
}
|
202 |
+
|
203 |
+
.litegraph.lite-search-item {
|
204 |
+
font-family: Tahoma, sans-serif;
|
205 |
+
background-color: rgba(0, 0, 0, 0.5);
|
206 |
+
color: white;
|
207 |
+
padding-top: 2px;
|
208 |
+
}
|
209 |
+
|
210 |
+
.litegraph.lite-search-item.not_in_filter{
|
211 |
+
/*background-color: rgba(50, 50, 50, 0.5);*/
|
212 |
+
/*color: #999;*/
|
213 |
+
color: #B99;
|
214 |
+
font-style: italic;
|
215 |
+
}
|
216 |
+
|
217 |
+
.litegraph.lite-search-item.generic_type{
|
218 |
+
/*background-color: rgba(50, 50, 50, 0.5);*/
|
219 |
+
/*color: #DD9;*/
|
220 |
+
color: #999;
|
221 |
+
font-style: italic;
|
222 |
+
}
|
223 |
+
|
224 |
+
.litegraph.lite-search-item:hover,
|
225 |
+
.litegraph.lite-search-item.selected {
|
226 |
+
cursor: pointer;
|
227 |
+
background-color: white;
|
228 |
+
color: black;
|
229 |
+
}
|
230 |
+
|
231 |
+
.litegraph.lite-search-item-type {
|
232 |
+
display: inline-block;
|
233 |
+
background: rgba(0,0,0,0.2);
|
234 |
+
margin-left: 5px;
|
235 |
+
font-size: 14px;
|
236 |
+
padding: 2px 5px;
|
237 |
+
position: relative;
|
238 |
+
top: -2px;
|
239 |
+
opacity: 0.8;
|
240 |
+
border-radius: 4px;
|
241 |
+
}
|
242 |
+
|
243 |
+
/* DIALOGs ******/
|
244 |
+
|
245 |
+
.litegraph .dialog {
|
246 |
+
position: absolute;
|
247 |
+
top: 50%;
|
248 |
+
left: 50%;
|
249 |
+
margin-top: -150px;
|
250 |
+
margin-left: -200px;
|
251 |
+
|
252 |
+
background-color: #2A2A2A;
|
253 |
+
|
254 |
+
min-width: 400px;
|
255 |
+
min-height: 200px;
|
256 |
+
box-shadow: 0 0 4px #111;
|
257 |
+
border-radius: 6px;
|
258 |
+
}
|
259 |
+
|
260 |
+
.litegraph .dialog.settings {
|
261 |
+
left: 10px;
|
262 |
+
top: 10px;
|
263 |
+
height: calc( 100% - 20px );
|
264 |
+
margin: auto;
|
265 |
+
max-width: 50%;
|
266 |
+
}
|
267 |
+
|
268 |
+
.litegraph .dialog.centered {
|
269 |
+
top: 50px;
|
270 |
+
left: 50%;
|
271 |
+
position: absolute;
|
272 |
+
transform: translateX(-50%);
|
273 |
+
min-width: 600px;
|
274 |
+
min-height: 300px;
|
275 |
+
height: calc( 100% - 100px );
|
276 |
+
margin: auto;
|
277 |
+
}
|
278 |
+
|
279 |
+
.litegraph .dialog .close {
|
280 |
+
float: right;
|
281 |
+
margin: 4px;
|
282 |
+
margin-right: 10px;
|
283 |
+
cursor: pointer;
|
284 |
+
font-size: 1.4em;
|
285 |
+
}
|
286 |
+
|
287 |
+
.litegraph .dialog .close:hover {
|
288 |
+
color: white;
|
289 |
+
}
|
290 |
+
|
291 |
+
.litegraph .dialog .dialog-header {
|
292 |
+
color: #AAA;
|
293 |
+
border-bottom: 1px solid #161616;
|
294 |
+
}
|
295 |
+
|
296 |
+
.litegraph .dialog .dialog-header { height: 40px; }
|
297 |
+
.litegraph .dialog .dialog-footer { height: 50px; padding: 10px; border-top: 1px solid #1a1a1a;}
|
298 |
+
|
299 |
+
.litegraph .dialog .dialog-header .dialog-title {
|
300 |
+
font: 20px "Arial";
|
301 |
+
margin: 4px;
|
302 |
+
padding: 4px 10px;
|
303 |
+
display: inline-block;
|
304 |
+
}
|
305 |
+
|
306 |
+
.litegraph .dialog .dialog-content, .litegraph .dialog .dialog-alt-content {
|
307 |
+
height: calc(100% - 90px);
|
308 |
+
width: 100%;
|
309 |
+
min-height: 100px;
|
310 |
+
display: inline-block;
|
311 |
+
color: #AAA;
|
312 |
+
/*background-color: black;*/
|
313 |
+
overflow: auto;
|
314 |
+
}
|
315 |
+
|
316 |
+
.litegraph .dialog .dialog-content h3 {
|
317 |
+
margin: 10px;
|
318 |
+
}
|
319 |
+
|
320 |
+
.litegraph .dialog .dialog-content .connections {
|
321 |
+
flex-direction: row;
|
322 |
+
}
|
323 |
+
|
324 |
+
.litegraph .dialog .dialog-content .connections .connections_side {
|
325 |
+
width: calc(50% - 5px);
|
326 |
+
min-height: 100px;
|
327 |
+
background-color: black;
|
328 |
+
display: flex;
|
329 |
+
}
|
330 |
+
|
331 |
+
.litegraph .dialog .node_type {
|
332 |
+
font-size: 1.2em;
|
333 |
+
display: block;
|
334 |
+
margin: 10px;
|
335 |
+
}
|
336 |
+
|
337 |
+
.litegraph .dialog .node_desc {
|
338 |
+
opacity: 0.5;
|
339 |
+
display: block;
|
340 |
+
margin: 10px;
|
341 |
+
}
|
342 |
+
|
343 |
+
.litegraph .dialog .separator {
|
344 |
+
display: block;
|
345 |
+
width: calc( 100% - 4px );
|
346 |
+
height: 1px;
|
347 |
+
border-top: 1px solid #000;
|
348 |
+
border-bottom: 1px solid #333;
|
349 |
+
margin: 10px 2px;
|
350 |
+
padding: 0;
|
351 |
+
}
|
352 |
+
|
353 |
+
.litegraph .dialog .property {
|
354 |
+
margin-bottom: 2px;
|
355 |
+
padding: 4px;
|
356 |
+
}
|
357 |
+
|
358 |
+
.litegraph .dialog .property:hover {
|
359 |
+
background: #545454;
|
360 |
+
}
|
361 |
+
|
362 |
+
.litegraph .dialog .property_name {
|
363 |
+
color: #737373;
|
364 |
+
display: inline-block;
|
365 |
+
text-align: left;
|
366 |
+
vertical-align: top;
|
367 |
+
width: 160px;
|
368 |
+
padding-left: 4px;
|
369 |
+
overflow: hidden;
|
370 |
+
margin-right: 6px;
|
371 |
+
}
|
372 |
+
|
373 |
+
.litegraph .dialog .property:hover .property_name {
|
374 |
+
color: white;
|
375 |
+
}
|
376 |
+
|
377 |
+
.litegraph .dialog .property_value {
|
378 |
+
display: inline-block;
|
379 |
+
text-align: right;
|
380 |
+
color: #AAA;
|
381 |
+
background-color: #1A1A1A;
|
382 |
+
/*width: calc( 100% - 122px );*/
|
383 |
+
max-width: calc( 100% - 162px );
|
384 |
+
min-width: 200px;
|
385 |
+
max-height: 300px;
|
386 |
+
min-height: 20px;
|
387 |
+
padding: 4px;
|
388 |
+
padding-right: 12px;
|
389 |
+
overflow: hidden;
|
390 |
+
cursor: pointer;
|
391 |
+
border-radius: 3px;
|
392 |
+
}
|
393 |
+
|
394 |
+
.litegraph .dialog .property_value:hover {
|
395 |
+
color: white;
|
396 |
+
}
|
397 |
+
|
398 |
+
.litegraph .dialog .property.boolean .property_value {
|
399 |
+
padding-right: 30px;
|
400 |
+
color: #A88;
|
401 |
+
/*width: auto;
|
402 |
+
float: right;*/
|
403 |
+
}
|
404 |
+
|
405 |
+
.litegraph .dialog .property.boolean.bool-on .property_name{
|
406 |
+
color: #8A8;
|
407 |
+
}
|
408 |
+
.litegraph .dialog .property.boolean.bool-on .property_value{
|
409 |
+
color: #8A8;
|
410 |
+
}
|
411 |
+
|
412 |
+
.litegraph .dialog .btn {
|
413 |
+
border: 0;
|
414 |
+
border-radius: 4px;
|
415 |
+
padding: 4px 20px;
|
416 |
+
margin-left: 0px;
|
417 |
+
background-color: #060606;
|
418 |
+
color: #8e8e8e;
|
419 |
+
}
|
420 |
+
|
421 |
+
.litegraph .dialog .btn:hover {
|
422 |
+
background-color: #111;
|
423 |
+
color: #FFF;
|
424 |
+
}
|
425 |
+
|
426 |
+
.litegraph .dialog .btn.delete:hover {
|
427 |
+
background-color: #F33;
|
428 |
+
color: black;
|
429 |
+
}
|
430 |
+
|
431 |
+
.litegraph .subgraph_property {
|
432 |
+
padding: 4px;
|
433 |
+
}
|
434 |
+
|
435 |
+
.litegraph .subgraph_property:hover {
|
436 |
+
background-color: #333;
|
437 |
+
}
|
438 |
+
|
439 |
+
.litegraph .subgraph_property.extra {
|
440 |
+
margin-top: 8px;
|
441 |
+
}
|
442 |
+
|
443 |
+
.litegraph .subgraph_property span.name {
|
444 |
+
font-size: 1.3em;
|
445 |
+
padding-left: 4px;
|
446 |
+
}
|
447 |
+
|
448 |
+
.litegraph .subgraph_property span.type {
|
449 |
+
opacity: 0.5;
|
450 |
+
margin-right: 20px;
|
451 |
+
padding-left: 4px;
|
452 |
+
}
|
453 |
+
|
454 |
+
.litegraph .subgraph_property span.label {
|
455 |
+
display: inline-block;
|
456 |
+
width: 60px;
|
457 |
+
padding: 0px 10px;
|
458 |
+
}
|
459 |
+
|
460 |
+
.litegraph .subgraph_property input {
|
461 |
+
width: 140px;
|
462 |
+
color: #999;
|
463 |
+
background-color: #1A1A1A;
|
464 |
+
border-radius: 4px;
|
465 |
+
border: 0;
|
466 |
+
margin-right: 10px;
|
467 |
+
padding: 4px;
|
468 |
+
padding-left: 10px;
|
469 |
+
}
|
470 |
+
|
471 |
+
.litegraph .subgraph_property button {
|
472 |
+
background-color: #1c1c1c;
|
473 |
+
color: #aaa;
|
474 |
+
border: 0;
|
475 |
+
border-radius: 2px;
|
476 |
+
padding: 4px 10px;
|
477 |
+
cursor: pointer;
|
478 |
+
}
|
479 |
+
|
480 |
+
.litegraph .subgraph_property.extra {
|
481 |
+
color: #ccc;
|
482 |
+
}
|
483 |
+
|
484 |
+
.litegraph .subgraph_property.extra input {
|
485 |
+
background-color: #111;
|
486 |
+
}
|
487 |
+
|
488 |
+
.litegraph .bullet_icon {
|
489 |
+
margin-left: 10px;
|
490 |
+
border-radius: 10px;
|
491 |
+
width: 12px;
|
492 |
+
height: 12px;
|
493 |
+
background-color: #666;
|
494 |
+
display: inline-block;
|
495 |
+
margin-top: 2px;
|
496 |
+
margin-right: 4px;
|
497 |
+
transition: background-color 0.1s ease 0s;
|
498 |
+
-moz-transition: background-color 0.1s ease 0s;
|
499 |
+
}
|
500 |
+
|
501 |
+
.litegraph .bullet_icon:hover {
|
502 |
+
background-color: #698;
|
503 |
+
cursor: pointer;
|
504 |
+
}
|
505 |
+
|
506 |
+
/* OLD */
|
507 |
+
|
508 |
+
.graphcontextmenu {
|
509 |
+
padding: 4px;
|
510 |
+
min-width: 100px;
|
511 |
+
}
|
512 |
+
|
513 |
+
.graphcontextmenu-title {
|
514 |
+
color: #dde;
|
515 |
+
background-color: #222;
|
516 |
+
margin: 0;
|
517 |
+
padding: 2px;
|
518 |
+
cursor: default;
|
519 |
+
}
|
520 |
+
|
521 |
+
.graphmenu-entry {
|
522 |
+
box-sizing: border-box;
|
523 |
+
margin: 2px;
|
524 |
+
padding-left: 20px;
|
525 |
+
user-select: none;
|
526 |
+
-moz-user-select: none;
|
527 |
+
-webkit-user-select: none;
|
528 |
+
transition: all linear 0.3s;
|
529 |
+
}
|
530 |
+
|
531 |
+
.graphmenu-entry.event,
|
532 |
+
.litemenu-entry.event {
|
533 |
+
border-left: 8px solid orange;
|
534 |
+
padding-left: 12px;
|
535 |
+
}
|
536 |
+
|
537 |
+
.graphmenu-entry.disabled {
|
538 |
+
opacity: 0.3;
|
539 |
+
}
|
540 |
+
|
541 |
+
.graphmenu-entry.submenu {
|
542 |
+
border-right: 2px solid #eee;
|
543 |
+
}
|
544 |
+
|
545 |
+
.graphmenu-entry:hover {
|
546 |
+
background-color: #555;
|
547 |
+
}
|
548 |
+
|
549 |
+
.graphmenu-entry.separator {
|
550 |
+
background-color: #111;
|
551 |
+
border-bottom: 1px solid #666;
|
552 |
+
height: 1px;
|
553 |
+
width: calc(100% - 20px);
|
554 |
+
-moz-width: calc(100% - 20px);
|
555 |
+
-webkit-width: calc(100% - 20px);
|
556 |
+
}
|
557 |
+
|
558 |
+
.graphmenu-entry .property_name {
|
559 |
+
display: inline-block;
|
560 |
+
text-align: left;
|
561 |
+
min-width: 80px;
|
562 |
+
min-height: 1.2em;
|
563 |
+
}
|
564 |
+
|
565 |
+
.graphmenu-entry .property_value,
|
566 |
+
.litemenu-entry .property_value {
|
567 |
+
display: inline-block;
|
568 |
+
background-color: rgba(0, 0, 0, 0.5);
|
569 |
+
text-align: right;
|
570 |
+
min-width: 80px;
|
571 |
+
min-height: 1.2em;
|
572 |
+
vertical-align: middle;
|
573 |
+
padding-right: 10px;
|
574 |
+
}
|
575 |
+
|
576 |
+
.graphdialog {
|
577 |
+
position: absolute;
|
578 |
+
top: 10px;
|
579 |
+
left: 10px;
|
580 |
+
min-height: 2em;
|
581 |
+
background-color: #333;
|
582 |
+
font-size: 1.2em;
|
583 |
+
box-shadow: 0 0 10px black !important;
|
584 |
+
z-index: 10;
|
585 |
+
}
|
586 |
+
|
587 |
+
.graphdialog.rounded {
|
588 |
+
border-radius: 12px;
|
589 |
+
padding-right: 2px;
|
590 |
+
}
|
591 |
+
|
592 |
+
.graphdialog .name {
|
593 |
+
display: inline-block;
|
594 |
+
min-width: 60px;
|
595 |
+
min-height: 1.5em;
|
596 |
+
padding-left: 10px;
|
597 |
+
}
|
598 |
+
|
599 |
+
.graphdialog input,
|
600 |
+
.graphdialog textarea,
|
601 |
+
.graphdialog select {
|
602 |
+
margin: 3px;
|
603 |
+
min-width: 60px;
|
604 |
+
min-height: 1.5em;
|
605 |
+
background-color: black;
|
606 |
+
border: 0;
|
607 |
+
color: white;
|
608 |
+
padding-left: 10px;
|
609 |
+
outline: none;
|
610 |
+
}
|
611 |
+
|
612 |
+
.graphdialog textarea {
|
613 |
+
min-height: 150px;
|
614 |
+
}
|
615 |
+
|
616 |
+
.graphdialog button {
|
617 |
+
margin-top: 3px;
|
618 |
+
vertical-align: top;
|
619 |
+
background-color: #999;
|
620 |
+
border: 0;
|
621 |
+
}
|
622 |
+
|
623 |
+
.graphdialog button.rounded,
|
624 |
+
.graphdialog input.rounded {
|
625 |
+
border-radius: 0 12px 12px 0;
|
626 |
+
}
|
627 |
+
|
628 |
+
.graphdialog .helper {
|
629 |
+
overflow: auto;
|
630 |
+
max-height: 200px;
|
631 |
+
}
|
632 |
+
|
633 |
+
.graphdialog .help-item {
|
634 |
+
padding-left: 10px;
|
635 |
+
}
|
636 |
+
|
637 |
+
.graphdialog .help-item:hover,
|
638 |
+
.graphdialog .help-item.selected {
|
639 |
+
cursor: pointer;
|
640 |
+
background-color: white;
|
641 |
+
color: black;
|
642 |
+
}
|
643 |
+
|
644 |
+
.litegraph .dialog {
|
645 |
+
min-height: 0;
|
646 |
+
}
|
647 |
+
.litegraph .dialog .dialog-content {
|
648 |
+
display: block;
|
649 |
+
}
|
650 |
+
.litegraph .dialog .dialog-content .subgraph_property {
|
651 |
+
padding: 5px;
|
652 |
+
}
|
653 |
+
.litegraph .dialog .dialog-footer {
|
654 |
+
margin: 0;
|
655 |
+
}
|
656 |
+
.litegraph .dialog .dialog-footer .subgraph_property {
|
657 |
+
margin-top: 0;
|
658 |
+
display: flex;
|
659 |
+
align-items: center;
|
660 |
+
padding: 5px;
|
661 |
+
}
|
662 |
+
.litegraph .dialog .dialog-footer .subgraph_property .name {
|
663 |
+
flex: 1;
|
664 |
+
}
|
665 |
+
.litegraph .graphdialog {
|
666 |
+
display: flex;
|
667 |
+
align-items: center;
|
668 |
+
border-radius: 20px;
|
669 |
+
padding: 4px 10px;
|
670 |
+
position: fixed;
|
671 |
+
}
|
672 |
+
.litegraph .graphdialog .name {
|
673 |
+
padding: 0;
|
674 |
+
min-height: 0;
|
675 |
+
font-size: 16px;
|
676 |
+
vertical-align: middle;
|
677 |
+
}
|
678 |
+
.litegraph .graphdialog .value {
|
679 |
+
font-size: 16px;
|
680 |
+
min-height: 0;
|
681 |
+
margin: 0 10px;
|
682 |
+
padding: 2px 5px;
|
683 |
+
}
|
684 |
+
.litegraph .graphdialog input[type="checkbox"] {
|
685 |
+
width: 16px;
|
686 |
+
height: 16px;
|
687 |
+
}
|
688 |
+
.litegraph .graphdialog button {
|
689 |
+
padding: 4px 18px;
|
690 |
+
border-radius: 20px;
|
691 |
+
cursor: pointer;
|
692 |
+
}
|
693 |
+
|
ComfyUI/web/lib/litegraph.extensions.js
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Changes the background color of the canvas.
|
3 |
+
*
|
4 |
+
* @method updateBackground
|
5 |
+
* @param {image} String
|
6 |
+
* @param {clearBackgroundColor} String
|
7 |
+
* @
|
8 |
+
*/
|
9 |
+
LGraphCanvas.prototype.updateBackground = function (image, clearBackgroundColor) {
|
10 |
+
this._bg_img = new Image();
|
11 |
+
this._bg_img.name = image;
|
12 |
+
this._bg_img.src = image;
|
13 |
+
this._bg_img.onload = () => {
|
14 |
+
this.draw(true, true);
|
15 |
+
};
|
16 |
+
this.background_image = image;
|
17 |
+
|
18 |
+
this.clear_background = true;
|
19 |
+
this.clear_background_color = clearBackgroundColor;
|
20 |
+
this._pattern = null
|
21 |
+
}
|
ComfyUI/web/lib/materialdesignicons.min.css
ADDED
The diff for this file is too large to render.
See raw diff
|
|
ComfyUI/web/scripts/api.js
ADDED
@@ -0,0 +1,482 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class ComfyApi extends EventTarget {
|
2 |
+
#registered = new Set();
|
3 |
+
|
4 |
+
constructor() {
|
5 |
+
super();
|
6 |
+
this.api_host = location.host;
|
7 |
+
this.api_base = location.pathname.split('/').slice(0, -1).join('/');
|
8 |
+
this.initialClientId = sessionStorage.getItem("clientId");
|
9 |
+
}
|
10 |
+
|
11 |
+
apiURL(route) {
|
12 |
+
return this.api_base + route;
|
13 |
+
}
|
14 |
+
|
15 |
+
fetchApi(route, options) {
|
16 |
+
if (!options) {
|
17 |
+
options = {};
|
18 |
+
}
|
19 |
+
if (!options.headers) {
|
20 |
+
options.headers = {};
|
21 |
+
}
|
22 |
+
options.headers["Comfy-User"] = this.user;
|
23 |
+
return fetch(this.apiURL(route), options);
|
24 |
+
}
|
25 |
+
|
26 |
+
addEventListener(type, callback, options) {
|
27 |
+
super.addEventListener(type, callback, options);
|
28 |
+
this.#registered.add(type);
|
29 |
+
}
|
30 |
+
|
31 |
+
/**
|
32 |
+
* Poll status for colab and other things that don't support websockets.
|
33 |
+
*/
|
34 |
+
#pollQueue() {
|
35 |
+
setInterval(async () => {
|
36 |
+
try {
|
37 |
+
const resp = await this.fetchApi("/prompt");
|
38 |
+
const status = await resp.json();
|
39 |
+
this.dispatchEvent(new CustomEvent("status", { detail: status }));
|
40 |
+
} catch (error) {
|
41 |
+
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
42 |
+
}
|
43 |
+
}, 1000);
|
44 |
+
}
|
45 |
+
|
46 |
+
/**
|
47 |
+
* Creates and connects a WebSocket for realtime updates
|
48 |
+
* @param {boolean} isReconnect If the socket is connection is a reconnect attempt
|
49 |
+
*/
|
50 |
+
#createSocket(isReconnect) {
|
51 |
+
if (this.socket) {
|
52 |
+
return;
|
53 |
+
}
|
54 |
+
|
55 |
+
let opened = false;
|
56 |
+
let existingSession = window.name;
|
57 |
+
if (existingSession) {
|
58 |
+
existingSession = "?clientId=" + existingSession;
|
59 |
+
}
|
60 |
+
this.socket = new WebSocket(
|
61 |
+
`ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}`
|
62 |
+
);
|
63 |
+
this.socket.binaryType = "arraybuffer";
|
64 |
+
|
65 |
+
this.socket.addEventListener("open", () => {
|
66 |
+
opened = true;
|
67 |
+
if (isReconnect) {
|
68 |
+
this.dispatchEvent(new CustomEvent("reconnected"));
|
69 |
+
}
|
70 |
+
});
|
71 |
+
|
72 |
+
this.socket.addEventListener("error", () => {
|
73 |
+
if (this.socket) this.socket.close();
|
74 |
+
if (!isReconnect && !opened) {
|
75 |
+
this.#pollQueue();
|
76 |
+
}
|
77 |
+
});
|
78 |
+
|
79 |
+
this.socket.addEventListener("close", () => {
|
80 |
+
setTimeout(() => {
|
81 |
+
this.socket = null;
|
82 |
+
this.#createSocket(true);
|
83 |
+
}, 300);
|
84 |
+
if (opened) {
|
85 |
+
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
86 |
+
this.dispatchEvent(new CustomEvent("reconnecting"));
|
87 |
+
}
|
88 |
+
});
|
89 |
+
|
90 |
+
this.socket.addEventListener("message", (event) => {
|
91 |
+
try {
|
92 |
+
if (event.data instanceof ArrayBuffer) {
|
93 |
+
const view = new DataView(event.data);
|
94 |
+
const eventType = view.getUint32(0);
|
95 |
+
const buffer = event.data.slice(4);
|
96 |
+
switch (eventType) {
|
97 |
+
case 1:
|
98 |
+
const view2 = new DataView(event.data);
|
99 |
+
const imageType = view2.getUint32(0)
|
100 |
+
let imageMime
|
101 |
+
switch (imageType) {
|
102 |
+
case 1:
|
103 |
+
default:
|
104 |
+
imageMime = "image/jpeg";
|
105 |
+
break;
|
106 |
+
case 2:
|
107 |
+
imageMime = "image/png"
|
108 |
+
}
|
109 |
+
const imageBlob = new Blob([buffer.slice(4)], { type: imageMime });
|
110 |
+
this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob }));
|
111 |
+
break;
|
112 |
+
default:
|
113 |
+
throw new Error(`Unknown binary websocket message of type ${eventType}`);
|
114 |
+
}
|
115 |
+
}
|
116 |
+
else {
|
117 |
+
const msg = JSON.parse(event.data);
|
118 |
+
switch (msg.type) {
|
119 |
+
case "status":
|
120 |
+
if (msg.data.sid) {
|
121 |
+
this.clientId = msg.data.sid;
|
122 |
+
window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
|
123 |
+
sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
|
124 |
+
}
|
125 |
+
this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
|
126 |
+
break;
|
127 |
+
case "progress":
|
128 |
+
this.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
|
129 |
+
break;
|
130 |
+
case "executing":
|
131 |
+
this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.node }));
|
132 |
+
break;
|
133 |
+
case "executed":
|
134 |
+
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
|
135 |
+
break;
|
136 |
+
case "execution_start":
|
137 |
+
this.dispatchEvent(new CustomEvent("execution_start", { detail: msg.data }));
|
138 |
+
break;
|
139 |
+
case "execution_success":
|
140 |
+
this.dispatchEvent(new CustomEvent("execution_success", { detail: msg.data }));
|
141 |
+
break;
|
142 |
+
case "execution_error":
|
143 |
+
this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data }));
|
144 |
+
break;
|
145 |
+
case "execution_cached":
|
146 |
+
this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data }));
|
147 |
+
break;
|
148 |
+
default:
|
149 |
+
if (this.#registered.has(msg.type)) {
|
150 |
+
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
|
151 |
+
} else {
|
152 |
+
throw new Error(`Unknown message type ${msg.type}`);
|
153 |
+
}
|
154 |
+
}
|
155 |
+
}
|
156 |
+
} catch (error) {
|
157 |
+
console.warn("Unhandled message:", event.data, error);
|
158 |
+
}
|
159 |
+
});
|
160 |
+
}
|
161 |
+
|
162 |
+
/**
|
163 |
+
* Initialises sockets and realtime updates
|
164 |
+
*/
|
165 |
+
init() {
|
166 |
+
this.#createSocket();
|
167 |
+
}
|
168 |
+
|
169 |
+
/**
|
170 |
+
* Gets a list of extension urls
|
171 |
+
* @returns An array of script urls to import
|
172 |
+
*/
|
173 |
+
async getExtensions() {
|
174 |
+
const resp = await this.fetchApi("/extensions", { cache: "no-store" });
|
175 |
+
return await resp.json();
|
176 |
+
}
|
177 |
+
|
178 |
+
/**
|
179 |
+
* Gets a list of embedding names
|
180 |
+
* @returns An array of script urls to import
|
181 |
+
*/
|
182 |
+
async getEmbeddings() {
|
183 |
+
const resp = await this.fetchApi("/embeddings", { cache: "no-store" });
|
184 |
+
return await resp.json();
|
185 |
+
}
|
186 |
+
|
187 |
+
/**
|
188 |
+
* Loads node object definitions for the graph
|
189 |
+
* @returns The node definitions
|
190 |
+
*/
|
191 |
+
async getNodeDefs() {
|
192 |
+
const resp = await this.fetchApi("/object_info", { cache: "no-store" });
|
193 |
+
return await resp.json();
|
194 |
+
}
|
195 |
+
|
196 |
+
/**
|
197 |
+
*
|
198 |
+
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
|
199 |
+
* @param {object} prompt The prompt data to queue
|
200 |
+
*/
|
201 |
+
async queuePrompt(number, { output, workflow }) {
|
202 |
+
const body = {
|
203 |
+
client_id: this.clientId,
|
204 |
+
prompt: output,
|
205 |
+
extra_data: { extra_pnginfo: { workflow } },
|
206 |
+
};
|
207 |
+
|
208 |
+
if (number === -1) {
|
209 |
+
body.front = true;
|
210 |
+
} else if (number != 0) {
|
211 |
+
body.number = number;
|
212 |
+
}
|
213 |
+
|
214 |
+
const res = await this.fetchApi("/prompt", {
|
215 |
+
method: "POST",
|
216 |
+
headers: {
|
217 |
+
"Content-Type": "application/json",
|
218 |
+
},
|
219 |
+
body: JSON.stringify(body),
|
220 |
+
});
|
221 |
+
|
222 |
+
if (res.status !== 200) {
|
223 |
+
throw {
|
224 |
+
response: await res.json(),
|
225 |
+
};
|
226 |
+
}
|
227 |
+
|
228 |
+
return await res.json();
|
229 |
+
}
|
230 |
+
|
231 |
+
/**
|
232 |
+
* Loads a list of items (queue or history)
|
233 |
+
* @param {string} type The type of items to load, queue or history
|
234 |
+
* @returns The items of the specified type grouped by their status
|
235 |
+
*/
|
236 |
+
async getItems(type) {
|
237 |
+
if (type === "queue") {
|
238 |
+
return this.getQueue();
|
239 |
+
}
|
240 |
+
return this.getHistory();
|
241 |
+
}
|
242 |
+
|
243 |
+
/**
|
244 |
+
* Gets the current state of the queue
|
245 |
+
* @returns The currently running and queued items
|
246 |
+
*/
|
247 |
+
async getQueue() {
|
248 |
+
try {
|
249 |
+
const res = await this.fetchApi("/queue");
|
250 |
+
const data = await res.json();
|
251 |
+
return {
|
252 |
+
// Running action uses a different endpoint for cancelling
|
253 |
+
Running: data.queue_running.map((prompt) => ({
|
254 |
+
prompt,
|
255 |
+
remove: { name: "Cancel", cb: () => api.interrupt() },
|
256 |
+
})),
|
257 |
+
Pending: data.queue_pending.map((prompt) => ({ prompt })),
|
258 |
+
};
|
259 |
+
} catch (error) {
|
260 |
+
console.error(error);
|
261 |
+
return { Running: [], Pending: [] };
|
262 |
+
}
|
263 |
+
}
|
264 |
+
|
265 |
+
/**
|
266 |
+
* Gets the prompt execution history
|
267 |
+
* @returns Prompt history including node outputs
|
268 |
+
*/
|
269 |
+
async getHistory(max_items=200) {
|
270 |
+
try {
|
271 |
+
const res = await this.fetchApi(`/history?max_items=${max_items}`);
|
272 |
+
return { History: Object.values(await res.json()) };
|
273 |
+
} catch (error) {
|
274 |
+
console.error(error);
|
275 |
+
return { History: [] };
|
276 |
+
}
|
277 |
+
}
|
278 |
+
|
279 |
+
/**
|
280 |
+
* Gets system & device stats
|
281 |
+
* @returns System stats such as python version, OS, per device info
|
282 |
+
*/
|
283 |
+
async getSystemStats() {
|
284 |
+
const res = await this.fetchApi("/system_stats");
|
285 |
+
return await res.json();
|
286 |
+
}
|
287 |
+
|
288 |
+
/**
|
289 |
+
* Sends a POST request to the API
|
290 |
+
* @param {*} type The endpoint to post to
|
291 |
+
* @param {*} body Optional POST data
|
292 |
+
*/
|
293 |
+
async #postItem(type, body) {
|
294 |
+
try {
|
295 |
+
await this.fetchApi("/" + type, {
|
296 |
+
method: "POST",
|
297 |
+
headers: {
|
298 |
+
"Content-Type": "application/json",
|
299 |
+
},
|
300 |
+
body: body ? JSON.stringify(body) : undefined,
|
301 |
+
});
|
302 |
+
} catch (error) {
|
303 |
+
console.error(error);
|
304 |
+
}
|
305 |
+
}
|
306 |
+
|
307 |
+
/**
|
308 |
+
* Deletes an item from the specified list
|
309 |
+
* @param {string} type The type of item to delete, queue or history
|
310 |
+
* @param {number} id The id of the item to delete
|
311 |
+
*/
|
312 |
+
async deleteItem(type, id) {
|
313 |
+
await this.#postItem(type, { delete: [id] });
|
314 |
+
}
|
315 |
+
|
316 |
+
/**
|
317 |
+
* Clears the specified list
|
318 |
+
* @param {string} type The type of list to clear, queue or history
|
319 |
+
*/
|
320 |
+
async clearItems(type) {
|
321 |
+
await this.#postItem(type, { clear: true });
|
322 |
+
}
|
323 |
+
|
324 |
+
/**
|
325 |
+
* Interrupts the execution of the running prompt
|
326 |
+
*/
|
327 |
+
async interrupt() {
|
328 |
+
await this.#postItem("interrupt", null);
|
329 |
+
}
|
330 |
+
|
331 |
+
/**
|
332 |
+
* Gets user configuration data and where data should be stored
|
333 |
+
* @returns { Promise<{ storage: "server" | "browser", users?: Promise<string, unknown>, migrated?: boolean }> }
|
334 |
+
*/
|
335 |
+
async getUserConfig() {
|
336 |
+
return (await this.fetchApi("/users")).json();
|
337 |
+
}
|
338 |
+
|
339 |
+
/**
|
340 |
+
* Creates a new user
|
341 |
+
* @param { string } username
|
342 |
+
* @returns The fetch response
|
343 |
+
*/
|
344 |
+
createUser(username) {
|
345 |
+
return this.fetchApi("/users", {
|
346 |
+
method: "POST",
|
347 |
+
headers: {
|
348 |
+
"Content-Type": "application/json",
|
349 |
+
},
|
350 |
+
body: JSON.stringify({ username }),
|
351 |
+
});
|
352 |
+
}
|
353 |
+
|
354 |
+
/**
|
355 |
+
* Gets all setting values for the current user
|
356 |
+
* @returns { Promise<string, unknown> } A dictionary of id -> value
|
357 |
+
*/
|
358 |
+
async getSettings() {
|
359 |
+
return (await this.fetchApi("/settings")).json();
|
360 |
+
}
|
361 |
+
|
362 |
+
/**
|
363 |
+
* Gets a setting for the current user
|
364 |
+
* @param { string } id The id of the setting to fetch
|
365 |
+
* @returns { Promise<unknown> } The setting value
|
366 |
+
*/
|
367 |
+
async getSetting(id) {
|
368 |
+
return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json();
|
369 |
+
}
|
370 |
+
|
371 |
+
/**
|
372 |
+
* Stores a dictionary of settings for the current user
|
373 |
+
* @param { Record<string, unknown> } settings Dictionary of setting id -> value to save
|
374 |
+
* @returns { Promise<void> }
|
375 |
+
*/
|
376 |
+
async storeSettings(settings) {
|
377 |
+
return this.fetchApi(`/settings`, {
|
378 |
+
method: "POST",
|
379 |
+
body: JSON.stringify(settings)
|
380 |
+
});
|
381 |
+
}
|
382 |
+
|
383 |
+
/**
|
384 |
+
* Stores a setting for the current user
|
385 |
+
* @param { string } id The id of the setting to update
|
386 |
+
* @param { unknown } value The value of the setting
|
387 |
+
* @returns { Promise<void> }
|
388 |
+
*/
|
389 |
+
async storeSetting(id, value) {
|
390 |
+
return this.fetchApi(`/settings/${encodeURIComponent(id)}`, {
|
391 |
+
method: "POST",
|
392 |
+
body: JSON.stringify(value)
|
393 |
+
});
|
394 |
+
}
|
395 |
+
|
396 |
+
/**
|
397 |
+
* Gets a user data file for the current user
|
398 |
+
* @param { string } file The name of the userdata file to load
|
399 |
+
* @param { RequestInit } [options]
|
400 |
+
* @returns { Promise<Response> } The fetch response object
|
401 |
+
*/
|
402 |
+
async getUserData(file, options) {
|
403 |
+
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options);
|
404 |
+
}
|
405 |
+
|
406 |
+
/**
|
407 |
+
* Stores a user data file for the current user
|
408 |
+
* @param { string } file The name of the userdata file to save
|
409 |
+
* @param { unknown } data The data to save to the file
|
410 |
+
* @param { RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } } [options]
|
411 |
+
* @returns { Promise<Response> }
|
412 |
+
*/
|
413 |
+
async storeUserData(file, data, options = { overwrite: true, stringify: true, throwOnError: true }) {
|
414 |
+
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options?.overwrite}`, {
|
415 |
+
method: "POST",
|
416 |
+
body: options?.stringify ? JSON.stringify(data) : data,
|
417 |
+
...options,
|
418 |
+
});
|
419 |
+
if (resp.status !== 200 && options?.throwOnError !== false) {
|
420 |
+
throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`);
|
421 |
+
}
|
422 |
+
return resp;
|
423 |
+
}
|
424 |
+
|
425 |
+
/**
|
426 |
+
* Deletes a user data file for the current user
|
427 |
+
* @param { string } file The name of the userdata file to delete
|
428 |
+
*/
|
429 |
+
async deleteUserData(file) {
|
430 |
+
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
|
431 |
+
method: "DELETE",
|
432 |
+
});
|
433 |
+
if (resp.status !== 204) {
|
434 |
+
throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`);
|
435 |
+
}
|
436 |
+
}
|
437 |
+
|
438 |
+
/**
|
439 |
+
* Move a user data file for the current user
|
440 |
+
* @param { string } source The userdata file to move
|
441 |
+
* @param { string } dest The destination for the file
|
442 |
+
*/
|
443 |
+
async moveUserData(source, dest, options = { overwrite: false }) {
|
444 |
+
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, {
|
445 |
+
method: "POST",
|
446 |
+
});
|
447 |
+
return resp;
|
448 |
+
}
|
449 |
+
|
450 |
+
/**
|
451 |
+
* @overload
|
452 |
+
* Lists user data files for the current user
|
453 |
+
* @param { string } dir The directory in which to list files
|
454 |
+
* @param { boolean } [recurse] If the listing should be recursive
|
455 |
+
* @param { true } [split] If the paths should be split based on the os path separator
|
456 |
+
* @returns { Promise<string[][]>> } The list of split file paths in the format [fullPath, ...splitPath]
|
457 |
+
*/
|
458 |
+
/**
|
459 |
+
* @overload
|
460 |
+
* Lists user data files for the current user
|
461 |
+
* @param { string } dir The directory in which to list files
|
462 |
+
* @param { boolean } [recurse] If the listing should be recursive
|
463 |
+
* @param { false | undefined } [split] If the paths should be split based on the os path separator
|
464 |
+
* @returns { Promise<string[]>> } The list of files
|
465 |
+
*/
|
466 |
+
async listUserData(dir, recurse, split) {
|
467 |
+
const resp = await this.fetchApi(
|
468 |
+
`/userdata?${new URLSearchParams({
|
469 |
+
recurse,
|
470 |
+
dir,
|
471 |
+
split,
|
472 |
+
})}`
|
473 |
+
);
|
474 |
+
if (resp.status === 404) return [];
|
475 |
+
if (resp.status !== 200) {
|
476 |
+
throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`);
|
477 |
+
}
|
478 |
+
return resp.json();
|
479 |
+
}
|
480 |
+
}
|
481 |
+
|
482 |
+
export const api = new ComfyApi();
|
ComfyUI/web/scripts/app.js
ADDED
@@ -0,0 +1,2459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComfyLogging } from "./logging.js";
|
2 |
+
import { ComfyWidgets, initWidgets } from "./widgets.js";
|
3 |
+
import { ComfyUI, $el } from "./ui.js";
|
4 |
+
import { api } from "./api.js";
|
5 |
+
import { defaultGraph } from "./defaultGraph.js";
|
6 |
+
import { getPngMetadata, getWebpMetadata, getFlacMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
|
7 |
+
import { addDomClippingSetting } from "./domWidget.js";
|
8 |
+
import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js";
|
9 |
+
import { ComfyAppMenu } from "./ui/menu/index.js";
|
10 |
+
import { getStorageValue, setStorageValue } from "./utils.js";
|
11 |
+
import { ComfyWorkflowManager } from "./workflows.js";
|
12 |
+
export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview";
|
13 |
+
|
14 |
+
function sanitizeNodeName(string) {
|
15 |
+
let entityMap = {
|
16 |
+
'&': '',
|
17 |
+
'<': '',
|
18 |
+
'>': '',
|
19 |
+
'"': '',
|
20 |
+
"'": '',
|
21 |
+
'`': '',
|
22 |
+
'=': ''
|
23 |
+
};
|
24 |
+
return String(string).replace(/[&<>"'`=]/g, function fromEntityMap (s) {
|
25 |
+
return entityMap[s];
|
26 |
+
});
|
27 |
+
}
|
28 |
+
|
29 |
+
/**
|
30 |
+
* @typedef {import("types/comfy").ComfyExtension} ComfyExtension
|
31 |
+
*/
|
32 |
+
|
33 |
+
export class ComfyApp {
|
34 |
+
/**
|
35 |
+
* List of entries to queue
|
36 |
+
* @type {{number: number, batchCount: number}[]}
|
37 |
+
*/
|
38 |
+
#queueItems = [];
|
39 |
+
/**
|
40 |
+
* If the queue is currently being processed
|
41 |
+
* @type {boolean}
|
42 |
+
*/
|
43 |
+
#processingQueue = false;
|
44 |
+
|
45 |
+
/**
|
46 |
+
* Content Clipboard
|
47 |
+
* @type {serialized node object}
|
48 |
+
*/
|
49 |
+
static clipspace = null;
|
50 |
+
static clipspace_invalidate_handler = null;
|
51 |
+
static open_maskeditor = null;
|
52 |
+
static clipspace_return_node = null;
|
53 |
+
|
54 |
+
constructor() {
|
55 |
+
this.ui = new ComfyUI(this);
|
56 |
+
this.logging = new ComfyLogging(this);
|
57 |
+
this.workflowManager = new ComfyWorkflowManager(this);
|
58 |
+
this.bodyTop = $el("div.comfyui-body-top", { parent: document.body });
|
59 |
+
this.bodyLeft = $el("div.comfyui-body-left", { parent: document.body });
|
60 |
+
this.bodyRight = $el("div.comfyui-body-right", { parent: document.body });
|
61 |
+
this.bodyBottom = $el("div.comfyui-body-bottom", { parent: document.body });
|
62 |
+
this.menu = new ComfyAppMenu(this);
|
63 |
+
|
64 |
+
/**
|
65 |
+
* List of extensions that are registered with the app
|
66 |
+
* @type {ComfyExtension[]}
|
67 |
+
*/
|
68 |
+
this.extensions = [];
|
69 |
+
|
70 |
+
/**
|
71 |
+
* Stores the execution output data for each node
|
72 |
+
* @type {Record<string, any>}
|
73 |
+
*/
|
74 |
+
this._nodeOutputs = {};
|
75 |
+
|
76 |
+
/**
|
77 |
+
* Stores the preview image data for each node
|
78 |
+
* @type {Record<string, Image>}
|
79 |
+
*/
|
80 |
+
this.nodePreviewImages = {};
|
81 |
+
|
82 |
+
/**
|
83 |
+
* If the shift key on the keyboard is pressed
|
84 |
+
* @type {boolean}
|
85 |
+
*/
|
86 |
+
this.shiftDown = false;
|
87 |
+
}
|
88 |
+
|
89 |
+
get nodeOutputs() {
|
90 |
+
return this._nodeOutputs;
|
91 |
+
}
|
92 |
+
|
93 |
+
set nodeOutputs(value) {
|
94 |
+
this._nodeOutputs = value;
|
95 |
+
this.#invokeExtensions("onNodeOutputsUpdated", value);
|
96 |
+
}
|
97 |
+
|
98 |
+
getPreviewFormatParam() {
|
99 |
+
let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat");
|
100 |
+
if(preview_format)
|
101 |
+
return `&preview=${preview_format}`;
|
102 |
+
else
|
103 |
+
return "";
|
104 |
+
}
|
105 |
+
|
106 |
+
getRandParam() {
|
107 |
+
return "&rand=" + Math.random();
|
108 |
+
}
|
109 |
+
|
110 |
+
static isImageNode(node) {
|
111 |
+
return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0);
|
112 |
+
}
|
113 |
+
|
114 |
+
static onClipspaceEditorSave() {
|
115 |
+
if(ComfyApp.clipspace_return_node) {
|
116 |
+
ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node);
|
117 |
+
}
|
118 |
+
}
|
119 |
+
|
120 |
+
static onClipspaceEditorClosed() {
|
121 |
+
ComfyApp.clipspace_return_node = null;
|
122 |
+
}
|
123 |
+
|
124 |
+
static copyToClipspace(node) {
|
125 |
+
var widgets = null;
|
126 |
+
if(node.widgets) {
|
127 |
+
widgets = node.widgets.map(({ type, name, value }) => ({ type, name, value }));
|
128 |
+
}
|
129 |
+
|
130 |
+
var imgs = undefined;
|
131 |
+
var orig_imgs = undefined;
|
132 |
+
if(node.imgs != undefined) {
|
133 |
+
imgs = [];
|
134 |
+
orig_imgs = [];
|
135 |
+
|
136 |
+
for (let i = 0; i < node.imgs.length; i++) {
|
137 |
+
imgs[i] = new Image();
|
138 |
+
imgs[i].src = node.imgs[i].src;
|
139 |
+
orig_imgs[i] = imgs[i];
|
140 |
+
}
|
141 |
+
}
|
142 |
+
|
143 |
+
var selectedIndex = 0;
|
144 |
+
if(node.imageIndex) {
|
145 |
+
selectedIndex = node.imageIndex;
|
146 |
+
}
|
147 |
+
|
148 |
+
ComfyApp.clipspace = {
|
149 |
+
'widgets': widgets,
|
150 |
+
'imgs': imgs,
|
151 |
+
'original_imgs': orig_imgs,
|
152 |
+
'images': node.images,
|
153 |
+
'selectedIndex': selectedIndex,
|
154 |
+
'img_paste_mode': 'selected' // reset to default im_paste_mode state on copy action
|
155 |
+
};
|
156 |
+
|
157 |
+
ComfyApp.clipspace_return_node = null;
|
158 |
+
|
159 |
+
if(ComfyApp.clipspace_invalidate_handler) {
|
160 |
+
ComfyApp.clipspace_invalidate_handler();
|
161 |
+
}
|
162 |
+
}
|
163 |
+
|
164 |
+
static pasteFromClipspace(node) {
|
165 |
+
if(ComfyApp.clipspace) {
|
166 |
+
// image paste
|
167 |
+
if(ComfyApp.clipspace.imgs && node.imgs) {
|
168 |
+
if(node.images && ComfyApp.clipspace.images) {
|
169 |
+
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
|
170 |
+
node.images = [ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']]];
|
171 |
+
}
|
172 |
+
else {
|
173 |
+
node.images = ComfyApp.clipspace.images;
|
174 |
+
}
|
175 |
+
|
176 |
+
if(app.nodeOutputs[node.id + ""])
|
177 |
+
app.nodeOutputs[node.id + ""].images = node.images;
|
178 |
+
}
|
179 |
+
|
180 |
+
if(ComfyApp.clipspace.imgs) {
|
181 |
+
// deep-copy to cut link with clipspace
|
182 |
+
if(ComfyApp.clipspace['img_paste_mode'] == 'selected') {
|
183 |
+
const img = new Image();
|
184 |
+
img.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
|
185 |
+
node.imgs = [img];
|
186 |
+
node.imageIndex = 0;
|
187 |
+
}
|
188 |
+
else {
|
189 |
+
const imgs = [];
|
190 |
+
for(let i=0; i<ComfyApp.clipspace.imgs.length; i++) {
|
191 |
+
imgs[i] = new Image();
|
192 |
+
imgs[i].src = ComfyApp.clipspace.imgs[i].src;
|
193 |
+
node.imgs = imgs;
|
194 |
+
}
|
195 |
+
}
|
196 |
+
}
|
197 |
+
}
|
198 |
+
|
199 |
+
if(node.widgets) {
|
200 |
+
if(ComfyApp.clipspace.images) {
|
201 |
+
const clip_image = ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']];
|
202 |
+
const index = node.widgets.findIndex(obj => obj.name === 'image');
|
203 |
+
if(index >= 0) {
|
204 |
+
if(node.widgets[index].type != 'image' && typeof node.widgets[index].value == "string" && clip_image.filename) {
|
205 |
+
node.widgets[index].value = (clip_image.subfolder?clip_image.subfolder+'/':'') + clip_image.filename + (clip_image.type?` [${clip_image.type}]`:'');
|
206 |
+
}
|
207 |
+
else {
|
208 |
+
node.widgets[index].value = clip_image;
|
209 |
+
}
|
210 |
+
}
|
211 |
+
}
|
212 |
+
if(ComfyApp.clipspace.widgets) {
|
213 |
+
ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => {
|
214 |
+
const prop = Object.values(node.widgets).find(obj => obj.type === type && obj.name === name);
|
215 |
+
if (prop && prop.type != 'button') {
|
216 |
+
if(prop.type != 'image' && typeof prop.value == "string" && value.filename) {
|
217 |
+
prop.value = (value.subfolder?value.subfolder+'/':'') + value.filename + (value.type?` [${value.type}]`:'');
|
218 |
+
}
|
219 |
+
else {
|
220 |
+
prop.value = value;
|
221 |
+
prop.callback(value);
|
222 |
+
}
|
223 |
+
}
|
224 |
+
});
|
225 |
+
}
|
226 |
+
}
|
227 |
+
|
228 |
+
app.graph.setDirtyCanvas(true);
|
229 |
+
}
|
230 |
+
}
|
231 |
+
|
232 |
+
/**
|
233 |
+
* Invoke an extension callback
|
234 |
+
* @param {keyof ComfyExtension} method The extension callback to execute
|
235 |
+
* @param {any[]} args Any arguments to pass to the callback
|
236 |
+
* @returns
|
237 |
+
*/
|
238 |
+
#invokeExtensions(method, ...args) {
|
239 |
+
let results = [];
|
240 |
+
for (const ext of this.extensions) {
|
241 |
+
if (method in ext) {
|
242 |
+
try {
|
243 |
+
results.push(ext[method](...args, this));
|
244 |
+
} catch (error) {
|
245 |
+
console.error(
|
246 |
+
`Error calling extension '${ext.name}' method '${method}'`,
|
247 |
+
{ error },
|
248 |
+
{ extension: ext },
|
249 |
+
{ args }
|
250 |
+
);
|
251 |
+
}
|
252 |
+
}
|
253 |
+
}
|
254 |
+
return results;
|
255 |
+
}
|
256 |
+
|
257 |
+
/**
|
258 |
+
* Invoke an async extension callback
|
259 |
+
* Each callback will be invoked concurrently
|
260 |
+
* @param {string} method The extension callback to execute
|
261 |
+
* @param {...any} args Any arguments to pass to the callback
|
262 |
+
* @returns
|
263 |
+
*/
|
264 |
+
async #invokeExtensionsAsync(method, ...args) {
|
265 |
+
return await Promise.all(
|
266 |
+
this.extensions.map(async (ext) => {
|
267 |
+
if (method in ext) {
|
268 |
+
try {
|
269 |
+
return await ext[method](...args, this);
|
270 |
+
} catch (error) {
|
271 |
+
console.error(
|
272 |
+
`Error calling extension '${ext.name}' method '${method}'`,
|
273 |
+
{ error },
|
274 |
+
{ extension: ext },
|
275 |
+
{ args }
|
276 |
+
);
|
277 |
+
}
|
278 |
+
}
|
279 |
+
})
|
280 |
+
);
|
281 |
+
}
|
282 |
+
|
283 |
+
#addRestoreWorkflowView() {
|
284 |
+
const serialize = LGraph.prototype.serialize;
|
285 |
+
const self = this;
|
286 |
+
LGraph.prototype.serialize = function() {
|
287 |
+
const workflow = serialize.apply(this, arguments);
|
288 |
+
|
289 |
+
// Store the drag & scale info in the serialized workflow if the setting is enabled
|
290 |
+
if (self.enableWorkflowViewRestore.value) {
|
291 |
+
if (!workflow.extra) {
|
292 |
+
workflow.extra = {};
|
293 |
+
}
|
294 |
+
workflow.extra.ds = {
|
295 |
+
scale: self.canvas.ds.scale,
|
296 |
+
offset: self.canvas.ds.offset,
|
297 |
+
};
|
298 |
+
} else if (workflow.extra?.ds) {
|
299 |
+
// Clear any old view data
|
300 |
+
delete workflow.extra.ds;
|
301 |
+
}
|
302 |
+
|
303 |
+
return workflow;
|
304 |
+
}
|
305 |
+
this.enableWorkflowViewRestore = this.ui.settings.addSetting({
|
306 |
+
id: "Comfy.EnableWorkflowViewRestore",
|
307 |
+
name: "Save and restore canvas position and zoom level in workflows",
|
308 |
+
type: "boolean",
|
309 |
+
defaultValue: true
|
310 |
+
});
|
311 |
+
}
|
312 |
+
|
313 |
+
/**
|
314 |
+
* Adds special context menu handling for nodes
|
315 |
+
* e.g. this adds Open Image functionality for nodes that show images
|
316 |
+
* @param {*} node The node to add the menu handler
|
317 |
+
*/
|
318 |
+
#addNodeContextMenuHandler(node) {
|
319 |
+
function getCopyImageOption(img) {
|
320 |
+
if (typeof window.ClipboardItem === "undefined") return [];
|
321 |
+
return [
|
322 |
+
{
|
323 |
+
content: "Copy Image",
|
324 |
+
callback: async () => {
|
325 |
+
const url = new URL(img.src);
|
326 |
+
url.searchParams.delete("preview");
|
327 |
+
|
328 |
+
const writeImage = async (blob) => {
|
329 |
+
await navigator.clipboard.write([
|
330 |
+
new ClipboardItem({
|
331 |
+
[blob.type]: blob,
|
332 |
+
}),
|
333 |
+
]);
|
334 |
+
};
|
335 |
+
|
336 |
+
try {
|
337 |
+
const data = await fetch(url);
|
338 |
+
const blob = await data.blob();
|
339 |
+
try {
|
340 |
+
await writeImage(blob);
|
341 |
+
} catch (error) {
|
342 |
+
// Chrome seems to only support PNG on write, convert and try again
|
343 |
+
if (blob.type !== "image/png") {
|
344 |
+
const canvas = $el("canvas", {
|
345 |
+
width: img.naturalWidth,
|
346 |
+
height: img.naturalHeight,
|
347 |
+
});
|
348 |
+
const ctx = canvas.getContext("2d");
|
349 |
+
let image;
|
350 |
+
if (typeof window.createImageBitmap === "undefined") {
|
351 |
+
image = new Image();
|
352 |
+
const p = new Promise((resolve, reject) => {
|
353 |
+
image.onload = resolve;
|
354 |
+
image.onerror = reject;
|
355 |
+
}).finally(() => {
|
356 |
+
URL.revokeObjectURL(image.src);
|
357 |
+
});
|
358 |
+
image.src = URL.createObjectURL(blob);
|
359 |
+
await p;
|
360 |
+
} else {
|
361 |
+
image = await createImageBitmap(blob);
|
362 |
+
}
|
363 |
+
try {
|
364 |
+
ctx.drawImage(image, 0, 0);
|
365 |
+
canvas.toBlob(writeImage, "image/png");
|
366 |
+
} finally {
|
367 |
+
if (typeof image.close === "function") {
|
368 |
+
image.close();
|
369 |
+
}
|
370 |
+
}
|
371 |
+
|
372 |
+
return;
|
373 |
+
}
|
374 |
+
throw error;
|
375 |
+
}
|
376 |
+
} catch (error) {
|
377 |
+
alert("Error copying image: " + (error.message ?? error));
|
378 |
+
}
|
379 |
+
},
|
380 |
+
},
|
381 |
+
];
|
382 |
+
}
|
383 |
+
|
384 |
+
node.prototype.getExtraMenuOptions = function (_, options) {
|
385 |
+
if (this.imgs) {
|
386 |
+
// If this node has images then we add an open in new tab item
|
387 |
+
let img;
|
388 |
+
if (this.imageIndex != null) {
|
389 |
+
// An image is selected so select that
|
390 |
+
img = this.imgs[this.imageIndex];
|
391 |
+
} else if (this.overIndex != null) {
|
392 |
+
// No image is selected but one is hovered
|
393 |
+
img = this.imgs[this.overIndex];
|
394 |
+
}
|
395 |
+
if (img) {
|
396 |
+
options.unshift(
|
397 |
+
{
|
398 |
+
content: "Open Image",
|
399 |
+
callback: () => {
|
400 |
+
let url = new URL(img.src);
|
401 |
+
url.searchParams.delete("preview");
|
402 |
+
window.open(url, "_blank");
|
403 |
+
},
|
404 |
+
},
|
405 |
+
...getCopyImageOption(img),
|
406 |
+
{
|
407 |
+
content: "Save Image",
|
408 |
+
callback: () => {
|
409 |
+
const a = document.createElement("a");
|
410 |
+
let url = new URL(img.src);
|
411 |
+
url.searchParams.delete("preview");
|
412 |
+
a.href = url;
|
413 |
+
a.setAttribute("download", new URLSearchParams(url.search).get("filename"));
|
414 |
+
document.body.append(a);
|
415 |
+
a.click();
|
416 |
+
requestAnimationFrame(() => a.remove());
|
417 |
+
},
|
418 |
+
}
|
419 |
+
);
|
420 |
+
}
|
421 |
+
}
|
422 |
+
|
423 |
+
options.push({
|
424 |
+
content: "Bypass",
|
425 |
+
callback: (obj) => {
|
426 |
+
if (this.mode === 4) this.mode = 0;
|
427 |
+
else this.mode = 4;
|
428 |
+
this.graph.change();
|
429 |
+
},
|
430 |
+
});
|
431 |
+
|
432 |
+
// prevent conflict of clipspace content
|
433 |
+
if (!ComfyApp.clipspace_return_node) {
|
434 |
+
options.push({
|
435 |
+
content: "Copy (Clipspace)",
|
436 |
+
callback: (obj) => {
|
437 |
+
ComfyApp.copyToClipspace(this);
|
438 |
+
},
|
439 |
+
});
|
440 |
+
|
441 |
+
if (ComfyApp.clipspace != null) {
|
442 |
+
options.push({
|
443 |
+
content: "Paste (Clipspace)",
|
444 |
+
callback: () => {
|
445 |
+
ComfyApp.pasteFromClipspace(this);
|
446 |
+
},
|
447 |
+
});
|
448 |
+
}
|
449 |
+
|
450 |
+
if (ComfyApp.isImageNode(this)) {
|
451 |
+
options.push({
|
452 |
+
content: "Open in MaskEditor",
|
453 |
+
callback: (obj) => {
|
454 |
+
ComfyApp.copyToClipspace(this);
|
455 |
+
ComfyApp.clipspace_return_node = this;
|
456 |
+
ComfyApp.open_maskeditor();
|
457 |
+
},
|
458 |
+
});
|
459 |
+
}
|
460 |
+
}
|
461 |
+
};
|
462 |
+
}
|
463 |
+
|
464 |
+
#addNodeKeyHandler(node) {
|
465 |
+
const app = this;
|
466 |
+
const origNodeOnKeyDown = node.prototype.onKeyDown;
|
467 |
+
|
468 |
+
node.prototype.onKeyDown = function(e) {
|
469 |
+
if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) {
|
470 |
+
return false;
|
471 |
+
}
|
472 |
+
|
473 |
+
if (this.flags.collapsed || !this.imgs || this.imageIndex === null) {
|
474 |
+
return;
|
475 |
+
}
|
476 |
+
|
477 |
+
let handled = false;
|
478 |
+
|
479 |
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
480 |
+
if (e.key === "ArrowLeft") {
|
481 |
+
this.imageIndex -= 1;
|
482 |
+
} else if (e.key === "ArrowRight") {
|
483 |
+
this.imageIndex += 1;
|
484 |
+
}
|
485 |
+
this.imageIndex %= this.imgs.length;
|
486 |
+
|
487 |
+
if (this.imageIndex < 0) {
|
488 |
+
this.imageIndex = this.imgs.length + this.imageIndex;
|
489 |
+
}
|
490 |
+
handled = true;
|
491 |
+
} else if (e.key === "Escape") {
|
492 |
+
this.imageIndex = null;
|
493 |
+
handled = true;
|
494 |
+
}
|
495 |
+
|
496 |
+
if (handled === true) {
|
497 |
+
e.preventDefault();
|
498 |
+
e.stopImmediatePropagation();
|
499 |
+
return false;
|
500 |
+
}
|
501 |
+
}
|
502 |
+
}
|
503 |
+
|
504 |
+
/**
|
505 |
+
* Adds Custom drawing logic for nodes
|
506 |
+
* e.g. Draws images and handles thumbnail navigation on nodes that output images
|
507 |
+
* @param {*} node The node to add the draw handler
|
508 |
+
*/
|
509 |
+
#addDrawBackgroundHandler(node) {
|
510 |
+
const app = this;
|
511 |
+
|
512 |
+
function getImageTop(node) {
|
513 |
+
let shiftY;
|
514 |
+
if (node.imageOffset != null) {
|
515 |
+
shiftY = node.imageOffset;
|
516 |
+
} else {
|
517 |
+
if (node.widgets?.length) {
|
518 |
+
const w = node.widgets[node.widgets.length - 1];
|
519 |
+
shiftY = w.last_y;
|
520 |
+
if (w.computeSize) {
|
521 |
+
shiftY += w.computeSize()[1] + 4;
|
522 |
+
}
|
523 |
+
else if(w.computedHeight) {
|
524 |
+
shiftY += w.computedHeight;
|
525 |
+
}
|
526 |
+
else {
|
527 |
+
shiftY += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
528 |
+
}
|
529 |
+
} else {
|
530 |
+
shiftY = node.computeSize()[1];
|
531 |
+
}
|
532 |
+
}
|
533 |
+
return shiftY;
|
534 |
+
}
|
535 |
+
|
536 |
+
node.prototype.setSizeForImage = function (force) {
|
537 |
+
if(!force && this.animatedImages) return;
|
538 |
+
|
539 |
+
if (this.inputHeight || this.freeWidgetSpace > 210) {
|
540 |
+
this.setSize(this.size);
|
541 |
+
return;
|
542 |
+
}
|
543 |
+
const minHeight = getImageTop(this) + 220;
|
544 |
+
if (this.size[1] < minHeight) {
|
545 |
+
this.setSize([this.size[0], minHeight]);
|
546 |
+
}
|
547 |
+
};
|
548 |
+
|
549 |
+
node.prototype.onDrawBackground = function (ctx) {
|
550 |
+
if (!this.flags.collapsed) {
|
551 |
+
let imgURLs = []
|
552 |
+
let imagesChanged = false
|
553 |
+
|
554 |
+
const output = app.nodeOutputs[this.id + ""];
|
555 |
+
if (output?.images) {
|
556 |
+
this.animatedImages = output?.animated?.find(Boolean);
|
557 |
+
if (this.images !== output.images) {
|
558 |
+
this.images = output.images;
|
559 |
+
imagesChanged = true;
|
560 |
+
imgURLs = imgURLs.concat(
|
561 |
+
output.images.map((params) => {
|
562 |
+
return api.apiURL(
|
563 |
+
"/view?" +
|
564 |
+
new URLSearchParams(params).toString() +
|
565 |
+
(this.animatedImages ? "" : app.getPreviewFormatParam()) + app.getRandParam()
|
566 |
+
);
|
567 |
+
})
|
568 |
+
);
|
569 |
+
}
|
570 |
+
}
|
571 |
+
|
572 |
+
const preview = app.nodePreviewImages[this.id + ""]
|
573 |
+
if (this.preview !== preview) {
|
574 |
+
this.preview = preview
|
575 |
+
imagesChanged = true;
|
576 |
+
if (preview != null) {
|
577 |
+
imgURLs.push(preview);
|
578 |
+
}
|
579 |
+
}
|
580 |
+
|
581 |
+
if (imagesChanged) {
|
582 |
+
this.imageIndex = null;
|
583 |
+
if (imgURLs.length > 0) {
|
584 |
+
Promise.all(
|
585 |
+
imgURLs.map((src) => {
|
586 |
+
return new Promise((r) => {
|
587 |
+
const img = new Image();
|
588 |
+
img.onload = () => r(img);
|
589 |
+
img.onerror = () => r(null);
|
590 |
+
img.src = src
|
591 |
+
});
|
592 |
+
})
|
593 |
+
).then((imgs) => {
|
594 |
+
if ((!output || this.images === output.images) && (!preview || this.preview === preview)) {
|
595 |
+
this.imgs = imgs.filter(Boolean);
|
596 |
+
this.setSizeForImage?.();
|
597 |
+
app.graph.setDirtyCanvas(true);
|
598 |
+
}
|
599 |
+
});
|
600 |
+
}
|
601 |
+
else {
|
602 |
+
this.imgs = null;
|
603 |
+
}
|
604 |
+
}
|
605 |
+
|
606 |
+
function calculateGrid(w, h, n) {
|
607 |
+
let columns, rows, cellsize;
|
608 |
+
|
609 |
+
if (w > h) {
|
610 |
+
cellsize = h;
|
611 |
+
columns = Math.ceil(w / cellsize);
|
612 |
+
rows = Math.ceil(n / columns);
|
613 |
+
} else {
|
614 |
+
cellsize = w;
|
615 |
+
rows = Math.ceil(h / cellsize);
|
616 |
+
columns = Math.ceil(n / rows);
|
617 |
+
}
|
618 |
+
|
619 |
+
while (columns * rows < n) {
|
620 |
+
cellsize++;
|
621 |
+
if (w >= h) {
|
622 |
+
columns = Math.ceil(w / cellsize);
|
623 |
+
rows = Math.ceil(n / columns);
|
624 |
+
} else {
|
625 |
+
rows = Math.ceil(h / cellsize);
|
626 |
+
columns = Math.ceil(n / rows);
|
627 |
+
}
|
628 |
+
}
|
629 |
+
|
630 |
+
const cell_size = Math.min(w/columns, h/rows);
|
631 |
+
return {cell_size, columns, rows};
|
632 |
+
}
|
633 |
+
|
634 |
+
function is_all_same_aspect_ratio(imgs) {
|
635 |
+
// assume: imgs.length >= 2
|
636 |
+
let ratio = imgs[0].naturalWidth/imgs[0].naturalHeight;
|
637 |
+
|
638 |
+
for(let i=1; i<imgs.length; i++) {
|
639 |
+
let this_ratio = imgs[i].naturalWidth/imgs[i].naturalHeight;
|
640 |
+
if(ratio != this_ratio)
|
641 |
+
return false;
|
642 |
+
}
|
643 |
+
|
644 |
+
return true;
|
645 |
+
}
|
646 |
+
|
647 |
+
if (this.imgs?.length) {
|
648 |
+
const widgetIdx = this.widgets?.findIndex((w) => w.name === ANIM_PREVIEW_WIDGET);
|
649 |
+
|
650 |
+
if(this.animatedImages) {
|
651 |
+
// Instead of using the canvas we'll use a IMG
|
652 |
+
if(widgetIdx > -1) {
|
653 |
+
// Replace content
|
654 |
+
const widget = this.widgets[widgetIdx];
|
655 |
+
widget.options.host.updateImages(this.imgs);
|
656 |
+
} else {
|
657 |
+
const host = createImageHost(this);
|
658 |
+
this.setSizeForImage(true);
|
659 |
+
const widget = this.addDOMWidget(ANIM_PREVIEW_WIDGET, "img", host.el, {
|
660 |
+
host,
|
661 |
+
getHeight: host.getHeight,
|
662 |
+
onDraw: host.onDraw,
|
663 |
+
hideOnZoom: false
|
664 |
+
});
|
665 |
+
widget.serializeValue = () => undefined;
|
666 |
+
widget.options.host.updateImages(this.imgs);
|
667 |
+
}
|
668 |
+
return;
|
669 |
+
}
|
670 |
+
|
671 |
+
if (widgetIdx > -1) {
|
672 |
+
this.widgets[widgetIdx].onRemove?.();
|
673 |
+
this.widgets.splice(widgetIdx, 1);
|
674 |
+
}
|
675 |
+
|
676 |
+
const canvas = app.graph.list_of_graphcanvas[0];
|
677 |
+
const mouse = canvas.graph_mouse;
|
678 |
+
if (!canvas.pointer_is_down && this.pointerDown) {
|
679 |
+
if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) {
|
680 |
+
this.imageIndex = this.pointerDown.index;
|
681 |
+
}
|
682 |
+
this.pointerDown = null;
|
683 |
+
}
|
684 |
+
|
685 |
+
let imageIndex = this.imageIndex;
|
686 |
+
const numImages = this.imgs.length;
|
687 |
+
if (numImages === 1 && !imageIndex) {
|
688 |
+
this.imageIndex = imageIndex = 0;
|
689 |
+
}
|
690 |
+
|
691 |
+
const top = getImageTop(this);
|
692 |
+
var shiftY = top;
|
693 |
+
|
694 |
+
let dw = this.size[0];
|
695 |
+
let dh = this.size[1];
|
696 |
+
dh -= shiftY;
|
697 |
+
|
698 |
+
if (imageIndex == null) {
|
699 |
+
var cellWidth, cellHeight, shiftX, cell_padding, cols;
|
700 |
+
|
701 |
+
const compact_mode = is_all_same_aspect_ratio(this.imgs);
|
702 |
+
if(!compact_mode) {
|
703 |
+
// use rectangle cell style and border line
|
704 |
+
cell_padding = 2;
|
705 |
+
const { cell_size, columns, rows } = calculateGrid(dw, dh, numImages);
|
706 |
+
cols = columns;
|
707 |
+
|
708 |
+
cellWidth = cell_size;
|
709 |
+
cellHeight = cell_size;
|
710 |
+
shiftX = (dw-cell_size*cols)/2;
|
711 |
+
shiftY = (dh-cell_size*rows)/2 + top;
|
712 |
+
}
|
713 |
+
else {
|
714 |
+
cell_padding = 0;
|
715 |
+
({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(this.imgs, dw, dh));
|
716 |
+
}
|
717 |
+
|
718 |
+
let anyHovered = false;
|
719 |
+
this.imageRects = [];
|
720 |
+
for (let i = 0; i < numImages; i++) {
|
721 |
+
const img = this.imgs[i];
|
722 |
+
const row = Math.floor(i / cols);
|
723 |
+
const col = i % cols;
|
724 |
+
const x = col * cellWidth + shiftX;
|
725 |
+
const y = row * cellHeight + shiftY;
|
726 |
+
if (!anyHovered) {
|
727 |
+
anyHovered = LiteGraph.isInsideRectangle(
|
728 |
+
mouse[0],
|
729 |
+
mouse[1],
|
730 |
+
x + this.pos[0],
|
731 |
+
y + this.pos[1],
|
732 |
+
cellWidth,
|
733 |
+
cellHeight
|
734 |
+
);
|
735 |
+
if (anyHovered) {
|
736 |
+
this.overIndex = i;
|
737 |
+
let value = 110;
|
738 |
+
if (canvas.pointer_is_down) {
|
739 |
+
if (!this.pointerDown || this.pointerDown.index !== i) {
|
740 |
+
this.pointerDown = { index: i, pos: [...mouse] };
|
741 |
+
}
|
742 |
+
value = 125;
|
743 |
+
}
|
744 |
+
ctx.filter = `contrast(${value}%) brightness(${value}%)`;
|
745 |
+
canvas.canvas.style.cursor = "pointer";
|
746 |
+
}
|
747 |
+
}
|
748 |
+
this.imageRects.push([x, y, cellWidth, cellHeight]);
|
749 |
+
|
750 |
+
let wratio = cellWidth/img.width;
|
751 |
+
let hratio = cellHeight/img.height;
|
752 |
+
var ratio = Math.min(wratio, hratio);
|
753 |
+
|
754 |
+
let imgHeight = ratio * img.height;
|
755 |
+
let imgY = row * cellHeight + shiftY + (cellHeight - imgHeight)/2;
|
756 |
+
let imgWidth = ratio * img.width;
|
757 |
+
let imgX = col * cellWidth + shiftX + (cellWidth - imgWidth)/2;
|
758 |
+
|
759 |
+
ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2);
|
760 |
+
if(!compact_mode) {
|
761 |
+
// rectangle cell and border line style
|
762 |
+
ctx.strokeStyle = "#8F8F8F";
|
763 |
+
ctx.lineWidth = 1;
|
764 |
+
ctx.strokeRect(x+cell_padding, y+cell_padding, cellWidth-cell_padding*2, cellHeight-cell_padding*2);
|
765 |
+
}
|
766 |
+
|
767 |
+
ctx.filter = "none";
|
768 |
+
}
|
769 |
+
|
770 |
+
if (!anyHovered) {
|
771 |
+
this.pointerDown = null;
|
772 |
+
this.overIndex = null;
|
773 |
+
}
|
774 |
+
} else {
|
775 |
+
// Draw individual
|
776 |
+
let w = this.imgs[imageIndex].naturalWidth;
|
777 |
+
let h = this.imgs[imageIndex].naturalHeight;
|
778 |
+
|
779 |
+
const scaleX = dw / w;
|
780 |
+
const scaleY = dh / h;
|
781 |
+
const scale = Math.min(scaleX, scaleY, 1);
|
782 |
+
|
783 |
+
w *= scale;
|
784 |
+
h *= scale;
|
785 |
+
|
786 |
+
let x = (dw - w) / 2;
|
787 |
+
let y = (dh - h) / 2 + shiftY;
|
788 |
+
ctx.drawImage(this.imgs[imageIndex], x, y, w, h);
|
789 |
+
|
790 |
+
const drawButton = (x, y, sz, text) => {
|
791 |
+
const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz);
|
792 |
+
let fill = "#333";
|
793 |
+
let textFill = "#fff";
|
794 |
+
let isClicking = false;
|
795 |
+
if (hovered) {
|
796 |
+
canvas.canvas.style.cursor = "pointer";
|
797 |
+
if (canvas.pointer_is_down) {
|
798 |
+
fill = "#1e90ff";
|
799 |
+
isClicking = true;
|
800 |
+
} else {
|
801 |
+
fill = "#eee";
|
802 |
+
textFill = "#000";
|
803 |
+
}
|
804 |
+
} else {
|
805 |
+
this.pointerWasDown = null;
|
806 |
+
}
|
807 |
+
|
808 |
+
ctx.fillStyle = fill;
|
809 |
+
ctx.beginPath();
|
810 |
+
ctx.roundRect(x, y, sz, sz, [4]);
|
811 |
+
ctx.fill();
|
812 |
+
ctx.fillStyle = textFill;
|
813 |
+
ctx.font = "12px Arial";
|
814 |
+
ctx.textAlign = "center";
|
815 |
+
ctx.fillText(text, x + 15, y + 20);
|
816 |
+
|
817 |
+
return isClicking;
|
818 |
+
};
|
819 |
+
|
820 |
+
if (numImages > 1) {
|
821 |
+
if (drawButton(dw - 40, dh + top - 40, 30, `${this.imageIndex + 1}/${numImages}`)) {
|
822 |
+
let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1;
|
823 |
+
if (!this.pointerDown || !this.pointerDown.index === i) {
|
824 |
+
this.pointerDown = { index: i, pos: [...mouse] };
|
825 |
+
}
|
826 |
+
}
|
827 |
+
|
828 |
+
if (drawButton(dw - 40, top + 10, 30, `x`)) {
|
829 |
+
if (!this.pointerDown || !this.pointerDown.index === null) {
|
830 |
+
this.pointerDown = { index: null, pos: [...mouse] };
|
831 |
+
}
|
832 |
+
}
|
833 |
+
}
|
834 |
+
}
|
835 |
+
}
|
836 |
+
}
|
837 |
+
};
|
838 |
+
}
|
839 |
+
|
840 |
+
/**
|
841 |
+
* Adds a handler allowing drag+drop of files onto the window to load workflows
|
842 |
+
*/
|
843 |
+
#addDropHandler() {
|
844 |
+
// Get prompt from dropped PNG or json
|
845 |
+
document.addEventListener("drop", async (event) => {
|
846 |
+
event.preventDefault();
|
847 |
+
event.stopPropagation();
|
848 |
+
|
849 |
+
const n = this.dragOverNode;
|
850 |
+
this.dragOverNode = null;
|
851 |
+
// Node handles file drop, we dont use the built in onDropFile handler as its buggy
|
852 |
+
// If you drag multiple files it will call it multiple times with the same file
|
853 |
+
if (n && n.onDragDrop && (await n.onDragDrop(event))) {
|
854 |
+
return;
|
855 |
+
}
|
856 |
+
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
|
857 |
+
if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") {
|
858 |
+
await this.handleFile(event.dataTransfer.files[0]);
|
859 |
+
} else {
|
860 |
+
// Try loading the first URI in the transfer list
|
861 |
+
const validTypes = ["text/uri-list", "text/x-moz-url"];
|
862 |
+
const match = [...event.dataTransfer.types].find((t) => validTypes.find(v => t === v));
|
863 |
+
if (match) {
|
864 |
+
const uri = event.dataTransfer.getData(match)?.split("\n")?.[0];
|
865 |
+
if (uri) {
|
866 |
+
await this.handleFile(await (await fetch(uri)).blob());
|
867 |
+
}
|
868 |
+
}
|
869 |
+
}
|
870 |
+
});
|
871 |
+
|
872 |
+
// Always clear over node on drag leave
|
873 |
+
this.canvasEl.addEventListener("dragleave", async () => {
|
874 |
+
if (this.dragOverNode) {
|
875 |
+
this.dragOverNode = null;
|
876 |
+
this.graph.setDirtyCanvas(false, true);
|
877 |
+
}
|
878 |
+
});
|
879 |
+
|
880 |
+
// Add handler for dropping onto a specific node
|
881 |
+
this.canvasEl.addEventListener(
|
882 |
+
"dragover",
|
883 |
+
(e) => {
|
884 |
+
this.canvas.adjustMouseEvent(e);
|
885 |
+
const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY);
|
886 |
+
if (node) {
|
887 |
+
if (node.onDragOver && node.onDragOver(e)) {
|
888 |
+
this.dragOverNode = node;
|
889 |
+
|
890 |
+
// dragover event is fired very frequently, run this on an animation frame
|
891 |
+
requestAnimationFrame(() => {
|
892 |
+
this.graph.setDirtyCanvas(false, true);
|
893 |
+
});
|
894 |
+
return;
|
895 |
+
}
|
896 |
+
}
|
897 |
+
this.dragOverNode = null;
|
898 |
+
},
|
899 |
+
false
|
900 |
+
);
|
901 |
+
}
|
902 |
+
|
903 |
+
/**
|
904 |
+
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
|
905 |
+
*/
|
906 |
+
#addPasteHandler() {
|
907 |
+
document.addEventListener("paste", async (e) => {
|
908 |
+
// ctrl+shift+v is used to paste nodes with connections
|
909 |
+
// this is handled by litegraph
|
910 |
+
if(this.shiftDown) return;
|
911 |
+
|
912 |
+
let data = (e.clipboardData || window.clipboardData);
|
913 |
+
const items = data.items;
|
914 |
+
|
915 |
+
// Look for image paste data
|
916 |
+
for (const item of items) {
|
917 |
+
if (item.type.startsWith('image/')) {
|
918 |
+
var imageNode = null;
|
919 |
+
|
920 |
+
// If an image node is selected, paste into it
|
921 |
+
if (this.canvas.current_node &&
|
922 |
+
this.canvas.current_node.is_selected &&
|
923 |
+
ComfyApp.isImageNode(this.canvas.current_node)) {
|
924 |
+
imageNode = this.canvas.current_node;
|
925 |
+
}
|
926 |
+
|
927 |
+
// No image node selected: add a new one
|
928 |
+
if (!imageNode) {
|
929 |
+
const newNode = LiteGraph.createNode("LoadImage");
|
930 |
+
newNode.pos = [...this.canvas.graph_mouse];
|
931 |
+
imageNode = this.graph.add(newNode);
|
932 |
+
this.graph.change();
|
933 |
+
}
|
934 |
+
const blob = item.getAsFile();
|
935 |
+
imageNode.pasteFile(blob);
|
936 |
+
return;
|
937 |
+
}
|
938 |
+
}
|
939 |
+
|
940 |
+
// No image found. Look for node data
|
941 |
+
data = data.getData("text/plain");
|
942 |
+
let workflow;
|
943 |
+
try {
|
944 |
+
data = data.slice(data.indexOf("{"));
|
945 |
+
workflow = JSON.parse(data);
|
946 |
+
} catch (err) {
|
947 |
+
try {
|
948 |
+
data = data.slice(data.indexOf("workflow\n"));
|
949 |
+
data = data.slice(data.indexOf("{"));
|
950 |
+
workflow = JSON.parse(data);
|
951 |
+
} catch (error) {}
|
952 |
+
}
|
953 |
+
|
954 |
+
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
|
955 |
+
await this.loadGraphData(workflow);
|
956 |
+
}
|
957 |
+
else {
|
958 |
+
if (e.target.type === "text" || e.target.type === "textarea") {
|
959 |
+
return;
|
960 |
+
}
|
961 |
+
|
962 |
+
// Litegraph default paste
|
963 |
+
this.canvas.pasteFromClipboard();
|
964 |
+
}
|
965 |
+
|
966 |
+
|
967 |
+
});
|
968 |
+
}
|
969 |
+
|
970 |
+
|
971 |
+
/**
|
972 |
+
* Adds a handler on copy that serializes selected nodes to JSON
|
973 |
+
*/
|
974 |
+
#addCopyHandler() {
|
975 |
+
document.addEventListener("copy", (e) => {
|
976 |
+
if (e.target.type === "text" || e.target.type === "textarea") {
|
977 |
+
// Default system copy
|
978 |
+
return;
|
979 |
+
}
|
980 |
+
|
981 |
+
// copy nodes and clear clipboard
|
982 |
+
if (e.target.className === "litegraph" && this.canvas.selected_nodes) {
|
983 |
+
this.canvas.copyToClipboard();
|
984 |
+
e.clipboardData.setData('text', ' '); //clearData doesn't remove images from clipboard
|
985 |
+
e.preventDefault();
|
986 |
+
e.stopImmediatePropagation();
|
987 |
+
return false;
|
988 |
+
}
|
989 |
+
});
|
990 |
+
}
|
991 |
+
|
992 |
+
|
993 |
+
/**
|
994 |
+
* Handle mouse
|
995 |
+
*
|
996 |
+
* Move group by header
|
997 |
+
*/
|
998 |
+
#addProcessMouseHandler() {
|
999 |
+
const self = this;
|
1000 |
+
|
1001 |
+
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
|
1002 |
+
LGraphCanvas.prototype.processMouseDown = function(e) {
|
1003 |
+
// prepare for ctrl+shift drag: zoom start
|
1004 |
+
if(e.ctrlKey && e.shiftKey && e.buttons) {
|
1005 |
+
self.zoom_drag_start = [e.x, e.y, this.ds.scale];
|
1006 |
+
return;
|
1007 |
+
}
|
1008 |
+
|
1009 |
+
const res = origProcessMouseDown.apply(this, arguments);
|
1010 |
+
|
1011 |
+
this.selected_group_moving = false;
|
1012 |
+
|
1013 |
+
if (this.selected_group && !this.selected_group_resizing) {
|
1014 |
+
var font_size =
|
1015 |
+
this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
|
1016 |
+
var height = font_size * 1.4;
|
1017 |
+
|
1018 |
+
// Move group by header
|
1019 |
+
if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) {
|
1020 |
+
this.selected_group_moving = true;
|
1021 |
+
}
|
1022 |
+
}
|
1023 |
+
|
1024 |
+
return res;
|
1025 |
+
}
|
1026 |
+
|
1027 |
+
const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove;
|
1028 |
+
LGraphCanvas.prototype.processMouseMove = function(e) {
|
1029 |
+
// handle ctrl+shift drag
|
1030 |
+
if(e.ctrlKey && e.shiftKey && self.zoom_drag_start) {
|
1031 |
+
// stop canvas zoom action
|
1032 |
+
if(!e.buttons) {
|
1033 |
+
self.zoom_drag_start = null;
|
1034 |
+
return;
|
1035 |
+
}
|
1036 |
+
|
1037 |
+
// calculate delta
|
1038 |
+
let deltaY = e.y - self.zoom_drag_start[1];
|
1039 |
+
let startScale = self.zoom_drag_start[2];
|
1040 |
+
|
1041 |
+
let scale = startScale - deltaY/100;
|
1042 |
+
|
1043 |
+
this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]);
|
1044 |
+
this.graph.change();
|
1045 |
+
|
1046 |
+
return;
|
1047 |
+
}
|
1048 |
+
|
1049 |
+
const orig_selected_group = this.selected_group;
|
1050 |
+
|
1051 |
+
if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
|
1052 |
+
this.selected_group = null;
|
1053 |
+
}
|
1054 |
+
|
1055 |
+
const res = origProcessMouseMove.apply(this, arguments);
|
1056 |
+
|
1057 |
+
if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
|
1058 |
+
this.selected_group = orig_selected_group;
|
1059 |
+
}
|
1060 |
+
|
1061 |
+
return res;
|
1062 |
+
};
|
1063 |
+
}
|
1064 |
+
|
1065 |
+
/**
|
1066 |
+
* Handle keypress
|
1067 |
+
*
|
1068 |
+
* Ctrl + M mute/unmute selected nodes
|
1069 |
+
*/
|
1070 |
+
#addProcessKeyHandler() {
|
1071 |
+
const self = this;
|
1072 |
+
const origProcessKey = LGraphCanvas.prototype.processKey;
|
1073 |
+
LGraphCanvas.prototype.processKey = function(e) {
|
1074 |
+
if (!this.graph) {
|
1075 |
+
return;
|
1076 |
+
}
|
1077 |
+
|
1078 |
+
var block_default = false;
|
1079 |
+
|
1080 |
+
if (e.target.localName == "input") {
|
1081 |
+
return;
|
1082 |
+
}
|
1083 |
+
|
1084 |
+
if (e.type == "keydown" && !e.repeat) {
|
1085 |
+
|
1086 |
+
// Ctrl + M mute/unmute
|
1087 |
+
if (e.key === 'm' && (e.metaKey || e.ctrlKey)) {
|
1088 |
+
if (this.selected_nodes) {
|
1089 |
+
for (var i in this.selected_nodes) {
|
1090 |
+
if (this.selected_nodes[i].mode === 2) { // never
|
1091 |
+
this.selected_nodes[i].mode = 0; // always
|
1092 |
+
} else {
|
1093 |
+
this.selected_nodes[i].mode = 2; // never
|
1094 |
+
}
|
1095 |
+
}
|
1096 |
+
}
|
1097 |
+
block_default = true;
|
1098 |
+
}
|
1099 |
+
|
1100 |
+
// Ctrl + B bypass
|
1101 |
+
if (e.key === 'b' && (e.metaKey || e.ctrlKey)) {
|
1102 |
+
if (this.selected_nodes) {
|
1103 |
+
for (var i in this.selected_nodes) {
|
1104 |
+
if (this.selected_nodes[i].mode === 4) { // never
|
1105 |
+
this.selected_nodes[i].mode = 0; // always
|
1106 |
+
} else {
|
1107 |
+
this.selected_nodes[i].mode = 4; // never
|
1108 |
+
}
|
1109 |
+
}
|
1110 |
+
}
|
1111 |
+
block_default = true;
|
1112 |
+
}
|
1113 |
+
|
1114 |
+
// Alt + C collapse/uncollapse
|
1115 |
+
if (e.key === 'c' && e.altKey) {
|
1116 |
+
if (this.selected_nodes) {
|
1117 |
+
for (var i in this.selected_nodes) {
|
1118 |
+
this.selected_nodes[i].collapse()
|
1119 |
+
}
|
1120 |
+
}
|
1121 |
+
block_default = true;
|
1122 |
+
}
|
1123 |
+
|
1124 |
+
// Ctrl+C Copy
|
1125 |
+
if ((e.key === 'c') && (e.metaKey || e.ctrlKey)) {
|
1126 |
+
// Trigger onCopy
|
1127 |
+
return true;
|
1128 |
+
}
|
1129 |
+
|
1130 |
+
// Ctrl+V Paste
|
1131 |
+
if ((e.key === 'v' || e.key == 'V') && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
1132 |
+
// Trigger onPaste
|
1133 |
+
return true;
|
1134 |
+
}
|
1135 |
+
|
1136 |
+
if((e.key === '+') && e.altKey) {
|
1137 |
+
block_default = true;
|
1138 |
+
let scale = this.ds.scale * 1.1;
|
1139 |
+
this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]);
|
1140 |
+
this.graph.change();
|
1141 |
+
}
|
1142 |
+
|
1143 |
+
if((e.key === '-') && e.altKey) {
|
1144 |
+
block_default = true;
|
1145 |
+
let scale = this.ds.scale * 1 / 1.1;
|
1146 |
+
this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]);
|
1147 |
+
this.graph.change();
|
1148 |
+
}
|
1149 |
+
}
|
1150 |
+
|
1151 |
+
this.graph.change();
|
1152 |
+
|
1153 |
+
if (block_default) {
|
1154 |
+
e.preventDefault();
|
1155 |
+
e.stopImmediatePropagation();
|
1156 |
+
return false;
|
1157 |
+
}
|
1158 |
+
|
1159 |
+
// Fall through to Litegraph defaults
|
1160 |
+
return origProcessKey.apply(this, arguments);
|
1161 |
+
};
|
1162 |
+
}
|
1163 |
+
|
1164 |
+
/**
|
1165 |
+
* Draws group header bar
|
1166 |
+
*/
|
1167 |
+
#addDrawGroupsHandler() {
|
1168 |
+
const self = this;
|
1169 |
+
|
1170 |
+
const origDrawGroups = LGraphCanvas.prototype.drawGroups;
|
1171 |
+
LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
|
1172 |
+
if (!this.graph) {
|
1173 |
+
return;
|
1174 |
+
}
|
1175 |
+
|
1176 |
+
var groups = this.graph._groups;
|
1177 |
+
|
1178 |
+
ctx.save();
|
1179 |
+
ctx.globalAlpha = 0.7 * this.editor_alpha;
|
1180 |
+
|
1181 |
+
for (var i = 0; i < groups.length; ++i) {
|
1182 |
+
var group = groups[i];
|
1183 |
+
|
1184 |
+
if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) {
|
1185 |
+
continue;
|
1186 |
+
} //out of the visible area
|
1187 |
+
|
1188 |
+
ctx.fillStyle = group.color || "#335";
|
1189 |
+
ctx.strokeStyle = group.color || "#335";
|
1190 |
+
var pos = group._pos;
|
1191 |
+
var size = group._size;
|
1192 |
+
ctx.globalAlpha = 0.25 * this.editor_alpha;
|
1193 |
+
ctx.beginPath();
|
1194 |
+
var font_size =
|
1195 |
+
group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
|
1196 |
+
ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4);
|
1197 |
+
ctx.fill();
|
1198 |
+
ctx.globalAlpha = this.editor_alpha;
|
1199 |
+
}
|
1200 |
+
|
1201 |
+
ctx.restore();
|
1202 |
+
|
1203 |
+
const res = origDrawGroups.apply(this, arguments);
|
1204 |
+
return res;
|
1205 |
+
}
|
1206 |
+
}
|
1207 |
+
|
1208 |
+
/**
|
1209 |
+
* Draws node highlights (executing, drag drop) and progress bar
|
1210 |
+
*/
|
1211 |
+
#addDrawNodeHandler() {
|
1212 |
+
const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape;
|
1213 |
+
const self = this;
|
1214 |
+
|
1215 |
+
LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
|
1216 |
+
const res = origDrawNodeShape.apply(this, arguments);
|
1217 |
+
|
1218 |
+
const nodeErrors = self.lastNodeErrors?.[node.id];
|
1219 |
+
|
1220 |
+
let color = null;
|
1221 |
+
let lineWidth = 1;
|
1222 |
+
if (node.id === +self.runningNodeId) {
|
1223 |
+
color = "#0f0";
|
1224 |
+
} else if (self.dragOverNode && node.id === self.dragOverNode.id) {
|
1225 |
+
color = "dodgerblue";
|
1226 |
+
}
|
1227 |
+
else if (nodeErrors?.errors) {
|
1228 |
+
color = "red";
|
1229 |
+
lineWidth = 2;
|
1230 |
+
}
|
1231 |
+
else if (self.lastExecutionError && +self.lastExecutionError.node_id === node.id) {
|
1232 |
+
color = "#f0f";
|
1233 |
+
lineWidth = 2;
|
1234 |
+
}
|
1235 |
+
|
1236 |
+
if (color) {
|
1237 |
+
const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE;
|
1238 |
+
ctx.lineWidth = lineWidth;
|
1239 |
+
ctx.globalAlpha = 0.8;
|
1240 |
+
ctx.beginPath();
|
1241 |
+
if (shape == LiteGraph.BOX_SHAPE)
|
1242 |
+
ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
|
1243 |
+
else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed))
|
1244 |
+
ctx.roundRect(
|
1245 |
+
-6,
|
1246 |
+
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
1247 |
+
12 + size[0] + 1,
|
1248 |
+
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
1249 |
+
this.round_radius * 2
|
1250 |
+
);
|
1251 |
+
else if (shape == LiteGraph.CARD_SHAPE)
|
1252 |
+
ctx.roundRect(
|
1253 |
+
-6,
|
1254 |
+
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
1255 |
+
12 + size[0] + 1,
|
1256 |
+
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
1257 |
+
[this.round_radius * 2, this.round_radius * 2, 2, 2]
|
1258 |
+
);
|
1259 |
+
else if (shape == LiteGraph.CIRCLE_SHAPE)
|
1260 |
+
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
|
1261 |
+
ctx.strokeStyle = color;
|
1262 |
+
ctx.stroke();
|
1263 |
+
ctx.strokeStyle = fgcolor;
|
1264 |
+
ctx.globalAlpha = 1;
|
1265 |
+
}
|
1266 |
+
|
1267 |
+
if (self.progress && node.id === +self.runningNodeId) {
|
1268 |
+
ctx.fillStyle = "green";
|
1269 |
+
ctx.fillRect(0, 0, size[0] * (self.progress.value / self.progress.max), 6);
|
1270 |
+
ctx.fillStyle = bgcolor;
|
1271 |
+
}
|
1272 |
+
|
1273 |
+
// Highlight inputs that failed validation
|
1274 |
+
if (nodeErrors) {
|
1275 |
+
ctx.lineWidth = 2;
|
1276 |
+
ctx.strokeStyle = "red";
|
1277 |
+
for (const error of nodeErrors.errors) {
|
1278 |
+
if (error.extra_info && error.extra_info.input_name) {
|
1279 |
+
const inputIndex = node.findInputSlot(error.extra_info.input_name)
|
1280 |
+
if (inputIndex !== -1) {
|
1281 |
+
let pos = node.getConnectionPos(true, inputIndex);
|
1282 |
+
ctx.beginPath();
|
1283 |
+
ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false)
|
1284 |
+
ctx.stroke();
|
1285 |
+
}
|
1286 |
+
}
|
1287 |
+
}
|
1288 |
+
}
|
1289 |
+
|
1290 |
+
return res;
|
1291 |
+
};
|
1292 |
+
|
1293 |
+
const origDrawNode = LGraphCanvas.prototype.drawNode;
|
1294 |
+
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
1295 |
+
var editor_alpha = this.editor_alpha;
|
1296 |
+
var old_color = node.bgcolor;
|
1297 |
+
|
1298 |
+
if (node.mode === 2) { // never
|
1299 |
+
this.editor_alpha = 0.4;
|
1300 |
+
}
|
1301 |
+
|
1302 |
+
if (node.mode === 4) { // never
|
1303 |
+
node.bgcolor = "#FF00FF";
|
1304 |
+
this.editor_alpha = 0.2;
|
1305 |
+
}
|
1306 |
+
|
1307 |
+
const res = origDrawNode.apply(this, arguments);
|
1308 |
+
|
1309 |
+
this.editor_alpha = editor_alpha;
|
1310 |
+
node.bgcolor = old_color;
|
1311 |
+
|
1312 |
+
return res;
|
1313 |
+
};
|
1314 |
+
}
|
1315 |
+
|
1316 |
+
/**
|
1317 |
+
* Handles updates from the API socket
|
1318 |
+
*/
|
1319 |
+
#addApiUpdateHandlers() {
|
1320 |
+
api.addEventListener("status", ({ detail }) => {
|
1321 |
+
this.ui.setStatus(detail);
|
1322 |
+
});
|
1323 |
+
|
1324 |
+
api.addEventListener("reconnecting", () => {
|
1325 |
+
this.ui.dialog.show("Reconnecting...");
|
1326 |
+
});
|
1327 |
+
|
1328 |
+
api.addEventListener("reconnected", () => {
|
1329 |
+
this.ui.dialog.close();
|
1330 |
+
});
|
1331 |
+
|
1332 |
+
api.addEventListener("progress", ({ detail }) => {
|
1333 |
+
if (this.workflowManager.activePrompt?.workflow
|
1334 |
+
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
|
1335 |
+
this.progress = detail;
|
1336 |
+
this.graph.setDirtyCanvas(true, false);
|
1337 |
+
});
|
1338 |
+
|
1339 |
+
api.addEventListener("executing", ({ detail }) => {
|
1340 |
+
if (this.workflowManager.activePrompt ?.workflow
|
1341 |
+
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
|
1342 |
+
this.progress = null;
|
1343 |
+
this.runningNodeId = detail;
|
1344 |
+
this.graph.setDirtyCanvas(true, false);
|
1345 |
+
delete this.nodePreviewImages[this.runningNodeId]
|
1346 |
+
});
|
1347 |
+
|
1348 |
+
api.addEventListener("executed", ({ detail }) => {
|
1349 |
+
if (this.workflowManager.activePrompt ?.workflow
|
1350 |
+
&& this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return;
|
1351 |
+
const output = this.nodeOutputs[detail.node];
|
1352 |
+
if (detail.merge && output) {
|
1353 |
+
for (const k in detail.output ?? {}) {
|
1354 |
+
const v = output[k];
|
1355 |
+
if (v instanceof Array) {
|
1356 |
+
output[k] = v.concat(detail.output[k]);
|
1357 |
+
} else {
|
1358 |
+
output[k] = detail.output[k];
|
1359 |
+
}
|
1360 |
+
}
|
1361 |
+
} else {
|
1362 |
+
this.nodeOutputs[detail.node] = detail.output;
|
1363 |
+
}
|
1364 |
+
const node = this.graph.getNodeById(detail.node);
|
1365 |
+
if (node) {
|
1366 |
+
if (node.onExecuted)
|
1367 |
+
node.onExecuted(detail.output);
|
1368 |
+
}
|
1369 |
+
});
|
1370 |
+
|
1371 |
+
api.addEventListener("execution_start", ({ detail }) => {
|
1372 |
+
this.runningNodeId = null;
|
1373 |
+
this.lastExecutionError = null
|
1374 |
+
this.graph._nodes.forEach((node) => {
|
1375 |
+
if (node.onExecutionStart)
|
1376 |
+
node.onExecutionStart()
|
1377 |
+
})
|
1378 |
+
});
|
1379 |
+
|
1380 |
+
api.addEventListener("execution_error", ({ detail }) => {
|
1381 |
+
this.lastExecutionError = detail;
|
1382 |
+
const formattedError = this.#formatExecutionError(detail);
|
1383 |
+
this.ui.dialog.show(formattedError);
|
1384 |
+
this.canvas.draw(true, true);
|
1385 |
+
});
|
1386 |
+
|
1387 |
+
api.addEventListener("b_preview", ({ detail }) => {
|
1388 |
+
const id = this.runningNodeId
|
1389 |
+
if (id == null)
|
1390 |
+
return;
|
1391 |
+
|
1392 |
+
const blob = detail
|
1393 |
+
const blobUrl = URL.createObjectURL(blob)
|
1394 |
+
this.nodePreviewImages[id] = [blobUrl]
|
1395 |
+
});
|
1396 |
+
|
1397 |
+
api.init();
|
1398 |
+
}
|
1399 |
+
|
1400 |
+
#addKeyboardHandler() {
|
1401 |
+
window.addEventListener("keydown", (e) => {
|
1402 |
+
this.shiftDown = e.shiftKey;
|
1403 |
+
});
|
1404 |
+
window.addEventListener("keyup", (e) => {
|
1405 |
+
this.shiftDown = e.shiftKey;
|
1406 |
+
});
|
1407 |
+
}
|
1408 |
+
|
1409 |
+
#addConfigureHandler() {
|
1410 |
+
const app = this;
|
1411 |
+
const configure = LGraph.prototype.configure;
|
1412 |
+
// Flag that the graph is configuring to prevent nodes from running checks while its still loading
|
1413 |
+
LGraph.prototype.configure = function () {
|
1414 |
+
app.configuringGraph = true;
|
1415 |
+
try {
|
1416 |
+
return configure.apply(this, arguments);
|
1417 |
+
} finally {
|
1418 |
+
app.configuringGraph = false;
|
1419 |
+
}
|
1420 |
+
};
|
1421 |
+
}
|
1422 |
+
|
1423 |
+
#addAfterConfigureHandler() {
|
1424 |
+
const app = this;
|
1425 |
+
const onConfigure = app.graph.onConfigure;
|
1426 |
+
app.graph.onConfigure = function () {
|
1427 |
+
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
|
1428 |
+
for (const node of app.graph._nodes) {
|
1429 |
+
node.onGraphConfigured?.();
|
1430 |
+
}
|
1431 |
+
|
1432 |
+
const r = onConfigure?.apply(this, arguments);
|
1433 |
+
|
1434 |
+
// Fire after onConfigure, used by primitves to generate widget using input nodes config
|
1435 |
+
for (const node of app.graph._nodes) {
|
1436 |
+
node.onAfterGraphConfigured?.();
|
1437 |
+
}
|
1438 |
+
|
1439 |
+
return r;
|
1440 |
+
};
|
1441 |
+
}
|
1442 |
+
|
1443 |
+
/**
|
1444 |
+
* Loads all extensions from the API into the window in parallel
|
1445 |
+
*/
|
1446 |
+
async #loadExtensions() {
|
1447 |
+
const extensions = await api.getExtensions();
|
1448 |
+
this.logging.addEntry("Comfy.App", "debug", { Extensions: extensions });
|
1449 |
+
|
1450 |
+
const extensionPromises = extensions.map(async ext => {
|
1451 |
+
try {
|
1452 |
+
await import(api.apiURL(ext));
|
1453 |
+
} catch (error) {
|
1454 |
+
console.error("Error loading extension", ext, error);
|
1455 |
+
}
|
1456 |
+
});
|
1457 |
+
|
1458 |
+
await Promise.all(extensionPromises);
|
1459 |
+
try {
|
1460 |
+
this.menu.workflows.registerExtension(this);
|
1461 |
+
} catch (error) {
|
1462 |
+
console.error(error);
|
1463 |
+
}
|
1464 |
+
}
|
1465 |
+
|
1466 |
+
async #migrateSettings() {
|
1467 |
+
this.isNewUserSession = true;
|
1468 |
+
// Store all current settings
|
1469 |
+
const settings = Object.keys(this.ui.settings).reduce((p, n) => {
|
1470 |
+
const v = localStorage[`Comfy.Settings.${n}`];
|
1471 |
+
if (v) {
|
1472 |
+
try {
|
1473 |
+
p[n] = JSON.parse(v);
|
1474 |
+
} catch (error) {}
|
1475 |
+
}
|
1476 |
+
return p;
|
1477 |
+
}, {});
|
1478 |
+
|
1479 |
+
await api.storeSettings(settings);
|
1480 |
+
}
|
1481 |
+
|
1482 |
+
async #setUser() {
|
1483 |
+
const userConfig = await api.getUserConfig();
|
1484 |
+
this.storageLocation = userConfig.storage;
|
1485 |
+
if (typeof userConfig.migrated == "boolean") {
|
1486 |
+
// Single user mode migrated true/false for if the default user is created
|
1487 |
+
if (!userConfig.migrated && this.storageLocation === "server") {
|
1488 |
+
// Default user not created yet
|
1489 |
+
await this.#migrateSettings();
|
1490 |
+
}
|
1491 |
+
return;
|
1492 |
+
}
|
1493 |
+
|
1494 |
+
this.multiUserServer = true;
|
1495 |
+
let user = localStorage["Comfy.userId"];
|
1496 |
+
const users = userConfig.users ?? {};
|
1497 |
+
if (!user || !users[user]) {
|
1498 |
+
// This will rarely be hit so move the loading to on demand
|
1499 |
+
const { UserSelectionScreen } = await import("./ui/userSelection.js");
|
1500 |
+
|
1501 |
+
this.ui.menuContainer.style.display = "none";
|
1502 |
+
const { userId, username, created } = await new UserSelectionScreen().show(users, user);
|
1503 |
+
this.ui.menuContainer.style.display = "";
|
1504 |
+
|
1505 |
+
user = userId;
|
1506 |
+
localStorage["Comfy.userName"] = username;
|
1507 |
+
localStorage["Comfy.userId"] = user;
|
1508 |
+
|
1509 |
+
if (created) {
|
1510 |
+
api.user = user;
|
1511 |
+
await this.#migrateSettings();
|
1512 |
+
}
|
1513 |
+
}
|
1514 |
+
|
1515 |
+
api.user = user;
|
1516 |
+
|
1517 |
+
this.ui.settings.addSetting({
|
1518 |
+
id: "Comfy.SwitchUser",
|
1519 |
+
name: "Switch User",
|
1520 |
+
type: (name) => {
|
1521 |
+
let currentUser = localStorage["Comfy.userName"];
|
1522 |
+
if (currentUser) {
|
1523 |
+
currentUser = ` (${currentUser})`;
|
1524 |
+
}
|
1525 |
+
return $el("tr", [
|
1526 |
+
$el("td", [
|
1527 |
+
$el("label", {
|
1528 |
+
textContent: name,
|
1529 |
+
}),
|
1530 |
+
]),
|
1531 |
+
$el("td", [
|
1532 |
+
$el("button", {
|
1533 |
+
textContent: name + (currentUser ?? ""),
|
1534 |
+
onclick: () => {
|
1535 |
+
delete localStorage["Comfy.userId"];
|
1536 |
+
delete localStorage["Comfy.userName"];
|
1537 |
+
window.location.reload();
|
1538 |
+
},
|
1539 |
+
}),
|
1540 |
+
]),
|
1541 |
+
]);
|
1542 |
+
},
|
1543 |
+
});
|
1544 |
+
}
|
1545 |
+
|
1546 |
+
/**
|
1547 |
+
* Set up the app on the page
|
1548 |
+
*/
|
1549 |
+
async setup() {
|
1550 |
+
await this.#setUser();
|
1551 |
+
|
1552 |
+
// Create and mount the LiteGraph in the DOM
|
1553 |
+
const mainCanvas = document.createElement("canvas")
|
1554 |
+
mainCanvas.style.touchAction = "none"
|
1555 |
+
const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { id: "graph-canvas" }));
|
1556 |
+
canvasEl.tabIndex = "1";
|
1557 |
+
document.body.append(canvasEl);
|
1558 |
+
this.resizeCanvas();
|
1559 |
+
|
1560 |
+
await Promise.all([this.workflowManager.loadWorkflows(), this.ui.settings.load()]);
|
1561 |
+
await this.#loadExtensions();
|
1562 |
+
|
1563 |
+
addDomClippingSetting();
|
1564 |
+
this.#addProcessMouseHandler();
|
1565 |
+
this.#addProcessKeyHandler();
|
1566 |
+
this.#addConfigureHandler();
|
1567 |
+
this.#addApiUpdateHandlers();
|
1568 |
+
this.#addRestoreWorkflowView();
|
1569 |
+
|
1570 |
+
this.graph = new LGraph();
|
1571 |
+
|
1572 |
+
this.#addAfterConfigureHandler();
|
1573 |
+
|
1574 |
+
this.canvas = new LGraphCanvas(canvasEl, this.graph);
|
1575 |
+
this.ctx = canvasEl.getContext("2d");
|
1576 |
+
|
1577 |
+
LiteGraph.release_link_on_empty_shows_menu = true;
|
1578 |
+
LiteGraph.alt_drag_do_clone_nodes = true;
|
1579 |
+
|
1580 |
+
this.graph.start();
|
1581 |
+
|
1582 |
+
// Ensure the canvas fills the window
|
1583 |
+
this.resizeCanvas();
|
1584 |
+
window.addEventListener("resize", () => this.resizeCanvas());
|
1585 |
+
const ro = new ResizeObserver(() => this.resizeCanvas());
|
1586 |
+
ro.observe(this.bodyTop);
|
1587 |
+
ro.observe(this.bodyLeft);
|
1588 |
+
ro.observe(this.bodyRight);
|
1589 |
+
ro.observe(this.bodyBottom);
|
1590 |
+
|
1591 |
+
await this.#invokeExtensionsAsync("init");
|
1592 |
+
await this.registerNodes();
|
1593 |
+
initWidgets(this);
|
1594 |
+
|
1595 |
+
// Load previous workflow
|
1596 |
+
let restored = false;
|
1597 |
+
try {
|
1598 |
+
const loadWorkflow = async (json) => {
|
1599 |
+
if (json) {
|
1600 |
+
const workflow = JSON.parse(json);
|
1601 |
+
const workflowName = getStorageValue("Comfy.PreviousWorkflow");
|
1602 |
+
await this.loadGraphData(workflow, true, true, workflowName);
|
1603 |
+
return true;
|
1604 |
+
}
|
1605 |
+
};
|
1606 |
+
const clientId = api.initialClientId ?? api.clientId;
|
1607 |
+
restored =
|
1608 |
+
(clientId && (await loadWorkflow(sessionStorage.getItem(`workflow:${clientId}`)))) ||
|
1609 |
+
(await loadWorkflow(localStorage.getItem("workflow")));
|
1610 |
+
} catch (err) {
|
1611 |
+
console.error("Error loading previous workflow", err);
|
1612 |
+
}
|
1613 |
+
|
1614 |
+
// We failed to restore a workflow so load the default
|
1615 |
+
if (!restored) {
|
1616 |
+
await this.loadGraphData();
|
1617 |
+
}
|
1618 |
+
|
1619 |
+
// Save current workflow automatically
|
1620 |
+
setInterval(() => {
|
1621 |
+
const workflow = JSON.stringify(this.graph.serialize());
|
1622 |
+
localStorage.setItem("workflow", workflow);
|
1623 |
+
if (api.clientId) {
|
1624 |
+
sessionStorage.setItem(`workflow:${api.clientId}`, workflow);
|
1625 |
+
}
|
1626 |
+
}, 1000);
|
1627 |
+
|
1628 |
+
this.#addDrawNodeHandler();
|
1629 |
+
this.#addDrawGroupsHandler();
|
1630 |
+
this.#addDropHandler();
|
1631 |
+
this.#addCopyHandler();
|
1632 |
+
this.#addPasteHandler();
|
1633 |
+
this.#addKeyboardHandler();
|
1634 |
+
|
1635 |
+
await this.#invokeExtensionsAsync("setup");
|
1636 |
+
}
|
1637 |
+
|
1638 |
+
resizeCanvas() {
|
1639 |
+
// Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845
|
1640 |
+
const scale = Math.max(window.devicePixelRatio, 1);
|
1641 |
+
|
1642 |
+
// Clear fixed width and height while calculating rect so it uses 100% instead
|
1643 |
+
this.canvasEl.height = this.canvasEl.width = "";
|
1644 |
+
const { width, height } = this.canvasEl.getBoundingClientRect();
|
1645 |
+
this.canvasEl.width = Math.round(width * scale);
|
1646 |
+
this.canvasEl.height = Math.round(height * scale);
|
1647 |
+
this.canvasEl.getContext("2d").scale(scale, scale);
|
1648 |
+
this.canvas?.draw(true, true);
|
1649 |
+
}
|
1650 |
+
|
1651 |
+
/**
|
1652 |
+
* Registers nodes with the graph
|
1653 |
+
*/
|
1654 |
+
async registerNodes() {
|
1655 |
+
const app = this;
|
1656 |
+
// Load node definitions from the backend
|
1657 |
+
const defs = await api.getNodeDefs();
|
1658 |
+
await this.registerNodesFromDefs(defs);
|
1659 |
+
await this.#invokeExtensionsAsync("registerCustomNodes");
|
1660 |
+
}
|
1661 |
+
|
1662 |
+
getWidgetType(inputData, inputName) {
|
1663 |
+
const type = inputData[0];
|
1664 |
+
|
1665 |
+
if (Array.isArray(type)) {
|
1666 |
+
return "COMBO";
|
1667 |
+
} else if (`${type}:${inputName}` in this.widgets) {
|
1668 |
+
return `${type}:${inputName}`;
|
1669 |
+
} else if (type in this.widgets) {
|
1670 |
+
return type;
|
1671 |
+
} else {
|
1672 |
+
return null;
|
1673 |
+
}
|
1674 |
+
}
|
1675 |
+
|
1676 |
+
async registerNodeDef(nodeId, nodeData) {
|
1677 |
+
const self = this;
|
1678 |
+
const node = Object.assign(
|
1679 |
+
function ComfyNode() {
|
1680 |
+
var inputs = nodeData["input"]["required"];
|
1681 |
+
if (nodeData["input"]["optional"] != undefined) {
|
1682 |
+
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]);
|
1683 |
+
}
|
1684 |
+
const config = { minWidth: 1, minHeight: 1 };
|
1685 |
+
for (const inputName in inputs) {
|
1686 |
+
const inputData = inputs[inputName];
|
1687 |
+
const type = inputData[0];
|
1688 |
+
|
1689 |
+
let widgetCreated = true;
|
1690 |
+
const widgetType = self.getWidgetType(inputData, inputName);
|
1691 |
+
if(widgetType) {
|
1692 |
+
if(widgetType === "COMBO") {
|
1693 |
+
Object.assign(config, self.widgets.COMBO(this, inputName, inputData, app) || {});
|
1694 |
+
} else {
|
1695 |
+
Object.assign(config, self.widgets[widgetType](this, inputName, inputData, app) || {});
|
1696 |
+
}
|
1697 |
+
} else {
|
1698 |
+
// Node connection inputs
|
1699 |
+
this.addInput(inputName, type);
|
1700 |
+
widgetCreated = false;
|
1701 |
+
}
|
1702 |
+
|
1703 |
+
if(widgetCreated && inputData[1]?.forceInput && config?.widget) {
|
1704 |
+
if (!config.widget.options) config.widget.options = {};
|
1705 |
+
config.widget.options.forceInput = inputData[1].forceInput;
|
1706 |
+
}
|
1707 |
+
if(widgetCreated && inputData[1]?.defaultInput && config?.widget) {
|
1708 |
+
if (!config.widget.options) config.widget.options = {};
|
1709 |
+
config.widget.options.defaultInput = inputData[1].defaultInput;
|
1710 |
+
}
|
1711 |
+
}
|
1712 |
+
|
1713 |
+
for (const o in nodeData["output"]) {
|
1714 |
+
let output = nodeData["output"][o];
|
1715 |
+
if(output instanceof Array) output = "COMBO";
|
1716 |
+
const outputName = nodeData["output_name"][o] || output;
|
1717 |
+
const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ;
|
1718 |
+
this.addOutput(outputName, output, { shape: outputShape });
|
1719 |
+
}
|
1720 |
+
|
1721 |
+
const s = this.computeSize();
|
1722 |
+
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
1723 |
+
s[1] = Math.max(config.minHeight, s[1]);
|
1724 |
+
this.size = s;
|
1725 |
+
this.serialize_widgets = true;
|
1726 |
+
|
1727 |
+
app.#invokeExtensionsAsync("nodeCreated", this);
|
1728 |
+
},
|
1729 |
+
{
|
1730 |
+
title: nodeData.display_name || nodeData.name,
|
1731 |
+
comfyClass: nodeData.name,
|
1732 |
+
nodeData
|
1733 |
+
}
|
1734 |
+
);
|
1735 |
+
node.prototype.comfyClass = nodeData.name;
|
1736 |
+
|
1737 |
+
this.#addNodeContextMenuHandler(node);
|
1738 |
+
this.#addDrawBackgroundHandler(node, app);
|
1739 |
+
this.#addNodeKeyHandler(node);
|
1740 |
+
|
1741 |
+
await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData);
|
1742 |
+
LiteGraph.registerNodeType(nodeId, node);
|
1743 |
+
node.category = nodeData.category;
|
1744 |
+
}
|
1745 |
+
|
1746 |
+
async registerNodesFromDefs(defs) {
|
1747 |
+
await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
|
1748 |
+
|
1749 |
+
// Generate list of known widgets
|
1750 |
+
this.widgets = Object.assign(
|
1751 |
+
{},
|
1752 |
+
ComfyWidgets,
|
1753 |
+
...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
|
1754 |
+
);
|
1755 |
+
|
1756 |
+
// Register a node for each definition
|
1757 |
+
for (const nodeId in defs) {
|
1758 |
+
this.registerNodeDef(nodeId, defs[nodeId]);
|
1759 |
+
}
|
1760 |
+
}
|
1761 |
+
|
1762 |
+
loadTemplateData(templateData) {
|
1763 |
+
if (!templateData?.templates) {
|
1764 |
+
return;
|
1765 |
+
}
|
1766 |
+
|
1767 |
+
const old = localStorage.getItem("litegrapheditor_clipboard");
|
1768 |
+
|
1769 |
+
var maxY, nodeBottom, node;
|
1770 |
+
|
1771 |
+
for (const template of templateData.templates) {
|
1772 |
+
if (!template?.data) {
|
1773 |
+
continue;
|
1774 |
+
}
|
1775 |
+
|
1776 |
+
localStorage.setItem("litegrapheditor_clipboard", template.data);
|
1777 |
+
app.canvas.pasteFromClipboard();
|
1778 |
+
|
1779 |
+
// Move mouse position down to paste the next template below
|
1780 |
+
|
1781 |
+
maxY = false;
|
1782 |
+
|
1783 |
+
for (const i in app.canvas.selected_nodes) {
|
1784 |
+
node = app.canvas.selected_nodes[i];
|
1785 |
+
|
1786 |
+
nodeBottom = node.pos[1] + node.size[1];
|
1787 |
+
|
1788 |
+
if (maxY === false || nodeBottom > maxY) {
|
1789 |
+
maxY = nodeBottom;
|
1790 |
+
}
|
1791 |
+
}
|
1792 |
+
|
1793 |
+
app.canvas.graph_mouse[1] = maxY + 50;
|
1794 |
+
}
|
1795 |
+
|
1796 |
+
localStorage.setItem("litegrapheditor_clipboard", old);
|
1797 |
+
}
|
1798 |
+
|
1799 |
+
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
1800 |
+
let seenTypes = new Set();
|
1801 |
+
|
1802 |
+
this.ui.dialog.show(
|
1803 |
+
$el("div.comfy-missing-nodes", [
|
1804 |
+
$el("span", { textContent: "When loading the graph, the following node types were not found: " }),
|
1805 |
+
$el(
|
1806 |
+
"ul",
|
1807 |
+
Array.from(new Set(missingNodeTypes)).map((t) => {
|
1808 |
+
let children = [];
|
1809 |
+
if (typeof t === "object") {
|
1810 |
+
if(seenTypes.has(t.type)) return null;
|
1811 |
+
seenTypes.add(t.type);
|
1812 |
+
children.push($el("span", { textContent: t.type }));
|
1813 |
+
if (t.hint) {
|
1814 |
+
children.push($el("span", { textContent: t.hint }));
|
1815 |
+
}
|
1816 |
+
if (t.action) {
|
1817 |
+
children.push($el("button", { onclick: t.action.callback, textContent: t.action.text }));
|
1818 |
+
}
|
1819 |
+
} else {
|
1820 |
+
if(seenTypes.has(t)) return null;
|
1821 |
+
seenTypes.add(t);
|
1822 |
+
children.push($el("span", { textContent: t }));
|
1823 |
+
}
|
1824 |
+
return $el("li", children);
|
1825 |
+
}).filter(Boolean)
|
1826 |
+
),
|
1827 |
+
...(hasAddedNodes
|
1828 |
+
? [$el("span", { textContent: "Nodes that have failed to load will show as red on the graph." })]
|
1829 |
+
: []),
|
1830 |
+
])
|
1831 |
+
);
|
1832 |
+
this.logging.addEntry("Comfy.App", "warn", {
|
1833 |
+
MissingNodes: missingNodeTypes,
|
1834 |
+
});
|
1835 |
+
}
|
1836 |
+
|
1837 |
+
async changeWorkflow(callback, workflow = null) {
|
1838 |
+
try {
|
1839 |
+
this.workflowManager.activeWorkflow?.changeTracker?.store()
|
1840 |
+
} catch (error) {
|
1841 |
+
console.error(error);
|
1842 |
+
}
|
1843 |
+
await callback();
|
1844 |
+
try {
|
1845 |
+
this.workflowManager.setWorkflow(workflow);
|
1846 |
+
this.workflowManager.activeWorkflow?.track()
|
1847 |
+
} catch (error) {
|
1848 |
+
console.error(error);
|
1849 |
+
}
|
1850 |
+
}
|
1851 |
+
|
1852 |
+
/**
|
1853 |
+
* Populates the graph with the specified workflow data
|
1854 |
+
* @param {*} graphData A serialized graph object
|
1855 |
+
* @param { boolean } clean If the graph state, e.g. images, should be cleared
|
1856 |
+
* @param { boolean } restore_view If the graph position should be restored
|
1857 |
+
* @param { import("./workflows.js").ComfyWorkflowInstance | null } workflow The workflow
|
1858 |
+
*/
|
1859 |
+
async loadGraphData(graphData, clean = true, restore_view = true, workflow = null) {
|
1860 |
+
if (clean !== false) {
|
1861 |
+
this.clean();
|
1862 |
+
}
|
1863 |
+
|
1864 |
+
let reset_invalid_values = false;
|
1865 |
+
if (!graphData) {
|
1866 |
+
graphData = defaultGraph;
|
1867 |
+
reset_invalid_values = true;
|
1868 |
+
}
|
1869 |
+
|
1870 |
+
if (typeof structuredClone === "undefined")
|
1871 |
+
{
|
1872 |
+
graphData = JSON.parse(JSON.stringify(graphData));
|
1873 |
+
}else
|
1874 |
+
{
|
1875 |
+
graphData = structuredClone(graphData);
|
1876 |
+
}
|
1877 |
+
|
1878 |
+
try {
|
1879 |
+
this.workflowManager.setWorkflow(workflow);
|
1880 |
+
} catch (error) {
|
1881 |
+
console.error(error);
|
1882 |
+
}
|
1883 |
+
|
1884 |
+
const missingNodeTypes = [];
|
1885 |
+
await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes);
|
1886 |
+
for (let n of graphData.nodes) {
|
1887 |
+
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
1888 |
+
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
1889 |
+
if (n.type == "ConditioningAverage ") n.type = "ConditioningAverage"; //typo fix
|
1890 |
+
if (n.type == "SDV_img2vid_Conditioning") n.type = "SVD_img2vid_Conditioning"; //typo fix
|
1891 |
+
|
1892 |
+
// Find missing node types
|
1893 |
+
if (!(n.type in LiteGraph.registered_node_types)) {
|
1894 |
+
missingNodeTypes.push(n.type);
|
1895 |
+
n.type = sanitizeNodeName(n.type);
|
1896 |
+
}
|
1897 |
+
}
|
1898 |
+
|
1899 |
+
try {
|
1900 |
+
this.graph.configure(graphData);
|
1901 |
+
if (restore_view && this.enableWorkflowViewRestore.value && graphData.extra?.ds) {
|
1902 |
+
this.canvas.ds.offset = graphData.extra.ds.offset;
|
1903 |
+
this.canvas.ds.scale = graphData.extra.ds.scale;
|
1904 |
+
}
|
1905 |
+
|
1906 |
+
try {
|
1907 |
+
this.workflowManager.activeWorkflow?.track()
|
1908 |
+
} catch (error) {
|
1909 |
+
}
|
1910 |
+
} catch (error) {
|
1911 |
+
let errorHint = [];
|
1912 |
+
// Try extracting filename to see if it was caused by an extension script
|
1913 |
+
const filename = error.fileName || (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1];
|
1914 |
+
const pos = (filename || "").indexOf("/extensions/");
|
1915 |
+
if (pos > -1) {
|
1916 |
+
errorHint.push(
|
1917 |
+
$el("span", { textContent: "This may be due to the following script:" }),
|
1918 |
+
$el("br"),
|
1919 |
+
$el("span", {
|
1920 |
+
style: {
|
1921 |
+
fontWeight: "bold",
|
1922 |
+
},
|
1923 |
+
textContent: filename.substring(pos),
|
1924 |
+
})
|
1925 |
+
);
|
1926 |
+
}
|
1927 |
+
|
1928 |
+
// Show dialog to let the user know something went wrong loading the data
|
1929 |
+
this.ui.dialog.show(
|
1930 |
+
$el("div", [
|
1931 |
+
$el("p", { textContent: "Loading aborted due to error reloading workflow data" }),
|
1932 |
+
$el("pre", {
|
1933 |
+
style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" },
|
1934 |
+
textContent: error.toString(),
|
1935 |
+
}),
|
1936 |
+
$el("pre", {
|
1937 |
+
style: {
|
1938 |
+
padding: "5px",
|
1939 |
+
color: "#ccc",
|
1940 |
+
fontSize: "10px",
|
1941 |
+
maxHeight: "50vh",
|
1942 |
+
overflow: "auto",
|
1943 |
+
backgroundColor: "rgba(0,0,0,0.2)",
|
1944 |
+
},
|
1945 |
+
textContent: error.stack || "No stacktrace available",
|
1946 |
+
}),
|
1947 |
+
...errorHint,
|
1948 |
+
]).outerHTML
|
1949 |
+
);
|
1950 |
+
|
1951 |
+
return;
|
1952 |
+
}
|
1953 |
+
|
1954 |
+
for (const node of this.graph._nodes) {
|
1955 |
+
const size = node.computeSize();
|
1956 |
+
size[0] = Math.max(node.size[0], size[0]);
|
1957 |
+
size[1] = Math.max(node.size[1], size[1]);
|
1958 |
+
node.size = size;
|
1959 |
+
|
1960 |
+
if (node.widgets) {
|
1961 |
+
// If you break something in the backend and want to patch workflows in the frontend
|
1962 |
+
// This is the place to do this
|
1963 |
+
for (let widget of node.widgets) {
|
1964 |
+
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
|
1965 |
+
if (widget.name == "sampler_name") {
|
1966 |
+
if (widget.value.startsWith("sample_")) {
|
1967 |
+
widget.value = widget.value.slice(7);
|
1968 |
+
}
|
1969 |
+
if (widget.value === "euler_pp" || widget.value === "euler_ancestral_pp") {
|
1970 |
+
widget.value = widget.value.slice(0, -3);
|
1971 |
+
for (let w of node.widgets) {
|
1972 |
+
if (w.name == "cfg") {
|
1973 |
+
w.value *= 2.0;
|
1974 |
+
}
|
1975 |
+
}
|
1976 |
+
}
|
1977 |
+
}
|
1978 |
+
}
|
1979 |
+
if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") {
|
1980 |
+
if (widget.name == "control_after_generate") {
|
1981 |
+
if (widget.value === true) {
|
1982 |
+
widget.value = "randomize";
|
1983 |
+
} else if (widget.value === false) {
|
1984 |
+
widget.value = "fixed";
|
1985 |
+
}
|
1986 |
+
}
|
1987 |
+
}
|
1988 |
+
if (reset_invalid_values) {
|
1989 |
+
if (widget.type == "combo") {
|
1990 |
+
if (!widget.options.values.includes(widget.value) && widget.options.values.length > 0) {
|
1991 |
+
widget.value = widget.options.values[0];
|
1992 |
+
}
|
1993 |
+
}
|
1994 |
+
}
|
1995 |
+
}
|
1996 |
+
}
|
1997 |
+
|
1998 |
+
this.#invokeExtensions("loadedGraphNode", node);
|
1999 |
+
}
|
2000 |
+
|
2001 |
+
if (missingNodeTypes.length) {
|
2002 |
+
this.showMissingNodesError(missingNodeTypes);
|
2003 |
+
}
|
2004 |
+
await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes);
|
2005 |
+
requestAnimationFrame(() => {
|
2006 |
+
this.graph.setDirtyCanvas(true, true);
|
2007 |
+
});
|
2008 |
+
}
|
2009 |
+
|
2010 |
+
/**
|
2011 |
+
* Converts the current graph workflow for sending to the API
|
2012 |
+
* @returns The workflow and node links
|
2013 |
+
*/
|
2014 |
+
async graphToPrompt(graph = this.graph, clean = true) {
|
2015 |
+
for (const outerNode of graph.computeExecutionOrder(false)) {
|
2016 |
+
if (outerNode.widgets) {
|
2017 |
+
for (const widget of outerNode.widgets) {
|
2018 |
+
// Allow widgets to run callbacks before a prompt has been queued
|
2019 |
+
// e.g. random seed before every gen
|
2020 |
+
widget.beforeQueued?.();
|
2021 |
+
}
|
2022 |
+
}
|
2023 |
+
|
2024 |
+
const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode];
|
2025 |
+
for (const node of innerNodes) {
|
2026 |
+
if (node.isVirtualNode) {
|
2027 |
+
// Don't serialize frontend only nodes but let them make changes
|
2028 |
+
if (node.applyToGraph) {
|
2029 |
+
node.applyToGraph();
|
2030 |
+
}
|
2031 |
+
}
|
2032 |
+
}
|
2033 |
+
}
|
2034 |
+
|
2035 |
+
const workflow = graph.serialize();
|
2036 |
+
const output = {};
|
2037 |
+
// Process nodes in order of execution
|
2038 |
+
for (const outerNode of graph.computeExecutionOrder(false)) {
|
2039 |
+
const skipNode = outerNode.mode === 2 || outerNode.mode === 4;
|
2040 |
+
const innerNodes = (!skipNode && outerNode.getInnerNodes) ? outerNode.getInnerNodes() : [outerNode];
|
2041 |
+
for (const node of innerNodes) {
|
2042 |
+
if (node.isVirtualNode) {
|
2043 |
+
continue;
|
2044 |
+
}
|
2045 |
+
|
2046 |
+
if (node.mode === 2 || node.mode === 4) {
|
2047 |
+
// Don't serialize muted nodes
|
2048 |
+
continue;
|
2049 |
+
}
|
2050 |
+
|
2051 |
+
const inputs = {};
|
2052 |
+
const widgets = node.widgets;
|
2053 |
+
|
2054 |
+
// Store all widget values
|
2055 |
+
if (widgets) {
|
2056 |
+
for (const i in widgets) {
|
2057 |
+
const widget = widgets[i];
|
2058 |
+
if (!widget.options || widget.options.serialize !== false) {
|
2059 |
+
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(node, i) : widget.value;
|
2060 |
+
}
|
2061 |
+
}
|
2062 |
+
}
|
2063 |
+
|
2064 |
+
// Store all node links
|
2065 |
+
for (let i in node.inputs) {
|
2066 |
+
let parent = node.getInputNode(i);
|
2067 |
+
if (parent) {
|
2068 |
+
let link = node.getInputLink(i);
|
2069 |
+
while (parent.mode === 4 || parent.isVirtualNode) {
|
2070 |
+
let found = false;
|
2071 |
+
if (parent.isVirtualNode) {
|
2072 |
+
link = parent.getInputLink(link.origin_slot);
|
2073 |
+
if (link) {
|
2074 |
+
parent = parent.getInputNode(link.target_slot);
|
2075 |
+
if (parent) {
|
2076 |
+
found = true;
|
2077 |
+
}
|
2078 |
+
}
|
2079 |
+
} else if (link && parent.mode === 4) {
|
2080 |
+
let all_inputs = [link.origin_slot];
|
2081 |
+
if (parent.inputs) {
|
2082 |
+
all_inputs = all_inputs.concat(Object.keys(parent.inputs))
|
2083 |
+
for (let parent_input in all_inputs) {
|
2084 |
+
parent_input = all_inputs[parent_input];
|
2085 |
+
if (parent.inputs[parent_input]?.type === node.inputs[i].type) {
|
2086 |
+
link = parent.getInputLink(parent_input);
|
2087 |
+
if (link) {
|
2088 |
+
parent = parent.getInputNode(parent_input);
|
2089 |
+
}
|
2090 |
+
found = true;
|
2091 |
+
break;
|
2092 |
+
}
|
2093 |
+
}
|
2094 |
+
}
|
2095 |
+
}
|
2096 |
+
|
2097 |
+
if (!found) {
|
2098 |
+
break;
|
2099 |
+
}
|
2100 |
+
}
|
2101 |
+
|
2102 |
+
if (link) {
|
2103 |
+
if (parent?.updateLink) {
|
2104 |
+
link = parent.updateLink(link);
|
2105 |
+
}
|
2106 |
+
if (link) {
|
2107 |
+
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
|
2108 |
+
}
|
2109 |
+
}
|
2110 |
+
}
|
2111 |
+
}
|
2112 |
+
|
2113 |
+
let node_data = {
|
2114 |
+
inputs,
|
2115 |
+
class_type: node.comfyClass,
|
2116 |
+
};
|
2117 |
+
|
2118 |
+
if (this.ui.settings.getSettingValue("Comfy.DevMode")) {
|
2119 |
+
// Ignored by the backend.
|
2120 |
+
node_data["_meta"] = {
|
2121 |
+
title: node.title,
|
2122 |
+
}
|
2123 |
+
}
|
2124 |
+
|
2125 |
+
output[String(node.id)] = node_data;
|
2126 |
+
}
|
2127 |
+
}
|
2128 |
+
|
2129 |
+
// Remove inputs connected to removed nodes
|
2130 |
+
if(clean) {
|
2131 |
+
for (const o in output) {
|
2132 |
+
for (const i in output[o].inputs) {
|
2133 |
+
if (Array.isArray(output[o].inputs[i])
|
2134 |
+
&& output[o].inputs[i].length === 2
|
2135 |
+
&& !output[output[o].inputs[i][0]]) {
|
2136 |
+
delete output[o].inputs[i];
|
2137 |
+
}
|
2138 |
+
}
|
2139 |
+
}
|
2140 |
+
}
|
2141 |
+
|
2142 |
+
return { workflow, output };
|
2143 |
+
}
|
2144 |
+
|
2145 |
+
#formatPromptError(error) {
|
2146 |
+
if (error == null) {
|
2147 |
+
return "(unknown error)"
|
2148 |
+
}
|
2149 |
+
else if (typeof error === "string") {
|
2150 |
+
return error;
|
2151 |
+
}
|
2152 |
+
else if (error.stack && error.message) {
|
2153 |
+
return error.toString()
|
2154 |
+
}
|
2155 |
+
else if (error.response) {
|
2156 |
+
let message = error.response.error.message;
|
2157 |
+
if (error.response.error.details)
|
2158 |
+
message += ": " + error.response.error.details;
|
2159 |
+
for (const [nodeID, nodeError] of Object.entries(error.response.node_errors)) {
|
2160 |
+
message += "\n" + nodeError.class_type + ":"
|
2161 |
+
for (const errorReason of nodeError.errors) {
|
2162 |
+
message += "\n - " + errorReason.message + ": " + errorReason.details
|
2163 |
+
}
|
2164 |
+
}
|
2165 |
+
return message
|
2166 |
+
}
|
2167 |
+
return "(unknown error)"
|
2168 |
+
}
|
2169 |
+
|
2170 |
+
#formatExecutionError(error) {
|
2171 |
+
if (error == null) {
|
2172 |
+
return "(unknown error)"
|
2173 |
+
}
|
2174 |
+
|
2175 |
+
const traceback = error.traceback.join("")
|
2176 |
+
const nodeId = error.node_id
|
2177 |
+
const nodeType = error.node_type
|
2178 |
+
|
2179 |
+
return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}`
|
2180 |
+
}
|
2181 |
+
|
2182 |
+
async queuePrompt(number, batchCount = 1) {
|
2183 |
+
this.#queueItems.push({ number, batchCount });
|
2184 |
+
|
2185 |
+
// Only have one action process the items so each one gets a unique seed correctly
|
2186 |
+
if (this.#processingQueue) {
|
2187 |
+
return;
|
2188 |
+
}
|
2189 |
+
|
2190 |
+
this.#processingQueue = true;
|
2191 |
+
this.lastNodeErrors = null;
|
2192 |
+
|
2193 |
+
try {
|
2194 |
+
while (this.#queueItems.length) {
|
2195 |
+
({ number, batchCount } = this.#queueItems.pop());
|
2196 |
+
|
2197 |
+
for (let i = 0; i < batchCount; i++) {
|
2198 |
+
const p = await this.graphToPrompt();
|
2199 |
+
|
2200 |
+
try {
|
2201 |
+
const res = await api.queuePrompt(number, p);
|
2202 |
+
this.lastNodeErrors = res.node_errors;
|
2203 |
+
if (this.lastNodeErrors.length > 0) {
|
2204 |
+
this.canvas.draw(true, true);
|
2205 |
+
} else {
|
2206 |
+
try {
|
2207 |
+
this.workflowManager.storePrompt({
|
2208 |
+
id: res.prompt_id,
|
2209 |
+
nodes: Object.keys(p.output)
|
2210 |
+
});
|
2211 |
+
} catch (error) {
|
2212 |
+
}
|
2213 |
+
}
|
2214 |
+
} catch (error) {
|
2215 |
+
const formattedError = this.#formatPromptError(error)
|
2216 |
+
this.ui.dialog.show(formattedError);
|
2217 |
+
if (error.response) {
|
2218 |
+
this.lastNodeErrors = error.response.node_errors;
|
2219 |
+
this.canvas.draw(true, true);
|
2220 |
+
}
|
2221 |
+
break;
|
2222 |
+
}
|
2223 |
+
|
2224 |
+
for (const n of p.workflow.nodes) {
|
2225 |
+
const node = graph.getNodeById(n.id);
|
2226 |
+
if (node.widgets) {
|
2227 |
+
for (const widget of node.widgets) {
|
2228 |
+
// Allow widgets to run callbacks after a prompt has been queued
|
2229 |
+
// e.g. random seed after every gen
|
2230 |
+
if (widget.afterQueued) {
|
2231 |
+
widget.afterQueued();
|
2232 |
+
}
|
2233 |
+
}
|
2234 |
+
}
|
2235 |
+
}
|
2236 |
+
|
2237 |
+
this.canvas.draw(true, true);
|
2238 |
+
await this.ui.queue.update();
|
2239 |
+
}
|
2240 |
+
}
|
2241 |
+
} finally {
|
2242 |
+
this.#processingQueue = false;
|
2243 |
+
}
|
2244 |
+
api.dispatchEvent(new CustomEvent("promptQueued", { detail: { number, batchCount } }));
|
2245 |
+
return !this.lastNodeErrors;
|
2246 |
+
}
|
2247 |
+
|
2248 |
+
showErrorOnFileLoad(file) {
|
2249 |
+
this.ui.dialog.show(
|
2250 |
+
$el("div", [
|
2251 |
+
$el("p", {textContent: `Unable to find workflow in ${file.name}`})
|
2252 |
+
]).outerHTML
|
2253 |
+
);
|
2254 |
+
}
|
2255 |
+
|
2256 |
+
/**
|
2257 |
+
* Loads workflow data from the specified file
|
2258 |
+
* @param {File} file
|
2259 |
+
*/
|
2260 |
+
async handleFile(file) {
|
2261 |
+
const removeExt = f => {
|
2262 |
+
if(!f) return f;
|
2263 |
+
const p = f.lastIndexOf(".");
|
2264 |
+
if(p === -1) return f;
|
2265 |
+
return f.substring(0, p);
|
2266 |
+
};
|
2267 |
+
|
2268 |
+
const fileName = removeExt(file.name);
|
2269 |
+
if (file.type === "image/png") {
|
2270 |
+
const pngInfo = await getPngMetadata(file);
|
2271 |
+
if (pngInfo?.workflow) {
|
2272 |
+
await this.loadGraphData(JSON.parse(pngInfo.workflow), true, true, fileName);
|
2273 |
+
} else if (pngInfo?.prompt) {
|
2274 |
+
this.loadApiJson(JSON.parse(pngInfo.prompt), fileName);
|
2275 |
+
} else if (pngInfo?.parameters) {
|
2276 |
+
this.changeWorkflow(() => {
|
2277 |
+
importA1111(this.graph, pngInfo.parameters);
|
2278 |
+
}, fileName)
|
2279 |
+
} else {
|
2280 |
+
this.showErrorOnFileLoad(file);
|
2281 |
+
}
|
2282 |
+
} else if (file.type === "image/webp") {
|
2283 |
+
const pngInfo = await getWebpMetadata(file);
|
2284 |
+
// Support loading workflows from that webp custom node.
|
2285 |
+
const workflow = pngInfo?.workflow || pngInfo?.Workflow;
|
2286 |
+
const prompt = pngInfo?.prompt || pngInfo?.Prompt;
|
2287 |
+
|
2288 |
+
if (workflow) {
|
2289 |
+
this.loadGraphData(JSON.parse(workflow), true, true, fileName);
|
2290 |
+
} else if (prompt) {
|
2291 |
+
this.loadApiJson(JSON.parse(prompt), fileName);
|
2292 |
+
} else {
|
2293 |
+
this.showErrorOnFileLoad(file);
|
2294 |
+
}
|
2295 |
+
} else if (file.type === "audio/flac" || file.type === "audio/x-flac") {
|
2296 |
+
const pngInfo = await getFlacMetadata(file);
|
2297 |
+
// Support loading workflows from that webp custom node.
|
2298 |
+
const workflow = pngInfo?.workflow;
|
2299 |
+
const prompt = pngInfo?.prompt;
|
2300 |
+
|
2301 |
+
if (workflow) {
|
2302 |
+
this.loadGraphData(JSON.parse(workflow), true, true, fileName);
|
2303 |
+
} else if (prompt) {
|
2304 |
+
this.loadApiJson(JSON.parse(prompt), fileName);
|
2305 |
+
} else {
|
2306 |
+
this.showErrorOnFileLoad(file);
|
2307 |
+
}
|
2308 |
+
} else if (file.type === "application/json" || file.name?.endsWith(".json")) {
|
2309 |
+
const reader = new FileReader();
|
2310 |
+
reader.onload = async () => {
|
2311 |
+
const jsonContent = JSON.parse(reader.result);
|
2312 |
+
if (jsonContent?.templates) {
|
2313 |
+
this.loadTemplateData(jsonContent);
|
2314 |
+
} else if(this.isApiJson(jsonContent)) {
|
2315 |
+
this.loadApiJson(jsonContent, fileName);
|
2316 |
+
} else {
|
2317 |
+
await this.loadGraphData(jsonContent, true, true, fileName);
|
2318 |
+
}
|
2319 |
+
};
|
2320 |
+
reader.readAsText(file);
|
2321 |
+
} else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) {
|
2322 |
+
const info = await getLatentMetadata(file);
|
2323 |
+
if (info.workflow) {
|
2324 |
+
await this.loadGraphData(JSON.parse(info.workflow), true, true, fileName);
|
2325 |
+
} else if (info.prompt) {
|
2326 |
+
this.loadApiJson(JSON.parse(info.prompt));
|
2327 |
+
} else {
|
2328 |
+
this.showErrorOnFileLoad(file);
|
2329 |
+
}
|
2330 |
+
} else {
|
2331 |
+
this.showErrorOnFileLoad(file);
|
2332 |
+
}
|
2333 |
+
}
|
2334 |
+
|
2335 |
+
isApiJson(data) {
|
2336 |
+
return Object.values(data).every((v) => v.class_type);
|
2337 |
+
}
|
2338 |
+
|
2339 |
+
loadApiJson(apiData, fileName) {
|
2340 |
+
const missingNodeTypes = Object.values(apiData).filter((n) => !LiteGraph.registered_node_types[n.class_type]);
|
2341 |
+
if (missingNodeTypes.length) {
|
2342 |
+
this.showMissingNodesError(missingNodeTypes.map(t => t.class_type), false);
|
2343 |
+
return;
|
2344 |
+
}
|
2345 |
+
|
2346 |
+
const ids = Object.keys(apiData);
|
2347 |
+
app.graph.clear();
|
2348 |
+
for (const id of ids) {
|
2349 |
+
const data = apiData[id];
|
2350 |
+
const node = LiteGraph.createNode(data.class_type);
|
2351 |
+
node.id = isNaN(+id) ? id : +id;
|
2352 |
+
node.title = data._meta?.title ?? node.title
|
2353 |
+
app.graph.add(node);
|
2354 |
+
}
|
2355 |
+
|
2356 |
+
this.changeWorkflow(() => {
|
2357 |
+
for (const id of ids) {
|
2358 |
+
const data = apiData[id];
|
2359 |
+
const node = app.graph.getNodeById(id);
|
2360 |
+
for (const input in data.inputs ?? {}) {
|
2361 |
+
const value = data.inputs[input];
|
2362 |
+
if (value instanceof Array) {
|
2363 |
+
const [fromId, fromSlot] = value;
|
2364 |
+
const fromNode = app.graph.getNodeById(fromId);
|
2365 |
+
let toSlot = node.inputs?.findIndex((inp) => inp.name === input);
|
2366 |
+
if (toSlot == null || toSlot === -1) {
|
2367 |
+
try {
|
2368 |
+
// Target has no matching input, most likely a converted widget
|
2369 |
+
const widget = node.widgets?.find((w) => w.name === input);
|
2370 |
+
if (widget && node.convertWidgetToInput?.(widget)) {
|
2371 |
+
toSlot = node.inputs?.length - 1;
|
2372 |
+
}
|
2373 |
+
} catch (error) {}
|
2374 |
+
}
|
2375 |
+
if (toSlot != null || toSlot !== -1) {
|
2376 |
+
fromNode.connect(fromSlot, node, toSlot);
|
2377 |
+
}
|
2378 |
+
} else {
|
2379 |
+
const widget = node.widgets?.find((w) => w.name === input);
|
2380 |
+
if (widget) {
|
2381 |
+
widget.value = value;
|
2382 |
+
widget.callback?.(value);
|
2383 |
+
}
|
2384 |
+
}
|
2385 |
+
}
|
2386 |
+
}
|
2387 |
+
app.graph.arrange();
|
2388 |
+
}, fileName);
|
2389 |
+
}
|
2390 |
+
|
2391 |
+
/**
|
2392 |
+
* Registers a Comfy web extension with the app
|
2393 |
+
* @param {ComfyExtension} extension
|
2394 |
+
*/
|
2395 |
+
registerExtension(extension) {
|
2396 |
+
if (!extension.name) {
|
2397 |
+
throw new Error("Extensions must have a 'name' property.");
|
2398 |
+
}
|
2399 |
+
if (this.extensions.find((ext) => ext.name === extension.name)) {
|
2400 |
+
throw new Error(`Extension named '${extension.name}' already registered.`);
|
2401 |
+
}
|
2402 |
+
this.extensions.push(extension);
|
2403 |
+
}
|
2404 |
+
|
2405 |
+
/**
|
2406 |
+
* Refresh combo list on whole nodes
|
2407 |
+
*/
|
2408 |
+
async refreshComboInNodes() {
|
2409 |
+
const defs = await api.getNodeDefs();
|
2410 |
+
|
2411 |
+
for (const nodeId in defs) {
|
2412 |
+
this.registerNodeDef(nodeId, defs[nodeId]);
|
2413 |
+
}
|
2414 |
+
|
2415 |
+
for(let nodeNum in this.graph._nodes) {
|
2416 |
+
const node = this.graph._nodes[nodeNum];
|
2417 |
+
const def = defs[node.type];
|
2418 |
+
|
2419 |
+
// Allow primitive nodes to handle refresh
|
2420 |
+
node.refreshComboInNode?.(defs);
|
2421 |
+
|
2422 |
+
if(!def)
|
2423 |
+
continue;
|
2424 |
+
|
2425 |
+
for(const widgetNum in node.widgets) {
|
2426 |
+
const widget = node.widgets[widgetNum]
|
2427 |
+
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
|
2428 |
+
widget.options.values = def["input"]["required"][widget.name][0];
|
2429 |
+
|
2430 |
+
if(widget.name != 'image' && !widget.options.values.includes(widget.value)) {
|
2431 |
+
widget.value = widget.options.values[0];
|
2432 |
+
widget.callback(widget.value);
|
2433 |
+
}
|
2434 |
+
}
|
2435 |
+
}
|
2436 |
+
}
|
2437 |
+
|
2438 |
+
await this.#invokeExtensionsAsync("refreshComboInNodes", defs);
|
2439 |
+
}
|
2440 |
+
|
2441 |
+
resetView() {
|
2442 |
+
app.canvas.ds.scale = 1;
|
2443 |
+
app.canvas.ds.offset = [0, 0]
|
2444 |
+
app.graph.setDirtyCanvas(true, true);
|
2445 |
+
}
|
2446 |
+
|
2447 |
+
/**
|
2448 |
+
* Clean current state
|
2449 |
+
*/
|
2450 |
+
clean() {
|
2451 |
+
this.nodeOutputs = {};
|
2452 |
+
this.nodePreviewImages = {}
|
2453 |
+
this.lastNodeErrors = null;
|
2454 |
+
this.lastExecutionError = null;
|
2455 |
+
this.runningNodeId = null;
|
2456 |
+
}
|
2457 |
+
}
|
2458 |
+
|
2459 |
+
export const app = new ComfyApp();
|
ComfyUI/web/scripts/changeTracker.js
ADDED
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { api } from "./api.js";
|
4 |
+
import { clone } from "./utils.js";
|
5 |
+
|
6 |
+
export class ChangeTracker {
|
7 |
+
static MAX_HISTORY = 50;
|
8 |
+
#app;
|
9 |
+
undo = [];
|
10 |
+
redo = [];
|
11 |
+
activeState = null;
|
12 |
+
isOurLoad = false;
|
13 |
+
/** @type { import("./workflows").ComfyWorkflow | null } */
|
14 |
+
workflow;
|
15 |
+
|
16 |
+
ds;
|
17 |
+
nodeOutputs;
|
18 |
+
|
19 |
+
get app() {
|
20 |
+
return this.#app ?? this.workflow.manager.app;
|
21 |
+
}
|
22 |
+
|
23 |
+
constructor(workflow) {
|
24 |
+
this.workflow = workflow;
|
25 |
+
}
|
26 |
+
|
27 |
+
#setApp(app) {
|
28 |
+
this.#app = app;
|
29 |
+
}
|
30 |
+
|
31 |
+
store() {
|
32 |
+
this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] };
|
33 |
+
}
|
34 |
+
|
35 |
+
restore() {
|
36 |
+
if (this.ds) {
|
37 |
+
this.app.canvas.ds.scale = this.ds.scale;
|
38 |
+
this.app.canvas.ds.offset = this.ds.offset;
|
39 |
+
}
|
40 |
+
if (this.nodeOutputs) {
|
41 |
+
this.app.nodeOutputs = this.nodeOutputs;
|
42 |
+
}
|
43 |
+
}
|
44 |
+
|
45 |
+
checkState() {
|
46 |
+
if (!this.app.graph) return;
|
47 |
+
|
48 |
+
const currentState = this.app.graph.serialize();
|
49 |
+
if (!this.activeState) {
|
50 |
+
this.activeState = clone(currentState);
|
51 |
+
return;
|
52 |
+
}
|
53 |
+
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
|
54 |
+
this.undo.push(this.activeState);
|
55 |
+
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
|
56 |
+
this.undo.shift();
|
57 |
+
}
|
58 |
+
this.activeState = clone(currentState);
|
59 |
+
this.redo.length = 0;
|
60 |
+
this.workflow.unsaved = true;
|
61 |
+
api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState }));
|
62 |
+
}
|
63 |
+
}
|
64 |
+
|
65 |
+
async updateState(source, target) {
|
66 |
+
const prevState = source.pop();
|
67 |
+
if (prevState) {
|
68 |
+
target.push(this.activeState);
|
69 |
+
this.isOurLoad = true;
|
70 |
+
await this.app.loadGraphData(prevState, false, false, this.workflow);
|
71 |
+
this.activeState = prevState;
|
72 |
+
}
|
73 |
+
}
|
74 |
+
|
75 |
+
async undoRedo(e) {
|
76 |
+
if (e.ctrlKey || e.metaKey) {
|
77 |
+
if (e.key === "y") {
|
78 |
+
this.updateState(this.redo, this.undo);
|
79 |
+
return true;
|
80 |
+
} else if (e.key === "z") {
|
81 |
+
this.updateState(this.undo, this.redo);
|
82 |
+
return true;
|
83 |
+
}
|
84 |
+
}
|
85 |
+
}
|
86 |
+
|
87 |
+
/** @param { import("./app.js").ComfyApp } app */
|
88 |
+
static init(app) {
|
89 |
+
const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
|
90 |
+
globalTracker.#setApp(app);
|
91 |
+
|
92 |
+
const loadGraphData = app.loadGraphData;
|
93 |
+
app.loadGraphData = async function () {
|
94 |
+
const v = await loadGraphData.apply(this, arguments);
|
95 |
+
const ct = changeTracker();
|
96 |
+
if (ct.isOurLoad) {
|
97 |
+
ct.isOurLoad = false;
|
98 |
+
} else {
|
99 |
+
ct.checkState();
|
100 |
+
}
|
101 |
+
return v;
|
102 |
+
};
|
103 |
+
|
104 |
+
let keyIgnored = false;
|
105 |
+
window.addEventListener(
|
106 |
+
"keydown",
|
107 |
+
(e) => {
|
108 |
+
const activeEl = document.activeElement;
|
109 |
+
requestAnimationFrame(async () => {
|
110 |
+
let bindInputEl;
|
111 |
+
// If we are auto queue in change mode then we do want to trigger on inputs
|
112 |
+
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
|
113 |
+
if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") {
|
114 |
+
// Ignore events on inputs, they have their native history
|
115 |
+
return;
|
116 |
+
}
|
117 |
+
bindInputEl = activeEl;
|
118 |
+
}
|
119 |
+
|
120 |
+
keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta";
|
121 |
+
if (keyIgnored) return;
|
122 |
+
|
123 |
+
// Check if this is a ctrl+z ctrl+y
|
124 |
+
if (await changeTracker().undoRedo(e)) return;
|
125 |
+
|
126 |
+
// If our active element is some type of input then handle changes after they're done
|
127 |
+
if (ChangeTracker.bindInput(bindInputEl)) return;
|
128 |
+
changeTracker().checkState();
|
129 |
+
});
|
130 |
+
},
|
131 |
+
true
|
132 |
+
);
|
133 |
+
|
134 |
+
window.addEventListener("keyup", (e) => {
|
135 |
+
if (keyIgnored) {
|
136 |
+
keyIgnored = false;
|
137 |
+
changeTracker().checkState();
|
138 |
+
}
|
139 |
+
});
|
140 |
+
|
141 |
+
// Handle clicking DOM elements (e.g. widgets)
|
142 |
+
window.addEventListener("mouseup", () => {
|
143 |
+
changeTracker().checkState();
|
144 |
+
});
|
145 |
+
|
146 |
+
// Handle prompt queue event for dynamic widget changes
|
147 |
+
api.addEventListener("promptQueued", () => {
|
148 |
+
changeTracker().checkState();
|
149 |
+
});
|
150 |
+
|
151 |
+
// Handle litegraph clicks
|
152 |
+
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
|
153 |
+
LGraphCanvas.prototype.processMouseUp = function (e) {
|
154 |
+
const v = processMouseUp.apply(this, arguments);
|
155 |
+
changeTracker().checkState();
|
156 |
+
return v;
|
157 |
+
};
|
158 |
+
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
159 |
+
LGraphCanvas.prototype.processMouseDown = function (e) {
|
160 |
+
const v = processMouseDown.apply(this, arguments);
|
161 |
+
changeTracker().checkState();
|
162 |
+
return v;
|
163 |
+
};
|
164 |
+
|
165 |
+
// Handle litegraph context menu for COMBO widgets
|
166 |
+
const close = LiteGraph.ContextMenu.prototype.close;
|
167 |
+
LiteGraph.ContextMenu.prototype.close = function (e) {
|
168 |
+
const v = close.apply(this, arguments);
|
169 |
+
changeTracker().checkState();
|
170 |
+
return v;
|
171 |
+
};
|
172 |
+
|
173 |
+
// Detects nodes being added via the node search dialog
|
174 |
+
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
|
175 |
+
LiteGraph.LGraph.prototype.onNodeAdded = function () {
|
176 |
+
const v = onNodeAdded?.apply(this, arguments);
|
177 |
+
if (!app?.configuringGraph) {
|
178 |
+
const ct = changeTracker();
|
179 |
+
if (!ct.isOurLoad) {
|
180 |
+
ct.checkState();
|
181 |
+
}
|
182 |
+
}
|
183 |
+
return v;
|
184 |
+
};
|
185 |
+
|
186 |
+
// Store node outputs
|
187 |
+
api.addEventListener("executed", ({ detail }) => {
|
188 |
+
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
|
189 |
+
if (!prompt?.workflow) return;
|
190 |
+
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
|
191 |
+
const output = nodeOutputs[detail.node];
|
192 |
+
if (detail.merge && output) {
|
193 |
+
for (const k in detail.output ?? {}) {
|
194 |
+
const v = output[k];
|
195 |
+
if (v instanceof Array) {
|
196 |
+
output[k] = v.concat(detail.output[k]);
|
197 |
+
} else {
|
198 |
+
output[k] = detail.output[k];
|
199 |
+
}
|
200 |
+
}
|
201 |
+
} else {
|
202 |
+
nodeOutputs[detail.node] = detail.output;
|
203 |
+
}
|
204 |
+
});
|
205 |
+
}
|
206 |
+
|
207 |
+
static bindInput(app, activeEl) {
|
208 |
+
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") {
|
209 |
+
for (const evt of ["change", "input", "blur"]) {
|
210 |
+
if (`on${evt}` in activeEl) {
|
211 |
+
const listener = () => {
|
212 |
+
app.workflowManager.activeWorkflow.changeTracker.checkState();
|
213 |
+
activeEl.removeEventListener(evt, listener);
|
214 |
+
};
|
215 |
+
activeEl.addEventListener(evt, listener);
|
216 |
+
return true;
|
217 |
+
}
|
218 |
+
}
|
219 |
+
}
|
220 |
+
}
|
221 |
+
|
222 |
+
static graphEqual(a, b, path = "") {
|
223 |
+
if (a === b) return true;
|
224 |
+
|
225 |
+
if (typeof a == "object" && a && typeof b == "object" && b) {
|
226 |
+
const keys = Object.getOwnPropertyNames(a);
|
227 |
+
|
228 |
+
if (keys.length != Object.getOwnPropertyNames(b).length) {
|
229 |
+
return false;
|
230 |
+
}
|
231 |
+
|
232 |
+
for (const key of keys) {
|
233 |
+
let av = a[key];
|
234 |
+
let bv = b[key];
|
235 |
+
if (!path && key === "nodes") {
|
236 |
+
// Nodes need to be sorted as the order changes when selecting nodes
|
237 |
+
av = [...av].sort((a, b) => a.id - b.id);
|
238 |
+
bv = [...bv].sort((a, b) => a.id - b.id);
|
239 |
+
} else if (path === "extra.ds") {
|
240 |
+
// Ignore view changes
|
241 |
+
continue;
|
242 |
+
}
|
243 |
+
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
|
244 |
+
return false;
|
245 |
+
}
|
246 |
+
}
|
247 |
+
|
248 |
+
return true;
|
249 |
+
}
|
250 |
+
|
251 |
+
return false;
|
252 |
+
}
|
253 |
+
}
|
254 |
+
|
255 |
+
const globalTracker = new ChangeTracker({});
|
ComfyUI/web/scripts/defaultGraph.js
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const defaultGraph = {
|
2 |
+
last_node_id: 9,
|
3 |
+
last_link_id: 9,
|
4 |
+
nodes: [
|
5 |
+
{
|
6 |
+
id: 7,
|
7 |
+
type: "CLIPTextEncode",
|
8 |
+
pos: [413, 389],
|
9 |
+
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
|
10 |
+
flags: {},
|
11 |
+
order: 3,
|
12 |
+
mode: 0,
|
13 |
+
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
|
14 |
+
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
|
15 |
+
properties: {},
|
16 |
+
widgets_values: ["text, watermark"],
|
17 |
+
},
|
18 |
+
{
|
19 |
+
id: 6,
|
20 |
+
type: "CLIPTextEncode",
|
21 |
+
pos: [415, 186],
|
22 |
+
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
|
23 |
+
flags: {},
|
24 |
+
order: 2,
|
25 |
+
mode: 0,
|
26 |
+
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
|
27 |
+
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
|
28 |
+
properties: {},
|
29 |
+
widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"],
|
30 |
+
},
|
31 |
+
{
|
32 |
+
id: 5,
|
33 |
+
type: "EmptyLatentImage",
|
34 |
+
pos: [473, 609],
|
35 |
+
size: { 0: 315, 1: 106 },
|
36 |
+
flags: {},
|
37 |
+
order: 1,
|
38 |
+
mode: 0,
|
39 |
+
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
|
40 |
+
properties: {},
|
41 |
+
widgets_values: [512, 512, 1],
|
42 |
+
},
|
43 |
+
{
|
44 |
+
id: 3,
|
45 |
+
type: "KSampler",
|
46 |
+
pos: [863, 186],
|
47 |
+
size: { 0: 315, 1: 262 },
|
48 |
+
flags: {},
|
49 |
+
order: 4,
|
50 |
+
mode: 0,
|
51 |
+
inputs: [
|
52 |
+
{ name: "model", type: "MODEL", link: 1 },
|
53 |
+
{ name: "positive", type: "CONDITIONING", link: 4 },
|
54 |
+
{ name: "negative", type: "CONDITIONING", link: 6 },
|
55 |
+
{ name: "latent_image", type: "LATENT", link: 2 },
|
56 |
+
],
|
57 |
+
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
|
58 |
+
properties: {},
|
59 |
+
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
|
60 |
+
},
|
61 |
+
{
|
62 |
+
id: 8,
|
63 |
+
type: "VAEDecode",
|
64 |
+
pos: [1209, 188],
|
65 |
+
size: { 0: 210, 1: 46 },
|
66 |
+
flags: {},
|
67 |
+
order: 5,
|
68 |
+
mode: 0,
|
69 |
+
inputs: [
|
70 |
+
{ name: "samples", type: "LATENT", link: 7 },
|
71 |
+
{ name: "vae", type: "VAE", link: 8 },
|
72 |
+
],
|
73 |
+
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
|
74 |
+
properties: {},
|
75 |
+
},
|
76 |
+
{
|
77 |
+
id: 9,
|
78 |
+
type: "SaveImage",
|
79 |
+
pos: [1451, 189],
|
80 |
+
size: { 0: 210, 1: 26 },
|
81 |
+
flags: {},
|
82 |
+
order: 6,
|
83 |
+
mode: 0,
|
84 |
+
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
|
85 |
+
properties: {},
|
86 |
+
},
|
87 |
+
{
|
88 |
+
id: 4,
|
89 |
+
type: "CheckpointLoaderSimple",
|
90 |
+
pos: [26, 474],
|
91 |
+
size: { 0: 315, 1: 98 },
|
92 |
+
flags: {},
|
93 |
+
order: 0,
|
94 |
+
mode: 0,
|
95 |
+
outputs: [
|
96 |
+
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
|
97 |
+
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
|
98 |
+
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
|
99 |
+
],
|
100 |
+
properties: {},
|
101 |
+
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
|
102 |
+
},
|
103 |
+
],
|
104 |
+
links: [
|
105 |
+
[1, 4, 0, 3, 0, "MODEL"],
|
106 |
+
[2, 5, 0, 3, 3, "LATENT"],
|
107 |
+
[3, 4, 1, 6, 0, "CLIP"],
|
108 |
+
[4, 6, 0, 3, 1, "CONDITIONING"],
|
109 |
+
[5, 4, 1, 7, 0, "CLIP"],
|
110 |
+
[6, 7, 0, 3, 2, "CONDITIONING"],
|
111 |
+
[7, 3, 0, 8, 0, "LATENT"],
|
112 |
+
[8, 4, 2, 8, 1, "VAE"],
|
113 |
+
[9, 8, 0, 9, 0, "IMAGE"],
|
114 |
+
],
|
115 |
+
groups: [],
|
116 |
+
config: {},
|
117 |
+
extra: {},
|
118 |
+
version: 0.4,
|
119 |
+
};
|
ComfyUI/web/scripts/domWidget.js
ADDED
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { app, ANIM_PREVIEW_WIDGET } from "./app.js";
|
2 |
+
|
3 |
+
const SIZE = Symbol();
|
4 |
+
|
5 |
+
function intersect(a, b) {
|
6 |
+
const x = Math.max(a.x, b.x);
|
7 |
+
const num1 = Math.min(a.x + a.width, b.x + b.width);
|
8 |
+
const y = Math.max(a.y, b.y);
|
9 |
+
const num2 = Math.min(a.y + a.height, b.y + b.height);
|
10 |
+
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
|
11 |
+
else return null;
|
12 |
+
}
|
13 |
+
|
14 |
+
function getClipPath(node, element) {
|
15 |
+
const selectedNode = Object.values(app.canvas.selected_nodes)[0];
|
16 |
+
if (selectedNode && selectedNode !== node) {
|
17 |
+
const elRect = element.getBoundingClientRect();
|
18 |
+
const MARGIN = 7;
|
19 |
+
const scale = app.canvas.ds.scale;
|
20 |
+
|
21 |
+
const bounding = selectedNode.getBounding();
|
22 |
+
const intersection = intersect(
|
23 |
+
{ x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale },
|
24 |
+
{
|
25 |
+
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
|
26 |
+
y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN,
|
27 |
+
width: bounding[2] + MARGIN + MARGIN,
|
28 |
+
height: bounding[3] + MARGIN + MARGIN,
|
29 |
+
}
|
30 |
+
);
|
31 |
+
|
32 |
+
if (!intersection) {
|
33 |
+
return "";
|
34 |
+
}
|
35 |
+
|
36 |
+
const widgetRect = element.getBoundingClientRect();
|
37 |
+
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
|
38 |
+
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
|
39 |
+
const clipWidth = intersection[2] + "px";
|
40 |
+
const clipHeight = intersection[3] + "px";
|
41 |
+
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
|
42 |
+
return path;
|
43 |
+
}
|
44 |
+
return "";
|
45 |
+
}
|
46 |
+
|
47 |
+
function computeSize(size) {
|
48 |
+
if (this.widgets?.[0]?.last_y == null) return;
|
49 |
+
|
50 |
+
let y = this.widgets[0].last_y;
|
51 |
+
let freeSpace = size[1] - y;
|
52 |
+
|
53 |
+
let widgetHeight = 0;
|
54 |
+
let dom = [];
|
55 |
+
for (const w of this.widgets) {
|
56 |
+
if (w.type === "converted-widget") {
|
57 |
+
// Ignore
|
58 |
+
delete w.computedHeight;
|
59 |
+
} else if (w.computeSize) {
|
60 |
+
widgetHeight += w.computeSize()[1] + 4;
|
61 |
+
} else if (w.element) {
|
62 |
+
// Extract DOM widget size info
|
63 |
+
const styles = getComputedStyle(w.element);
|
64 |
+
let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
|
65 |
+
let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
|
66 |
+
|
67 |
+
let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height");
|
68 |
+
if (prefHeight.endsWith?.("%")) {
|
69 |
+
prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
|
70 |
+
} else {
|
71 |
+
prefHeight = parseInt(prefHeight);
|
72 |
+
if (isNaN(minHeight)) {
|
73 |
+
minHeight = prefHeight;
|
74 |
+
}
|
75 |
+
}
|
76 |
+
if (isNaN(minHeight)) {
|
77 |
+
minHeight = 50;
|
78 |
+
}
|
79 |
+
if (!isNaN(maxHeight)) {
|
80 |
+
if (!isNaN(prefHeight)) {
|
81 |
+
prefHeight = Math.min(prefHeight, maxHeight);
|
82 |
+
} else {
|
83 |
+
prefHeight = maxHeight;
|
84 |
+
}
|
85 |
+
}
|
86 |
+
dom.push({
|
87 |
+
minHeight,
|
88 |
+
prefHeight,
|
89 |
+
w,
|
90 |
+
});
|
91 |
+
} else {
|
92 |
+
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
93 |
+
}
|
94 |
+
}
|
95 |
+
|
96 |
+
freeSpace -= widgetHeight;
|
97 |
+
|
98 |
+
// Calculate sizes with all widgets at their min height
|
99 |
+
const prefGrow = []; // Nodes that want to grow to their prefd size
|
100 |
+
const canGrow = []; // Nodes that can grow to auto size
|
101 |
+
let growBy = 0;
|
102 |
+
for (const d of dom) {
|
103 |
+
freeSpace -= d.minHeight;
|
104 |
+
if (isNaN(d.prefHeight)) {
|
105 |
+
canGrow.push(d);
|
106 |
+
d.w.computedHeight = d.minHeight;
|
107 |
+
} else {
|
108 |
+
const diff = d.prefHeight - d.minHeight;
|
109 |
+
if (diff > 0) {
|
110 |
+
prefGrow.push(d);
|
111 |
+
growBy += diff;
|
112 |
+
d.diff = diff;
|
113 |
+
} else {
|
114 |
+
d.w.computedHeight = d.minHeight;
|
115 |
+
}
|
116 |
+
}
|
117 |
+
}
|
118 |
+
|
119 |
+
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
|
120 |
+
// Allocate space for image
|
121 |
+
freeSpace -= 220;
|
122 |
+
}
|
123 |
+
|
124 |
+
this.freeWidgetSpace = freeSpace;
|
125 |
+
|
126 |
+
if (freeSpace < 0) {
|
127 |
+
// Not enough space for all widgets so we need to grow
|
128 |
+
size[1] -= freeSpace;
|
129 |
+
this.graph.setDirtyCanvas(true);
|
130 |
+
} else {
|
131 |
+
// Share the space between each
|
132 |
+
const growDiff = freeSpace - growBy;
|
133 |
+
if (growDiff > 0) {
|
134 |
+
// All pref sizes can be fulfilled
|
135 |
+
freeSpace = growDiff;
|
136 |
+
for (const d of prefGrow) {
|
137 |
+
d.w.computedHeight = d.prefHeight;
|
138 |
+
}
|
139 |
+
} else {
|
140 |
+
// We need to grow evenly
|
141 |
+
const shared = -growDiff / prefGrow.length;
|
142 |
+
for (const d of prefGrow) {
|
143 |
+
d.w.computedHeight = d.prefHeight - shared;
|
144 |
+
}
|
145 |
+
freeSpace = 0;
|
146 |
+
}
|
147 |
+
|
148 |
+
if (freeSpace > 0 && canGrow.length) {
|
149 |
+
// Grow any that are auto height
|
150 |
+
const shared = freeSpace / canGrow.length;
|
151 |
+
for (const d of canGrow) {
|
152 |
+
d.w.computedHeight += shared;
|
153 |
+
}
|
154 |
+
}
|
155 |
+
}
|
156 |
+
|
157 |
+
// Position each of the widgets
|
158 |
+
for (const w of this.widgets) {
|
159 |
+
w.y = y;
|
160 |
+
if (w.computedHeight) {
|
161 |
+
y += w.computedHeight;
|
162 |
+
} else if (w.computeSize) {
|
163 |
+
y += w.computeSize()[1] + 4;
|
164 |
+
} else {
|
165 |
+
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
166 |
+
}
|
167 |
+
}
|
168 |
+
}
|
169 |
+
|
170 |
+
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
|
171 |
+
const elementWidgets = new Set();
|
172 |
+
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
173 |
+
LGraphCanvas.prototype.computeVisibleNodes = function () {
|
174 |
+
const visibleNodes = computeVisibleNodes.apply(this, arguments);
|
175 |
+
for (const node of app.graph._nodes) {
|
176 |
+
if (elementWidgets.has(node)) {
|
177 |
+
const hidden = visibleNodes.indexOf(node) === -1;
|
178 |
+
for (const w of node.widgets) {
|
179 |
+
if (w.element) {
|
180 |
+
w.element.hidden = hidden;
|
181 |
+
w.element.style.display = hidden ? "none" : undefined;
|
182 |
+
if (hidden) {
|
183 |
+
w.options.onHide?.(w);
|
184 |
+
}
|
185 |
+
}
|
186 |
+
}
|
187 |
+
}
|
188 |
+
}
|
189 |
+
|
190 |
+
return visibleNodes;
|
191 |
+
};
|
192 |
+
|
193 |
+
let enableDomClipping = true;
|
194 |
+
|
195 |
+
export function addDomClippingSetting() {
|
196 |
+
app.ui.settings.addSetting({
|
197 |
+
id: "Comfy.DOMClippingEnabled",
|
198 |
+
name: "Enable DOM element clipping (enabling may reduce performance)",
|
199 |
+
type: "boolean",
|
200 |
+
defaultValue: enableDomClipping,
|
201 |
+
onChange(value) {
|
202 |
+
enableDomClipping = !!value;
|
203 |
+
},
|
204 |
+
});
|
205 |
+
}
|
206 |
+
|
207 |
+
LGraphNode.prototype.addDOMWidget = function (name, type, element, options) {
|
208 |
+
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
|
209 |
+
|
210 |
+
if (!element.parentElement) {
|
211 |
+
document.body.append(element);
|
212 |
+
}
|
213 |
+
element.hidden = true;
|
214 |
+
element.style.display = "none";
|
215 |
+
|
216 |
+
let mouseDownHandler;
|
217 |
+
if (element.blur) {
|
218 |
+
mouseDownHandler = (event) => {
|
219 |
+
if (!element.contains(event.target)) {
|
220 |
+
element.blur();
|
221 |
+
}
|
222 |
+
};
|
223 |
+
document.addEventListener("mousedown", mouseDownHandler);
|
224 |
+
}
|
225 |
+
|
226 |
+
const widget = {
|
227 |
+
type,
|
228 |
+
name,
|
229 |
+
get value() {
|
230 |
+
return options.getValue?.() ?? undefined;
|
231 |
+
},
|
232 |
+
set value(v) {
|
233 |
+
options.setValue?.(v);
|
234 |
+
widget.callback?.(widget.value);
|
235 |
+
},
|
236 |
+
draw: function (ctx, node, widgetWidth, y, widgetHeight) {
|
237 |
+
if (widget.computedHeight == null) {
|
238 |
+
computeSize.call(node, node.size);
|
239 |
+
}
|
240 |
+
|
241 |
+
const hidden =
|
242 |
+
node.flags?.collapsed ||
|
243 |
+
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
244 |
+
widget.computedHeight <= 0 ||
|
245 |
+
widget.type === "converted-widget"||
|
246 |
+
widget.type === "hidden";
|
247 |
+
element.hidden = hidden;
|
248 |
+
element.style.display = hidden ? "none" : null;
|
249 |
+
if (hidden) {
|
250 |
+
widget.options.onHide?.(widget);
|
251 |
+
return;
|
252 |
+
}
|
253 |
+
|
254 |
+
const margin = 10;
|
255 |
+
const elRect = ctx.canvas.getBoundingClientRect();
|
256 |
+
const transform = new DOMMatrix()
|
257 |
+
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
|
258 |
+
.multiplySelf(ctx.getTransform())
|
259 |
+
.translateSelf(margin, margin + y );
|
260 |
+
|
261 |
+
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
262 |
+
|
263 |
+
Object.assign(element.style, {
|
264 |
+
transformOrigin: "0 0",
|
265 |
+
transform: scale,
|
266 |
+
left: `${transform.a + transform.e + elRect.left}px`,
|
267 |
+
top: `${transform.d + transform.f + elRect.top}px`,
|
268 |
+
width: `${widgetWidth - margin * 2}px`,
|
269 |
+
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
|
270 |
+
position: "absolute",
|
271 |
+
zIndex: app.graph._nodes.indexOf(node),
|
272 |
+
});
|
273 |
+
|
274 |
+
if (enableDomClipping) {
|
275 |
+
element.style.clipPath = getClipPath(node, element);
|
276 |
+
element.style.willChange = "clip-path";
|
277 |
+
}
|
278 |
+
|
279 |
+
this.options.onDraw?.(widget);
|
280 |
+
},
|
281 |
+
element,
|
282 |
+
options,
|
283 |
+
onRemove() {
|
284 |
+
if (mouseDownHandler) {
|
285 |
+
document.removeEventListener("mousedown", mouseDownHandler);
|
286 |
+
}
|
287 |
+
element.remove();
|
288 |
+
},
|
289 |
+
};
|
290 |
+
|
291 |
+
for (const evt of options.selectOn) {
|
292 |
+
element.addEventListener(evt, () => {
|
293 |
+
app.canvas.selectNode(this);
|
294 |
+
app.canvas.bringToFront(this);
|
295 |
+
});
|
296 |
+
}
|
297 |
+
|
298 |
+
this.addCustomWidget(widget);
|
299 |
+
elementWidgets.add(this);
|
300 |
+
|
301 |
+
const collapse = this.collapse;
|
302 |
+
this.collapse = function() {
|
303 |
+
collapse.apply(this, arguments);
|
304 |
+
if(this.flags?.collapsed) {
|
305 |
+
element.hidden = true;
|
306 |
+
element.style.display = "none";
|
307 |
+
}
|
308 |
+
}
|
309 |
+
|
310 |
+
const onRemoved = this.onRemoved;
|
311 |
+
this.onRemoved = function () {
|
312 |
+
element.remove();
|
313 |
+
elementWidgets.delete(this);
|
314 |
+
onRemoved?.apply(this, arguments);
|
315 |
+
};
|
316 |
+
|
317 |
+
if (!this[SIZE]) {
|
318 |
+
this[SIZE] = true;
|
319 |
+
const onResize = this.onResize;
|
320 |
+
this.onResize = function (size) {
|
321 |
+
options.beforeResize?.call(widget, this);
|
322 |
+
computeSize.call(this, size);
|
323 |
+
onResize?.apply(this, arguments);
|
324 |
+
options.afterResize?.call(widget, this);
|
325 |
+
};
|
326 |
+
}
|
327 |
+
|
328 |
+
return widget;
|
329 |
+
};
|
ComfyUI/web/scripts/logging.js
ADDED
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { $el, ComfyDialog } from "./ui.js";
|
2 |
+
import { api } from "./api.js";
|
3 |
+
|
4 |
+
$el("style", {
|
5 |
+
textContent: `
|
6 |
+
.comfy-logging-logs {
|
7 |
+
display: grid;
|
8 |
+
color: var(--fg-color);
|
9 |
+
white-space: pre-wrap;
|
10 |
+
}
|
11 |
+
.comfy-logging-log {
|
12 |
+
display: contents;
|
13 |
+
}
|
14 |
+
.comfy-logging-title {
|
15 |
+
background: var(--tr-even-bg-color);
|
16 |
+
font-weight: bold;
|
17 |
+
margin-bottom: 5px;
|
18 |
+
text-align: center;
|
19 |
+
}
|
20 |
+
.comfy-logging-log div {
|
21 |
+
background: var(--row-bg);
|
22 |
+
padding: 5px;
|
23 |
+
}
|
24 |
+
`,
|
25 |
+
parent: document.body,
|
26 |
+
});
|
27 |
+
|
28 |
+
// Stringify function supporting max depth and removal of circular references
|
29 |
+
// https://stackoverflow.com/a/57193345
|
30 |
+
function stringify(val, depth, replacer, space, onGetObjID) {
|
31 |
+
depth = isNaN(+depth) ? 1 : depth;
|
32 |
+
var recursMap = new WeakMap();
|
33 |
+
function _build(val, depth, o, a, r) {
|
34 |
+
// (JSON.stringify() has it's own rules, which we respect here by using it for property iteration)
|
35 |
+
return !val || typeof val != "object"
|
36 |
+
? val
|
37 |
+
: ((r = recursMap.has(val)),
|
38 |
+
recursMap.set(val, true),
|
39 |
+
(a = Array.isArray(val)),
|
40 |
+
r
|
41 |
+
? (o = (onGetObjID && onGetObjID(val)) || null)
|
42 |
+
: JSON.stringify(val, function (k, v) {
|
43 |
+
if (a || depth > 0) {
|
44 |
+
if (replacer) v = replacer(k, v);
|
45 |
+
if (!k) return (a = Array.isArray(v)), (val = v);
|
46 |
+
!o && (o = a ? [] : {});
|
47 |
+
o[k] = _build(v, a ? depth : depth - 1);
|
48 |
+
}
|
49 |
+
}),
|
50 |
+
o === void 0 ? (a ? [] : {}) : o);
|
51 |
+
}
|
52 |
+
return JSON.stringify(_build(val, depth), null, space);
|
53 |
+
}
|
54 |
+
|
55 |
+
const jsonReplacer = (k, v, ui) => {
|
56 |
+
if (v instanceof Array && v.length === 1) {
|
57 |
+
v = v[0];
|
58 |
+
}
|
59 |
+
if (v instanceof Date) {
|
60 |
+
v = v.toISOString();
|
61 |
+
if (ui) {
|
62 |
+
v = v.split("T")[1];
|
63 |
+
}
|
64 |
+
}
|
65 |
+
if (v instanceof Error) {
|
66 |
+
let err = "";
|
67 |
+
if (v.name) err += v.name + "\n";
|
68 |
+
if (v.message) err += v.message + "\n";
|
69 |
+
if (v.stack) err += v.stack + "\n";
|
70 |
+
if (!err) {
|
71 |
+
err = v.toString();
|
72 |
+
}
|
73 |
+
v = err;
|
74 |
+
}
|
75 |
+
return v;
|
76 |
+
};
|
77 |
+
|
78 |
+
const fileInput = $el("input", {
|
79 |
+
type: "file",
|
80 |
+
accept: ".json",
|
81 |
+
style: { display: "none" },
|
82 |
+
parent: document.body,
|
83 |
+
});
|
84 |
+
|
85 |
+
class ComfyLoggingDialog extends ComfyDialog {
|
86 |
+
constructor(logging) {
|
87 |
+
super();
|
88 |
+
this.logging = logging;
|
89 |
+
}
|
90 |
+
|
91 |
+
clear() {
|
92 |
+
this.logging.clear();
|
93 |
+
this.show();
|
94 |
+
}
|
95 |
+
|
96 |
+
export() {
|
97 |
+
const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], {
|
98 |
+
type: "application/json",
|
99 |
+
});
|
100 |
+
const url = URL.createObjectURL(blob);
|
101 |
+
const a = $el("a", {
|
102 |
+
href: url,
|
103 |
+
download: `comfyui-logs-${Date.now()}.json`,
|
104 |
+
style: { display: "none" },
|
105 |
+
parent: document.body,
|
106 |
+
});
|
107 |
+
a.click();
|
108 |
+
setTimeout(function () {
|
109 |
+
a.remove();
|
110 |
+
window.URL.revokeObjectURL(url);
|
111 |
+
}, 0);
|
112 |
+
}
|
113 |
+
|
114 |
+
import() {
|
115 |
+
fileInput.onchange = () => {
|
116 |
+
const reader = new FileReader();
|
117 |
+
reader.onload = () => {
|
118 |
+
fileInput.remove();
|
119 |
+
try {
|
120 |
+
const obj = JSON.parse(reader.result);
|
121 |
+
if (obj instanceof Array) {
|
122 |
+
this.show(obj);
|
123 |
+
} else {
|
124 |
+
throw new Error("Invalid file selected.");
|
125 |
+
}
|
126 |
+
} catch (error) {
|
127 |
+
alert("Unable to load logs: " + error.message);
|
128 |
+
}
|
129 |
+
};
|
130 |
+
reader.readAsText(fileInput.files[0]);
|
131 |
+
};
|
132 |
+
fileInput.click();
|
133 |
+
}
|
134 |
+
|
135 |
+
createButtons() {
|
136 |
+
return [
|
137 |
+
$el("button", {
|
138 |
+
type: "button",
|
139 |
+
textContent: "Clear",
|
140 |
+
onclick: () => this.clear(),
|
141 |
+
}),
|
142 |
+
$el("button", {
|
143 |
+
type: "button",
|
144 |
+
textContent: "Export logs...",
|
145 |
+
onclick: () => this.export(),
|
146 |
+
}),
|
147 |
+
$el("button", {
|
148 |
+
type: "button",
|
149 |
+
textContent: "View exported logs...",
|
150 |
+
onclick: () => this.import(),
|
151 |
+
}),
|
152 |
+
...super.createButtons(),
|
153 |
+
];
|
154 |
+
}
|
155 |
+
|
156 |
+
getTypeColor(type) {
|
157 |
+
switch (type) {
|
158 |
+
case "error":
|
159 |
+
return "red";
|
160 |
+
case "warn":
|
161 |
+
return "orange";
|
162 |
+
case "debug":
|
163 |
+
return "dodgerblue";
|
164 |
+
}
|
165 |
+
}
|
166 |
+
|
167 |
+
show(entries) {
|
168 |
+
if (!entries) entries = this.logging.entries;
|
169 |
+
this.element.style.width = "100%";
|
170 |
+
const cols = {
|
171 |
+
source: "Source",
|
172 |
+
type: "Type",
|
173 |
+
timestamp: "Timestamp",
|
174 |
+
message: "Message",
|
175 |
+
};
|
176 |
+
const keys = Object.keys(cols);
|
177 |
+
const headers = Object.values(cols).map((title) =>
|
178 |
+
$el("div.comfy-logging-title", {
|
179 |
+
textContent: title,
|
180 |
+
})
|
181 |
+
);
|
182 |
+
const rows = entries.map((entry, i) => {
|
183 |
+
return $el(
|
184 |
+
"div.comfy-logging-log",
|
185 |
+
{
|
186 |
+
$: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`),
|
187 |
+
},
|
188 |
+
keys.map((key) => {
|
189 |
+
let v = entry[key];
|
190 |
+
let color;
|
191 |
+
if (key === "type") {
|
192 |
+
color = this.getTypeColor(v);
|
193 |
+
} else {
|
194 |
+
v = jsonReplacer(key, v, true);
|
195 |
+
|
196 |
+
if (typeof v === "object") {
|
197 |
+
v = stringify(v, 5, jsonReplacer, " ");
|
198 |
+
}
|
199 |
+
}
|
200 |
+
|
201 |
+
return $el("div", {
|
202 |
+
style: {
|
203 |
+
color,
|
204 |
+
},
|
205 |
+
textContent: v,
|
206 |
+
});
|
207 |
+
})
|
208 |
+
);
|
209 |
+
});
|
210 |
+
|
211 |
+
const grid = $el(
|
212 |
+
"div.comfy-logging-logs",
|
213 |
+
{
|
214 |
+
style: {
|
215 |
+
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
|
216 |
+
},
|
217 |
+
},
|
218 |
+
[...headers, ...rows]
|
219 |
+
);
|
220 |
+
const els = [grid];
|
221 |
+
if (!this.logging.enabled) {
|
222 |
+
els.unshift(
|
223 |
+
$el("h3", {
|
224 |
+
style: { textAlign: "center" },
|
225 |
+
textContent: "Logging is disabled",
|
226 |
+
})
|
227 |
+
);
|
228 |
+
}
|
229 |
+
super.show($el("div", els));
|
230 |
+
}
|
231 |
+
}
|
232 |
+
|
233 |
+
export class ComfyLogging {
|
234 |
+
/**
|
235 |
+
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
|
236 |
+
*/
|
237 |
+
entries = [];
|
238 |
+
|
239 |
+
#enabled;
|
240 |
+
#console = {};
|
241 |
+
|
242 |
+
get enabled() {
|
243 |
+
return this.#enabled;
|
244 |
+
}
|
245 |
+
|
246 |
+
set enabled(value) {
|
247 |
+
if (value === this.#enabled) return;
|
248 |
+
if (value) {
|
249 |
+
this.patchConsole();
|
250 |
+
} else {
|
251 |
+
this.unpatchConsole();
|
252 |
+
}
|
253 |
+
this.#enabled = value;
|
254 |
+
}
|
255 |
+
|
256 |
+
constructor(app) {
|
257 |
+
this.app = app;
|
258 |
+
|
259 |
+
this.dialog = new ComfyLoggingDialog(this);
|
260 |
+
this.addSetting();
|
261 |
+
this.catchUnhandled();
|
262 |
+
this.addInitData();
|
263 |
+
}
|
264 |
+
|
265 |
+
addSetting() {
|
266 |
+
const settingId = "Comfy.Logging.Enabled";
|
267 |
+
const htmlSettingId = settingId.replaceAll(".", "-");
|
268 |
+
const setting = this.app.ui.settings.addSetting({
|
269 |
+
id: settingId,
|
270 |
+
name: settingId,
|
271 |
+
defaultValue: true,
|
272 |
+
onChange: (value) => {
|
273 |
+
this.enabled = value;
|
274 |
+
},
|
275 |
+
type: (name, setter, value) => {
|
276 |
+
return $el("tr", [
|
277 |
+
$el("td", [
|
278 |
+
$el("label", {
|
279 |
+
textContent: "Logging",
|
280 |
+
for: htmlSettingId,
|
281 |
+
}),
|
282 |
+
]),
|
283 |
+
$el("td", [
|
284 |
+
$el("input", {
|
285 |
+
id: htmlSettingId,
|
286 |
+
type: "checkbox",
|
287 |
+
checked: value,
|
288 |
+
onchange: (event) => {
|
289 |
+
setter(event.target.checked);
|
290 |
+
},
|
291 |
+
}),
|
292 |
+
$el("button", {
|
293 |
+
textContent: "View Logs",
|
294 |
+
onclick: () => {
|
295 |
+
this.app.ui.settings.element.close();
|
296 |
+
this.dialog.show();
|
297 |
+
},
|
298 |
+
style: {
|
299 |
+
fontSize: "14px",
|
300 |
+
display: "block",
|
301 |
+
marginTop: "5px",
|
302 |
+
},
|
303 |
+
}),
|
304 |
+
]),
|
305 |
+
]);
|
306 |
+
},
|
307 |
+
});
|
308 |
+
this.enabled = setting.value;
|
309 |
+
}
|
310 |
+
|
311 |
+
patchConsole() {
|
312 |
+
// Capture common console outputs
|
313 |
+
const self = this;
|
314 |
+
for (const type of ["log", "warn", "error", "debug"]) {
|
315 |
+
const orig = console[type];
|
316 |
+
this.#console[type] = orig;
|
317 |
+
console[type] = function () {
|
318 |
+
orig.apply(console, arguments);
|
319 |
+
self.addEntry("console", type, ...arguments);
|
320 |
+
};
|
321 |
+
}
|
322 |
+
}
|
323 |
+
|
324 |
+
unpatchConsole() {
|
325 |
+
// Restore original console functions
|
326 |
+
for (const type of Object.keys(this.#console)) {
|
327 |
+
console[type] = this.#console[type];
|
328 |
+
}
|
329 |
+
this.#console = {};
|
330 |
+
}
|
331 |
+
|
332 |
+
catchUnhandled() {
|
333 |
+
// Capture uncaught errors
|
334 |
+
window.addEventListener("error", (e) => {
|
335 |
+
this.addEntry("window", "error", e.error ?? "Unknown error");
|
336 |
+
return false;
|
337 |
+
});
|
338 |
+
|
339 |
+
window.addEventListener("unhandledrejection", (e) => {
|
340 |
+
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
|
341 |
+
});
|
342 |
+
}
|
343 |
+
|
344 |
+
clear() {
|
345 |
+
this.entries = [];
|
346 |
+
}
|
347 |
+
|
348 |
+
addEntry(source, type, ...args) {
|
349 |
+
if (this.enabled) {
|
350 |
+
this.entries.push({
|
351 |
+
source,
|
352 |
+
type,
|
353 |
+
timestamp: new Date(),
|
354 |
+
message: args,
|
355 |
+
});
|
356 |
+
}
|
357 |
+
}
|
358 |
+
|
359 |
+
log(source, ...args) {
|
360 |
+
this.addEntry(source, "log", ...args);
|
361 |
+
}
|
362 |
+
|
363 |
+
async addInitData() {
|
364 |
+
if (!this.enabled) return;
|
365 |
+
const source = "ComfyUI.Logging";
|
366 |
+
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
|
367 |
+
const systemStats = await api.getSystemStats();
|
368 |
+
this.addEntry(source, "debug", systemStats);
|
369 |
+
}
|
370 |
+
}
|
ComfyUI/web/scripts/pnginfo.js
ADDED
@@ -0,0 +1,506 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { api } from "./api.js";
|
2 |
+
|
3 |
+
export function getPngMetadata(file) {
|
4 |
+
return new Promise((r) => {
|
5 |
+
const reader = new FileReader();
|
6 |
+
reader.onload = (event) => {
|
7 |
+
// Get the PNG data as a Uint8Array
|
8 |
+
const pngData = new Uint8Array(event.target.result);
|
9 |
+
const dataView = new DataView(pngData.buffer);
|
10 |
+
|
11 |
+
// Check that the PNG signature is present
|
12 |
+
if (dataView.getUint32(0) !== 0x89504e47) {
|
13 |
+
console.error("Not a valid PNG file");
|
14 |
+
r();
|
15 |
+
return;
|
16 |
+
}
|
17 |
+
|
18 |
+
// Start searching for chunks after the PNG signature
|
19 |
+
let offset = 8;
|
20 |
+
let txt_chunks = {};
|
21 |
+
// Loop through the chunks in the PNG file
|
22 |
+
while (offset < pngData.length) {
|
23 |
+
// Get the length of the chunk
|
24 |
+
const length = dataView.getUint32(offset);
|
25 |
+
// Get the chunk type
|
26 |
+
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
|
27 |
+
if (type === "tEXt" || type == "comf" || type === "iTXt") {
|
28 |
+
// Get the keyword
|
29 |
+
let keyword_end = offset + 8;
|
30 |
+
while (pngData[keyword_end] !== 0) {
|
31 |
+
keyword_end++;
|
32 |
+
}
|
33 |
+
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
|
34 |
+
// Get the text
|
35 |
+
const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
|
36 |
+
const contentJson = new TextDecoder("utf-8").decode(contentArraySegment);
|
37 |
+
txt_chunks[keyword] = contentJson;
|
38 |
+
}
|
39 |
+
|
40 |
+
offset += 12 + length;
|
41 |
+
}
|
42 |
+
|
43 |
+
r(txt_chunks);
|
44 |
+
};
|
45 |
+
|
46 |
+
reader.readAsArrayBuffer(file);
|
47 |
+
});
|
48 |
+
}
|
49 |
+
|
50 |
+
function parseExifData(exifData) {
|
51 |
+
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
|
52 |
+
const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === "II";
|
53 |
+
|
54 |
+
// Function to read 16-bit and 32-bit integers from binary data
|
55 |
+
function readInt(offset, isLittleEndian, length) {
|
56 |
+
let arr = exifData.slice(offset, offset + length)
|
57 |
+
if (length === 2) {
|
58 |
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(0, isLittleEndian);
|
59 |
+
} else if (length === 4) {
|
60 |
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(0, isLittleEndian);
|
61 |
+
}
|
62 |
+
}
|
63 |
+
|
64 |
+
// Read the offset to the first IFD (Image File Directory)
|
65 |
+
const ifdOffset = readInt(4, isLittleEndian, 4);
|
66 |
+
|
67 |
+
function parseIFD(offset) {
|
68 |
+
const numEntries = readInt(offset, isLittleEndian, 2);
|
69 |
+
const result = {};
|
70 |
+
|
71 |
+
for (let i = 0; i < numEntries; i++) {
|
72 |
+
const entryOffset = offset + 2 + i * 12;
|
73 |
+
const tag = readInt(entryOffset, isLittleEndian, 2);
|
74 |
+
const type = readInt(entryOffset + 2, isLittleEndian, 2);
|
75 |
+
const numValues = readInt(entryOffset + 4, isLittleEndian, 4);
|
76 |
+
const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4);
|
77 |
+
|
78 |
+
// Read the value(s) based on the data type
|
79 |
+
let value;
|
80 |
+
if (type === 2) {
|
81 |
+
// ASCII string
|
82 |
+
value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1));
|
83 |
+
}
|
84 |
+
|
85 |
+
result[tag] = value;
|
86 |
+
}
|
87 |
+
|
88 |
+
return result;
|
89 |
+
}
|
90 |
+
|
91 |
+
// Parse the first IFD
|
92 |
+
const ifdData = parseIFD(ifdOffset);
|
93 |
+
return ifdData;
|
94 |
+
}
|
95 |
+
|
96 |
+
function splitValues(input) {
|
97 |
+
var output = {};
|
98 |
+
for (var key in input) {
|
99 |
+
var value = input[key];
|
100 |
+
var splitValues = value.split(':', 2);
|
101 |
+
output[splitValues[0]] = splitValues[1];
|
102 |
+
}
|
103 |
+
return output;
|
104 |
+
}
|
105 |
+
|
106 |
+
export function getWebpMetadata(file) {
|
107 |
+
return new Promise((r) => {
|
108 |
+
const reader = new FileReader();
|
109 |
+
reader.onload = (event) => {
|
110 |
+
const webp = new Uint8Array(event.target.result);
|
111 |
+
const dataView = new DataView(webp.buffer);
|
112 |
+
|
113 |
+
// Check that the WEBP signature is present
|
114 |
+
if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) {
|
115 |
+
console.error("Not a valid WEBP file");
|
116 |
+
r();
|
117 |
+
return;
|
118 |
+
}
|
119 |
+
|
120 |
+
// Start searching for chunks after the WEBP signature
|
121 |
+
let offset = 12;
|
122 |
+
let txt_chunks = {};
|
123 |
+
// Loop through the chunks in the WEBP file
|
124 |
+
while (offset < webp.length) {
|
125 |
+
const chunk_length = dataView.getUint32(offset + 4, true);
|
126 |
+
const chunk_type = String.fromCharCode(...webp.slice(offset, offset + 4));
|
127 |
+
if (chunk_type === "EXIF") {
|
128 |
+
if (String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == "Exif\0\0") {
|
129 |
+
offset += 6;
|
130 |
+
}
|
131 |
+
let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length));
|
132 |
+
for (var key in data) {
|
133 |
+
var value = data[key];
|
134 |
+
let index = value.indexOf(':');
|
135 |
+
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
|
136 |
+
}
|
137 |
+
break;
|
138 |
+
}
|
139 |
+
|
140 |
+
offset += 8 + chunk_length;
|
141 |
+
}
|
142 |
+
|
143 |
+
r(txt_chunks);
|
144 |
+
};
|
145 |
+
|
146 |
+
reader.readAsArrayBuffer(file);
|
147 |
+
});
|
148 |
+
}
|
149 |
+
|
150 |
+
export function getLatentMetadata(file) {
|
151 |
+
return new Promise((r) => {
|
152 |
+
const reader = new FileReader();
|
153 |
+
reader.onload = (event) => {
|
154 |
+
const safetensorsData = new Uint8Array(event.target.result);
|
155 |
+
const dataView = new DataView(safetensorsData.buffer);
|
156 |
+
let header_size = dataView.getUint32(0, true);
|
157 |
+
let offset = 8;
|
158 |
+
let header = JSON.parse(new TextDecoder().decode(safetensorsData.slice(offset, offset + header_size)));
|
159 |
+
r(header.__metadata__);
|
160 |
+
};
|
161 |
+
|
162 |
+
var slice = file.slice(0, 1024 * 1024 * 4);
|
163 |
+
reader.readAsArrayBuffer(slice);
|
164 |
+
});
|
165 |
+
}
|
166 |
+
|
167 |
+
|
168 |
+
function getString(dataView, offset, length) {
|
169 |
+
let string = '';
|
170 |
+
for (let i = 0; i < length; i++) {
|
171 |
+
string += String.fromCharCode(dataView.getUint8(offset + i));
|
172 |
+
}
|
173 |
+
return string;
|
174 |
+
}
|
175 |
+
|
176 |
+
// Function to parse the Vorbis Comment block
|
177 |
+
function parseVorbisComment(dataView) {
|
178 |
+
let offset = 0;
|
179 |
+
const vendorLength = dataView.getUint32(offset, true);
|
180 |
+
offset += 4;
|
181 |
+
const vendorString = getString(dataView, offset, vendorLength);
|
182 |
+
offset += vendorLength;
|
183 |
+
|
184 |
+
const userCommentListLength = dataView.getUint32(offset, true);
|
185 |
+
offset += 4;
|
186 |
+
const comments = {};
|
187 |
+
for (let i = 0; i < userCommentListLength; i++) {
|
188 |
+
const commentLength = dataView.getUint32(offset, true);
|
189 |
+
offset += 4;
|
190 |
+
const comment = getString(dataView, offset, commentLength);
|
191 |
+
offset += commentLength;
|
192 |
+
|
193 |
+
const [key, value] = comment.split('=');
|
194 |
+
|
195 |
+
comments[key] = value;
|
196 |
+
}
|
197 |
+
|
198 |
+
return comments;
|
199 |
+
}
|
200 |
+
|
201 |
+
// Function to read a FLAC file and parse Vorbis comments
|
202 |
+
export function getFlacMetadata(file) {
|
203 |
+
return new Promise((r) => {
|
204 |
+
const reader = new FileReader();
|
205 |
+
reader.onload = function(event) {
|
206 |
+
const arrayBuffer = event.target.result;
|
207 |
+
const dataView = new DataView(arrayBuffer);
|
208 |
+
|
209 |
+
// Verify the FLAC signature
|
210 |
+
const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4));
|
211 |
+
if (signature !== 'fLaC') {
|
212 |
+
console.error('Not a valid FLAC file');
|
213 |
+
return;
|
214 |
+
}
|
215 |
+
|
216 |
+
// Parse metadata blocks
|
217 |
+
let offset = 4;
|
218 |
+
let vorbisComment = null;
|
219 |
+
while (offset < dataView.byteLength) {
|
220 |
+
const isLastBlock = dataView.getUint8(offset) & 0x80;
|
221 |
+
const blockType = dataView.getUint8(offset) & 0x7F;
|
222 |
+
const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF;
|
223 |
+
offset += 4;
|
224 |
+
|
225 |
+
if (blockType === 4) { // Vorbis Comment block type
|
226 |
+
vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize));
|
227 |
+
}
|
228 |
+
|
229 |
+
offset += blockSize;
|
230 |
+
if (isLastBlock) break;
|
231 |
+
}
|
232 |
+
|
233 |
+
r(vorbisComment);
|
234 |
+
};
|
235 |
+
reader.readAsArrayBuffer(file);
|
236 |
+
});
|
237 |
+
}
|
238 |
+
|
239 |
+
export async function importA1111(graph, parameters) {
|
240 |
+
const p = parameters.lastIndexOf("\nSteps:");
|
241 |
+
if (p > -1) {
|
242 |
+
const embeddings = await api.getEmbeddings();
|
243 |
+
const opts = parameters
|
244 |
+
.substr(p)
|
245 |
+
.split("\n")[1]
|
246 |
+
.match(new RegExp("\\s*([^:]+:\\s*([^\"\\{].*?|\".*?\"|\\{.*?\\}))\\s*(,|$)", "g"))
|
247 |
+
.reduce((p, n) => {
|
248 |
+
const s = n.split(":");
|
249 |
+
if (s[1].endsWith(',')) {
|
250 |
+
s[1] = s[1].substr(0, s[1].length -1);
|
251 |
+
}
|
252 |
+
p[s[0].trim().toLowerCase()] = s[1].trim();
|
253 |
+
return p;
|
254 |
+
}, {});
|
255 |
+
const p2 = parameters.lastIndexOf("\nNegative prompt:", p);
|
256 |
+
if (p2 > -1) {
|
257 |
+
let positive = parameters.substr(0, p2).trim();
|
258 |
+
let negative = parameters.substring(p2 + 18, p).trim();
|
259 |
+
|
260 |
+
const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple");
|
261 |
+
const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer");
|
262 |
+
const positiveNode = LiteGraph.createNode("CLIPTextEncode");
|
263 |
+
const negativeNode = LiteGraph.createNode("CLIPTextEncode");
|
264 |
+
const samplerNode = LiteGraph.createNode("KSampler");
|
265 |
+
const imageNode = LiteGraph.createNode("EmptyLatentImage");
|
266 |
+
const vaeNode = LiteGraph.createNode("VAEDecode");
|
267 |
+
const vaeLoaderNode = LiteGraph.createNode("VAELoader");
|
268 |
+
const saveNode = LiteGraph.createNode("SaveImage");
|
269 |
+
let hrSamplerNode = null;
|
270 |
+
let hrSteps = null;
|
271 |
+
|
272 |
+
const ceil64 = (v) => Math.ceil(v / 64) * 64;
|
273 |
+
|
274 |
+
function getWidget(node, name) {
|
275 |
+
return node.widgets.find((w) => w.name === name);
|
276 |
+
}
|
277 |
+
|
278 |
+
function setWidgetValue(node, name, value, isOptionPrefix) {
|
279 |
+
const w = getWidget(node, name);
|
280 |
+
if (isOptionPrefix) {
|
281 |
+
const o = w.options.values.find((w) => w.startsWith(value));
|
282 |
+
if (o) {
|
283 |
+
w.value = o;
|
284 |
+
} else {
|
285 |
+
console.warn(`Unknown value '${value}' for widget '${name}'`, node);
|
286 |
+
w.value = value;
|
287 |
+
}
|
288 |
+
} else {
|
289 |
+
w.value = value;
|
290 |
+
}
|
291 |
+
}
|
292 |
+
|
293 |
+
function createLoraNodes(clipNode, text, prevClip, prevModel) {
|
294 |
+
const loras = [];
|
295 |
+
text = text.replace(/<lora:([^:]+:[^>]+)>/g, function (m, c) {
|
296 |
+
const s = c.split(":");
|
297 |
+
const weight = parseFloat(s[1]);
|
298 |
+
if (isNaN(weight)) {
|
299 |
+
console.warn("Invalid LORA", m);
|
300 |
+
} else {
|
301 |
+
loras.push({ name: s[0], weight });
|
302 |
+
}
|
303 |
+
return "";
|
304 |
+
});
|
305 |
+
|
306 |
+
for (const l of loras) {
|
307 |
+
const loraNode = LiteGraph.createNode("LoraLoader");
|
308 |
+
graph.add(loraNode);
|
309 |
+
setWidgetValue(loraNode, "lora_name", l.name, true);
|
310 |
+
setWidgetValue(loraNode, "strength_model", l.weight);
|
311 |
+
setWidgetValue(loraNode, "strength_clip", l.weight);
|
312 |
+
prevModel.node.connect(prevModel.index, loraNode, 0);
|
313 |
+
prevClip.node.connect(prevClip.index, loraNode, 1);
|
314 |
+
prevModel = { node: loraNode, index: 0 };
|
315 |
+
prevClip = { node: loraNode, index: 1 };
|
316 |
+
}
|
317 |
+
|
318 |
+
prevClip.node.connect(1, clipNode, 0);
|
319 |
+
prevModel.node.connect(0, samplerNode, 0);
|
320 |
+
if (hrSamplerNode) {
|
321 |
+
prevModel.node.connect(0, hrSamplerNode, 0);
|
322 |
+
}
|
323 |
+
|
324 |
+
return { text, prevModel, prevClip };
|
325 |
+
}
|
326 |
+
|
327 |
+
function replaceEmbeddings(text) {
|
328 |
+
if(!embeddings.length) return text;
|
329 |
+
return text.replaceAll(
|
330 |
+
new RegExp(
|
331 |
+
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
|
332 |
+
"ig"
|
333 |
+
),
|
334 |
+
"embedding:$1"
|
335 |
+
);
|
336 |
+
}
|
337 |
+
|
338 |
+
function popOpt(name) {
|
339 |
+
const v = opts[name];
|
340 |
+
delete opts[name];
|
341 |
+
return v;
|
342 |
+
}
|
343 |
+
|
344 |
+
graph.clear();
|
345 |
+
graph.add(ckptNode);
|
346 |
+
graph.add(clipSkipNode);
|
347 |
+
graph.add(positiveNode);
|
348 |
+
graph.add(negativeNode);
|
349 |
+
graph.add(samplerNode);
|
350 |
+
graph.add(imageNode);
|
351 |
+
graph.add(vaeNode);
|
352 |
+
graph.add(vaeLoaderNode);
|
353 |
+
graph.add(saveNode);
|
354 |
+
|
355 |
+
ckptNode.connect(1, clipSkipNode, 0);
|
356 |
+
clipSkipNode.connect(0, positiveNode, 0);
|
357 |
+
clipSkipNode.connect(0, negativeNode, 0);
|
358 |
+
ckptNode.connect(0, samplerNode, 0);
|
359 |
+
positiveNode.connect(0, samplerNode, 1);
|
360 |
+
negativeNode.connect(0, samplerNode, 2);
|
361 |
+
imageNode.connect(0, samplerNode, 3);
|
362 |
+
vaeNode.connect(0, saveNode, 0);
|
363 |
+
samplerNode.connect(0, vaeNode, 0);
|
364 |
+
vaeLoaderNode.connect(0, vaeNode, 1);
|
365 |
+
|
366 |
+
const handlers = {
|
367 |
+
model(v) {
|
368 |
+
setWidgetValue(ckptNode, "ckpt_name", v, true);
|
369 |
+
},
|
370 |
+
"vae"(v) {
|
371 |
+
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
|
372 |
+
},
|
373 |
+
"cfg scale"(v) {
|
374 |
+
setWidgetValue(samplerNode, "cfg", +v);
|
375 |
+
},
|
376 |
+
"clip skip"(v) {
|
377 |
+
setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v);
|
378 |
+
},
|
379 |
+
sampler(v) {
|
380 |
+
let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_");
|
381 |
+
if (name.includes("karras")) {
|
382 |
+
name = name.replace("karras", "").replace(/_+$/, "");
|
383 |
+
setWidgetValue(samplerNode, "scheduler", "karras");
|
384 |
+
} else {
|
385 |
+
setWidgetValue(samplerNode, "scheduler", "normal");
|
386 |
+
}
|
387 |
+
const w = getWidget(samplerNode, "sampler_name");
|
388 |
+
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
|
389 |
+
if (o) {
|
390 |
+
setWidgetValue(samplerNode, "sampler_name", o);
|
391 |
+
}
|
392 |
+
},
|
393 |
+
size(v) {
|
394 |
+
const wxh = v.split("x");
|
395 |
+
const w = ceil64(+wxh[0]);
|
396 |
+
const h = ceil64(+wxh[1]);
|
397 |
+
const hrUp = popOpt("hires upscale");
|
398 |
+
const hrSz = popOpt("hires resize");
|
399 |
+
hrSteps = popOpt("hires steps");
|
400 |
+
let hrMethod = popOpt("hires upscaler");
|
401 |
+
|
402 |
+
setWidgetValue(imageNode, "width", w);
|
403 |
+
setWidgetValue(imageNode, "height", h);
|
404 |
+
|
405 |
+
if (hrUp || hrSz) {
|
406 |
+
let uw, uh;
|
407 |
+
if (hrUp) {
|
408 |
+
uw = w * hrUp;
|
409 |
+
uh = h * hrUp;
|
410 |
+
} else {
|
411 |
+
const s = hrSz.split("x");
|
412 |
+
uw = +s[0];
|
413 |
+
uh = +s[1];
|
414 |
+
}
|
415 |
+
|
416 |
+
let upscaleNode;
|
417 |
+
let latentNode;
|
418 |
+
|
419 |
+
if (hrMethod.startsWith("Latent")) {
|
420 |
+
latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale");
|
421 |
+
graph.add(upscaleNode);
|
422 |
+
samplerNode.connect(0, upscaleNode, 0);
|
423 |
+
|
424 |
+
switch (hrMethod) {
|
425 |
+
case "Latent (nearest-exact)":
|
426 |
+
hrMethod = "nearest-exact";
|
427 |
+
break;
|
428 |
+
}
|
429 |
+
setWidgetValue(upscaleNode, "upscale_method", hrMethod, true);
|
430 |
+
} else {
|
431 |
+
const decode = LiteGraph.createNode("VAEDecodeTiled");
|
432 |
+
graph.add(decode);
|
433 |
+
samplerNode.connect(0, decode, 0);
|
434 |
+
vaeLoaderNode.connect(0, decode, 1);
|
435 |
+
|
436 |
+
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
|
437 |
+
graph.add(upscaleLoaderNode);
|
438 |
+
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
|
439 |
+
|
440 |
+
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
|
441 |
+
graph.add(modelUpscaleNode);
|
442 |
+
decode.connect(0, modelUpscaleNode, 1);
|
443 |
+
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
|
444 |
+
|
445 |
+
upscaleNode = LiteGraph.createNode("ImageScale");
|
446 |
+
graph.add(upscaleNode);
|
447 |
+
modelUpscaleNode.connect(0, upscaleNode, 0);
|
448 |
+
|
449 |
+
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
|
450 |
+
graph.add(vaeEncodeNode);
|
451 |
+
upscaleNode.connect(0, vaeEncodeNode, 0);
|
452 |
+
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
|
453 |
+
}
|
454 |
+
|
455 |
+
setWidgetValue(upscaleNode, "width", ceil64(uw));
|
456 |
+
setWidgetValue(upscaleNode, "height", ceil64(uh));
|
457 |
+
|
458 |
+
hrSamplerNode = LiteGraph.createNode("KSampler");
|
459 |
+
graph.add(hrSamplerNode);
|
460 |
+
ckptNode.connect(0, hrSamplerNode, 0);
|
461 |
+
positiveNode.connect(0, hrSamplerNode, 1);
|
462 |
+
negativeNode.connect(0, hrSamplerNode, 2);
|
463 |
+
latentNode.connect(0, hrSamplerNode, 3);
|
464 |
+
hrSamplerNode.connect(0, vaeNode, 0);
|
465 |
+
}
|
466 |
+
},
|
467 |
+
steps(v) {
|
468 |
+
setWidgetValue(samplerNode, "steps", +v);
|
469 |
+
},
|
470 |
+
seed(v) {
|
471 |
+
setWidgetValue(samplerNode, "seed", +v);
|
472 |
+
},
|
473 |
+
};
|
474 |
+
|
475 |
+
for (const opt in opts) {
|
476 |
+
if (opt in handlers) {
|
477 |
+
handlers[opt](popOpt(opt));
|
478 |
+
}
|
479 |
+
}
|
480 |
+
|
481 |
+
if (hrSamplerNode) {
|
482 |
+
setWidgetValue(hrSamplerNode, "steps", hrSteps? +hrSteps : getWidget(samplerNode, "steps").value);
|
483 |
+
setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value);
|
484 |
+
setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value);
|
485 |
+
setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value);
|
486 |
+
setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1"));
|
487 |
+
}
|
488 |
+
|
489 |
+
let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 });
|
490 |
+
positive = n.text;
|
491 |
+
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
|
492 |
+
negative = n.text;
|
493 |
+
|
494 |
+
setWidgetValue(positiveNode, "text", replaceEmbeddings(positive));
|
495 |
+
setWidgetValue(negativeNode, "text", replaceEmbeddings(negative));
|
496 |
+
|
497 |
+
graph.arrange();
|
498 |
+
|
499 |
+
for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) {
|
500 |
+
delete opts[opt];
|
501 |
+
}
|
502 |
+
|
503 |
+
console.warn("Unhandled parameters:", opts);
|
504 |
+
}
|
505 |
+
}
|
506 |
+
}
|
ComfyUI/web/scripts/ui.js
ADDED
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { api } from "./api.js";
|
2 |
+
import { ComfyDialog as _ComfyDialog } from "./ui/dialog.js";
|
3 |
+
import { toggleSwitch } from "./ui/toggleSwitch.js";
|
4 |
+
import { ComfySettingsDialog } from "./ui/settings.js";
|
5 |
+
|
6 |
+
export const ComfyDialog = _ComfyDialog;
|
7 |
+
|
8 |
+
/**
|
9 |
+
* @template { string | (keyof HTMLElementTagNameMap) } K
|
10 |
+
* @typedef { K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement } ElementType
|
11 |
+
*/
|
12 |
+
|
13 |
+
/**
|
14 |
+
* @template { string | (keyof HTMLElementTagNameMap) } K
|
15 |
+
* @param { K } tag HTML Element Tag and optional classes e.g. div.class1.class2
|
16 |
+
* @param { string | Element | Element[] | ({
|
17 |
+
* parent?: Element,
|
18 |
+
* $?: (el: ElementType<K>) => void,
|
19 |
+
* dataset?: DOMStringMap,
|
20 |
+
* style?: Partial<CSSStyleDeclaration>,
|
21 |
+
* for?: string
|
22 |
+
* } & Omit<Partial<ElementType<K>>, "style">) | undefined } [propsOrChildren]
|
23 |
+
* @param { string | Element | Element[] | undefined } [children]
|
24 |
+
* @returns { ElementType<K> }
|
25 |
+
*/
|
26 |
+
export function $el(tag, propsOrChildren, children) {
|
27 |
+
const split = tag.split(".");
|
28 |
+
const element = document.createElement(split.shift());
|
29 |
+
if (split.length > 0) {
|
30 |
+
element.classList.add(...split);
|
31 |
+
}
|
32 |
+
|
33 |
+
if (propsOrChildren) {
|
34 |
+
if (typeof propsOrChildren === "string") {
|
35 |
+
propsOrChildren = { textContent: propsOrChildren };
|
36 |
+
} else if (propsOrChildren instanceof Element) {
|
37 |
+
propsOrChildren = [propsOrChildren];
|
38 |
+
}
|
39 |
+
if (Array.isArray(propsOrChildren)) {
|
40 |
+
element.append(...propsOrChildren);
|
41 |
+
} else {
|
42 |
+
const {parent, $: cb, dataset, style} = propsOrChildren;
|
43 |
+
delete propsOrChildren.parent;
|
44 |
+
delete propsOrChildren.$;
|
45 |
+
delete propsOrChildren.dataset;
|
46 |
+
delete propsOrChildren.style;
|
47 |
+
|
48 |
+
if (Object.hasOwn(propsOrChildren, "for")) {
|
49 |
+
element.setAttribute("for", propsOrChildren.for)
|
50 |
+
}
|
51 |
+
|
52 |
+
if (style) {
|
53 |
+
Object.assign(element.style, style);
|
54 |
+
}
|
55 |
+
|
56 |
+
if (dataset) {
|
57 |
+
Object.assign(element.dataset, dataset);
|
58 |
+
}
|
59 |
+
|
60 |
+
Object.assign(element, propsOrChildren);
|
61 |
+
if (children) {
|
62 |
+
element.append(...(children instanceof Array ? children.filter(Boolean) : [children]));
|
63 |
+
}
|
64 |
+
|
65 |
+
if (parent) {
|
66 |
+
parent.append(element);
|
67 |
+
}
|
68 |
+
|
69 |
+
if (cb) {
|
70 |
+
cb(element);
|
71 |
+
}
|
72 |
+
}
|
73 |
+
}
|
74 |
+
return element;
|
75 |
+
}
|
76 |
+
|
77 |
+
function dragElement(dragEl, settings) {
|
78 |
+
var posDiffX = 0,
|
79 |
+
posDiffY = 0,
|
80 |
+
posStartX = 0,
|
81 |
+
posStartY = 0,
|
82 |
+
newPosX = 0,
|
83 |
+
newPosY = 0;
|
84 |
+
if (dragEl.getElementsByClassName("drag-handle")[0]) {
|
85 |
+
// if present, the handle is where you move the DIV from:
|
86 |
+
dragEl.getElementsByClassName("drag-handle")[0].onmousedown = dragMouseDown;
|
87 |
+
} else {
|
88 |
+
// otherwise, move the DIV from anywhere inside the DIV:
|
89 |
+
dragEl.onmousedown = dragMouseDown;
|
90 |
+
}
|
91 |
+
|
92 |
+
// When the element resizes (e.g. view queue) ensure it is still in the windows bounds
|
93 |
+
const resizeObserver = new ResizeObserver(() => {
|
94 |
+
ensureInBounds();
|
95 |
+
}).observe(dragEl);
|
96 |
+
|
97 |
+
function ensureInBounds() {
|
98 |
+
try {
|
99 |
+
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft));
|
100 |
+
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop));
|
101 |
+
|
102 |
+
positionElement();
|
103 |
+
}
|
104 |
+
catch(exception){
|
105 |
+
// robust
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
function positionElement() {
|
110 |
+
if(dragEl.style.display === "none") return;
|
111 |
+
|
112 |
+
const halfWidth = document.body.clientWidth / 2;
|
113 |
+
const anchorRight = newPosX + dragEl.clientWidth / 2 > halfWidth;
|
114 |
+
|
115 |
+
// set the element's new position:
|
116 |
+
if (anchorRight) {
|
117 |
+
dragEl.style.left = "unset";
|
118 |
+
dragEl.style.right = document.body.clientWidth - newPosX - dragEl.clientWidth + "px";
|
119 |
+
} else {
|
120 |
+
dragEl.style.left = newPosX + "px";
|
121 |
+
dragEl.style.right = "unset";
|
122 |
+
}
|
123 |
+
|
124 |
+
dragEl.style.top = newPosY + "px";
|
125 |
+
dragEl.style.bottom = "unset";
|
126 |
+
|
127 |
+
if (savePos) {
|
128 |
+
localStorage.setItem(
|
129 |
+
"Comfy.MenuPosition",
|
130 |
+
JSON.stringify({
|
131 |
+
x: dragEl.offsetLeft,
|
132 |
+
y: dragEl.offsetTop,
|
133 |
+
})
|
134 |
+
);
|
135 |
+
}
|
136 |
+
}
|
137 |
+
|
138 |
+
function restorePos() {
|
139 |
+
let pos = localStorage.getItem("Comfy.MenuPosition");
|
140 |
+
if (pos) {
|
141 |
+
pos = JSON.parse(pos);
|
142 |
+
newPosX = pos.x;
|
143 |
+
newPosY = pos.y;
|
144 |
+
positionElement();
|
145 |
+
ensureInBounds();
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
let savePos = undefined;
|
150 |
+
settings.addSetting({
|
151 |
+
id: "Comfy.MenuPosition",
|
152 |
+
name: "Save menu position",
|
153 |
+
type: "boolean",
|
154 |
+
defaultValue: savePos,
|
155 |
+
onChange(value) {
|
156 |
+
if (savePos === undefined && value) {
|
157 |
+
restorePos();
|
158 |
+
}
|
159 |
+
savePos = value;
|
160 |
+
},
|
161 |
+
});
|
162 |
+
|
163 |
+
function dragMouseDown(e) {
|
164 |
+
e = e || window.event;
|
165 |
+
e.preventDefault();
|
166 |
+
// get the mouse cursor position at startup:
|
167 |
+
posStartX = e.clientX;
|
168 |
+
posStartY = e.clientY;
|
169 |
+
document.onmouseup = closeDragElement;
|
170 |
+
// call a function whenever the cursor moves:
|
171 |
+
document.onmousemove = elementDrag;
|
172 |
+
}
|
173 |
+
|
174 |
+
function elementDrag(e) {
|
175 |
+
e = e || window.event;
|
176 |
+
e.preventDefault();
|
177 |
+
|
178 |
+
dragEl.classList.add("comfy-menu-manual-pos");
|
179 |
+
|
180 |
+
// calculate the new cursor position:
|
181 |
+
posDiffX = e.clientX - posStartX;
|
182 |
+
posDiffY = e.clientY - posStartY;
|
183 |
+
posStartX = e.clientX;
|
184 |
+
posStartY = e.clientY;
|
185 |
+
|
186 |
+
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft + posDiffX));
|
187 |
+
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop + posDiffY));
|
188 |
+
|
189 |
+
positionElement();
|
190 |
+
}
|
191 |
+
|
192 |
+
window.addEventListener("resize", () => {
|
193 |
+
ensureInBounds();
|
194 |
+
});
|
195 |
+
|
196 |
+
function closeDragElement() {
|
197 |
+
// stop moving when mouse button is released:
|
198 |
+
document.onmouseup = null;
|
199 |
+
document.onmousemove = null;
|
200 |
+
}
|
201 |
+
|
202 |
+
return restorePos;
|
203 |
+
}
|
204 |
+
|
205 |
+
class ComfyList {
|
206 |
+
#type;
|
207 |
+
#text;
|
208 |
+
#reverse;
|
209 |
+
|
210 |
+
constructor(text, type, reverse) {
|
211 |
+
this.#text = text;
|
212 |
+
this.#type = type || text.toLowerCase();
|
213 |
+
this.#reverse = reverse || false;
|
214 |
+
this.element = $el("div.comfy-list");
|
215 |
+
this.element.style.display = "none";
|
216 |
+
}
|
217 |
+
|
218 |
+
get visible() {
|
219 |
+
return this.element.style.display !== "none";
|
220 |
+
}
|
221 |
+
|
222 |
+
async load() {
|
223 |
+
const items = await api.getItems(this.#type);
|
224 |
+
this.element.replaceChildren(
|
225 |
+
...Object.keys(items).flatMap((section) => [
|
226 |
+
$el("h4", {
|
227 |
+
textContent: section,
|
228 |
+
}),
|
229 |
+
$el("div.comfy-list-items", [
|
230 |
+
...(this.#reverse ? items[section].reverse() : items[section]).map((item) => {
|
231 |
+
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
|
232 |
+
const removeAction = item.remove || {
|
233 |
+
name: "Delete",
|
234 |
+
cb: () => api.deleteItem(this.#type, item.prompt[1]),
|
235 |
+
};
|
236 |
+
return $el("div", {textContent: item.prompt[0] + ": "}, [
|
237 |
+
$el("button", {
|
238 |
+
textContent: "Load",
|
239 |
+
onclick: async () => {
|
240 |
+
await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow, true, false);
|
241 |
+
if (item.outputs) {
|
242 |
+
app.nodeOutputs = item.outputs;
|
243 |
+
}
|
244 |
+
},
|
245 |
+
}),
|
246 |
+
$el("button", {
|
247 |
+
textContent: removeAction.name,
|
248 |
+
onclick: async () => {
|
249 |
+
await removeAction.cb();
|
250 |
+
await this.update();
|
251 |
+
},
|
252 |
+
}),
|
253 |
+
]);
|
254 |
+
}),
|
255 |
+
]),
|
256 |
+
]),
|
257 |
+
$el("div.comfy-list-actions", [
|
258 |
+
$el("button", {
|
259 |
+
textContent: "Clear " + this.#text,
|
260 |
+
onclick: async () => {
|
261 |
+
await api.clearItems(this.#type);
|
262 |
+
await this.load();
|
263 |
+
},
|
264 |
+
}),
|
265 |
+
$el("button", {textContent: "Refresh", onclick: () => this.load()}),
|
266 |
+
])
|
267 |
+
);
|
268 |
+
}
|
269 |
+
|
270 |
+
async update() {
|
271 |
+
if (this.visible) {
|
272 |
+
await this.load();
|
273 |
+
}
|
274 |
+
}
|
275 |
+
|
276 |
+
async show() {
|
277 |
+
this.element.style.display = "block";
|
278 |
+
this.button.textContent = "Close";
|
279 |
+
|
280 |
+
await this.load();
|
281 |
+
}
|
282 |
+
|
283 |
+
hide() {
|
284 |
+
this.element.style.display = "none";
|
285 |
+
this.button.textContent = "View " + this.#text;
|
286 |
+
}
|
287 |
+
|
288 |
+
toggle() {
|
289 |
+
if (this.visible) {
|
290 |
+
this.hide();
|
291 |
+
return false;
|
292 |
+
} else {
|
293 |
+
this.show();
|
294 |
+
return true;
|
295 |
+
}
|
296 |
+
}
|
297 |
+
}
|
298 |
+
|
299 |
+
export class ComfyUI {
|
300 |
+
constructor(app) {
|
301 |
+
this.app = app;
|
302 |
+
this.dialog = new ComfyDialog();
|
303 |
+
this.settings = new ComfySettingsDialog(app);
|
304 |
+
|
305 |
+
this.batchCount = 1;
|
306 |
+
this.lastQueueSize = 0;
|
307 |
+
this.queue = new ComfyList("Queue");
|
308 |
+
this.history = new ComfyList("History", "history", true);
|
309 |
+
|
310 |
+
api.addEventListener("status", () => {
|
311 |
+
this.queue.update();
|
312 |
+
this.history.update();
|
313 |
+
});
|
314 |
+
|
315 |
+
const confirmClear = this.settings.addSetting({
|
316 |
+
id: "Comfy.ConfirmClear",
|
317 |
+
name: "Require confirmation when clearing workflow",
|
318 |
+
type: "boolean",
|
319 |
+
defaultValue: true,
|
320 |
+
});
|
321 |
+
|
322 |
+
const promptFilename = this.settings.addSetting({
|
323 |
+
id: "Comfy.PromptFilename",
|
324 |
+
name: "Prompt for filename when saving workflow",
|
325 |
+
type: "boolean",
|
326 |
+
defaultValue: true,
|
327 |
+
});
|
328 |
+
|
329 |
+
/**
|
330 |
+
* file format for preview
|
331 |
+
*
|
332 |
+
* format;quality
|
333 |
+
*
|
334 |
+
* ex)
|
335 |
+
* webp;50 -> webp, quality 50
|
336 |
+
* jpeg;80 -> rgb, jpeg, quality 80
|
337 |
+
*
|
338 |
+
* @type {string}
|
339 |
+
*/
|
340 |
+
const previewImage = this.settings.addSetting({
|
341 |
+
id: "Comfy.PreviewFormat",
|
342 |
+
name: "When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.",
|
343 |
+
type: "text",
|
344 |
+
defaultValue: "",
|
345 |
+
});
|
346 |
+
|
347 |
+
this.settings.addSetting({
|
348 |
+
id: "Comfy.DisableSliders",
|
349 |
+
name: "Disable sliders.",
|
350 |
+
type: "boolean",
|
351 |
+
defaultValue: false,
|
352 |
+
});
|
353 |
+
|
354 |
+
this.settings.addSetting({
|
355 |
+
id: "Comfy.DisableFloatRounding",
|
356 |
+
name: "Disable rounding floats (requires page reload).",
|
357 |
+
type: "boolean",
|
358 |
+
defaultValue: false,
|
359 |
+
});
|
360 |
+
|
361 |
+
this.settings.addSetting({
|
362 |
+
id: "Comfy.FloatRoundingPrecision",
|
363 |
+
name: "Decimal places [0 = auto] (requires page reload).",
|
364 |
+
type: "slider",
|
365 |
+
attrs: {
|
366 |
+
min: 0,
|
367 |
+
max: 6,
|
368 |
+
step: 1,
|
369 |
+
},
|
370 |
+
defaultValue: 0,
|
371 |
+
});
|
372 |
+
|
373 |
+
const fileInput = $el("input", {
|
374 |
+
id: "comfy-file-input",
|
375 |
+
type: "file",
|
376 |
+
accept: ".json,image/png,.latent,.safetensors,image/webp,audio/flac",
|
377 |
+
style: {display: "none"},
|
378 |
+
parent: document.body,
|
379 |
+
onchange: () => {
|
380 |
+
app.handleFile(fileInput.files[0]);
|
381 |
+
},
|
382 |
+
});
|
383 |
+
|
384 |
+
this.loadFile = () => fileInput.click();
|
385 |
+
|
386 |
+
const autoQueueModeEl = toggleSwitch(
|
387 |
+
"autoQueueMode",
|
388 |
+
[
|
389 |
+
{ text: "instant", tooltip: "A new prompt will be queued as soon as the queue reaches 0" },
|
390 |
+
{ text: "change", tooltip: "A new prompt will be queued when the queue is at 0 and the graph is/has changed" },
|
391 |
+
],
|
392 |
+
{
|
393 |
+
onChange: (value) => {
|
394 |
+
this.autoQueueMode = value.item.value;
|
395 |
+
},
|
396 |
+
}
|
397 |
+
);
|
398 |
+
autoQueueModeEl.style.display = "none";
|
399 |
+
|
400 |
+
api.addEventListener("graphChanged", () => {
|
401 |
+
if (this.autoQueueMode === "change" && this.autoQueueEnabled === true) {
|
402 |
+
if (this.lastQueueSize === 0) {
|
403 |
+
this.graphHasChanged = false;
|
404 |
+
app.queuePrompt(0, this.batchCount);
|
405 |
+
} else {
|
406 |
+
this.graphHasChanged = true;
|
407 |
+
}
|
408 |
+
}
|
409 |
+
});
|
410 |
+
|
411 |
+
this.menuHamburger = $el(
|
412 |
+
"div.comfy-menu-hamburger",
|
413 |
+
{
|
414 |
+
parent: document.body,
|
415 |
+
onclick: () => {
|
416 |
+
this.menuContainer.style.display = "block";
|
417 |
+
this.menuHamburger.style.display = "none";
|
418 |
+
},
|
419 |
+
},
|
420 |
+
[$el("div"), $el("div"), $el("div")]
|
421 |
+
);
|
422 |
+
|
423 |
+
this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
|
424 |
+
$el("div.drag-handle.comfy-menu-header", {
|
425 |
+
style: {
|
426 |
+
overflow: "hidden",
|
427 |
+
position: "relative",
|
428 |
+
width: "100%",
|
429 |
+
cursor: "default"
|
430 |
+
}
|
431 |
+
}, [
|
432 |
+
$el("span.drag-handle"),
|
433 |
+
$el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }),
|
434 |
+
$el("div.comfy-menu-actions", [
|
435 |
+
$el("button.comfy-settings-btn", {
|
436 |
+
textContent: "⚙️",
|
437 |
+
onclick: () => this.settings.show(),
|
438 |
+
}),
|
439 |
+
$el("button.comfy-close-menu-btn", {
|
440 |
+
textContent: "\u00d7",
|
441 |
+
onclick: () => {
|
442 |
+
this.menuContainer.style.display = "none";
|
443 |
+
this.menuHamburger.style.display = "flex";
|
444 |
+
},
|
445 |
+
}),
|
446 |
+
]),
|
447 |
+
]),
|
448 |
+
$el("button.comfy-queue-btn", {
|
449 |
+
id: "queue-button",
|
450 |
+
textContent: "Queue Prompt",
|
451 |
+
onclick: () => app.queuePrompt(0, this.batchCount),
|
452 |
+
}),
|
453 |
+
$el("div", {}, [
|
454 |
+
$el("label", {innerHTML: "Extra options"}, [
|
455 |
+
$el("input", {
|
456 |
+
type: "checkbox",
|
457 |
+
onchange: (i) => {
|
458 |
+
document.getElementById("extraOptions").style.display = i.srcElement.checked ? "block" : "none";
|
459 |
+
this.batchCount = i.srcElement.checked ? document.getElementById("batchCountInputRange").value : 1;
|
460 |
+
document.getElementById("autoQueueCheckbox").checked = false;
|
461 |
+
this.autoQueueEnabled = false;
|
462 |
+
},
|
463 |
+
}),
|
464 |
+
]),
|
465 |
+
]),
|
466 |
+
$el("div", {id: "extraOptions", style: {width: "100%", display: "none"}}, [
|
467 |
+
$el("div",[
|
468 |
+
|
469 |
+
$el("label", {innerHTML: "Batch count"}),
|
470 |
+
$el("input", {
|
471 |
+
id: "batchCountInputNumber",
|
472 |
+
type: "number",
|
473 |
+
value: this.batchCount,
|
474 |
+
min: "1",
|
475 |
+
style: {width: "35%", "margin-left": "0.4em"},
|
476 |
+
oninput: (i) => {
|
477 |
+
this.batchCount = i.target.value;
|
478 |
+
document.getElementById("batchCountInputRange").value = this.batchCount;
|
479 |
+
},
|
480 |
+
}),
|
481 |
+
$el("input", {
|
482 |
+
id: "batchCountInputRange",
|
483 |
+
type: "range",
|
484 |
+
min: "1",
|
485 |
+
max: "100",
|
486 |
+
value: this.batchCount,
|
487 |
+
oninput: (i) => {
|
488 |
+
this.batchCount = i.srcElement.value;
|
489 |
+
document.getElementById("batchCountInputNumber").value = i.srcElement.value;
|
490 |
+
},
|
491 |
+
}),
|
492 |
+
]),
|
493 |
+
$el("div",[
|
494 |
+
$el("label",{
|
495 |
+
for:"autoQueueCheckbox",
|
496 |
+
innerHTML: "Auto Queue"
|
497 |
+
}),
|
498 |
+
$el("input", {
|
499 |
+
id: "autoQueueCheckbox",
|
500 |
+
type: "checkbox",
|
501 |
+
checked: false,
|
502 |
+
title: "Automatically queue prompt when the queue size hits 0",
|
503 |
+
onchange: (e) => {
|
504 |
+
this.autoQueueEnabled = e.target.checked;
|
505 |
+
autoQueueModeEl.style.display = this.autoQueueEnabled ? "" : "none";
|
506 |
+
}
|
507 |
+
}),
|
508 |
+
autoQueueModeEl
|
509 |
+
])
|
510 |
+
]),
|
511 |
+
$el("div.comfy-menu-btns", [
|
512 |
+
$el("button", {
|
513 |
+
id: "queue-front-button",
|
514 |
+
textContent: "Queue Front",
|
515 |
+
onclick: () => app.queuePrompt(-1, this.batchCount)
|
516 |
+
}),
|
517 |
+
$el("button", {
|
518 |
+
$: (b) => (this.queue.button = b),
|
519 |
+
id: "comfy-view-queue-button",
|
520 |
+
textContent: "View Queue",
|
521 |
+
onclick: () => {
|
522 |
+
this.history.hide();
|
523 |
+
this.queue.toggle();
|
524 |
+
},
|
525 |
+
}),
|
526 |
+
$el("button", {
|
527 |
+
$: (b) => (this.history.button = b),
|
528 |
+
id: "comfy-view-history-button",
|
529 |
+
textContent: "View History",
|
530 |
+
onclick: () => {
|
531 |
+
this.queue.hide();
|
532 |
+
this.history.toggle();
|
533 |
+
},
|
534 |
+
}),
|
535 |
+
]),
|
536 |
+
this.queue.element,
|
537 |
+
this.history.element,
|
538 |
+
$el("button", {
|
539 |
+
id: "comfy-save-button",
|
540 |
+
textContent: "Save",
|
541 |
+
onclick: () => {
|
542 |
+
let filename = "workflow.json";
|
543 |
+
if (promptFilename.value) {
|
544 |
+
filename = prompt("Save workflow as:", filename);
|
545 |
+
if (!filename) return;
|
546 |
+
if (!filename.toLowerCase().endsWith(".json")) {
|
547 |
+
filename += ".json";
|
548 |
+
}
|
549 |
+
}
|
550 |
+
app.graphToPrompt().then(p=>{
|
551 |
+
const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string
|
552 |
+
const blob = new Blob([json], {type: "application/json"});
|
553 |
+
const url = URL.createObjectURL(blob);
|
554 |
+
const a = $el("a", {
|
555 |
+
href: url,
|
556 |
+
download: filename,
|
557 |
+
style: {display: "none"},
|
558 |
+
parent: document.body,
|
559 |
+
});
|
560 |
+
a.click();
|
561 |
+
setTimeout(function () {
|
562 |
+
a.remove();
|
563 |
+
window.URL.revokeObjectURL(url);
|
564 |
+
}, 0);
|
565 |
+
});
|
566 |
+
},
|
567 |
+
}),
|
568 |
+
$el("button", {
|
569 |
+
id: "comfy-dev-save-api-button",
|
570 |
+
textContent: "Save (API Format)",
|
571 |
+
style: {width: "100%", display: "none"},
|
572 |
+
onclick: () => {
|
573 |
+
let filename = "workflow_api.json";
|
574 |
+
if (promptFilename.value) {
|
575 |
+
filename = prompt("Save workflow (API) as:", filename);
|
576 |
+
if (!filename) return;
|
577 |
+
if (!filename.toLowerCase().endsWith(".json")) {
|
578 |
+
filename += ".json";
|
579 |
+
}
|
580 |
+
}
|
581 |
+
app.graphToPrompt().then(p=>{
|
582 |
+
const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
|
583 |
+
const blob = new Blob([json], {type: "application/json"});
|
584 |
+
const url = URL.createObjectURL(blob);
|
585 |
+
const a = $el("a", {
|
586 |
+
href: url,
|
587 |
+
download: filename,
|
588 |
+
style: {display: "none"},
|
589 |
+
parent: document.body,
|
590 |
+
});
|
591 |
+
a.click();
|
592 |
+
setTimeout(function () {
|
593 |
+
a.remove();
|
594 |
+
window.URL.revokeObjectURL(url);
|
595 |
+
}, 0);
|
596 |
+
});
|
597 |
+
},
|
598 |
+
}),
|
599 |
+
$el("button", {id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click()}),
|
600 |
+
$el("button", {
|
601 |
+
id: "comfy-refresh-button",
|
602 |
+
textContent: "Refresh",
|
603 |
+
onclick: () => app.refreshComboInNodes()
|
604 |
+
}),
|
605 |
+
$el("button", {id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace()}),
|
606 |
+
$el("button", {
|
607 |
+
id: "comfy-clear-button", textContent: "Clear", onclick: () => {
|
608 |
+
if (!confirmClear.value || confirm("Clear workflow?")) {
|
609 |
+
app.clean();
|
610 |
+
app.graph.clear();
|
611 |
+
app.resetView();
|
612 |
+
}
|
613 |
+
}
|
614 |
+
}),
|
615 |
+
$el("button", {
|
616 |
+
id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => {
|
617 |
+
if (!confirmClear.value || confirm("Load default workflow?")) {
|
618 |
+
app.resetView();
|
619 |
+
await app.loadGraphData()
|
620 |
+
}
|
621 |
+
}
|
622 |
+
}),
|
623 |
+
$el("button", {
|
624 |
+
id: "comfy-reset-view-button", textContent: "Reset View", onclick: async () => {
|
625 |
+
app.resetView();
|
626 |
+
}
|
627 |
+
}),
|
628 |
+
]);
|
629 |
+
|
630 |
+
const devMode = this.settings.addSetting({
|
631 |
+
id: "Comfy.DevMode",
|
632 |
+
name: "Enable Dev mode Options",
|
633 |
+
type: "boolean",
|
634 |
+
defaultValue: false,
|
635 |
+
onChange: function(value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none"},
|
636 |
+
});
|
637 |
+
|
638 |
+
this.restoreMenuPosition = dragElement(this.menuContainer, this.settings);
|
639 |
+
|
640 |
+
this.setStatus({exec_info: {queue_remaining: "X"}});
|
641 |
+
}
|
642 |
+
|
643 |
+
setStatus(status) {
|
644 |
+
this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
|
645 |
+
if (status) {
|
646 |
+
if (
|
647 |
+
this.lastQueueSize != 0 &&
|
648 |
+
status.exec_info.queue_remaining == 0 &&
|
649 |
+
this.autoQueueEnabled &&
|
650 |
+
(this.autoQueueMode === "instant" || this.graphHasChanged) &&
|
651 |
+
!app.lastExecutionError
|
652 |
+
) {
|
653 |
+
app.queuePrompt(0, this.batchCount);
|
654 |
+
status.exec_info.queue_remaining += this.batchCount;
|
655 |
+
this.graphHasChanged = false;
|
656 |
+
}
|
657 |
+
this.lastQueueSize = status.exec_info.queue_remaining;
|
658 |
+
}
|
659 |
+
}
|
660 |
+
}
|
ComfyUI/web/scripts/ui/components/asyncDialog.js
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComfyDialog } from "../dialog.js";
|
2 |
+
import { $el } from "../../ui.js";
|
3 |
+
|
4 |
+
export class ComfyAsyncDialog extends ComfyDialog {
|
5 |
+
#resolve;
|
6 |
+
|
7 |
+
constructor(actions) {
|
8 |
+
super(
|
9 |
+
"dialog.comfy-dialog.comfyui-dialog",
|
10 |
+
actions?.map((opt) => {
|
11 |
+
if (typeof opt === "string") {
|
12 |
+
opt = { text: opt };
|
13 |
+
}
|
14 |
+
return $el("button.comfyui-button", {
|
15 |
+
type: "button",
|
16 |
+
textContent: opt.text,
|
17 |
+
onclick: () => this.close(opt.value ?? opt.text),
|
18 |
+
});
|
19 |
+
})
|
20 |
+
);
|
21 |
+
}
|
22 |
+
|
23 |
+
show(html) {
|
24 |
+
this.element.addEventListener("close", () => {
|
25 |
+
this.close();
|
26 |
+
});
|
27 |
+
|
28 |
+
super.show(html);
|
29 |
+
|
30 |
+
return new Promise((resolve) => {
|
31 |
+
this.#resolve = resolve;
|
32 |
+
});
|
33 |
+
}
|
34 |
+
|
35 |
+
showModal(html) {
|
36 |
+
this.element.addEventListener("close", () => {
|
37 |
+
this.close();
|
38 |
+
});
|
39 |
+
|
40 |
+
super.show(html);
|
41 |
+
this.element.showModal();
|
42 |
+
|
43 |
+
return new Promise((resolve) => {
|
44 |
+
this.#resolve = resolve;
|
45 |
+
});
|
46 |
+
}
|
47 |
+
|
48 |
+
close(result = null) {
|
49 |
+
this.#resolve(result);
|
50 |
+
this.element.close();
|
51 |
+
super.close();
|
52 |
+
}
|
53 |
+
|
54 |
+
static async prompt({ title = null, message, actions }) {
|
55 |
+
const dialog = new ComfyAsyncDialog(actions);
|
56 |
+
const content = [$el("span", message)];
|
57 |
+
if (title) {
|
58 |
+
content.unshift($el("h3", title));
|
59 |
+
}
|
60 |
+
const res = await dialog.showModal(content);
|
61 |
+
dialog.element.remove();
|
62 |
+
return res;
|
63 |
+
}
|
64 |
+
}
|
ComfyUI/web/scripts/ui/components/button.js
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { $el } from "../../ui.js";
|
4 |
+
import { applyClasses, toggleElement } from "../utils.js";
|
5 |
+
import { prop } from "../../utils.js";
|
6 |
+
|
7 |
+
/**
|
8 |
+
* @typedef {{
|
9 |
+
* icon?: string;
|
10 |
+
* overIcon?: string;
|
11 |
+
* iconSize?: number;
|
12 |
+
* content?: string | HTMLElement;
|
13 |
+
* tooltip?: string;
|
14 |
+
* enabled?: boolean;
|
15 |
+
* action?: (e: Event, btn: ComfyButton) => void,
|
16 |
+
* classList?: import("../utils.js").ClassList,
|
17 |
+
* visibilitySetting?: { id: string, showValue: any },
|
18 |
+
* app?: import("../../app.js").ComfyApp
|
19 |
+
* }} ComfyButtonProps
|
20 |
+
*/
|
21 |
+
export class ComfyButton {
|
22 |
+
#over = 0;
|
23 |
+
#popupOpen = false;
|
24 |
+
isOver = false;
|
25 |
+
iconElement = $el("i.mdi");
|
26 |
+
contentElement = $el("span");
|
27 |
+
/**
|
28 |
+
* @type {import("./popup.js").ComfyPopup}
|
29 |
+
*/
|
30 |
+
popup;
|
31 |
+
|
32 |
+
/**
|
33 |
+
* @param {ComfyButtonProps} opts
|
34 |
+
*/
|
35 |
+
constructor({
|
36 |
+
icon,
|
37 |
+
overIcon,
|
38 |
+
iconSize,
|
39 |
+
content,
|
40 |
+
tooltip,
|
41 |
+
action,
|
42 |
+
classList = "comfyui-button",
|
43 |
+
visibilitySetting,
|
44 |
+
app,
|
45 |
+
enabled = true,
|
46 |
+
}) {
|
47 |
+
this.element = $el("button", {
|
48 |
+
onmouseenter: () => {
|
49 |
+
this.isOver = true;
|
50 |
+
if(this.overIcon) {
|
51 |
+
this.updateIcon();
|
52 |
+
}
|
53 |
+
},
|
54 |
+
onmouseleave: () => {
|
55 |
+
this.isOver = false;
|
56 |
+
if(this.overIcon) {
|
57 |
+
this.updateIcon();
|
58 |
+
}
|
59 |
+
}
|
60 |
+
|
61 |
+
}, [this.iconElement, this.contentElement]);
|
62 |
+
|
63 |
+
this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon }));
|
64 |
+
this.overIcon = prop(this, "overIcon", overIcon, () => {
|
65 |
+
if(this.isOver) {
|
66 |
+
this.updateIcon();
|
67 |
+
}
|
68 |
+
});
|
69 |
+
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
|
70 |
+
this.content = prop(
|
71 |
+
this,
|
72 |
+
"content",
|
73 |
+
content,
|
74 |
+
toggleElement(this.contentElement, {
|
75 |
+
onShow: (el, v) => {
|
76 |
+
if (typeof v === "string") {
|
77 |
+
el.textContent = v;
|
78 |
+
} else {
|
79 |
+
el.replaceChildren(v);
|
80 |
+
}
|
81 |
+
},
|
82 |
+
})
|
83 |
+
);
|
84 |
+
|
85 |
+
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
|
86 |
+
if (v) {
|
87 |
+
this.element.title = v;
|
88 |
+
} else {
|
89 |
+
this.element.removeAttribute("title");
|
90 |
+
}
|
91 |
+
});
|
92 |
+
this.classList = prop(this, "classList", classList, this.updateClasses);
|
93 |
+
this.hidden = prop(this, "hidden", false, this.updateClasses);
|
94 |
+
this.enabled = prop(this, "enabled", enabled, () => {
|
95 |
+
this.updateClasses();
|
96 |
+
this.element.disabled = !this.enabled;
|
97 |
+
});
|
98 |
+
this.action = prop(this, "action", action);
|
99 |
+
this.element.addEventListener("click", (e) => {
|
100 |
+
if (this.popup) {
|
101 |
+
// we are either a touch device or triggered by click not hover
|
102 |
+
if (!this.#over) {
|
103 |
+
this.popup.toggle();
|
104 |
+
}
|
105 |
+
}
|
106 |
+
this.action?.(e, this);
|
107 |
+
});
|
108 |
+
|
109 |
+
if (visibilitySetting?.id) {
|
110 |
+
const settingUpdated = () => {
|
111 |
+
this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue;
|
112 |
+
};
|
113 |
+
app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated);
|
114 |
+
settingUpdated();
|
115 |
+
}
|
116 |
+
}
|
117 |
+
|
118 |
+
updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
|
119 |
+
updateClasses = () => {
|
120 |
+
const internalClasses = [];
|
121 |
+
if (this.hidden) {
|
122 |
+
internalClasses.push("hidden");
|
123 |
+
}
|
124 |
+
if (!this.enabled) {
|
125 |
+
internalClasses.push("disabled");
|
126 |
+
}
|
127 |
+
if (this.popup) {
|
128 |
+
if (this.#popupOpen) {
|
129 |
+
internalClasses.push("popup-open");
|
130 |
+
} else {
|
131 |
+
internalClasses.push("popup-closed");
|
132 |
+
}
|
133 |
+
}
|
134 |
+
applyClasses(this.element, this.classList, ...internalClasses);
|
135 |
+
};
|
136 |
+
|
137 |
+
/**
|
138 |
+
*
|
139 |
+
* @param { import("./popup.js").ComfyPopup } popup
|
140 |
+
* @param { "click" | "hover" } mode
|
141 |
+
*/
|
142 |
+
withPopup(popup, mode = "click") {
|
143 |
+
this.popup = popup;
|
144 |
+
|
145 |
+
if (mode === "hover") {
|
146 |
+
for (const el of [this.element, this.popup.element]) {
|
147 |
+
el.addEventListener("mouseenter", () => {
|
148 |
+
this.popup.open = !!++this.#over;
|
149 |
+
});
|
150 |
+
el.addEventListener("mouseleave", () => {
|
151 |
+
this.popup.open = !!--this.#over;
|
152 |
+
});
|
153 |
+
}
|
154 |
+
}
|
155 |
+
|
156 |
+
popup.addEventListener("change", () => {
|
157 |
+
this.#popupOpen = popup.open;
|
158 |
+
this.updateClasses();
|
159 |
+
});
|
160 |
+
|
161 |
+
return this;
|
162 |
+
}
|
163 |
+
}
|
ComfyUI/web/scripts/ui/components/buttonGroup.js
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { $el } from "../../ui.js";
|
4 |
+
import { ComfyButton } from "./button.js";
|
5 |
+
import { prop } from "../../utils.js";
|
6 |
+
|
7 |
+
export class ComfyButtonGroup {
|
8 |
+
element = $el("div.comfyui-button-group");
|
9 |
+
|
10 |
+
/** @param {Array<ComfyButton | HTMLElement>} buttons */
|
11 |
+
constructor(...buttons) {
|
12 |
+
this.buttons = prop(this, "buttons", buttons, () => this.update());
|
13 |
+
}
|
14 |
+
|
15 |
+
/**
|
16 |
+
* @param {ComfyButton} button
|
17 |
+
* @param {number} index
|
18 |
+
*/
|
19 |
+
insert(button, index) {
|
20 |
+
this.buttons.splice(index, 0, button);
|
21 |
+
this.update();
|
22 |
+
}
|
23 |
+
|
24 |
+
/** @param {ComfyButton} button */
|
25 |
+
append(button) {
|
26 |
+
this.buttons.push(button);
|
27 |
+
this.update();
|
28 |
+
}
|
29 |
+
|
30 |
+
/** @param {ComfyButton|number} indexOrButton */
|
31 |
+
remove(indexOrButton) {
|
32 |
+
if (typeof indexOrButton !== "number") {
|
33 |
+
indexOrButton = this.buttons.indexOf(indexOrButton);
|
34 |
+
}
|
35 |
+
if (indexOrButton > -1) {
|
36 |
+
const r = this.buttons.splice(indexOrButton, 1);
|
37 |
+
this.update();
|
38 |
+
return r;
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
update() {
|
43 |
+
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
|
44 |
+
}
|
45 |
+
}
|
ComfyUI/web/scripts/ui/components/popup.js
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { prop } from "../../utils.js";
|
4 |
+
import { $el } from "../../ui.js";
|
5 |
+
import { applyClasses } from "../utils.js";
|
6 |
+
|
7 |
+
export class ComfyPopup extends EventTarget {
|
8 |
+
element = $el("div.comfyui-popup");
|
9 |
+
|
10 |
+
/**
|
11 |
+
* @param {{
|
12 |
+
* target: HTMLElement,
|
13 |
+
* container?: HTMLElement,
|
14 |
+
* classList?: import("../utils.js").ClassList,
|
15 |
+
* ignoreTarget?: boolean,
|
16 |
+
* closeOnEscape?: boolean,
|
17 |
+
* position?: "absolute" | "relative",
|
18 |
+
* horizontal?: "left" | "right"
|
19 |
+
* }} param0
|
20 |
+
* @param {...HTMLElement} children
|
21 |
+
*/
|
22 |
+
constructor(
|
23 |
+
{
|
24 |
+
target,
|
25 |
+
container = document.body,
|
26 |
+
classList = "",
|
27 |
+
ignoreTarget = true,
|
28 |
+
closeOnEscape = true,
|
29 |
+
position = "absolute",
|
30 |
+
horizontal = "left",
|
31 |
+
},
|
32 |
+
...children
|
33 |
+
) {
|
34 |
+
super();
|
35 |
+
this.target = target;
|
36 |
+
this.ignoreTarget = ignoreTarget;
|
37 |
+
this.container = container;
|
38 |
+
this.position = position;
|
39 |
+
this.closeOnEscape = closeOnEscape;
|
40 |
+
this.horizontal = horizontal;
|
41 |
+
|
42 |
+
container.append(this.element);
|
43 |
+
|
44 |
+
this.children = prop(this, "children", children, () => {
|
45 |
+
this.element.replaceChildren(...this.children);
|
46 |
+
this.update();
|
47 |
+
});
|
48 |
+
this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal));
|
49 |
+
this.open = prop(this, "open", false, (v, o) => {
|
50 |
+
if (v === o) return;
|
51 |
+
if (v) {
|
52 |
+
this.#show();
|
53 |
+
} else {
|
54 |
+
this.#hide();
|
55 |
+
}
|
56 |
+
});
|
57 |
+
}
|
58 |
+
|
59 |
+
toggle() {
|
60 |
+
this.open = !this.open;
|
61 |
+
}
|
62 |
+
|
63 |
+
#hide() {
|
64 |
+
this.element.classList.remove("open");
|
65 |
+
window.removeEventListener("resize", this.update);
|
66 |
+
window.removeEventListener("click", this.#clickHandler, { capture: true });
|
67 |
+
window.removeEventListener("keydown", this.#escHandler, { capture: true });
|
68 |
+
|
69 |
+
this.dispatchEvent(new CustomEvent("close"));
|
70 |
+
this.dispatchEvent(new CustomEvent("change"));
|
71 |
+
}
|
72 |
+
|
73 |
+
#show() {
|
74 |
+
this.element.classList.add("open");
|
75 |
+
this.update();
|
76 |
+
|
77 |
+
window.addEventListener("resize", this.update);
|
78 |
+
window.addEventListener("click", this.#clickHandler, { capture: true });
|
79 |
+
if (this.closeOnEscape) {
|
80 |
+
window.addEventListener("keydown", this.#escHandler, { capture: true });
|
81 |
+
}
|
82 |
+
|
83 |
+
this.dispatchEvent(new CustomEvent("open"));
|
84 |
+
this.dispatchEvent(new CustomEvent("change"));
|
85 |
+
}
|
86 |
+
|
87 |
+
#escHandler = (e) => {
|
88 |
+
if (e.key === "Escape") {
|
89 |
+
this.open = false;
|
90 |
+
e.preventDefault();
|
91 |
+
e.stopImmediatePropagation();
|
92 |
+
}
|
93 |
+
};
|
94 |
+
|
95 |
+
#clickHandler = (e) => {
|
96 |
+
/** @type {any} */
|
97 |
+
const target = e.target;
|
98 |
+
if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) {
|
99 |
+
this.open = false;
|
100 |
+
}
|
101 |
+
};
|
102 |
+
|
103 |
+
update = () => {
|
104 |
+
const rect = this.target.getBoundingClientRect();
|
105 |
+
this.element.style.setProperty("--bottom", "unset");
|
106 |
+
if (this.position === "absolute") {
|
107 |
+
if (this.horizontal === "left") {
|
108 |
+
this.element.style.setProperty("--left", rect.left + "px");
|
109 |
+
} else {
|
110 |
+
this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px");
|
111 |
+
}
|
112 |
+
this.element.style.setProperty("--top", rect.bottom + "px");
|
113 |
+
this.element.style.setProperty("--limit", rect.bottom + "px");
|
114 |
+
} else {
|
115 |
+
this.element.style.setProperty("--left", 0 + "px");
|
116 |
+
this.element.style.setProperty("--top", rect.height + "px");
|
117 |
+
this.element.style.setProperty("--limit", rect.height + "px");
|
118 |
+
}
|
119 |
+
|
120 |
+
const thisRect = this.element.getBoundingClientRect();
|
121 |
+
if (thisRect.height < 30) {
|
122 |
+
// Move up instead
|
123 |
+
this.element.style.setProperty("--top", "unset");
|
124 |
+
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
|
125 |
+
this.element.style.setProperty("--limit", rect.height + 5 + "px");
|
126 |
+
}
|
127 |
+
};
|
128 |
+
}
|
ComfyUI/web/scripts/ui/components/splitButton.js
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { $el } from "../../ui.js";
|
4 |
+
import { ComfyButton } from "./button.js";
|
5 |
+
import { prop } from "../../utils.js";
|
6 |
+
import { ComfyPopup } from "./popup.js";
|
7 |
+
|
8 |
+
export class ComfySplitButton {
|
9 |
+
/**
|
10 |
+
* @param {{
|
11 |
+
* primary: ComfyButton,
|
12 |
+
* mode?: "hover" | "click",
|
13 |
+
* horizontal?: "left" | "right",
|
14 |
+
* position?: "relative" | "absolute"
|
15 |
+
* }} param0
|
16 |
+
* @param {Array<ComfyButton> | Array<HTMLElement>} items
|
17 |
+
*/
|
18 |
+
constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) {
|
19 |
+
this.arrow = new ComfyButton({
|
20 |
+
icon: "chevron-down",
|
21 |
+
});
|
22 |
+
this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [
|
23 |
+
$el("div.comfyui-split-primary", primary.element),
|
24 |
+
$el("div.comfyui-split-arrow", this.arrow.element),
|
25 |
+
]);
|
26 |
+
this.popup = new ComfyPopup({
|
27 |
+
target: this.element,
|
28 |
+
container: position === "relative" ? this.element : document.body,
|
29 |
+
classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
|
30 |
+
closeOnEscape: mode === "click",
|
31 |
+
position,
|
32 |
+
horizontal,
|
33 |
+
});
|
34 |
+
|
35 |
+
this.arrow.withPopup(this.popup, mode);
|
36 |
+
|
37 |
+
this.items = prop(this, "items", items, () => this.update());
|
38 |
+
}
|
39 |
+
|
40 |
+
update() {
|
41 |
+
this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b));
|
42 |
+
}
|
43 |
+
}
|
ComfyUI/web/scripts/ui/dialog.js
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { $el } from "../ui.js";
|
2 |
+
|
3 |
+
export class ComfyDialog extends EventTarget {
|
4 |
+
#buttons;
|
5 |
+
|
6 |
+
constructor(type = "div", buttons = null) {
|
7 |
+
super();
|
8 |
+
this.#buttons = buttons;
|
9 |
+
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
|
10 |
+
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
|
11 |
+
]);
|
12 |
+
}
|
13 |
+
|
14 |
+
createButtons() {
|
15 |
+
return (
|
16 |
+
this.#buttons ?? [
|
17 |
+
$el("button", {
|
18 |
+
type: "button",
|
19 |
+
textContent: "Close",
|
20 |
+
onclick: () => this.close(),
|
21 |
+
}),
|
22 |
+
]
|
23 |
+
);
|
24 |
+
}
|
25 |
+
|
26 |
+
close() {
|
27 |
+
this.element.style.display = "none";
|
28 |
+
}
|
29 |
+
|
30 |
+
show(html) {
|
31 |
+
if (typeof html === "string") {
|
32 |
+
this.textElement.innerHTML = html;
|
33 |
+
} else {
|
34 |
+
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
|
35 |
+
}
|
36 |
+
this.element.style.display = "flex";
|
37 |
+
}
|
38 |
+
}
|
ComfyUI/web/scripts/ui/draggableList.js
ADDED
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
/*
|
3 |
+
Original implementation:
|
4 |
+
https://github.com/TahaSh/drag-to-reorder
|
5 |
+
MIT License
|
6 |
+
|
7 |
+
Copyright (c) 2023 Taha Shashtari
|
8 |
+
|
9 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
10 |
+
of this software and associated documentation files (the "Software"), to deal
|
11 |
+
in the Software without restriction, including without limitation the rights
|
12 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13 |
+
copies of the Software, and to permit persons to whom the Software is
|
14 |
+
furnished to do so, subject to the following conditions:
|
15 |
+
|
16 |
+
The above copyright notice and this permission notice shall be included in all
|
17 |
+
copies or substantial portions of the Software.
|
18 |
+
|
19 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
20 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
21 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
22 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
23 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
24 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
25 |
+
SOFTWARE.
|
26 |
+
*/
|
27 |
+
|
28 |
+
import { $el } from "../ui.js";
|
29 |
+
|
30 |
+
$el("style", {
|
31 |
+
parent: document.head,
|
32 |
+
textContent: `
|
33 |
+
.draggable-item {
|
34 |
+
position: relative;
|
35 |
+
will-change: transform;
|
36 |
+
user-select: none;
|
37 |
+
}
|
38 |
+
.draggable-item.is-idle {
|
39 |
+
transition: 0.25s ease transform;
|
40 |
+
}
|
41 |
+
.draggable-item.is-draggable {
|
42 |
+
z-index: 10;
|
43 |
+
}
|
44 |
+
`
|
45 |
+
});
|
46 |
+
|
47 |
+
export class DraggableList extends EventTarget {
|
48 |
+
listContainer;
|
49 |
+
draggableItem;
|
50 |
+
pointerStartX;
|
51 |
+
pointerStartY;
|
52 |
+
scrollYMax;
|
53 |
+
itemsGap = 0;
|
54 |
+
items = [];
|
55 |
+
itemSelector;
|
56 |
+
handleClass = "drag-handle";
|
57 |
+
off = [];
|
58 |
+
offDrag = [];
|
59 |
+
|
60 |
+
constructor(element, itemSelector) {
|
61 |
+
super();
|
62 |
+
this.listContainer = element;
|
63 |
+
this.itemSelector = itemSelector;
|
64 |
+
|
65 |
+
if (!this.listContainer) return;
|
66 |
+
|
67 |
+
this.off.push(this.on(this.listContainer, "mousedown", this.dragStart));
|
68 |
+
this.off.push(this.on(this.listContainer, "touchstart", this.dragStart));
|
69 |
+
this.off.push(this.on(document, "mouseup", this.dragEnd));
|
70 |
+
this.off.push(this.on(document, "touchend", this.dragEnd));
|
71 |
+
}
|
72 |
+
|
73 |
+
getAllItems() {
|
74 |
+
if (!this.items?.length) {
|
75 |
+
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
|
76 |
+
this.items.forEach((element) => {
|
77 |
+
element.classList.add("is-idle");
|
78 |
+
});
|
79 |
+
}
|
80 |
+
return this.items;
|
81 |
+
}
|
82 |
+
|
83 |
+
getIdleItems() {
|
84 |
+
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
|
85 |
+
}
|
86 |
+
|
87 |
+
isItemAbove(item) {
|
88 |
+
return item.hasAttribute("data-is-above");
|
89 |
+
}
|
90 |
+
|
91 |
+
isItemToggled(item) {
|
92 |
+
return item.hasAttribute("data-is-toggled");
|
93 |
+
}
|
94 |
+
|
95 |
+
on(source, event, listener, options) {
|
96 |
+
listener = listener.bind(this);
|
97 |
+
source.addEventListener(event, listener, options);
|
98 |
+
return () => source.removeEventListener(event, listener);
|
99 |
+
}
|
100 |
+
|
101 |
+
dragStart(e) {
|
102 |
+
if (e.target.classList.contains(this.handleClass)) {
|
103 |
+
this.draggableItem = e.target.closest(this.itemSelector);
|
104 |
+
}
|
105 |
+
|
106 |
+
if (!this.draggableItem) return;
|
107 |
+
|
108 |
+
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
109 |
+
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
110 |
+
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
111 |
+
|
112 |
+
this.setItemsGap();
|
113 |
+
this.initDraggableItem();
|
114 |
+
this.initItemsState();
|
115 |
+
|
116 |
+
this.offDrag.push(this.on(document, "mousemove", this.drag));
|
117 |
+
this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false }));
|
118 |
+
|
119 |
+
this.dispatchEvent(
|
120 |
+
new CustomEvent("dragstart", {
|
121 |
+
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
|
122 |
+
})
|
123 |
+
);
|
124 |
+
}
|
125 |
+
|
126 |
+
setItemsGap() {
|
127 |
+
if (this.getIdleItems().length <= 1) {
|
128 |
+
this.itemsGap = 0;
|
129 |
+
return;
|
130 |
+
}
|
131 |
+
|
132 |
+
const item1 = this.getIdleItems()[0];
|
133 |
+
const item2 = this.getIdleItems()[1];
|
134 |
+
|
135 |
+
const item1Rect = item1.getBoundingClientRect();
|
136 |
+
const item2Rect = item2.getBoundingClientRect();
|
137 |
+
|
138 |
+
this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top);
|
139 |
+
}
|
140 |
+
|
141 |
+
initItemsState() {
|
142 |
+
this.getIdleItems().forEach((item, i) => {
|
143 |
+
if (this.getAllItems().indexOf(this.draggableItem) > i) {
|
144 |
+
item.dataset.isAbove = "";
|
145 |
+
}
|
146 |
+
});
|
147 |
+
}
|
148 |
+
|
149 |
+
initDraggableItem() {
|
150 |
+
this.draggableItem.classList.remove("is-idle");
|
151 |
+
this.draggableItem.classList.add("is-draggable");
|
152 |
+
}
|
153 |
+
|
154 |
+
drag(e) {
|
155 |
+
if (!this.draggableItem) return;
|
156 |
+
|
157 |
+
e.preventDefault();
|
158 |
+
|
159 |
+
const clientX = e.clientX || e.touches[0].clientX;
|
160 |
+
const clientY = e.clientY || e.touches[0].clientY;
|
161 |
+
|
162 |
+
const listRect = this.listContainer.getBoundingClientRect();
|
163 |
+
|
164 |
+
if (clientY > listRect.bottom) {
|
165 |
+
if (this.listContainer.scrollTop < this.scrollYMax) {
|
166 |
+
this.listContainer.scrollBy(0, 10);
|
167 |
+
this.pointerStartY -= 10;
|
168 |
+
}
|
169 |
+
} else if (clientY < listRect.top && this.listContainer.scrollTop > 0) {
|
170 |
+
this.pointerStartY += 10;
|
171 |
+
this.listContainer.scrollBy(0, -10);
|
172 |
+
}
|
173 |
+
|
174 |
+
const pointerOffsetX = clientX - this.pointerStartX;
|
175 |
+
const pointerOffsetY = clientY - this.pointerStartY;
|
176 |
+
|
177 |
+
this.updateIdleItemsStateAndPosition();
|
178 |
+
this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`;
|
179 |
+
}
|
180 |
+
|
181 |
+
updateIdleItemsStateAndPosition() {
|
182 |
+
const draggableItemRect = this.draggableItem.getBoundingClientRect();
|
183 |
+
const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2;
|
184 |
+
|
185 |
+
// Update state
|
186 |
+
this.getIdleItems().forEach((item) => {
|
187 |
+
const itemRect = item.getBoundingClientRect();
|
188 |
+
const itemY = itemRect.top + itemRect.height / 2;
|
189 |
+
if (this.isItemAbove(item)) {
|
190 |
+
if (draggableItemY <= itemY) {
|
191 |
+
item.dataset.isToggled = "";
|
192 |
+
} else {
|
193 |
+
delete item.dataset.isToggled;
|
194 |
+
}
|
195 |
+
} else {
|
196 |
+
if (draggableItemY >= itemY) {
|
197 |
+
item.dataset.isToggled = "";
|
198 |
+
} else {
|
199 |
+
delete item.dataset.isToggled;
|
200 |
+
}
|
201 |
+
}
|
202 |
+
});
|
203 |
+
|
204 |
+
// Update position
|
205 |
+
this.getIdleItems().forEach((item) => {
|
206 |
+
if (this.isItemToggled(item)) {
|
207 |
+
const direction = this.isItemAbove(item) ? 1 : -1;
|
208 |
+
item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`;
|
209 |
+
} else {
|
210 |
+
item.style.transform = "";
|
211 |
+
}
|
212 |
+
});
|
213 |
+
}
|
214 |
+
|
215 |
+
dragEnd() {
|
216 |
+
if (!this.draggableItem) return;
|
217 |
+
|
218 |
+
this.applyNewItemsOrder();
|
219 |
+
this.cleanup();
|
220 |
+
}
|
221 |
+
|
222 |
+
applyNewItemsOrder() {
|
223 |
+
const reorderedItems = [];
|
224 |
+
|
225 |
+
let oldPosition = -1;
|
226 |
+
this.getAllItems().forEach((item, index) => {
|
227 |
+
if (item === this.draggableItem) {
|
228 |
+
oldPosition = index;
|
229 |
+
return;
|
230 |
+
}
|
231 |
+
if (!this.isItemToggled(item)) {
|
232 |
+
reorderedItems[index] = item;
|
233 |
+
return;
|
234 |
+
}
|
235 |
+
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1;
|
236 |
+
reorderedItems[newIndex] = item;
|
237 |
+
});
|
238 |
+
|
239 |
+
for (let index = 0; index < this.getAllItems().length; index++) {
|
240 |
+
const item = reorderedItems[index];
|
241 |
+
if (typeof item === "undefined") {
|
242 |
+
reorderedItems[index] = this.draggableItem;
|
243 |
+
}
|
244 |
+
}
|
245 |
+
|
246 |
+
reorderedItems.forEach((item) => {
|
247 |
+
this.listContainer.appendChild(item);
|
248 |
+
});
|
249 |
+
|
250 |
+
this.items = reorderedItems;
|
251 |
+
|
252 |
+
this.dispatchEvent(
|
253 |
+
new CustomEvent("dragend", {
|
254 |
+
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
|
255 |
+
})
|
256 |
+
);
|
257 |
+
}
|
258 |
+
|
259 |
+
cleanup() {
|
260 |
+
this.itemsGap = 0;
|
261 |
+
this.items = [];
|
262 |
+
this.unsetDraggableItem();
|
263 |
+
this.unsetItemState();
|
264 |
+
|
265 |
+
this.offDrag.forEach((f) => f());
|
266 |
+
this.offDrag = [];
|
267 |
+
}
|
268 |
+
|
269 |
+
unsetDraggableItem() {
|
270 |
+
this.draggableItem.style = null;
|
271 |
+
this.draggableItem.classList.remove("is-draggable");
|
272 |
+
this.draggableItem.classList.add("is-idle");
|
273 |
+
this.draggableItem = null;
|
274 |
+
}
|
275 |
+
|
276 |
+
unsetItemState() {
|
277 |
+
this.getIdleItems().forEach((item, i) => {
|
278 |
+
delete item.dataset.isAbove;
|
279 |
+
delete item.dataset.isToggled;
|
280 |
+
item.style.transform = "";
|
281 |
+
});
|
282 |
+
}
|
283 |
+
|
284 |
+
dispose() {
|
285 |
+
this.off.forEach((f) => f());
|
286 |
+
}
|
287 |
+
}
|
ComfyUI/web/scripts/ui/imagePreview.js
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { $el } from "../ui.js";
|
2 |
+
|
3 |
+
export function calculateImageGrid(imgs, dw, dh) {
|
4 |
+
let best = 0;
|
5 |
+
let w = imgs[0].naturalWidth;
|
6 |
+
let h = imgs[0].naturalHeight;
|
7 |
+
const numImages = imgs.length;
|
8 |
+
|
9 |
+
let cellWidth, cellHeight, cols, rows, shiftX;
|
10 |
+
// compact style
|
11 |
+
for (let c = 1; c <= numImages; c++) {
|
12 |
+
const r = Math.ceil(numImages / c);
|
13 |
+
const cW = dw / c;
|
14 |
+
const cH = dh / r;
|
15 |
+
const scaleX = cW / w;
|
16 |
+
const scaleY = cH / h;
|
17 |
+
|
18 |
+
const scale = Math.min(scaleX, scaleY, 1);
|
19 |
+
const imageW = w * scale;
|
20 |
+
const imageH = h * scale;
|
21 |
+
const area = imageW * imageH * numImages;
|
22 |
+
|
23 |
+
if (area > best) {
|
24 |
+
best = area;
|
25 |
+
cellWidth = imageW;
|
26 |
+
cellHeight = imageH;
|
27 |
+
cols = c;
|
28 |
+
rows = r;
|
29 |
+
shiftX = c * ((cW - imageW) / 2);
|
30 |
+
}
|
31 |
+
}
|
32 |
+
|
33 |
+
return { cellWidth, cellHeight, cols, rows, shiftX };
|
34 |
+
}
|
35 |
+
|
36 |
+
export function createImageHost(node) {
|
37 |
+
const el = $el("div.comfy-img-preview");
|
38 |
+
let currentImgs;
|
39 |
+
let first = true;
|
40 |
+
|
41 |
+
function updateSize() {
|
42 |
+
let w = null;
|
43 |
+
let h = null;
|
44 |
+
|
45 |
+
if (currentImgs) {
|
46 |
+
let elH = el.clientHeight;
|
47 |
+
if (first) {
|
48 |
+
first = false;
|
49 |
+
// On first run, if we are small then grow a bit
|
50 |
+
if (elH < 190) {
|
51 |
+
elH = 190;
|
52 |
+
}
|
53 |
+
el.style.setProperty("--comfy-widget-min-height", elH);
|
54 |
+
} else {
|
55 |
+
el.style.setProperty("--comfy-widget-min-height", null);
|
56 |
+
}
|
57 |
+
|
58 |
+
const nw = node.size[0];
|
59 |
+
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
|
60 |
+
w += "px";
|
61 |
+
h += "px";
|
62 |
+
|
63 |
+
el.style.setProperty("--comfy-img-preview-width", w);
|
64 |
+
el.style.setProperty("--comfy-img-preview-height", h);
|
65 |
+
}
|
66 |
+
}
|
67 |
+
return {
|
68 |
+
el,
|
69 |
+
updateImages(imgs) {
|
70 |
+
if (imgs !== currentImgs) {
|
71 |
+
if (currentImgs == null) {
|
72 |
+
requestAnimationFrame(() => {
|
73 |
+
updateSize();
|
74 |
+
});
|
75 |
+
}
|
76 |
+
el.replaceChildren(...imgs);
|
77 |
+
currentImgs = imgs;
|
78 |
+
node.onResize(node.size);
|
79 |
+
node.graph.setDirtyCanvas(true, true);
|
80 |
+
}
|
81 |
+
},
|
82 |
+
getHeight() {
|
83 |
+
updateSize();
|
84 |
+
},
|
85 |
+
onDraw() {
|
86 |
+
// Element from point uses a hittest find elements so we need to toggle pointer events
|
87 |
+
el.style.pointerEvents = "all";
|
88 |
+
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
|
89 |
+
el.style.pointerEvents = "none";
|
90 |
+
|
91 |
+
if(!over) return;
|
92 |
+
// Set the overIndex so Open Image etc work
|
93 |
+
const idx = currentImgs.indexOf(over);
|
94 |
+
node.overIndex = idx;
|
95 |
+
},
|
96 |
+
};
|
97 |
+
}
|
ComfyUI/web/scripts/ui/menu/index.js
ADDED
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { $el } from "../../ui.js";
|
4 |
+
import { downloadBlob } from "../../utils.js";
|
5 |
+
import { ComfyButton } from "../components/button.js";
|
6 |
+
import { ComfyButtonGroup } from "../components/buttonGroup.js";
|
7 |
+
import { ComfySplitButton } from "../components/splitButton.js";
|
8 |
+
import { ComfyViewHistoryButton } from "./viewHistory.js";
|
9 |
+
import { ComfyQueueButton } from "./queueButton.js";
|
10 |
+
import { ComfyWorkflowsMenu } from "./workflows.js";
|
11 |
+
import { ComfyViewQueueButton } from "./viewQueue.js";
|
12 |
+
import { getInteruptButton } from "./interruptButton.js";
|
13 |
+
|
14 |
+
const collapseOnMobile = (t) => {
|
15 |
+
(t.element ?? t).classList.add("comfyui-menu-mobile-collapse");
|
16 |
+
return t;
|
17 |
+
};
|
18 |
+
const showOnMobile = (t) => {
|
19 |
+
(t.element ?? t).classList.add("lt-lg-show");
|
20 |
+
return t;
|
21 |
+
};
|
22 |
+
|
23 |
+
export class ComfyAppMenu {
|
24 |
+
#sizeBreak = "lg";
|
25 |
+
#lastSizeBreaks = {
|
26 |
+
lg: null,
|
27 |
+
md: null,
|
28 |
+
sm: null,
|
29 |
+
xs: null,
|
30 |
+
};
|
31 |
+
#sizeBreaks = Object.keys(this.#lastSizeBreaks);
|
32 |
+
#cachedInnerSize = null;
|
33 |
+
#cacheTimeout = null;
|
34 |
+
|
35 |
+
/**
|
36 |
+
* @param { import("../../app.js").ComfyApp } app
|
37 |
+
*/
|
38 |
+
constructor(app) {
|
39 |
+
this.app = app;
|
40 |
+
|
41 |
+
this.workflows = new ComfyWorkflowsMenu(app);
|
42 |
+
const getSaveButton = (t) =>
|
43 |
+
new ComfyButton({
|
44 |
+
icon: "content-save",
|
45 |
+
tooltip: "Save the current workflow",
|
46 |
+
action: () => app.workflowManager.activeWorkflow.save(),
|
47 |
+
content: t,
|
48 |
+
});
|
49 |
+
|
50 |
+
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
|
51 |
+
this.saveButton = new ComfySplitButton(
|
52 |
+
{
|
53 |
+
primary: getSaveButton(),
|
54 |
+
mode: "hover",
|
55 |
+
position: "absolute",
|
56 |
+
},
|
57 |
+
getSaveButton("Save"),
|
58 |
+
new ComfyButton({
|
59 |
+
icon: "content-save-edit",
|
60 |
+
content: "Save As",
|
61 |
+
tooltip: "Save the current graph as a new workflow",
|
62 |
+
action: () => app.workflowManager.activeWorkflow.save(true),
|
63 |
+
}),
|
64 |
+
new ComfyButton({
|
65 |
+
icon: "download",
|
66 |
+
content: "Export",
|
67 |
+
tooltip: "Export the current workflow as JSON",
|
68 |
+
action: () => this.exportWorkflow("workflow", "workflow"),
|
69 |
+
}),
|
70 |
+
new ComfyButton({
|
71 |
+
icon: "api",
|
72 |
+
content: "Export (API Format)",
|
73 |
+
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
|
74 |
+
action: () => this.exportWorkflow("workflow_api", "output"),
|
75 |
+
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
|
76 |
+
app,
|
77 |
+
})
|
78 |
+
);
|
79 |
+
this.actionsGroup = new ComfyButtonGroup(
|
80 |
+
new ComfyButton({
|
81 |
+
icon: "refresh",
|
82 |
+
content: "Refresh",
|
83 |
+
tooltip: "Refresh widgets in nodes to find new models or files",
|
84 |
+
action: () => app.refreshComboInNodes(),
|
85 |
+
}),
|
86 |
+
new ComfyButton({
|
87 |
+
icon: "clipboard-edit-outline",
|
88 |
+
content: "Clipspace",
|
89 |
+
tooltip: "Open Clipspace window",
|
90 |
+
action: () => app["openClipspace"](),
|
91 |
+
}),
|
92 |
+
new ComfyButton({
|
93 |
+
icon: "fit-to-page-outline",
|
94 |
+
content: "Reset View",
|
95 |
+
tooltip: "Reset the canvas view",
|
96 |
+
action: () => app.resetView(),
|
97 |
+
}),
|
98 |
+
new ComfyButton({
|
99 |
+
icon: "cancel",
|
100 |
+
content: "Clear",
|
101 |
+
tooltip: "Clears current workflow",
|
102 |
+
action: () => {
|
103 |
+
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
|
104 |
+
app.clean();
|
105 |
+
app.graph.clear();
|
106 |
+
}
|
107 |
+
},
|
108 |
+
})
|
109 |
+
);
|
110 |
+
this.settingsGroup = new ComfyButtonGroup(
|
111 |
+
new ComfyButton({
|
112 |
+
icon: "cog",
|
113 |
+
content: "Settings",
|
114 |
+
tooltip: "Open settings",
|
115 |
+
action: () => {
|
116 |
+
app.ui.settings.show();
|
117 |
+
},
|
118 |
+
})
|
119 |
+
);
|
120 |
+
this.viewGroup = new ComfyButtonGroup(
|
121 |
+
new ComfyViewHistoryButton(app).element,
|
122 |
+
new ComfyViewQueueButton(app).element,
|
123 |
+
getInteruptButton("nlg-hide").element
|
124 |
+
);
|
125 |
+
this.mobileMenuButton = new ComfyButton({
|
126 |
+
icon: "menu",
|
127 |
+
action: (_, btn) => {
|
128 |
+
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
|
129 |
+
window.dispatchEvent(new Event("resize"));
|
130 |
+
},
|
131 |
+
classList: "comfyui-button comfyui-menu-button",
|
132 |
+
});
|
133 |
+
|
134 |
+
this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [
|
135 |
+
this.logo,
|
136 |
+
this.workflows.element,
|
137 |
+
this.saveButton.element,
|
138 |
+
collapseOnMobile(this.actionsGroup).element,
|
139 |
+
$el("section.comfyui-menu-push"),
|
140 |
+
collapseOnMobile(this.settingsGroup).element,
|
141 |
+
collapseOnMobile(this.viewGroup).element,
|
142 |
+
|
143 |
+
getInteruptButton("lt-lg-show").element,
|
144 |
+
new ComfyQueueButton(app).element,
|
145 |
+
showOnMobile(this.mobileMenuButton).element,
|
146 |
+
]);
|
147 |
+
|
148 |
+
let resizeHandler;
|
149 |
+
this.menuPositionSetting = app.ui.settings.addSetting({
|
150 |
+
id: "Comfy.UseNewMenu",
|
151 |
+
defaultValue: "Disabled",
|
152 |
+
name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.",
|
153 |
+
type: "combo",
|
154 |
+
options: ["Disabled", "Top", "Bottom"],
|
155 |
+
onChange: async (v) => {
|
156 |
+
if (v && v !== "Disabled") {
|
157 |
+
if (!resizeHandler) {
|
158 |
+
resizeHandler = () => {
|
159 |
+
this.calculateSizeBreak();
|
160 |
+
};
|
161 |
+
window.addEventListener("resize", resizeHandler);
|
162 |
+
}
|
163 |
+
this.updatePosition(v);
|
164 |
+
} else {
|
165 |
+
if (resizeHandler) {
|
166 |
+
window.removeEventListener("resize", resizeHandler);
|
167 |
+
resizeHandler = null;
|
168 |
+
}
|
169 |
+
document.body.style.removeProperty("display");
|
170 |
+
app.ui.menuContainer.style.removeProperty("display");
|
171 |
+
this.element.style.display = "none";
|
172 |
+
app.ui.restoreMenuPosition();
|
173 |
+
}
|
174 |
+
window.dispatchEvent(new Event("resize"));
|
175 |
+
},
|
176 |
+
});
|
177 |
+
}
|
178 |
+
|
179 |
+
updatePosition(v) {
|
180 |
+
document.body.style.display = "grid";
|
181 |
+
this.app.ui.menuContainer.style.display = "none";
|
182 |
+
this.element.style.removeProperty("display");
|
183 |
+
this.position = v;
|
184 |
+
if (v === "Bottom") {
|
185 |
+
this.app.bodyBottom.append(this.element);
|
186 |
+
} else {
|
187 |
+
this.app.bodyTop.prepend(this.element);
|
188 |
+
}
|
189 |
+
this.calculateSizeBreak();
|
190 |
+
}
|
191 |
+
|
192 |
+
updateSizeBreak(idx, prevIdx, direction) {
|
193 |
+
const newSize = this.#sizeBreaks[idx];
|
194 |
+
if (newSize === this.#sizeBreak) return;
|
195 |
+
this.#cachedInnerSize = null;
|
196 |
+
clearTimeout(this.#cacheTimeout);
|
197 |
+
|
198 |
+
this.#sizeBreak = this.#sizeBreaks[idx];
|
199 |
+
for (let i = 0; i < this.#sizeBreaks.length; i++) {
|
200 |
+
const sz = this.#sizeBreaks[i];
|
201 |
+
if (sz === this.#sizeBreak) {
|
202 |
+
this.element.classList.add(sz);
|
203 |
+
} else {
|
204 |
+
this.element.classList.remove(sz);
|
205 |
+
}
|
206 |
+
if (i < idx) {
|
207 |
+
this.element.classList.add("lt-" + sz);
|
208 |
+
} else {
|
209 |
+
this.element.classList.remove("lt-" + sz);
|
210 |
+
}
|
211 |
+
}
|
212 |
+
|
213 |
+
if (idx) {
|
214 |
+
// We're on a small screen, force the menu at the top
|
215 |
+
if (this.position !== "Top") {
|
216 |
+
this.updatePosition("Top");
|
217 |
+
}
|
218 |
+
} else if (this.position != this.menuPositionSetting.value) {
|
219 |
+
// Restore user position
|
220 |
+
this.updatePosition(this.menuPositionSetting.value);
|
221 |
+
}
|
222 |
+
|
223 |
+
// Allow multiple updates, but prevent bouncing
|
224 |
+
if (!direction) {
|
225 |
+
direction = prevIdx - idx;
|
226 |
+
} else if (direction != prevIdx - idx) {
|
227 |
+
return;
|
228 |
+
}
|
229 |
+
this.calculateSizeBreak(direction);
|
230 |
+
}
|
231 |
+
|
232 |
+
calculateSizeBreak(direction = 0) {
|
233 |
+
let idx = this.#sizeBreaks.indexOf(this.#sizeBreak);
|
234 |
+
const currIdx = idx;
|
235 |
+
const innerSize = this.calculateInnerSize(idx);
|
236 |
+
if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) {
|
237 |
+
if (idx > 0) {
|
238 |
+
idx--;
|
239 |
+
}
|
240 |
+
} else if (innerSize > this.element.clientWidth) {
|
241 |
+
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
|
242 |
+
// We need to shrink
|
243 |
+
if (idx < this.#sizeBreaks.length - 1) {
|
244 |
+
idx++;
|
245 |
+
}
|
246 |
+
}
|
247 |
+
|
248 |
+
this.updateSizeBreak(idx, currIdx, direction);
|
249 |
+
}
|
250 |
+
|
251 |
+
calculateInnerSize(idx) {
|
252 |
+
// Cache the inner size to prevent too much calculation when resizing the window
|
253 |
+
clearTimeout(this.#cacheTimeout);
|
254 |
+
if (this.#cachedInnerSize) {
|
255 |
+
// Extend cache time
|
256 |
+
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
257 |
+
} else {
|
258 |
+
let innerSize = 0;
|
259 |
+
let count = 1;
|
260 |
+
for (const c of this.element.children) {
|
261 |
+
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
|
262 |
+
if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items
|
263 |
+
innerSize += c.clientWidth;
|
264 |
+
count++;
|
265 |
+
}
|
266 |
+
innerSize += 8 * count;
|
267 |
+
this.#cachedInnerSize = innerSize;
|
268 |
+
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
269 |
+
}
|
270 |
+
return this.#cachedInnerSize;
|
271 |
+
}
|
272 |
+
|
273 |
+
/**
|
274 |
+
* @param {string} defaultName
|
275 |
+
*/
|
276 |
+
getFilename(defaultName) {
|
277 |
+
if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) {
|
278 |
+
defaultName = prompt("Save workflow as:", defaultName);
|
279 |
+
if (!defaultName) return;
|
280 |
+
if (!defaultName.toLowerCase().endsWith(".json")) {
|
281 |
+
defaultName += ".json";
|
282 |
+
}
|
283 |
+
}
|
284 |
+
return defaultName;
|
285 |
+
}
|
286 |
+
|
287 |
+
/**
|
288 |
+
* @param {string} [filename]
|
289 |
+
* @param { "workflow" | "output" } [promptProperty]
|
290 |
+
*/
|
291 |
+
async exportWorkflow(filename, promptProperty) {
|
292 |
+
if (this.app.workflowManager.activeWorkflow?.path) {
|
293 |
+
filename = this.app.workflowManager.activeWorkflow.name;
|
294 |
+
}
|
295 |
+
const p = await this.app.graphToPrompt();
|
296 |
+
const json = JSON.stringify(p[promptProperty], null, 2);
|
297 |
+
const blob = new Blob([json], { type: "application/json" });
|
298 |
+
const file = this.getFilename(filename);
|
299 |
+
if (!file) return;
|
300 |
+
downloadBlob(file, blob);
|
301 |
+
}
|
302 |
+
}
|
ComfyUI/web/scripts/ui/menu/interruptButton.js
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { api } from "../../api.js";
|
4 |
+
import { ComfyButton } from "../components/button.js";
|
5 |
+
|
6 |
+
export function getInteruptButton(visibility) {
|
7 |
+
const btn = new ComfyButton({
|
8 |
+
icon: "close",
|
9 |
+
tooltip: "Cancel current generation",
|
10 |
+
enabled: false,
|
11 |
+
action: () => {
|
12 |
+
api.interrupt();
|
13 |
+
},
|
14 |
+
classList: ["comfyui-button", "comfyui-interrupt-button", visibility],
|
15 |
+
});
|
16 |
+
|
17 |
+
api.addEventListener("status", ({ detail }) => {
|
18 |
+
const sz = detail?.exec_info?.queue_remaining;
|
19 |
+
btn.enabled = sz > 0;
|
20 |
+
});
|
21 |
+
|
22 |
+
return btn;
|
23 |
+
}
|