6030 lines
		
	
	
		
			208 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
		
			Vendored
		
	
	
		
			Generated
		
	
	
			
		
		
	
	
			6030 lines
		
	
	
		
			208 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
		
			Vendored
		
	
	
		
			Generated
		
	
	
| var __defProp = Object.defineProperty;
 | |
| var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
 | |
| import { C as ComfyDialog, $ as $el, a as ComfyApp, b as app, L as LGraphCanvas, c as LiteGraph, d as LGraphNode, e as applyTextReplacements, f as ComfyWidgets, g as addValueControlWidgets, D as DraggableList, h as api, i as LGraphGroup, u as useToastStore } from "./index-Dfv2aLsq.js";
 | |
| class ClipspaceDialog extends ComfyDialog {
 | |
|   static {
 | |
|     __name(this, "ClipspaceDialog");
 | |
|   }
 | |
|   static items = [];
 | |
|   static instance = null;
 | |
|   static registerButton(name, contextPredicate, callback) {
 | |
|     const item = $el("button", {
 | |
|       type: "button",
 | |
|       textContent: name,
 | |
|       contextPredicate,
 | |
|       onclick: callback
 | |
|     });
 | |
|     ClipspaceDialog.items.push(item);
 | |
|   }
 | |
|   static invalidatePreview() {
 | |
|     if (ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) {
 | |
|       const img_preview = document.getElementById(
 | |
|         "clipspace_preview"
 | |
|       );
 | |
|       if (img_preview) {
 | |
|         img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src;
 | |
|         img_preview.style.maxHeight = "100%";
 | |
|         img_preview.style.maxWidth = "100%";
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   static invalidate() {
 | |
|     if (ClipspaceDialog.instance) {
 | |
|       const self = ClipspaceDialog.instance;
 | |
|       const children = $el("div.comfy-modal-content", [
 | |
|         self.createImgSettings(),
 | |
|         ...self.createButtons()
 | |
|       ]);
 | |
|       if (self.element) {
 | |
|         self.element.removeChild(self.element.firstChild);
 | |
|         self.element.appendChild(children);
 | |
|       } else {
 | |
|         self.element = $el("div.comfy-modal", { parent: document.body }, [
 | |
|           children
 | |
|         ]);
 | |
|       }
 | |
|       if (self.element.children[0].children.length <= 1) {
 | |
|         self.element.children[0].appendChild(
 | |
|           $el("p", {}, [
 | |
|             "Unable to find the features to edit content of a format stored in the current Clipspace."
 | |
|           ])
 | |
|         );
 | |
|       }
 | |
|       ClipspaceDialog.invalidatePreview();
 | |
|     }
 | |
|   }
 | |
|   constructor() {
 | |
|     super();
 | |
|   }
 | |
|   createButtons() {
 | |
|     const buttons = [];
 | |
|     for (let idx in ClipspaceDialog.items) {
 | |
|       const item = ClipspaceDialog.items[idx];
 | |
|       if (!item.contextPredicate || item.contextPredicate())
 | |
|         buttons.push(ClipspaceDialog.items[idx]);
 | |
|     }
 | |
|     buttons.push(
 | |
|       $el("button", {
 | |
|         type: "button",
 | |
|         textContent: "Close",
 | |
|         onclick: /* @__PURE__ */ __name(() => {
 | |
|           this.close();
 | |
|         }, "onclick")
 | |
|       })
 | |
|     );
 | |
|     return buttons;
 | |
|   }
 | |
|   createImgSettings() {
 | |
|     if (ComfyApp.clipspace.imgs) {
 | |
|       const combo_items = [];
 | |
|       const imgs = ComfyApp.clipspace.imgs;
 | |
|       for (let i = 0; i < imgs.length; i++) {
 | |
|         combo_items.push($el("option", { value: i }, [`${i}`]));
 | |
|       }
 | |
|       const combo1 = $el(
 | |
|         "select",
 | |
|         {
 | |
|           id: "clipspace_img_selector",
 | |
|           onchange: /* @__PURE__ */ __name((event) => {
 | |
|             ComfyApp.clipspace["selectedIndex"] = event.target.selectedIndex;
 | |
|             ClipspaceDialog.invalidatePreview();
 | |
|           }, "onchange")
 | |
|         },
 | |
|         combo_items
 | |
|       );
 | |
|       const row1 = $el("tr", {}, [
 | |
|         $el("td", {}, [$el("font", { color: "white" }, ["Select Image"])]),
 | |
|         $el("td", {}, [combo1])
 | |
|       ]);
 | |
|       const combo2 = $el(
 | |
|         "select",
 | |
|         {
 | |
|           id: "clipspace_img_paste_mode",
 | |
|           onchange: /* @__PURE__ */ __name((event) => {
 | |
|             ComfyApp.clipspace["img_paste_mode"] = event.target.value;
 | |
|           }, "onchange")
 | |
|         },
 | |
|         [
 | |
|           $el("option", { value: "selected" }, "selected"),
 | |
|           $el("option", { value: "all" }, "all")
 | |
|         ]
 | |
|       );
 | |
|       combo2.value = ComfyApp.clipspace["img_paste_mode"];
 | |
|       const row2 = $el("tr", {}, [
 | |
|         $el("td", {}, [$el("font", { color: "white" }, ["Paste Mode"])]),
 | |
|         $el("td", {}, [combo2])
 | |
|       ]);
 | |
|       const td = $el(
 | |
|         "td",
 | |
|         { align: "center", width: "100px", height: "100px", colSpan: "2" },
 | |
|         [$el("img", { id: "clipspace_preview", ondragstart: /* @__PURE__ */ __name(() => false, "ondragstart") }, [])]
 | |
|       );
 | |
|       const row3 = $el("tr", {}, [td]);
 | |
|       return $el("table", {}, [row1, row2, row3]);
 | |
|     } else {
 | |
|       return [];
 | |
|     }
 | |
|   }
 | |
|   createImgPreview() {
 | |
|     if (ComfyApp.clipspace.imgs) {
 | |
|       return $el("img", { id: "clipspace_preview", ondragstart: /* @__PURE__ */ __name(() => false, "ondragstart") });
 | |
|     } else return [];
 | |
|   }
 | |
|   show() {
 | |
|     const img_preview = document.getElementById("clipspace_preview");
 | |
|     ClipspaceDialog.invalidate();
 | |
|     this.element.style.display = "block";
 | |
|   }
 | |
| }
 | |
| app.registerExtension({
 | |
|   name: "Comfy.Clipspace",
 | |
|   init(app2) {
 | |
|     app2.openClipspace = function() {
 | |
|       if (!ClipspaceDialog.instance) {
 | |
|         ClipspaceDialog.instance = new ClipspaceDialog();
 | |
|         ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
 | |
|       }
 | |
|       if (ComfyApp.clipspace) {
 | |
|         ClipspaceDialog.instance.show();
 | |
|       } else app2.ui.dialog.show("Clipspace is Empty!");
 | |
|     };
 | |
|   }
 | |
| });
 | |
| window.comfyAPI = window.comfyAPI || {};
 | |
| window.comfyAPI.clipspace = window.comfyAPI.clipspace || {};
 | |
| window.comfyAPI.clipspace.ClipspaceDialog = ClipspaceDialog;
 | |
| const colorPalettes = {
 | |
|   dark: {
 | |
|     id: "dark",
 | |
|     name: "Dark (Default)",
 | |
|     colors: {
 | |
|       node_slot: {
 | |
|         CLIP: "#FFD500",
 | |
|         // bright yellow
 | |
|         CLIP_VISION: "#A8DADC",
 | |
|         // light blue-gray
 | |
|         CLIP_VISION_OUTPUT: "#ad7452",
 | |
|         // rusty brown-orange
 | |
|         CONDITIONING: "#FFA931",
 | |
|         // vibrant orange-yellow
 | |
|         CONTROL_NET: "#6EE7B7",
 | |
|         // soft mint green
 | |
|         IMAGE: "#64B5F6",
 | |
|         // bright sky blue
 | |
|         LATENT: "#FF9CF9",
 | |
|         // light pink-purple
 | |
|         MASK: "#81C784",
 | |
|         // muted green
 | |
|         MODEL: "#B39DDB",
 | |
|         // light lavender-purple
 | |
|         STYLE_MODEL: "#C2FFAE",
 | |
|         // light green-yellow
 | |
|         VAE: "#FF6E6E",
 | |
|         // bright red
 | |
|         NOISE: "#B0B0B0",
 | |
|         // gray
 | |
|         GUIDER: "#66FFFF",
 | |
|         // cyan
 | |
|         SAMPLER: "#ECB4B4",
 | |
|         // very soft red
 | |
|         SIGMAS: "#CDFFCD",
 | |
|         // soft lime green
 | |
|         TAESD: "#DCC274"
 | |
|         // cheesecake
 | |
|       },
 | |
|       litegraph_base: {
 | |
|         BACKGROUND_IMAGE: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
 | |
|         CLEAR_BACKGROUND_COLOR: "#222",
 | |
|         NODE_TITLE_COLOR: "#999",
 | |
|         NODE_SELECTED_TITLE_COLOR: "#FFF",
 | |
|         NODE_TEXT_SIZE: 14,
 | |
|         NODE_TEXT_COLOR: "#AAA",
 | |
|         NODE_SUBTEXT_SIZE: 12,
 | |
|         NODE_DEFAULT_COLOR: "#333",
 | |
|         NODE_DEFAULT_BGCOLOR: "#353535",
 | |
|         NODE_DEFAULT_BOXCOLOR: "#666",
 | |
|         NODE_DEFAULT_SHAPE: "box",
 | |
|         NODE_BOX_OUTLINE_COLOR: "#FFF",
 | |
|         DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
 | |
|         DEFAULT_GROUP_FONT: 24,
 | |
|         WIDGET_BGCOLOR: "#222",
 | |
|         WIDGET_OUTLINE_COLOR: "#666",
 | |
|         WIDGET_TEXT_COLOR: "#DDD",
 | |
|         WIDGET_SECONDARY_TEXT_COLOR: "#999",
 | |
|         LINK_COLOR: "#9A9",
 | |
|         EVENT_LINK_COLOR: "#A86",
 | |
|         CONNECTING_LINK_COLOR: "#AFA"
 | |
|       },
 | |
|       comfy_base: {
 | |
|         "fg-color": "#fff",
 | |
|         "bg-color": "#202020",
 | |
|         "comfy-menu-bg": "#353535",
 | |
|         "comfy-input-bg": "#222",
 | |
|         "input-text": "#ddd",
 | |
|         "descrip-text": "#999",
 | |
|         "drag-text": "#ccc",
 | |
|         "error-text": "#ff4444",
 | |
|         "border-color": "#4e4e4e",
 | |
|         "tr-even-bg-color": "#222",
 | |
|         "tr-odd-bg-color": "#353535",
 | |
|         "content-bg": "#4e4e4e",
 | |
|         "content-fg": "#fff",
 | |
|         "content-hover-bg": "#222",
 | |
|         "content-hover-fg": "#fff"
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   light: {
 | |
|     id: "light",
 | |
|     name: "Light",
 | |
|     colors: {
 | |
|       node_slot: {
 | |
|         CLIP: "#FFA726",
 | |
|         // orange
 | |
|         CLIP_VISION: "#5C6BC0",
 | |
|         // indigo
 | |
|         CLIP_VISION_OUTPUT: "#8D6E63",
 | |
|         // brown
 | |
|         CONDITIONING: "#EF5350",
 | |
|         // red
 | |
|         CONTROL_NET: "#66BB6A",
 | |
|         // green
 | |
|         IMAGE: "#42A5F5",
 | |
|         // blue
 | |
|         LATENT: "#AB47BC",
 | |
|         // purple
 | |
|         MASK: "#9CCC65",
 | |
|         // light green
 | |
|         MODEL: "#7E57C2",
 | |
|         // deep purple
 | |
|         STYLE_MODEL: "#D4E157",
 | |
|         // lime
 | |
|         VAE: "#FF7043"
 | |
|         // deep orange
 | |
|       },
 | |
|       litegraph_base: {
 | |
|         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==",
 | |
|         CLEAR_BACKGROUND_COLOR: "lightgray",
 | |
|         NODE_TITLE_COLOR: "#222",
 | |
|         NODE_SELECTED_TITLE_COLOR: "#000",
 | |
|         NODE_TEXT_SIZE: 14,
 | |
|         NODE_TEXT_COLOR: "#444",
 | |
|         NODE_SUBTEXT_SIZE: 12,
 | |
|         NODE_DEFAULT_COLOR: "#F7F7F7",
 | |
|         NODE_DEFAULT_BGCOLOR: "#F5F5F5",
 | |
|         NODE_DEFAULT_BOXCOLOR: "#CCC",
 | |
|         NODE_DEFAULT_SHAPE: "box",
 | |
|         NODE_BOX_OUTLINE_COLOR: "#000",
 | |
|         DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.1)",
 | |
|         DEFAULT_GROUP_FONT: 24,
 | |
|         WIDGET_BGCOLOR: "#D4D4D4",
 | |
|         WIDGET_OUTLINE_COLOR: "#999",
 | |
|         WIDGET_TEXT_COLOR: "#222",
 | |
|         WIDGET_SECONDARY_TEXT_COLOR: "#555",
 | |
|         LINK_COLOR: "#4CAF50",
 | |
|         EVENT_LINK_COLOR: "#FF9800",
 | |
|         CONNECTING_LINK_COLOR: "#2196F3"
 | |
|       },
 | |
|       comfy_base: {
 | |
|         "fg-color": "#222",
 | |
|         "bg-color": "#DDD",
 | |
|         "comfy-menu-bg": "#F5F5F5",
 | |
|         "comfy-input-bg": "#C9C9C9",
 | |
|         "input-text": "#222",
 | |
|         "descrip-text": "#444",
 | |
|         "drag-text": "#555",
 | |
|         "error-text": "#F44336",
 | |
|         "border-color": "#888",
 | |
|         "tr-even-bg-color": "#f9f9f9",
 | |
|         "tr-odd-bg-color": "#fff",
 | |
|         "content-bg": "#e0e0e0",
 | |
|         "content-fg": "#222",
 | |
|         "content-hover-bg": "#adadad",
 | |
|         "content-hover-fg": "#222"
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   solarized: {
 | |
|     id: "solarized",
 | |
|     name: "Solarized",
 | |
|     colors: {
 | |
|       node_slot: {
 | |
|         CLIP: "#2AB7CA",
 | |
|         // light blue
 | |
|         CLIP_VISION: "#6c71c4",
 | |
|         // blue violet
 | |
|         CLIP_VISION_OUTPUT: "#859900",
 | |
|         // olive green
 | |
|         CONDITIONING: "#d33682",
 | |
|         // magenta
 | |
|         CONTROL_NET: "#d1ffd7",
 | |
|         // light mint green
 | |
|         IMAGE: "#5940bb",
 | |
|         // deep blue violet
 | |
|         LATENT: "#268bd2",
 | |
|         // blue
 | |
|         MASK: "#CCC9E7",
 | |
|         // light purple-gray
 | |
|         MODEL: "#dc322f",
 | |
|         // red
 | |
|         STYLE_MODEL: "#1a998a",
 | |
|         // teal
 | |
|         UPSCALE_MODEL: "#054A29",
 | |
|         // dark green
 | |
|         VAE: "#facfad"
 | |
|         // light pink-orange
 | |
|       },
 | |
|       litegraph_base: {
 | |
|         NODE_TITLE_COLOR: "#fdf6e3",
 | |
|         // Base3
 | |
|         NODE_SELECTED_TITLE_COLOR: "#A9D400",
 | |
|         NODE_TEXT_SIZE: 14,
 | |
|         NODE_TEXT_COLOR: "#657b83",
 | |
|         // Base00
 | |
|         NODE_SUBTEXT_SIZE: 12,
 | |
|         NODE_DEFAULT_COLOR: "#094656",
 | |
|         NODE_DEFAULT_BGCOLOR: "#073642",
 | |
|         // Base02
 | |
|         NODE_DEFAULT_BOXCOLOR: "#839496",
 | |
|         // Base0
 | |
|         NODE_DEFAULT_SHAPE: "box",
 | |
|         NODE_BOX_OUTLINE_COLOR: "#fdf6e3",
 | |
|         // Base3
 | |
|         DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
 | |
|         DEFAULT_GROUP_FONT: 24,
 | |
|         WIDGET_BGCOLOR: "#002b36",
 | |
|         // Base03
 | |
|         WIDGET_OUTLINE_COLOR: "#839496",
 | |
|         // Base0
 | |
|         WIDGET_TEXT_COLOR: "#fdf6e3",
 | |
|         // Base3
 | |
|         WIDGET_SECONDARY_TEXT_COLOR: "#93a1a1",
 | |
|         // Base1
 | |
|         LINK_COLOR: "#2aa198",
 | |
|         // Solarized Cyan
 | |
|         EVENT_LINK_COLOR: "#268bd2",
 | |
|         // Solarized Blue
 | |
|         CONNECTING_LINK_COLOR: "#859900"
 | |
|         // Solarized Green
 | |
|       },
 | |
|       comfy_base: {
 | |
|         "fg-color": "#fdf6e3",
 | |
|         // Base3
 | |
|         "bg-color": "#002b36",
 | |
|         // Base03
 | |
|         "comfy-menu-bg": "#073642",
 | |
|         // Base02
 | |
|         "comfy-input-bg": "#002b36",
 | |
|         // Base03
 | |
|         "input-text": "#93a1a1",
 | |
|         // Base1
 | |
|         "descrip-text": "#586e75",
 | |
|         // Base01
 | |
|         "drag-text": "#839496",
 | |
|         // Base0
 | |
|         "error-text": "#dc322f",
 | |
|         // Solarized Red
 | |
|         "border-color": "#657b83",
 | |
|         // Base00
 | |
|         "tr-even-bg-color": "#002b36",
 | |
|         "tr-odd-bg-color": "#073642",
 | |
|         "content-bg": "#657b83",
 | |
|         "content-fg": "#fdf6e3",
 | |
|         "content-hover-bg": "#002b36",
 | |
|         "content-hover-fg": "#fdf6e3"
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   arc: {
 | |
|     id: "arc",
 | |
|     name: "Arc",
 | |
|     colors: {
 | |
|       node_slot: {
 | |
|         BOOLEAN: "",
 | |
|         CLIP: "#eacb8b",
 | |
|         CLIP_VISION: "#A8DADC",
 | |
|         CLIP_VISION_OUTPUT: "#ad7452",
 | |
|         CONDITIONING: "#cf876f",
 | |
|         CONTROL_NET: "#00d78d",
 | |
|         CONTROL_NET_WEIGHTS: "",
 | |
|         FLOAT: "",
 | |
|         GLIGEN: "",
 | |
|         IMAGE: "#80a1c0",
 | |
|         IMAGEUPLOAD: "",
 | |
|         INT: "",
 | |
|         LATENT: "#b38ead",
 | |
|         LATENT_KEYFRAME: "",
 | |
|         MASK: "#a3bd8d",
 | |
|         MODEL: "#8978a7",
 | |
|         SAMPLER: "",
 | |
|         SIGMAS: "",
 | |
|         STRING: "",
 | |
|         STYLE_MODEL: "#C2FFAE",
 | |
|         T2I_ADAPTER_WEIGHTS: "",
 | |
|         TAESD: "#DCC274",
 | |
|         TIMESTEP_KEYFRAME: "",
 | |
|         UPSCALE_MODEL: "",
 | |
|         VAE: "#be616b"
 | |
|       },
 | |
|       litegraph_base: {
 | |
|         BACKGROUND_IMAGE: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==",
 | |
|         CLEAR_BACKGROUND_COLOR: "#2b2f38",
 | |
|         NODE_TITLE_COLOR: "#b2b7bd",
 | |
|         NODE_SELECTED_TITLE_COLOR: "#FFF",
 | |
|         NODE_TEXT_SIZE: 14,
 | |
|         NODE_TEXT_COLOR: "#AAA",
 | |
|         NODE_SUBTEXT_SIZE: 12,
 | |
|         NODE_DEFAULT_COLOR: "#2b2f38",
 | |
|         NODE_DEFAULT_BGCOLOR: "#242730",
 | |
|         NODE_DEFAULT_BOXCOLOR: "#6e7581",
 | |
|         NODE_DEFAULT_SHAPE: "box",
 | |
|         NODE_BOX_OUTLINE_COLOR: "#FFF",
 | |
|         DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
 | |
|         DEFAULT_GROUP_FONT: 22,
 | |
|         WIDGET_BGCOLOR: "#2b2f38",
 | |
|         WIDGET_OUTLINE_COLOR: "#6e7581",
 | |
|         WIDGET_TEXT_COLOR: "#DDD",
 | |
|         WIDGET_SECONDARY_TEXT_COLOR: "#b2b7bd",
 | |
|         LINK_COLOR: "#9A9",
 | |
|         EVENT_LINK_COLOR: "#A86",
 | |
|         CONNECTING_LINK_COLOR: "#AFA"
 | |
|       },
 | |
|       comfy_base: {
 | |
|         "fg-color": "#fff",
 | |
|         "bg-color": "#2b2f38",
 | |
|         "comfy-menu-bg": "#242730",
 | |
|         "comfy-input-bg": "#2b2f38",
 | |
|         "input-text": "#ddd",
 | |
|         "descrip-text": "#b2b7bd",
 | |
|         "drag-text": "#ccc",
 | |
|         "error-text": "#ff4444",
 | |
|         "border-color": "#6e7581",
 | |
|         "tr-even-bg-color": "#2b2f38",
 | |
|         "tr-odd-bg-color": "#242730",
 | |
|         "content-bg": "#6e7581",
 | |
|         "content-fg": "#fff",
 | |
|         "content-hover-bg": "#2b2f38",
 | |
|         "content-hover-fg": "#fff"
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   nord: {
 | |
|     id: "nord",
 | |
|     name: "Nord",
 | |
|     colors: {
 | |
|       node_slot: {
 | |
|         BOOLEAN: "",
 | |
|         CLIP: "#eacb8b",
 | |
|         CLIP_VISION: "#A8DADC",
 | |
|         CLIP_VISION_OUTPUT: "#ad7452",
 | |
|         CONDITIONING: "#cf876f",
 | |
|         CONTROL_NET: "#00d78d",
 | |
|         CONTROL_NET_WEIGHTS: "",
 | |
|         FLOAT: "",
 | |
|         GLIGEN: "",
 | |
|         IMAGE: "#80a1c0",
 | |
|         IMAGEUPLOAD: "",
 | |
|         INT: "",
 | |
|         LATENT: "#b38ead",
 | |
|         LATENT_KEYFRAME: "",
 | |
|         MASK: "#a3bd8d",
 | |
|         MODEL: "#8978a7",
 | |
|         SAMPLER: "",
 | |
|         SIGMAS: "",
 | |
|         STRING: "",
 | |
|         STYLE_MODEL: "#C2FFAE",
 | |
|         T2I_ADAPTER_WEIGHTS: "",
 | |
|         TAESD: "#DCC274",
 | |
|         TIMESTEP_KEYFRAME: "",
 | |
|         UPSCALE_MODEL: "",
 | |
|         VAE: "#be616b"
 | |
|       },
 | |
|       litegraph_base: {
 | |
|         BACKGROUND_IMAGE: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC",
 | |
|         CLEAR_BACKGROUND_COLOR: "#212732",
 | |
|         NODE_TITLE_COLOR: "#999",
 | |
|         NODE_SELECTED_TITLE_COLOR: "#e5eaf0",
 | |
|         NODE_TEXT_SIZE: 14,
 | |
|         NODE_TEXT_COLOR: "#bcc2c8",
 | |
|         NODE_SUBTEXT_SIZE: 12,
 | |
|         NODE_DEFAULT_COLOR: "#2e3440",
 | |
|         NODE_DEFAULT_BGCOLOR: "#161b22",
 | |
|         NODE_DEFAULT_BOXCOLOR: "#545d70",
 | |
|         NODE_DEFAULT_SHAPE: "box",
 | |
|         NODE_BOX_OUTLINE_COLOR: "#e5eaf0",
 | |
|         DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
 | |
|         DEFAULT_GROUP_FONT: 24,
 | |
|         WIDGET_BGCOLOR: "#2e3440",
 | |
|         WIDGET_OUTLINE_COLOR: "#545d70",
 | |
|         WIDGET_TEXT_COLOR: "#bcc2c8",
 | |
|         WIDGET_SECONDARY_TEXT_COLOR: "#999",
 | |
|         LINK_COLOR: "#9A9",
 | |
|         EVENT_LINK_COLOR: "#A86",
 | |
|         CONNECTING_LINK_COLOR: "#AFA"
 | |
|       },
 | |
|       comfy_base: {
 | |
|         "fg-color": "#e5eaf0",
 | |
|         "bg-color": "#2e3440",
 | |
|         "comfy-menu-bg": "#161b22",
 | |
|         "comfy-input-bg": "#2e3440",
 | |
|         "input-text": "#bcc2c8",
 | |
|         "descrip-text": "#999",
 | |
|         "drag-text": "#ccc",
 | |
|         "error-text": "#ff4444",
 | |
|         "border-color": "#545d70",
 | |
|         "tr-even-bg-color": "#2e3440",
 | |
|         "tr-odd-bg-color": "#161b22",
 | |
|         "content-bg": "#545d70",
 | |
|         "content-fg": "#e5eaf0",
 | |
|         "content-hover-bg": "#2e3440",
 | |
|         "content-hover-fg": "#e5eaf0"
 | |
|       }
 | |
|     }
 | |
|   },
 | |
|   github: {
 | |
|     id: "github",
 | |
|     name: "Github",
 | |
|     colors: {
 | |
|       node_slot: {
 | |
|         BOOLEAN: "",
 | |
|         CLIP: "#eacb8b",
 | |
|         CLIP_VISION: "#A8DADC",
 | |
|         CLIP_VISION_OUTPUT: "#ad7452",
 | |
|         CONDITIONING: "#cf876f",
 | |
|         CONTROL_NET: "#00d78d",
 | |
|         CONTROL_NET_WEIGHTS: "",
 | |
|         FLOAT: "",
 | |
|         GLIGEN: "",
 | |
|         IMAGE: "#80a1c0",
 | |
|         IMAGEUPLOAD: "",
 | |
|         INT: "",
 | |
|         LATENT: "#b38ead",
 | |
|         LATENT_KEYFRAME: "",
 | |
|         MASK: "#a3bd8d",
 | |
|         MODEL: "#8978a7",
 | |
|         SAMPLER: "",
 | |
|         SIGMAS: "",
 | |
|         STRING: "",
 | |
|         STYLE_MODEL: "#C2FFAE",
 | |
|         T2I_ADAPTER_WEIGHTS: "",
 | |
|         TAESD: "#DCC274",
 | |
|         TIMESTEP_KEYFRAME: "",
 | |
|         UPSCALE_MODEL: "",
 | |
|         VAE: "#be616b"
 | |
|       },
 | |
|       litegraph_base: {
 | |
|         BACKGROUND_IMAGE: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==",
 | |
|         CLEAR_BACKGROUND_COLOR: "#040506",
 | |
|         NODE_TITLE_COLOR: "#999",
 | |
|         NODE_SELECTED_TITLE_COLOR: "#e5eaf0",
 | |
|         NODE_TEXT_SIZE: 14,
 | |
|         NODE_TEXT_COLOR: "#bcc2c8",
 | |
|         NODE_SUBTEXT_SIZE: 12,
 | |
|         NODE_DEFAULT_COLOR: "#161b22",
 | |
|         NODE_DEFAULT_BGCOLOR: "#13171d",
 | |
|         NODE_DEFAULT_BOXCOLOR: "#30363d",
 | |
|         NODE_DEFAULT_SHAPE: "box",
 | |
|         NODE_BOX_OUTLINE_COLOR: "#e5eaf0",
 | |
|         DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)",
 | |
|         DEFAULT_GROUP_FONT: 24,
 | |
|         WIDGET_BGCOLOR: "#161b22",
 | |
|         WIDGET_OUTLINE_COLOR: "#30363d",
 | |
|         WIDGET_TEXT_COLOR: "#bcc2c8",
 | |
|         WIDGET_SECONDARY_TEXT_COLOR: "#999",
 | |
|         LINK_COLOR: "#9A9",
 | |
|         EVENT_LINK_COLOR: "#A86",
 | |
|         CONNECTING_LINK_COLOR: "#AFA"
 | |
|       },
 | |
|       comfy_base: {
 | |
|         "fg-color": "#e5eaf0",
 | |
|         "bg-color": "#161b22",
 | |
|         "comfy-menu-bg": "#13171d",
 | |
|         "comfy-input-bg": "#161b22",
 | |
|         "input-text": "#bcc2c8",
 | |
|         "descrip-text": "#999",
 | |
|         "drag-text": "#ccc",
 | |
|         "error-text": "#ff4444",
 | |
|         "border-color": "#30363d",
 | |
|         "tr-even-bg-color": "#161b22",
 | |
|         "tr-odd-bg-color": "#13171d",
 | |
|         "content-bg": "#30363d",
 | |
|         "content-fg": "#e5eaf0",
 | |
|         "content-hover-bg": "#161b22",
 | |
|         "content-hover-fg": "#e5eaf0"
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| };
 | |
| const id$4 = "Comfy.ColorPalette";
 | |
| const idCustomColorPalettes = "Comfy.CustomColorPalettes";
 | |
| const defaultColorPaletteId = "dark";
 | |
| const els = {
 | |
|   select: null
 | |
| };
 | |
| app.registerExtension({
 | |
|   name: id$4,
 | |
|   init() {
 | |
|     LGraphCanvas.prototype.updateBackground = function(image, clearBackgroundColor) {
 | |
|       this._bg_img = new Image();
 | |
|       this._bg_img.name = image;
 | |
|       this._bg_img.src = image;
 | |
|       this._bg_img.onload = () => {
 | |
|         this.draw(true, true);
 | |
|       };
 | |
|       this.background_image = image;
 | |
|       this.clear_background = true;
 | |
|       this.clear_background_color = clearBackgroundColor;
 | |
|       this._pattern = null;
 | |
|     };
 | |
|   },
 | |
|   addCustomNodeDefs(node_defs) {
 | |
|     const sortObjectKeys = /* @__PURE__ */ __name((unordered) => {
 | |
|       return Object.keys(unordered).sort().reduce((obj, key) => {
 | |
|         obj[key] = unordered[key];
 | |
|         return obj;
 | |
|       }, {});
 | |
|     }, "sortObjectKeys");
 | |
|     function getSlotTypes() {
 | |
|       var types = [];
 | |
|       const defs = node_defs;
 | |
|       for (const nodeId in defs) {
 | |
|         const nodeData = defs[nodeId];
 | |
|         var inputs = nodeData["input"]["required"];
 | |
|         if (nodeData["input"]["optional"] !== void 0) {
 | |
|           inputs = Object.assign(
 | |
|             {},
 | |
|             nodeData["input"]["required"],
 | |
|             nodeData["input"]["optional"]
 | |
|           );
 | |
|         }
 | |
|         for (const inputName in inputs) {
 | |
|           const inputData = inputs[inputName];
 | |
|           const type = inputData[0];
 | |
|           if (!Array.isArray(type)) {
 | |
|             types.push(type);
 | |
|           }
 | |
|         }
 | |
|         for (const o in nodeData["output"]) {
 | |
|           const output = nodeData["output"][o];
 | |
|           types.push(output);
 | |
|         }
 | |
|       }
 | |
|       return types;
 | |
|     }
 | |
|     __name(getSlotTypes, "getSlotTypes");
 | |
|     function completeColorPalette(colorPalette) {
 | |
|       var types = getSlotTypes();
 | |
|       for (const type of types) {
 | |
|         if (!colorPalette.colors.node_slot[type]) {
 | |
|           colorPalette.colors.node_slot[type] = "";
 | |
|         }
 | |
|       }
 | |
|       colorPalette.colors.node_slot = sortObjectKeys(
 | |
|         colorPalette.colors.node_slot
 | |
|       );
 | |
|       return colorPalette;
 | |
|     }
 | |
|     __name(completeColorPalette, "completeColorPalette");
 | |
|     const getColorPaletteTemplate = /* @__PURE__ */ __name(async () => {
 | |
|       let colorPalette = {
 | |
|         id: "my_color_palette_unique_id",
 | |
|         name: "My Color Palette",
 | |
|         colors: {
 | |
|           node_slot: {},
 | |
|           litegraph_base: {},
 | |
|           comfy_base: {}
 | |
|         }
 | |
|       };
 | |
|       const defaultColorPalette = colorPalettes[defaultColorPaletteId];
 | |
|       for (const key in defaultColorPalette.colors.litegraph_base) {
 | |
|         if (!colorPalette.colors.litegraph_base[key]) {
 | |
|           colorPalette.colors.litegraph_base[key] = "";
 | |
|         }
 | |
|       }
 | |
|       for (const key in defaultColorPalette.colors.comfy_base) {
 | |
|         if (!colorPalette.colors.comfy_base[key]) {
 | |
|           colorPalette.colors.comfy_base[key] = "";
 | |
|         }
 | |
|       }
 | |
|       return completeColorPalette(colorPalette);
 | |
|     }, "getColorPaletteTemplate");
 | |
|     const getCustomColorPalettes = /* @__PURE__ */ __name(() => {
 | |
|       return app.ui.settings.getSettingValue(idCustomColorPalettes, {});
 | |
|     }, "getCustomColorPalettes");
 | |
|     const setCustomColorPalettes = /* @__PURE__ */ __name((customColorPalettes) => {
 | |
|       return app.ui.settings.setSettingValue(
 | |
|         idCustomColorPalettes,
 | |
|         customColorPalettes
 | |
|       );
 | |
|     }, "setCustomColorPalettes");
 | |
|     const addCustomColorPalette = /* @__PURE__ */ __name(async (colorPalette) => {
 | |
|       if (typeof colorPalette !== "object") {
 | |
|         alert("Invalid color palette.");
 | |
|         return;
 | |
|       }
 | |
|       if (!colorPalette.id) {
 | |
|         alert("Color palette missing id.");
 | |
|         return;
 | |
|       }
 | |
|       if (!colorPalette.name) {
 | |
|         alert("Color palette missing name.");
 | |
|         return;
 | |
|       }
 | |
|       if (!colorPalette.colors) {
 | |
|         alert("Color palette missing colors.");
 | |
|         return;
 | |
|       }
 | |
|       if (colorPalette.colors.node_slot && typeof colorPalette.colors.node_slot !== "object") {
 | |
|         alert("Invalid color palette colors.node_slot.");
 | |
|         return;
 | |
|       }
 | |
|       const customColorPalettes = getCustomColorPalettes();
 | |
|       customColorPalettes[colorPalette.id] = colorPalette;
 | |
|       setCustomColorPalettes(customColorPalettes);
 | |
|       for (const option of els.select.childNodes) {
 | |
|         if (option.value === "custom_" + colorPalette.id) {
 | |
|           els.select.removeChild(option);
 | |
|         }
 | |
|       }
 | |
|       els.select.append(
 | |
|         $el("option", {
 | |
|           textContent: colorPalette.name + " (custom)",
 | |
|           value: "custom_" + colorPalette.id,
 | |
|           selected: true
 | |
|         })
 | |
|       );
 | |
|       setColorPalette("custom_" + colorPalette.id);
 | |
|       await loadColorPalette(colorPalette);
 | |
|     }, "addCustomColorPalette");
 | |
|     const deleteCustomColorPalette = /* @__PURE__ */ __name(async (colorPaletteId) => {
 | |
|       const customColorPalettes = getCustomColorPalettes();
 | |
|       delete customColorPalettes[colorPaletteId];
 | |
|       setCustomColorPalettes(customColorPalettes);
 | |
|       for (const opt of els.select.childNodes) {
 | |
|         const option = opt;
 | |
|         if (option.value === defaultColorPaletteId) {
 | |
|           option.selected = true;
 | |
|         }
 | |
|         if (option.value === "custom_" + colorPaletteId) {
 | |
|           els.select.removeChild(option);
 | |
|         }
 | |
|       }
 | |
|       setColorPalette(defaultColorPaletteId);
 | |
|       await loadColorPalette(getColorPalette());
 | |
|     }, "deleteCustomColorPalette");
 | |
|     const loadColorPalette = /* @__PURE__ */ __name(async (colorPalette) => {
 | |
|       colorPalette = await completeColorPalette(colorPalette);
 | |
|       if (colorPalette.colors) {
 | |
|         if (colorPalette.colors.node_slot) {
 | |
|           Object.assign(
 | |
|             // @ts-expect-error
 | |
|             app.canvas.default_connection_color_byType,
 | |
|             colorPalette.colors.node_slot
 | |
|           );
 | |
|           Object.assign(
 | |
|             LGraphCanvas.link_type_colors,
 | |
|             colorPalette.colors.node_slot
 | |
|           );
 | |
|         }
 | |
|         if (colorPalette.colors.litegraph_base) {
 | |
|           app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR;
 | |
|           app.canvas.default_link_color = colorPalette.colors.litegraph_base.LINK_COLOR;
 | |
|           for (const key in colorPalette.colors.litegraph_base) {
 | |
|             if (colorPalette.colors.litegraph_base.hasOwnProperty(key) && LiteGraph.hasOwnProperty(key)) {
 | |
|               LiteGraph[key] = colorPalette.colors.litegraph_base[key];
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         if (colorPalette.colors.comfy_base) {
 | |
|           const rootStyle = document.documentElement.style;
 | |
|           for (const key in colorPalette.colors.comfy_base) {
 | |
|             rootStyle.setProperty(
 | |
|               "--" + key,
 | |
|               colorPalette.colors.comfy_base[key]
 | |
|             );
 | |
|           }
 | |
|         }
 | |
|         app.canvas.draw(true, true);
 | |
|       }
 | |
|     }, "loadColorPalette");
 | |
|     const getColorPalette = /* @__PURE__ */ __name((colorPaletteId) => {
 | |
|       if (!colorPaletteId) {
 | |
|         colorPaletteId = app.ui.settings.getSettingValue(
 | |
|           id$4,
 | |
|           defaultColorPaletteId
 | |
|         );
 | |
|       }
 | |
|       if (colorPaletteId.startsWith("custom_")) {
 | |
|         colorPaletteId = colorPaletteId.substr(7);
 | |
|         let customColorPalettes = getCustomColorPalettes();
 | |
|         if (customColorPalettes[colorPaletteId]) {
 | |
|           return customColorPalettes[colorPaletteId];
 | |
|         }
 | |
|       }
 | |
|       return colorPalettes[colorPaletteId];
 | |
|     }, "getColorPalette");
 | |
|     const setColorPalette = /* @__PURE__ */ __name((colorPaletteId) => {
 | |
|       app.ui.settings.setSettingValue(id$4, colorPaletteId);
 | |
|     }, "setColorPalette");
 | |
|     const fileInput = $el("input", {
 | |
|       type: "file",
 | |
|       accept: ".json",
 | |
|       style: { display: "none" },
 | |
|       parent: document.body,
 | |
|       onchange: /* @__PURE__ */ __name(() => {
 | |
|         const file2 = fileInput.files[0];
 | |
|         if (file2.type === "application/json" || file2.name.endsWith(".json")) {
 | |
|           const reader = new FileReader();
 | |
|           reader.onload = async () => {
 | |
|             await addCustomColorPalette(JSON.parse(reader.result));
 | |
|           };
 | |
|           reader.readAsText(file2);
 | |
|         }
 | |
|       }, "onchange")
 | |
|     });
 | |
|     app.ui.settings.addSetting({
 | |
|       id: id$4,
 | |
|       category: ["Comfy", "ColorPalette"],
 | |
|       name: "Color Palette",
 | |
|       type: /* @__PURE__ */ __name((name, setter, value) => {
 | |
|         const options = [
 | |
|           ...Object.values(colorPalettes).map(
 | |
|             (c) => $el("option", {
 | |
|               textContent: c.name,
 | |
|               value: c.id,
 | |
|               selected: c.id === value
 | |
|             })
 | |
|           ),
 | |
|           ...Object.values(getCustomColorPalettes()).map(
 | |
|             (c) => $el("option", {
 | |
|               textContent: `${c.name} (custom)`,
 | |
|               value: `custom_${c.id}`,
 | |
|               selected: `custom_${c.id}` === value
 | |
|             })
 | |
|           )
 | |
|         ];
 | |
|         els.select = $el(
 | |
|           "select",
 | |
|           {
 | |
|             style: {
 | |
|               marginBottom: "0.15rem",
 | |
|               width: "100%"
 | |
|             },
 | |
|             onchange: /* @__PURE__ */ __name((e) => {
 | |
|               setter(e.target.value);
 | |
|             }, "onchange")
 | |
|           },
 | |
|           options
 | |
|         );
 | |
|         return $el("tr", [
 | |
|           $el("td", [
 | |
|             els.select,
 | |
|             $el(
 | |
|               "div",
 | |
|               {
 | |
|                 style: {
 | |
|                   display: "grid",
 | |
|                   gap: "4px",
 | |
|                   gridAutoFlow: "column"
 | |
|                 }
 | |
|               },
 | |
|               [
 | |
|                 $el("input", {
 | |
|                   type: "button",
 | |
|                   value: "Export",
 | |
|                   onclick: /* @__PURE__ */ __name(async () => {
 | |
|                     const colorPaletteId = app.ui.settings.getSettingValue(
 | |
|                       id$4,
 | |
|                       defaultColorPaletteId
 | |
|                     );
 | |
|                     const colorPalette = await completeColorPalette(
 | |
|                       getColorPalette(colorPaletteId)
 | |
|                     );
 | |
|                     const json = JSON.stringify(colorPalette, null, 2);
 | |
|                     const blob = new Blob([json], { type: "application/json" });
 | |
|                     const url = URL.createObjectURL(blob);
 | |
|                     const a = $el("a", {
 | |
|                       href: url,
 | |
|                       download: colorPaletteId + ".json",
 | |
|                       style: { display: "none" },
 | |
|                       parent: document.body
 | |
|                     });
 | |
|                     a.click();
 | |
|                     setTimeout(function() {
 | |
|                       a.remove();
 | |
|                       window.URL.revokeObjectURL(url);
 | |
|                     }, 0);
 | |
|                   }, "onclick")
 | |
|                 }),
 | |
|                 $el("input", {
 | |
|                   type: "button",
 | |
|                   value: "Import",
 | |
|                   onclick: /* @__PURE__ */ __name(() => {
 | |
|                     fileInput.click();
 | |
|                   }, "onclick")
 | |
|                 }),
 | |
|                 $el("input", {
 | |
|                   type: "button",
 | |
|                   value: "Template",
 | |
|                   onclick: /* @__PURE__ */ __name(async () => {
 | |
|                     const colorPalette = await getColorPaletteTemplate();
 | |
|                     const json = JSON.stringify(colorPalette, null, 2);
 | |
|                     const blob = new Blob([json], { type: "application/json" });
 | |
|                     const url = URL.createObjectURL(blob);
 | |
|                     const a = $el("a", {
 | |
|                       href: url,
 | |
|                       download: "color_palette.json",
 | |
|                       style: { display: "none" },
 | |
|                       parent: document.body
 | |
|                     });
 | |
|                     a.click();
 | |
|                     setTimeout(function() {
 | |
|                       a.remove();
 | |
|                       window.URL.revokeObjectURL(url);
 | |
|                     }, 0);
 | |
|                   }, "onclick")
 | |
|                 }),
 | |
|                 $el("input", {
 | |
|                   type: "button",
 | |
|                   value: "Delete",
 | |
|                   onclick: /* @__PURE__ */ __name(async () => {
 | |
|                     let colorPaletteId = app.ui.settings.getSettingValue(
 | |
|                       id$4,
 | |
|                       defaultColorPaletteId
 | |
|                     );
 | |
|                     if (colorPalettes[colorPaletteId]) {
 | |
|                       alert("You cannot delete a built-in color palette.");
 | |
|                       return;
 | |
|                     }
 | |
|                     if (colorPaletteId.startsWith("custom_")) {
 | |
|                       colorPaletteId = colorPaletteId.substr(7);
 | |
|                     }
 | |
|                     await deleteCustomColorPalette(colorPaletteId);
 | |
|                   }, "onclick")
 | |
|                 })
 | |
|               ]
 | |
|             )
 | |
|           ])
 | |
|         ]);
 | |
|       }, "type"),
 | |
|       defaultValue: defaultColorPaletteId,
 | |
|       async onChange(value) {
 | |
|         if (!value) {
 | |
|           return;
 | |
|         }
 | |
|         let palette = colorPalettes[value];
 | |
|         if (palette) {
 | |
|           await loadColorPalette(palette);
 | |
|         } else if (value.startsWith("custom_")) {
 | |
|           value = value.substr(7);
 | |
|           let customColorPalettes = getCustomColorPalettes();
 | |
|           if (customColorPalettes[value]) {
 | |
|             palette = customColorPalettes[value];
 | |
|             await loadColorPalette(customColorPalettes[value]);
 | |
|           }
 | |
|         }
 | |
|         let { BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR } = palette.colors.litegraph_base;
 | |
|         if (BACKGROUND_IMAGE === void 0 || CLEAR_BACKGROUND_COLOR === void 0) {
 | |
|           const base = colorPalettes["dark"].colors.litegraph_base;
 | |
|           BACKGROUND_IMAGE = base.BACKGROUND_IMAGE;
 | |
|           CLEAR_BACKGROUND_COLOR = base.CLEAR_BACKGROUND_COLOR;
 | |
|         }
 | |
|         app.canvas.updateBackground(BACKGROUND_IMAGE, CLEAR_BACKGROUND_COLOR);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| });
 | |
| const ext$2 = {
 | |
|   name: "Comfy.ContextMenuFilter",
 | |
|   init() {
 | |
|     const ctxMenu = LiteGraph.ContextMenu;
 | |
|     LiteGraph.ContextMenu = function(values, options) {
 | |
|       const ctx = new ctxMenu(values, options);
 | |
|       if (options?.className === "dark" && values?.length > 4) {
 | |
|         const filter = document.createElement("input");
 | |
|         filter.classList.add("comfy-context-menu-filter");
 | |
|         filter.placeholder = "Filter list";
 | |
|         ctx.root.prepend(filter);
 | |
|         const items = Array.from(
 | |
|           ctx.root.querySelectorAll(".litemenu-entry")
 | |
|         );
 | |
|         let displayedItems = [...items];
 | |
|         let itemCount = displayedItems.length;
 | |
|         requestAnimationFrame(() => {
 | |
|           const currentNode = LGraphCanvas.active_canvas.current_node;
 | |
|           const clickedComboValue = currentNode.widgets?.filter(
 | |
|             (w) => w.type === "combo" && w.options.values.length === values.length
 | |
|           ).find(
 | |
|             (w) => w.options.values.every((v, i) => v === values[i])
 | |
|           )?.value;
 | |
|           let selectedIndex = clickedComboValue ? values.findIndex((v) => v === clickedComboValue) : 0;
 | |
|           if (selectedIndex < 0) {
 | |
|             selectedIndex = 0;
 | |
|           }
 | |
|           let selectedItem = displayedItems[selectedIndex];
 | |
|           updateSelected();
 | |
|           function updateSelected() {
 | |
|             selectedItem?.style.setProperty("background-color", "");
 | |
|             selectedItem?.style.setProperty("color", "");
 | |
|             selectedItem = displayedItems[selectedIndex];
 | |
|             selectedItem?.style.setProperty(
 | |
|               "background-color",
 | |
|               "#ccc",
 | |
|               "important"
 | |
|             );
 | |
|             selectedItem?.style.setProperty("color", "#000", "important");
 | |
|           }
 | |
|           __name(updateSelected, "updateSelected");
 | |
|           const positionList = /* @__PURE__ */ __name(() => {
 | |
|             const rect = ctx.root.getBoundingClientRect();
 | |
|             if (rect.top < 0) {
 | |
|               const scale = 1 - ctx.root.getBoundingClientRect().height / ctx.root.clientHeight;
 | |
|               const shift = ctx.root.clientHeight * scale / 2;
 | |
|               ctx.root.style.top = -shift + "px";
 | |
|             }
 | |
|           }, "positionList");
 | |
|           filter.addEventListener("keydown", (event) => {
 | |
|             switch (event.key) {
 | |
|               case "ArrowUp":
 | |
|                 event.preventDefault();
 | |
|                 if (selectedIndex === 0) {
 | |
|                   selectedIndex = itemCount - 1;
 | |
|                 } else {
 | |
|                   selectedIndex--;
 | |
|                 }
 | |
|                 updateSelected();
 | |
|                 break;
 | |
|               case "ArrowRight":
 | |
|                 event.preventDefault();
 | |
|                 selectedIndex = itemCount - 1;
 | |
|                 updateSelected();
 | |
|                 break;
 | |
|               case "ArrowDown":
 | |
|                 event.preventDefault();
 | |
|                 if (selectedIndex === itemCount - 1) {
 | |
|                   selectedIndex = 0;
 | |
|                 } else {
 | |
|                   selectedIndex++;
 | |
|                 }
 | |
|                 updateSelected();
 | |
|                 break;
 | |
|               case "ArrowLeft":
 | |
|                 event.preventDefault();
 | |
|                 selectedIndex = 0;
 | |
|                 updateSelected();
 | |
|                 break;
 | |
|               case "Enter":
 | |
|                 selectedItem?.click();
 | |
|                 break;
 | |
|               case "Escape":
 | |
|                 ctx.close();
 | |
|                 break;
 | |
|             }
 | |
|           });
 | |
|           filter.addEventListener("input", () => {
 | |
|             const term = filter.value.toLocaleLowerCase();
 | |
|             displayedItems = items.filter((item) => {
 | |
|               const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term);
 | |
|               item.style.display = isVisible ? "block" : "none";
 | |
|               return isVisible;
 | |
|             });
 | |
|             selectedIndex = 0;
 | |
|             if (displayedItems.includes(selectedItem)) {
 | |
|               selectedIndex = displayedItems.findIndex(
 | |
|                 (d) => d === selectedItem
 | |
|               );
 | |
|             }
 | |
|             itemCount = displayedItems.length;
 | |
|             updateSelected();
 | |
|             if (options.event) {
 | |
|               let top = options.event.clientY - 10;
 | |
|               const bodyRect = document.body.getBoundingClientRect();
 | |
|               const rootRect = ctx.root.getBoundingClientRect();
 | |
|               if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) {
 | |
|                 top = Math.max(0, bodyRect.height - rootRect.height - 10);
 | |
|               }
 | |
|               ctx.root.style.top = top + "px";
 | |
|               positionList();
 | |
|             }
 | |
|           });
 | |
|           requestAnimationFrame(() => {
 | |
|             filter.focus();
 | |
|             positionList();
 | |
|           });
 | |
|         });
 | |
|       }
 | |
|       return ctx;
 | |
|     };
 | |
|     LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
 | |
|   }
 | |
| };
 | |
| app.registerExtension(ext$2);
 | |
| function stripComments(str) {
 | |
|   return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
 | |
| }
 | |
| __name(stripComments, "stripComments");
 | |
| app.registerExtension({
 | |
|   name: "Comfy.DynamicPrompts",
 | |
|   nodeCreated(node) {
 | |
|     if (node.widgets) {
 | |
|       const widgets = node.widgets.filter((n) => n.dynamicPrompts);
 | |
|       for (const widget of widgets) {
 | |
|         widget.serializeValue = (workflowNode, widgetIndex) => {
 | |
|           let prompt2 = stripComments(widget.value);
 | |
|           while (prompt2.replace("\\{", "").includes("{") && prompt2.replace("\\}", "").includes("}")) {
 | |
|             const startIndex = prompt2.replace("\\{", "00").indexOf("{");
 | |
|             const endIndex = prompt2.replace("\\}", "00").indexOf("}");
 | |
|             const optionsString = prompt2.substring(startIndex + 1, endIndex);
 | |
|             const options = optionsString.split("|");
 | |
|             const randomIndex = Math.floor(Math.random() * options.length);
 | |
|             const randomOption = options[randomIndex];
 | |
|             prompt2 = prompt2.substring(0, startIndex) + randomOption + prompt2.substring(endIndex + 1);
 | |
|           }
 | |
|           if (workflowNode?.widgets_values)
 | |
|             workflowNode.widgets_values[widgetIndex] = prompt2;
 | |
|           return prompt2;
 | |
|         };
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| });
 | |
| app.registerExtension({
 | |
|   name: "Comfy.EditAttention",
 | |
|   init() {
 | |
|     const editAttentionDelta = app.ui.settings.addSetting({
 | |
|       id: "Comfy.EditAttention.Delta",
 | |
|       name: "Ctrl+up/down precision",
 | |
|       type: "slider",
 | |
|       attrs: {
 | |
|         min: 0.01,
 | |
|         max: 0.5,
 | |
|         step: 0.01
 | |
|       },
 | |
|       defaultValue: 0.05
 | |
|     });
 | |
|     function incrementWeight(weight, delta) {
 | |
|       const floatWeight = parseFloat(weight);
 | |
|       if (isNaN(floatWeight)) return weight;
 | |
|       const newWeight = floatWeight + delta;
 | |
|       return String(Number(newWeight.toFixed(10)));
 | |
|     }
 | |
|     __name(incrementWeight, "incrementWeight");
 | |
|     function findNearestEnclosure(text, cursorPos) {
 | |
|       let start = cursorPos, end = cursorPos;
 | |
|       let openCount = 0, closeCount = 0;
 | |
|       while (start >= 0) {
 | |
|         start--;
 | |
|         if (text[start] === "(" && openCount === closeCount) break;
 | |
|         if (text[start] === "(") openCount++;
 | |
|         if (text[start] === ")") closeCount++;
 | |
|       }
 | |
|       if (start < 0) return false;
 | |
|       openCount = 0;
 | |
|       closeCount = 0;
 | |
|       while (end < text.length) {
 | |
|         if (text[end] === ")" && openCount === closeCount) break;
 | |
|         if (text[end] === "(") openCount++;
 | |
|         if (text[end] === ")") closeCount++;
 | |
|         end++;
 | |
|       }
 | |
|       if (end === text.length) return false;
 | |
|       return { start: start + 1, end };
 | |
|     }
 | |
|     __name(findNearestEnclosure, "findNearestEnclosure");
 | |
|     function addWeightToParentheses(text) {
 | |
|       const parenRegex = /^\((.*)\)$/;
 | |
|       const parenMatch = text.match(parenRegex);
 | |
|       const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
 | |
|       const floatMatch = text.match(floatRegex);
 | |
|       if (parenMatch && !floatMatch) {
 | |
|         return `(${parenMatch[1]}:1.0)`;
 | |
|       } else {
 | |
|         return text;
 | |
|       }
 | |
|     }
 | |
|     __name(addWeightToParentheses, "addWeightToParentheses");
 | |
|     function editAttention(event) {
 | |
|       const inputField = event.composedPath()[0];
 | |
|       const delta = parseFloat(editAttentionDelta.value);
 | |
|       if (inputField.tagName !== "TEXTAREA") return;
 | |
|       if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
 | |
|       if (!event.ctrlKey && !event.metaKey) return;
 | |
|       event.preventDefault();
 | |
|       let start = inputField.selectionStart;
 | |
|       let end = inputField.selectionEnd;
 | |
|       let selectedText = inputField.value.substring(start, end);
 | |
|       if (!selectedText) {
 | |
|         const nearestEnclosure = findNearestEnclosure(inputField.value, start);
 | |
|         if (nearestEnclosure) {
 | |
|           start = nearestEnclosure.start;
 | |
|           end = nearestEnclosure.end;
 | |
|           selectedText = inputField.value.substring(start, end);
 | |
|         } else {
 | |
|           const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n	";
 | |
|           while (!delimiters.includes(inputField.value[start - 1]) && start > 0) {
 | |
|             start--;
 | |
|           }
 | |
|           while (!delimiters.includes(inputField.value[end]) && end < inputField.value.length) {
 | |
|             end++;
 | |
|           }
 | |
|           selectedText = inputField.value.substring(start, end);
 | |
|           if (!selectedText) return;
 | |
|         }
 | |
|       }
 | |
|       if (selectedText[selectedText.length - 1] === " ") {
 | |
|         selectedText = selectedText.substring(0, selectedText.length - 1);
 | |
|         end -= 1;
 | |
|       }
 | |
|       if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") {
 | |
|         start -= 1;
 | |
|         end += 1;
 | |
|         selectedText = inputField.value.substring(start, end);
 | |
|       }
 | |
|       if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") {
 | |
|         selectedText = `(${selectedText})`;
 | |
|       }
 | |
|       selectedText = addWeightToParentheses(selectedText);
 | |
|       const weightDelta = event.key === "ArrowUp" ? delta : -delta;
 | |
|       const updatedText = selectedText.replace(
 | |
|         /\((.*):([+-]?\d+(?:\.\d+)?)\)/,
 | |
|         (match, text, weight) => {
 | |
|           weight = incrementWeight(weight, weightDelta);
 | |
|           if (weight == 1) {
 | |
|             return text;
 | |
|           } else {
 | |
|             return `(${text}:${weight})`;
 | |
|           }
 | |
|         }
 | |
|       );
 | |
|       inputField.setSelectionRange(start, end);
 | |
|       document.execCommand("insertText", false, updatedText);
 | |
|       inputField.setSelectionRange(start, start + updatedText.length);
 | |
|     }
 | |
|     __name(editAttention, "editAttention");
 | |
|     window.addEventListener("keydown", editAttention);
 | |
|   }
 | |
| });
 | |
| const CONVERTED_TYPE = "converted-widget";
 | |
| const VALID_TYPES = ["STRING", "combo", "number", "toggle", "BOOLEAN"];
 | |
| const CONFIG = Symbol();
 | |
| const GET_CONFIG = Symbol();
 | |
| const TARGET = Symbol();
 | |
| const replacePropertyName = "Run widget replace on values";
 | |
| class PrimitiveNode extends LGraphNode {
 | |
|   static {
 | |
|     __name(this, "PrimitiveNode");
 | |
|   }
 | |
|   controlValues;
 | |
|   lastType;
 | |
|   static category;
 | |
|   constructor(title) {
 | |
|     super(title);
 | |
|     this.addOutput("connect to widget input", "*");
 | |
|     this.serialize_widgets = true;
 | |
|     this.isVirtualNode = true;
 | |
|     if (!this.properties || !(replacePropertyName in this.properties)) {
 | |
|       this.addProperty(replacePropertyName, false, "boolean");
 | |
|     }
 | |
|   }
 | |
|   applyToGraph(extraLinks = []) {
 | |
|     if (!this.outputs[0].links?.length) return;
 | |
|     function get_links(node) {
 | |
|       let links2 = [];
 | |
|       for (const l of node.outputs[0].links) {
 | |
|         const linkInfo = app.graph.links[l];
 | |
|         const n = node.graph.getNodeById(linkInfo.target_id);
 | |
|         if (n.type == "Reroute") {
 | |
|           links2 = links2.concat(get_links(n));
 | |
|         } else {
 | |
|           links2.push(l);
 | |
|         }
 | |
|       }
 | |
|       return links2;
 | |
|     }
 | |
|     __name(get_links, "get_links");
 | |
|     let links = [
 | |
|       ...get_links(this).map((l) => app.graph.links[l]),
 | |
|       ...extraLinks
 | |
|     ];
 | |
|     let v = this.widgets?.[0].value;
 | |
|     if (v && this.properties[replacePropertyName]) {
 | |
|       v = applyTextReplacements(app, v);
 | |
|     }
 | |
|     for (const linkInfo of links) {
 | |
|       const node = this.graph.getNodeById(linkInfo.target_id);
 | |
|       const input = node.inputs[linkInfo.target_slot];
 | |
|       let widget;
 | |
|       if (input.widget[TARGET]) {
 | |
|         widget = input.widget[TARGET];
 | |
|       } else {
 | |
|         const widgetName = input.widget.name;
 | |
|         if (widgetName) {
 | |
|           widget = node.widgets.find((w) => w.name === widgetName);
 | |
|         }
 | |
|       }
 | |
|       if (widget) {
 | |
|         widget.value = v;
 | |
|         if (widget.callback) {
 | |
|           widget.callback(
 | |
|             widget.value,
 | |
|             app.canvas,
 | |
|             node,
 | |
|             app.canvas.graph_mouse,
 | |
|             {}
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   refreshComboInNode() {
 | |
|     const widget = this.widgets?.[0];
 | |
|     if (widget?.type === "combo") {
 | |
|       widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0];
 | |
|       if (!widget.options.values.includes(widget.value)) {
 | |
|         widget.value = widget.options.values[0];
 | |
|         widget.callback(widget.value);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   onAfterGraphConfigured() {
 | |
|     if (this.outputs[0].links?.length && !this.widgets?.length) {
 | |
|       if (!this.#onFirstConnection()) return;
 | |
|       if (this.widgets) {
 | |
|         for (let i = 0; i < this.widgets_values.length; i++) {
 | |
|           const w = this.widgets[i];
 | |
|           if (w) {
 | |
|             w.value = this.widgets_values[i];
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       this.#mergeWidgetConfig();
 | |
|     }
 | |
|   }
 | |
|   onConnectionsChange(_, index, connected) {
 | |
|     if (app.configuringGraph) {
 | |
|       return;
 | |
|     }
 | |
|     const links = this.outputs[0].links;
 | |
|     if (connected) {
 | |
|       if (links?.length && !this.widgets?.length) {
 | |
|         this.#onFirstConnection();
 | |
|       }
 | |
|     } else {
 | |
|       this.#mergeWidgetConfig();
 | |
|       if (!links?.length) {
 | |
|         this.onLastDisconnect();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   onConnectOutput(slot, type, input, target_node, target_slot) {
 | |
|     if (!input.widget) {
 | |
|       if (!(input.type in ComfyWidgets)) return false;
 | |
|     }
 | |
|     if (this.outputs[slot].links?.length) {
 | |
|       const valid = this.#isValidConnection(input);
 | |
|       if (valid) {
 | |
|         this.applyToGraph([{ target_id: target_node.id, target_slot }]);
 | |
|       }
 | |
|       return valid;
 | |
|     }
 | |
|   }
 | |
|   #onFirstConnection(recreating) {
 | |
|     if (!this.outputs[0].links) {
 | |
|       this.onLastDisconnect();
 | |
|       return;
 | |
|     }
 | |
|     const linkId = this.outputs[0].links[0];
 | |
|     const link = this.graph.links[linkId];
 | |
|     if (!link) return;
 | |
|     const theirNode = this.graph.getNodeById(link.target_id);
 | |
|     if (!theirNode || !theirNode.inputs) return;
 | |
|     const input = theirNode.inputs[link.target_slot];
 | |
|     if (!input) return;
 | |
|     let widget;
 | |
|     if (!input.widget) {
 | |
|       if (!(input.type in ComfyWidgets)) return;
 | |
|       widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] };
 | |
|     } else {
 | |
|       widget = input.widget;
 | |
|     }
 | |
|     const config = widget[GET_CONFIG]?.();
 | |
|     if (!config) return;
 | |
|     const { type } = getWidgetType(config);
 | |
|     this.outputs[0].type = type;
 | |
|     this.outputs[0].name = type;
 | |
|     this.outputs[0].widget = widget;
 | |
|     this.#createWidget(
 | |
|       widget[CONFIG] ?? config,
 | |
|       theirNode,
 | |
|       widget.name,
 | |
|       recreating,
 | |
|       widget[TARGET]
 | |
|     );
 | |
|   }
 | |
|   #createWidget(inputData, node, widgetName, recreating, targetWidget) {
 | |
|     let type = inputData[0];
 | |
|     if (type instanceof Array) {
 | |
|       type = "COMBO";
 | |
|     }
 | |
|     const size = this.size;
 | |
|     let widget;
 | |
|     if (type in ComfyWidgets) {
 | |
|       widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget;
 | |
|     } else {
 | |
|       widget = this.addWidget(type, "value", null, () => {
 | |
|       }, {});
 | |
|     }
 | |
|     if (targetWidget) {
 | |
|       widget.value = targetWidget.value;
 | |
|     } else if (node?.widgets && widget) {
 | |
|       const theirWidget = node.widgets.find((w) => w.name === widgetName);
 | |
|       if (theirWidget) {
 | |
|         widget.value = theirWidget.value;
 | |
|       }
 | |
|     }
 | |
|     if (!inputData?.[1]?.control_after_generate && (widget.type === "number" || widget.type === "combo")) {
 | |
|       let control_value = this.widgets_values?.[1];
 | |
|       if (!control_value) {
 | |
|         control_value = "fixed";
 | |
|       }
 | |
|       addValueControlWidgets(
 | |
|         this,
 | |
|         widget,
 | |
|         control_value,
 | |
|         void 0,
 | |
|         inputData
 | |
|       );
 | |
|       let filter = this.widgets_values?.[2];
 | |
|       if (filter && this.widgets.length === 3) {
 | |
|         this.widgets[2].value = filter;
 | |
|       }
 | |
|     }
 | |
|     const controlValues = this.controlValues;
 | |
|     if (this.lastType === this.widgets[0].type && controlValues?.length === this.widgets.length - 1) {
 | |
|       for (let i = 0; i < controlValues.length; i++) {
 | |
|         this.widgets[i + 1].value = controlValues[i];
 | |
|       }
 | |
|     }
 | |
|     const callback = widget.callback;
 | |
|     const self = this;
 | |
|     widget.callback = function() {
 | |
|       const r = callback ? callback.apply(this, arguments) : void 0;
 | |
|       self.applyToGraph();
 | |
|       return r;
 | |
|     };
 | |
|     this.size = [
 | |
|       Math.max(this.size[0], size[0]),
 | |
|       Math.max(this.size[1], size[1])
 | |
|     ];
 | |
|     if (!recreating) {
 | |
|       const sz = this.computeSize();
 | |
|       if (this.size[0] < sz[0]) {
 | |
|         this.size[0] = sz[0];
 | |
|       }
 | |
|       if (this.size[1] < sz[1]) {
 | |
|         this.size[1] = sz[1];
 | |
|       }
 | |
|       requestAnimationFrame(() => {
 | |
|         if (this.onResize) {
 | |
|           this.onResize(this.size);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   }
 | |
|   recreateWidget() {
 | |
|     const values = this.widgets?.map((w) => w.value);
 | |
|     this.#removeWidgets();
 | |
|     this.#onFirstConnection(true);
 | |
|     if (values?.length) {
 | |
|       for (let i = 0; i < this.widgets?.length; i++)
 | |
|         this.widgets[i].value = values[i];
 | |
|     }
 | |
|     return this.widgets?.[0];
 | |
|   }
 | |
|   #mergeWidgetConfig() {
 | |
|     const output = this.outputs[0];
 | |
|     const links = output.links;
 | |
|     const hasConfig = !!output.widget[CONFIG];
 | |
|     if (hasConfig) {
 | |
|       delete output.widget[CONFIG];
 | |
|     }
 | |
|     if (links?.length < 2 && hasConfig) {
 | |
|       if (links.length) {
 | |
|         this.recreateWidget();
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
|     const config1 = output.widget[GET_CONFIG]();
 | |
|     const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
 | |
|     if (!isNumber) return;
 | |
|     for (const linkId of links) {
 | |
|       const link = app.graph.links[linkId];
 | |
|       if (!link) continue;
 | |
|       const theirNode = app.graph.getNodeById(link.target_id);
 | |
|       const theirInput = theirNode.inputs[link.target_slot];
 | |
|       this.#isValidConnection(theirInput, hasConfig);
 | |
|     }
 | |
|   }
 | |
|   #isValidConnection(input, forceUpdate) {
 | |
|     const output = this.outputs[0];
 | |
|     const config2 = input.widget[GET_CONFIG]();
 | |
|     return !!mergeIfValid.call(
 | |
|       this,
 | |
|       output,
 | |
|       config2,
 | |
|       forceUpdate,
 | |
|       this.recreateWidget
 | |
|     );
 | |
|   }
 | |
|   #removeWidgets() {
 | |
|     if (this.widgets) {
 | |
|       for (const w of this.widgets) {
 | |
|         if (w.onRemove) {
 | |
|           w.onRemove();
 | |
|         }
 | |
|       }
 | |
|       this.controlValues = [];
 | |
|       this.lastType = this.widgets[0]?.type;
 | |
|       for (let i = 1; i < this.widgets.length; i++) {
 | |
|         this.controlValues.push(this.widgets[i].value);
 | |
|       }
 | |
|       setTimeout(() => {
 | |
|         delete this.lastType;
 | |
|         delete this.controlValues;
 | |
|       }, 15);
 | |
|       this.widgets.length = 0;
 | |
|     }
 | |
|   }
 | |
|   onLastDisconnect() {
 | |
|     this.outputs[0].type = "*";
 | |
|     this.outputs[0].name = "connect to widget input";
 | |
|     delete this.outputs[0].widget;
 | |
|     this.#removeWidgets();
 | |
|   }
 | |
| }
 | |
| function getWidgetConfig(slot) {
 | |
|   return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG]();
 | |
| }
 | |
| __name(getWidgetConfig, "getWidgetConfig");
 | |
| function getConfig(widgetName) {
 | |
|   const { nodeData } = this.constructor;
 | |
|   return nodeData?.input?.required?.[widgetName] ?? nodeData?.input?.optional?.[widgetName];
 | |
| }
 | |
| __name(getConfig, "getConfig");
 | |
| function isConvertibleWidget(widget, config) {
 | |
|   return (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput;
 | |
| }
 | |
| __name(isConvertibleWidget, "isConvertibleWidget");
 | |
| function hideWidget(node, widget, suffix = "") {
 | |
|   if (widget.type?.startsWith(CONVERTED_TYPE)) return;
 | |
|   widget.origType = widget.type;
 | |
|   widget.origComputeSize = widget.computeSize;
 | |
|   widget.origSerializeValue = widget.serializeValue;
 | |
|   widget.computeSize = () => [0, -4];
 | |
|   widget.type = CONVERTED_TYPE + suffix;
 | |
|   widget.serializeValue = () => {
 | |
|     if (!node.inputs) {
 | |
|       return void 0;
 | |
|     }
 | |
|     let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
 | |
|     if (!node_input || !node_input.link) {
 | |
|       return void 0;
 | |
|     }
 | |
|     return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
 | |
|   };
 | |
|   if (widget.linkedWidgets) {
 | |
|     for (const w of widget.linkedWidgets) {
 | |
|       hideWidget(node, w, ":" + widget.name);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| __name(hideWidget, "hideWidget");
 | |
| function showWidget(widget) {
 | |
|   widget.type = widget.origType;
 | |
|   widget.computeSize = widget.origComputeSize;
 | |
|   widget.serializeValue = widget.origSerializeValue;
 | |
|   delete widget.origType;
 | |
|   delete widget.origComputeSize;
 | |
|   delete widget.origSerializeValue;
 | |
|   if (widget.linkedWidgets) {
 | |
|     for (const w of widget.linkedWidgets) {
 | |
|       showWidget(w);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| __name(showWidget, "showWidget");
 | |
| function convertToInput(node, widget, config) {
 | |
|   hideWidget(node, widget);
 | |
|   const { type } = getWidgetType(config);
 | |
|   const sz = node.size;
 | |
|   node.addInput(widget.name, type, {
 | |
|     widget: { name: widget.name, [GET_CONFIG]: () => config }
 | |
|   });
 | |
|   for (const widget2 of node.widgets) {
 | |
|     widget2.last_y += LiteGraph.NODE_SLOT_HEIGHT;
 | |
|   }
 | |
|   node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
 | |
| }
 | |
| __name(convertToInput, "convertToInput");
 | |
| function convertToWidget(node, widget) {
 | |
|   showWidget(widget);
 | |
|   const sz = node.size;
 | |
|   node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
 | |
|   for (const widget2 of node.widgets) {
 | |
|     widget2.last_y -= LiteGraph.NODE_SLOT_HEIGHT;
 | |
|   }
 | |
|   node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
 | |
| }
 | |
| __name(convertToWidget, "convertToWidget");
 | |
| function getWidgetType(config) {
 | |
|   let type = config[0];
 | |
|   if (type instanceof Array) {
 | |
|     type = "COMBO";
 | |
|   }
 | |
|   return { type };
 | |
| }
 | |
| __name(getWidgetType, "getWidgetType");
 | |
| function isValidCombo(combo, obj) {
 | |
|   if (!(obj instanceof Array)) {
 | |
|     console.log(`connection rejected: tried to connect combo to ${obj}`);
 | |
|     return false;
 | |
|   }
 | |
|   if (combo.length !== obj.length) {
 | |
|     console.log(`connection rejected: combo lists dont match`);
 | |
|     return false;
 | |
|   }
 | |
|   if (combo.find((v, i) => obj[i] !== v)) {
 | |
|     console.log(`connection rejected: combo lists dont match`);
 | |
|     return false;
 | |
|   }
 | |
|   return true;
 | |
| }
 | |
| __name(isValidCombo, "isValidCombo");
 | |
| function isPrimitiveNode(node) {
 | |
|   return node.type === "PrimitiveNode";
 | |
| }
 | |
| __name(isPrimitiveNode, "isPrimitiveNode");
 | |
| function setWidgetConfig(slot, config, target) {
 | |
|   if (!slot.widget) return;
 | |
|   if (config) {
 | |
|     slot.widget[GET_CONFIG] = () => config;
 | |
|     slot.widget[TARGET] = target;
 | |
|   } else {
 | |
|     delete slot.widget;
 | |
|   }
 | |
|   if (slot.link) {
 | |
|     const link = app.graph.links[slot.link];
 | |
|     if (link) {
 | |
|       const originNode = app.graph.getNodeById(link.origin_id);
 | |
|       if (isPrimitiveNode(originNode)) {
 | |
|         if (config) {
 | |
|           originNode.recreateWidget();
 | |
|         } else if (!app.configuringGraph) {
 | |
|           originNode.disconnectOutput(0);
 | |
|           originNode.onLastDisconnect();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| __name(setWidgetConfig, "setWidgetConfig");
 | |
| function mergeIfValid(output, config2, forceUpdate, recreateWidget, config1) {
 | |
|   if (!config1) {
 | |
|     config1 = output.widget[CONFIG] ?? output.widget[GET_CONFIG]();
 | |
|   }
 | |
|   if (config1[0] instanceof Array) {
 | |
|     if (!isValidCombo(config1[0], config2[0])) return;
 | |
|   } else if (config1[0] !== config2[0]) {
 | |
|     console.log(`connection rejected: types dont match`, config1[0], config2[0]);
 | |
|     return;
 | |
|   }
 | |
|   const keys = /* @__PURE__ */ new Set([
 | |
|     ...Object.keys(config1[1] ?? {}),
 | |
|     ...Object.keys(config2[1] ?? {})
 | |
|   ]);
 | |
|   let customConfig;
 | |
|   const getCustomConfig = /* @__PURE__ */ __name(() => {
 | |
|     if (!customConfig) {
 | |
|       if (typeof structuredClone === "undefined") {
 | |
|         customConfig = JSON.parse(JSON.stringify(config1[1] ?? {}));
 | |
|       } else {
 | |
|         customConfig = structuredClone(config1[1] ?? {});
 | |
|       }
 | |
|     }
 | |
|     return customConfig;
 | |
|   }, "getCustomConfig");
 | |
|   const isNumber = config1[0] === "INT" || config1[0] === "FLOAT";
 | |
|   for (const k of keys.values()) {
 | |
|     if (k !== "default" && k !== "forceInput" && k !== "defaultInput" && k !== "control_after_generate" && k !== "multiline" && k !== "tooltip") {
 | |
|       let v1 = config1[1][k];
 | |
|       let v2 = config2[1]?.[k];
 | |
|       if (v1 === v2 || !v1 && !v2) continue;
 | |
|       if (isNumber) {
 | |
|         if (k === "min") {
 | |
|           const theirMax = config2[1]?.["max"];
 | |
|           if (theirMax != null && v1 > theirMax) {
 | |
|             console.log("connection rejected: min > max", v1, theirMax);
 | |
|             return;
 | |
|           }
 | |
|           getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2);
 | |
|           continue;
 | |
|         } else if (k === "max") {
 | |
|           const theirMin = config2[1]?.["min"];
 | |
|           if (theirMin != null && v1 < theirMin) {
 | |
|             console.log("connection rejected: max < min", v1, theirMin);
 | |
|             return;
 | |
|           }
 | |
|           getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2);
 | |
|           continue;
 | |
|         } else if (k === "step") {
 | |
|           let step;
 | |
|           if (v1 == null) {
 | |
|             step = v2;
 | |
|           } else if (v2 == null) {
 | |
|             step = v1;
 | |
|           } else {
 | |
|             if (v1 < v2) {
 | |
|               const a = v2;
 | |
|               v2 = v1;
 | |
|               v1 = a;
 | |
|             }
 | |
|             if (v1 % v2) {
 | |
|               console.log(
 | |
|                 "connection rejected: steps not divisible",
 | |
|                 "current:",
 | |
|                 v1,
 | |
|                 "new:",
 | |
|                 v2
 | |
|               );
 | |
|               return;
 | |
|             }
 | |
|             step = v1;
 | |
|           }
 | |
|           getCustomConfig()[k] = step;
 | |
|           continue;
 | |
|         }
 | |
|       }
 | |
|       console.log(`connection rejected: config ${k} values dont match`, v1, v2);
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
|   if (customConfig || forceUpdate) {
 | |
|     if (customConfig) {
 | |
|       output.widget[CONFIG] = [config1[0], customConfig];
 | |
|     }
 | |
|     const widget = recreateWidget?.call(this);
 | |
|     if (widget) {
 | |
|       const min = widget.options.min;
 | |
|       const max = widget.options.max;
 | |
|       if (min != null && widget.value < min) widget.value = min;
 | |
|       if (max != null && widget.value > max) widget.value = max;
 | |
|       widget.callback(widget.value);
 | |
|     }
 | |
|   }
 | |
|   return { customConfig };
 | |
| }
 | |
| __name(mergeIfValid, "mergeIfValid");
 | |
| let useConversionSubmenusSetting;
 | |
| app.registerExtension({
 | |
|   name: "Comfy.WidgetInputs",
 | |
|   init() {
 | |
|     useConversionSubmenusSetting = app.ui.settings.addSetting({
 | |
|       id: "Comfy.NodeInputConversionSubmenus",
 | |
|       name: "In the node context menu, place the entries that convert between input/widget in sub-menus.",
 | |
|       type: "boolean",
 | |
|       defaultValue: true
 | |
|     });
 | |
|   },
 | |
|   async beforeRegisterNodeDef(nodeType, nodeData, app2) {
 | |
|     const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
 | |
|     nodeType.prototype.convertWidgetToInput = function(widget) {
 | |
|       const config = getConfig.call(this, widget.name) ?? [
 | |
|         widget.type,
 | |
|         widget.options || {}
 | |
|       ];
 | |
|       if (!isConvertibleWidget(widget, config)) return false;
 | |
|       convertToInput(this, widget, config);
 | |
|       return true;
 | |
|     };
 | |
|     nodeType.prototype.getExtraMenuOptions = function(_, options) {
 | |
|       const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : void 0;
 | |
|       if (this.widgets) {
 | |
|         let toInput = [];
 | |
|         let toWidget = [];
 | |
|         for (const w of this.widgets) {
 | |
|           if (w.options?.forceInput) {
 | |
|             continue;
 | |
|           }
 | |
|           if (w.type === CONVERTED_TYPE) {
 | |
|             toWidget.push({
 | |
|               content: `Convert ${w.name} to widget`,
 | |
|               callback: /* @__PURE__ */ __name(() => convertToWidget(this, w), "callback")
 | |
|             });
 | |
|           } else {
 | |
|             const config = getConfig.call(this, w.name) ?? [
 | |
|               w.type,
 | |
|               w.options || {}
 | |
|             ];
 | |
|             if (isConvertibleWidget(w, config)) {
 | |
|               toInput.push({
 | |
|                 content: `Convert ${w.name} to input`,
 | |
|                 callback: /* @__PURE__ */ __name(() => convertToInput(this, w, config), "callback")
 | |
|               });
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         if (toInput.length) {
 | |
|           if (useConversionSubmenusSetting.value) {
 | |
|             options.push({
 | |
|               content: "Convert Widget to Input",
 | |
|               submenu: {
 | |
|                 options: toInput
 | |
|               }
 | |
|             });
 | |
|           } else {
 | |
|             options.push(...toInput, null);
 | |
|           }
 | |
|         }
 | |
|         if (toWidget.length) {
 | |
|           if (useConversionSubmenusSetting.value) {
 | |
|             options.push({
 | |
|               content: "Convert Input to Widget",
 | |
|               submenu: {
 | |
|                 options: toWidget
 | |
|               }
 | |
|             });
 | |
|           } else {
 | |
|             options.push(...toWidget, null);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return r;
 | |
|     };
 | |
|     nodeType.prototype.onGraphConfigured = function() {
 | |
|       if (!this.inputs) return;
 | |
|       for (const input of this.inputs) {
 | |
|         if (input.widget) {
 | |
|           if (!input.widget[GET_CONFIG]) {
 | |
|             input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name);
 | |
|           }
 | |
|           if (input.widget.config) {
 | |
|             if (input.widget.config[0] instanceof Array) {
 | |
|               input.type = "COMBO";
 | |
|               const link = app2.graph.links[input.link];
 | |
|               if (link) {
 | |
|                 link.type = input.type;
 | |
|               }
 | |
|             }
 | |
|             delete input.widget.config;
 | |
|           }
 | |
|           const w = this.widgets.find((w2) => w2.name === input.widget.name);
 | |
|           if (w) {
 | |
|             hideWidget(this, w);
 | |
|           } else {
 | |
|             convertToWidget(this, input);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|     const origOnNodeCreated = nodeType.prototype.onNodeCreated;
 | |
|     nodeType.prototype.onNodeCreated = function() {
 | |
|       const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : void 0;
 | |
|       if (!app2.configuringGraph && this.widgets) {
 | |
|         for (const w of this.widgets) {
 | |
|           if (w?.options?.forceInput || w?.options?.defaultInput) {
 | |
|             const config = getConfig.call(this, w.name) ?? [
 | |
|               w.type,
 | |
|               w.options || {}
 | |
|             ];
 | |
|             convertToInput(this, w, config);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return r;
 | |
|     };
 | |
|     const origOnConfigure = nodeType.prototype.onConfigure;
 | |
|     nodeType.prototype.onConfigure = function() {
 | |
|       const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : void 0;
 | |
|       if (!app2.configuringGraph && this.inputs) {
 | |
|         for (const input of this.inputs) {
 | |
|           if (input.widget && !input.widget[GET_CONFIG]) {
 | |
|             input.widget[GET_CONFIG] = () => getConfig.call(this, input.widget.name);
 | |
|             const w = this.widgets.find((w2) => w2.name === input.widget.name);
 | |
|             if (w) {
 | |
|               hideWidget(this, w);
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return r;
 | |
|     };
 | |
|     function isNodeAtPos(pos) {
 | |
|       for (const n of app2.graph._nodes) {
 | |
|         if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
 | |
|           return true;
 | |
|         }
 | |
|       }
 | |
|       return false;
 | |
|     }
 | |
|     __name(isNodeAtPos, "isNodeAtPos");
 | |
|     const origOnInputDblClick = nodeType.prototype.onInputDblClick;
 | |
|     const ignoreDblClick = Symbol();
 | |
|     nodeType.prototype.onInputDblClick = function(slot) {
 | |
|       const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : void 0;
 | |
|       const input = this.inputs[slot];
 | |
|       if (!input.widget || !input[ignoreDblClick]) {
 | |
|         if (!(input.type in ComfyWidgets) && !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array)) {
 | |
|           return r;
 | |
|         }
 | |
|       }
 | |
|       const node = LiteGraph.createNode("PrimitiveNode");
 | |
|       app2.graph.add(node);
 | |
|       const pos = [
 | |
|         this.pos[0] - node.size[0] - 30,
 | |
|         this.pos[1]
 | |
|       ];
 | |
|       while (isNodeAtPos(pos)) {
 | |
|         pos[1] += LiteGraph.NODE_TITLE_HEIGHT;
 | |
|       }
 | |
|       node.pos = pos;
 | |
|       node.connect(0, this, slot);
 | |
|       node.title = input.name;
 | |
|       input[ignoreDblClick] = true;
 | |
|       setTimeout(() => {
 | |
|         delete input[ignoreDblClick];
 | |
|       }, 300);
 | |
|       return r;
 | |
|     };
 | |
|     const onConnectInput = nodeType.prototype.onConnectInput;
 | |
|     nodeType.prototype.onConnectInput = function(targetSlot, type, output, originNode, originSlot) {
 | |
|       const v = onConnectInput?.(this, arguments);
 | |
|       if (type !== "COMBO") return v;
 | |
|       if (originNode.outputs[originSlot].widget) return v;
 | |
|       const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0];
 | |
|       if (!targetCombo || !(targetCombo instanceof Array)) return v;
 | |
|       const originConfig = originNode.constructor?.nodeData?.output?.[originSlot];
 | |
|       if (!originConfig || !isValidCombo(targetCombo, originConfig)) {
 | |
|         return false;
 | |
|       }
 | |
|       return v;
 | |
|     };
 | |
|   },
 | |
|   registerCustomNodes() {
 | |
|     LiteGraph.registerNodeType(
 | |
|       "PrimitiveNode",
 | |
|       Object.assign(PrimitiveNode, {
 | |
|         title: "Primitive"
 | |
|       })
 | |
|     );
 | |
|     PrimitiveNode.category = "utils";
 | |
|   }
 | |
| });
 | |
| window.comfyAPI = window.comfyAPI || {};
 | |
| window.comfyAPI.widgetInputs = window.comfyAPI.widgetInputs || {};
 | |
| window.comfyAPI.widgetInputs.getWidgetConfig = getWidgetConfig;
 | |
| window.comfyAPI.widgetInputs.setWidgetConfig = setWidgetConfig;
 | |
| window.comfyAPI.widgetInputs.mergeIfValid = mergeIfValid;
 | |
| const ORDER = Symbol();
 | |
| function merge(target, source) {
 | |
|   if (typeof target === "object" && typeof source === "object") {
 | |
|     for (const key in source) {
 | |
|       const sv = source[key];
 | |
|       if (typeof sv === "object") {
 | |
|         let tv = target[key];
 | |
|         if (!tv) tv = target[key] = {};
 | |
|         merge(tv, source[key]);
 | |
|       } else {
 | |
|         target[key] = sv;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   return target;
 | |
| }
 | |
| __name(merge, "merge");
 | |
| class ManageGroupDialog extends ComfyDialog {
 | |
|   static {
 | |
|     __name(this, "ManageGroupDialog");
 | |
|   }
 | |
|   tabs;
 | |
|   selectedNodeIndex;
 | |
|   selectedTab = "Inputs";
 | |
|   selectedGroup;
 | |
|   modifications = {};
 | |
|   nodeItems;
 | |
|   app;
 | |
|   groupNodeType;
 | |
|   groupNodeDef;
 | |
|   groupData;
 | |
|   innerNodesList;
 | |
|   widgetsPage;
 | |
|   inputsPage;
 | |
|   outputsPage;
 | |
|   draggable;
 | |
|   get selectedNodeInnerIndex() {
 | |
|     return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex;
 | |
|   }
 | |
|   constructor(app2) {
 | |
|     super();
 | |
|     this.app = app2;
 | |
|     this.element = $el("dialog.comfy-group-manage", {
 | |
|       parent: document.body
 | |
|     });
 | |
|   }
 | |
|   changeTab(tab) {
 | |
|     this.tabs[this.selectedTab].tab.classList.remove("active");
 | |
|     this.tabs[this.selectedTab].page.classList.remove("active");
 | |
|     this.tabs[tab].tab.classList.add("active");
 | |
|     this.tabs[tab].page.classList.add("active");
 | |
|     this.selectedTab = tab;
 | |
|   }
 | |
|   changeNode(index, force) {
 | |
|     if (!force && this.selectedNodeIndex === index) return;
 | |
|     if (this.selectedNodeIndex != null) {
 | |
|       this.nodeItems[this.selectedNodeIndex].classList.remove("selected");
 | |
|     }
 | |
|     this.nodeItems[index].classList.add("selected");
 | |
|     this.selectedNodeIndex = index;
 | |
|     if (!this.buildInputsPage() && this.selectedTab === "Inputs") {
 | |
|       this.changeTab("Widgets");
 | |
|     }
 | |
|     if (!this.buildWidgetsPage() && this.selectedTab === "Widgets") {
 | |
|       this.changeTab("Outputs");
 | |
|     }
 | |
|     if (!this.buildOutputsPage() && this.selectedTab === "Outputs") {
 | |
|       this.changeTab("Inputs");
 | |
|     }
 | |
|     this.changeTab(this.selectedTab);
 | |
|   }
 | |
|   getGroupData() {
 | |
|     this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
 | |
|     this.groupNodeDef = this.groupNodeType.nodeData;
 | |
|     this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
 | |
|   }
 | |
|   changeGroup(group, reset = true) {
 | |
|     this.selectedGroup = group;
 | |
|     this.getGroupData();
 | |
|     const nodes = this.groupData.nodeData.nodes;
 | |
|     this.nodeItems = nodes.map(
 | |
|       (n, i) => $el(
 | |
|         "li.draggable-item",
 | |
|         {
 | |
|           dataset: {
 | |
|             nodeindex: n.index + ""
 | |
|           },
 | |
|           onclick: /* @__PURE__ */ __name(() => {
 | |
|             this.changeNode(i);
 | |
|           }, "onclick")
 | |
|         },
 | |
|         [
 | |
|           $el("span.drag-handle"),
 | |
|           $el(
 | |
|             "div",
 | |
|             {
 | |
|               textContent: n.title ?? n.type
 | |
|             },
 | |
|             n.title ? $el("span", {
 | |
|               textContent: n.type
 | |
|             }) : []
 | |
|           )
 | |
|         ]
 | |
|       )
 | |
|     );
 | |
|     this.innerNodesList.replaceChildren(...this.nodeItems);
 | |
|     if (reset) {
 | |
|       this.selectedNodeIndex = null;
 | |
|       this.changeNode(0);
 | |
|     } else {
 | |
|       const items = this.draggable.getAllItems();
 | |
|       let index = items.findIndex((item) => item.classList.contains("selected"));
 | |
|       if (index === -1) index = this.selectedNodeIndex;
 | |
|       this.changeNode(index, true);
 | |
|     }
 | |
|     const ordered = [...nodes];
 | |
|     this.draggable?.dispose();
 | |
|     this.draggable = new DraggableList(this.innerNodesList, "li");
 | |
|     this.draggable.addEventListener(
 | |
|       "dragend",
 | |
|       ({ detail: { oldPosition, newPosition } }) => {
 | |
|         if (oldPosition === newPosition) return;
 | |
|         ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
 | |
|         for (let i = 0; i < ordered.length; i++) {
 | |
|           this.storeModification({
 | |
|             nodeIndex: ordered[i].index,
 | |
|             section: ORDER,
 | |
|             prop: "order",
 | |
|             value: i
 | |
|           });
 | |
|         }
 | |
|       }
 | |
|     );
 | |
|   }
 | |
|   storeModification(props) {
 | |
|     const { nodeIndex, section, prop, value } = props;
 | |
|     const groupMod = this.modifications[this.selectedGroup] ??= {};
 | |
|     const nodesMod = groupMod.nodes ??= {};
 | |
|     const nodeMod = nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {};
 | |
|     const typeMod = nodeMod[section] ??= {};
 | |
|     if (typeof value === "object") {
 | |
|       const objMod = typeMod[prop] ??= {};
 | |
|       Object.assign(objMod, value);
 | |
|     } else {
 | |
|       typeMod[prop] = value;
 | |
|     }
 | |
|   }
 | |
|   getEditElement(section, prop, value, placeholder, checked, checkable = true) {
 | |
|     if (value === placeholder) value = "";
 | |
|     const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop];
 | |
|     if (mods) {
 | |
|       if (mods.name != null) {
 | |
|         value = mods.name;
 | |
|       }
 | |
|       if (mods.visible != null) {
 | |
|         checked = mods.visible;
 | |
|       }
 | |
|     }
 | |
|     return $el("div", [
 | |
|       $el("input", {
 | |
|         value,
 | |
|         placeholder,
 | |
|         type: "text",
 | |
|         onchange: /* @__PURE__ */ __name((e) => {
 | |
|           this.storeModification({
 | |
|             section,
 | |
|             prop,
 | |
|             value: { name: e.target.value }
 | |
|           });
 | |
|         }, "onchange")
 | |
|       }),
 | |
|       $el("label", { textContent: "Visible" }, [
 | |
|         $el("input", {
 | |
|           type: "checkbox",
 | |
|           checked,
 | |
|           disabled: !checkable,
 | |
|           onchange: /* @__PURE__ */ __name((e) => {
 | |
|             this.storeModification({
 | |
|               section,
 | |
|               prop,
 | |
|               value: { visible: !!e.target.checked }
 | |
|             });
 | |
|           }, "onchange")
 | |
|         })
 | |
|       ])
 | |
|     ]);
 | |
|   }
 | |
|   buildWidgetsPage() {
 | |
|     const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
 | |
|     const items = Object.keys(widgets ?? {});
 | |
|     const type = app.graph.extra.groupNodes[this.selectedGroup];
 | |
|     const config = type.config?.[this.selectedNodeInnerIndex]?.input;
 | |
|     this.widgetsPage.replaceChildren(
 | |
|       ...items.map((oldName) => {
 | |
|         return this.getEditElement(
 | |
|           "input",
 | |
|           oldName,
 | |
|           widgets[oldName],
 | |
|           oldName,
 | |
|           config?.[oldName]?.visible !== false
 | |
|         );
 | |
|       })
 | |
|     );
 | |
|     return !!items.length;
 | |
|   }
 | |
|   buildInputsPage() {
 | |
|     const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex];
 | |
|     const items = Object.keys(inputs ?? {});
 | |
|     const type = app.graph.extra.groupNodes[this.selectedGroup];
 | |
|     const config = type.config?.[this.selectedNodeInnerIndex]?.input;
 | |
|     this.inputsPage.replaceChildren(
 | |
|       ...items.map((oldName) => {
 | |
|         let value = inputs[oldName];
 | |
|         if (!value) {
 | |
|           return;
 | |
|         }
 | |
|         return this.getEditElement(
 | |
|           "input",
 | |
|           oldName,
 | |
|           value,
 | |
|           oldName,
 | |
|           config?.[oldName]?.visible !== false
 | |
|         );
 | |
|       }).filter(Boolean)
 | |
|     );
 | |
|     return !!items.length;
 | |
|   }
 | |
|   buildOutputsPage() {
 | |
|     const nodes = this.groupData.nodeData.nodes;
 | |
|     const innerNodeDef = this.groupData.getNodeDef(
 | |
|       nodes[this.selectedNodeInnerIndex]
 | |
|     );
 | |
|     const outputs = innerNodeDef?.output ?? [];
 | |
|     const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
 | |
|     const type = app.graph.extra.groupNodes[this.selectedGroup];
 | |
|     const config = type.config?.[this.selectedNodeInnerIndex]?.output;
 | |
|     const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex];
 | |
|     const checkable = node.type !== "PrimitiveNode";
 | |
|     this.outputsPage.replaceChildren(
 | |
|       ...outputs.map((type2, slot) => {
 | |
|         const groupOutputIndex = groupOutputs?.[slot];
 | |
|         const oldName = innerNodeDef.output_name?.[slot] ?? type2;
 | |
|         let value = config?.[slot]?.name;
 | |
|         const visible = config?.[slot]?.visible || groupOutputIndex != null;
 | |
|         if (!value || value === oldName) {
 | |
|           value = "";
 | |
|         }
 | |
|         return this.getEditElement(
 | |
|           "output",
 | |
|           slot,
 | |
|           value,
 | |
|           oldName,
 | |
|           visible,
 | |
|           checkable
 | |
|         );
 | |
|       }).filter(Boolean)
 | |
|     );
 | |
|     return !!outputs.length;
 | |
|   }
 | |
|   show(type) {
 | |
|     const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort(
 | |
|       (a, b) => a.localeCompare(b)
 | |
|     );
 | |
|     this.innerNodesList = $el(
 | |
|       "ul.comfy-group-manage-list-items"
 | |
|     );
 | |
|     this.widgetsPage = $el("section.comfy-group-manage-node-page");
 | |
|     this.inputsPage = $el("section.comfy-group-manage-node-page");
 | |
|     this.outputsPage = $el("section.comfy-group-manage-node-page");
 | |
|     const pages = $el("div", [
 | |
|       this.widgetsPage,
 | |
|       this.inputsPage,
 | |
|       this.outputsPage
 | |
|     ]);
 | |
|     this.tabs = [
 | |
|       ["Inputs", this.inputsPage],
 | |
|       ["Widgets", this.widgetsPage],
 | |
|       ["Outputs", this.outputsPage]
 | |
|     ].reduce((p, [name, page]) => {
 | |
|       p[name] = {
 | |
|         tab: $el("a", {
 | |
|           onclick: /* @__PURE__ */ __name(() => {
 | |
|             this.changeTab(name);
 | |
|           }, "onclick"),
 | |
|           textContent: name
 | |
|         }),
 | |
|         page
 | |
|       };
 | |
|       return p;
 | |
|     }, {});
 | |
|     const outer = $el("div.comfy-group-manage-outer", [
 | |
|       $el("header", [
 | |
|         $el("h2", "Group Nodes"),
 | |
|         $el(
 | |
|           "select",
 | |
|           {
 | |
|             onchange: /* @__PURE__ */ __name((e) => {
 | |
|               this.changeGroup(e.target.value);
 | |
|             }, "onchange")
 | |
|           },
 | |
|           groupNodes.map(
 | |
|             (g) => $el("option", {
 | |
|               textContent: g,
 | |
|               selected: "workflow/" + g === type,
 | |
|               value: g
 | |
|             })
 | |
|           )
 | |
|         )
 | |
|       ]),
 | |
|       $el("main", [
 | |
|         $el("section.comfy-group-manage-list", this.innerNodesList),
 | |
|         $el("section.comfy-group-manage-node", [
 | |
|           $el(
 | |
|             "header",
 | |
|             Object.values(this.tabs).map((t) => t.tab)
 | |
|           ),
 | |
|           pages
 | |
|         ])
 | |
|       ]),
 | |
|       $el("footer", [
 | |
|         $el(
 | |
|           "button.comfy-btn",
 | |
|           {
 | |
|             onclick: /* @__PURE__ */ __name((e) => {
 | |
|               const node = app.graph._nodes.find(
 | |
|                 (n) => n.type === "workflow/" + this.selectedGroup
 | |
|               );
 | |
|               if (node) {
 | |
|                 alert(
 | |
|                   "This group node is in use in the current workflow, please first remove these."
 | |
|                 );
 | |
|                 return;
 | |
|               }
 | |
|               if (confirm(
 | |
|                 `Are you sure you want to remove the node: "${this.selectedGroup}"`
 | |
|               )) {
 | |
|                 delete app.graph.extra.groupNodes[this.selectedGroup];
 | |
|                 LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
 | |
|               }
 | |
|               this.show();
 | |
|             }, "onclick")
 | |
|           },
 | |
|           "Delete Group Node"
 | |
|         ),
 | |
|         $el(
 | |
|           "button.comfy-btn",
 | |
|           {
 | |
|             onclick: /* @__PURE__ */ __name(async () => {
 | |
|               let nodesByType;
 | |
|               let recreateNodes = [];
 | |
|               const types = {};
 | |
|               for (const g in this.modifications) {
 | |
|                 const type2 = app.graph.extra.groupNodes[g];
 | |
|                 let config = type2.config ??= {};
 | |
|                 let nodeMods = this.modifications[g]?.nodes;
 | |
|                 if (nodeMods) {
 | |
|                   const keys = Object.keys(nodeMods);
 | |
|                   if (nodeMods[keys[0]][ORDER]) {
 | |
|                     const orderedNodes = [];
 | |
|                     const orderedMods = {};
 | |
|                     const orderedConfig = {};
 | |
|                     for (const n of keys) {
 | |
|                       const order = nodeMods[n][ORDER].order;
 | |
|                       orderedNodes[order] = type2.nodes[+n];
 | |
|                       orderedMods[order] = nodeMods[n];
 | |
|                       orderedNodes[order].index = order;
 | |
|                     }
 | |
|                     for (const l of type2.links) {
 | |
|                       if (l[0] != null) l[0] = type2.nodes[l[0]].index;
 | |
|                       if (l[2] != null) l[2] = type2.nodes[l[2]].index;
 | |
|                     }
 | |
|                     if (type2.external) {
 | |
|                       for (const ext2 of type2.external) {
 | |
|                         ext2[0] = type2.nodes[ext2[0]];
 | |
|                       }
 | |
|                     }
 | |
|                     for (const id2 of keys) {
 | |
|                       if (config[id2]) {
 | |
|                         orderedConfig[type2.nodes[id2].index] = config[id2];
 | |
|                       }
 | |
|                       delete config[id2];
 | |
|                     }
 | |
|                     type2.nodes = orderedNodes;
 | |
|                     nodeMods = orderedMods;
 | |
|                     type2.config = config = orderedConfig;
 | |
|                   }
 | |
|                   merge(config, nodeMods);
 | |
|                 }
 | |
|                 types[g] = type2;
 | |
|                 if (!nodesByType) {
 | |
|                   nodesByType = app.graph._nodes.reduce((p, n) => {
 | |
|                     p[n.type] ??= [];
 | |
|                     p[n.type].push(n);
 | |
|                     return p;
 | |
|                   }, {});
 | |
|                 }
 | |
|                 const nodes = nodesByType["workflow/" + g];
 | |
|                 if (nodes) recreateNodes.push(...nodes);
 | |
|               }
 | |
|               await GroupNodeConfig.registerFromWorkflow(types, {});
 | |
|               for (const node of recreateNodes) {
 | |
|                 node.recreate();
 | |
|               }
 | |
|               this.modifications = {};
 | |
|               this.app.graph.setDirtyCanvas(true, true);
 | |
|               this.changeGroup(this.selectedGroup, false);
 | |
|             }, "onclick")
 | |
|           },
 | |
|           "Save"
 | |
|         ),
 | |
|         $el(
 | |
|           "button.comfy-btn",
 | |
|           { onclick: /* @__PURE__ */ __name(() => this.element.close(), "onclick") },
 | |
|           "Close"
 | |
|         )
 | |
|       ])
 | |
|     ]);
 | |
|     this.element.replaceChildren(outer);
 | |
|     this.changeGroup(
 | |
|       type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]
 | |
|     );
 | |
|     this.element.showModal();
 | |
|     this.element.addEventListener("close", () => {
 | |
|       this.draggable?.dispose();
 | |
|     });
 | |
|   }
 | |
| }
 | |
| window.comfyAPI = window.comfyAPI || {};
 | |
| window.comfyAPI.groupNodeManage = window.comfyAPI.groupNodeManage || {};
 | |
| window.comfyAPI.groupNodeManage.ManageGroupDialog = ManageGroupDialog;
 | |
| const GROUP = Symbol();
 | |
| const Workflow = {
 | |
|   InUse: {
 | |
|     Free: 0,
 | |
|     Registered: 1,
 | |
|     InWorkflow: 2
 | |
|   },
 | |
|   isInUseGroupNode(name) {
 | |
|     const id2 = `workflow/${name}`;
 | |
|     if (app.graph.extra?.groupNodes?.[name]) {
 | |
|       if (app.graph._nodes.find((n) => n.type === id2)) {
 | |
|         return Workflow.InUse.InWorkflow;
 | |
|       } else {
 | |
|         return Workflow.InUse.Registered;
 | |
|       }
 | |
|     }
 | |
|     return Workflow.InUse.Free;
 | |
|   },
 | |
|   storeGroupNode(name, data) {
 | |
|     let extra = app.graph.extra;
 | |
|     if (!extra) app.graph.extra = extra = {};
 | |
|     let groupNodes = extra.groupNodes;
 | |
|     if (!groupNodes) extra.groupNodes = groupNodes = {};
 | |
|     groupNodes[name] = data;
 | |
|   }
 | |
| };
 | |
| class GroupNodeBuilder {
 | |
|   static {
 | |
|     __name(this, "GroupNodeBuilder");
 | |
|   }
 | |
|   nodes;
 | |
|   nodeData;
 | |
|   constructor(nodes) {
 | |
|     this.nodes = nodes;
 | |
|   }
 | |
|   build() {
 | |
|     const name = this.getName();
 | |
|     if (!name) return;
 | |
|     this.sortNodes();
 | |
|     this.nodeData = this.getNodeData();
 | |
|     Workflow.storeGroupNode(name, this.nodeData);
 | |
|     return { name, nodeData: this.nodeData };
 | |
|   }
 | |
|   getName() {
 | |
|     const name = prompt("Enter group name");
 | |
|     if (!name) return;
 | |
|     const used = Workflow.isInUseGroupNode(name);
 | |
|     switch (used) {
 | |
|       case Workflow.InUse.InWorkflow:
 | |
|         alert(
 | |
|           "An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name."
 | |
|         );
 | |
|         return;
 | |
|       case Workflow.InUse.Registered:
 | |
|         if (!confirm(
 | |
|           "A group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?"
 | |
|         )) {
 | |
|           return;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|     return name;
 | |
|   }
 | |
|   sortNodes() {
 | |
|     const nodesInOrder = app.graph.computeExecutionOrder(false);
 | |
|     this.nodes = this.nodes.map((node) => ({ index: nodesInOrder.indexOf(node), node })).sort((a, b) => a.index - b.index || a.node.id - b.node.id).map(({ node }) => node);
 | |
|   }
 | |
|   getNodeData() {
 | |
|     const storeLinkTypes = /* @__PURE__ */ __name((config) => {
 | |
|       for (const link of config.links) {
 | |
|         const origin = app.graph.getNodeById(link[4]);
 | |
|         const type = origin.outputs[link[1]].type;
 | |
|         link.push(type);
 | |
|       }
 | |
|     }, "storeLinkTypes");
 | |
|     const storeExternalLinks = /* @__PURE__ */ __name((config) => {
 | |
|       config.external = [];
 | |
|       for (let i = 0; i < this.nodes.length; i++) {
 | |
|         const node = this.nodes[i];
 | |
|         if (!node.outputs?.length) continue;
 | |
|         for (let slot = 0; slot < node.outputs.length; slot++) {
 | |
|           let hasExternal = false;
 | |
|           const output = node.outputs[slot];
 | |
|           let type = output.type;
 | |
|           if (!output.links?.length) continue;
 | |
|           for (const l of output.links) {
 | |
|             const link = app.graph.links[l];
 | |
|             if (!link) continue;
 | |
|             if (type === "*") type = link.type;
 | |
|             if (!app.canvas.selected_nodes[link.target_id]) {
 | |
|               hasExternal = true;
 | |
|               break;
 | |
|             }
 | |
|           }
 | |
|           if (hasExternal) {
 | |
|             config.external.push([i, slot, type]);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }, "storeExternalLinks");
 | |
|     const backup = localStorage.getItem("litegrapheditor_clipboard");
 | |
|     try {
 | |
|       app.canvas.copyToClipboard(this.nodes);
 | |
|       const config = JSON.parse(
 | |
|         localStorage.getItem("litegrapheditor_clipboard")
 | |
|       );
 | |
|       storeLinkTypes(config);
 | |
|       storeExternalLinks(config);
 | |
|       return config;
 | |
|     } finally {
 | |
|       localStorage.setItem("litegrapheditor_clipboard", backup);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| class GroupNodeConfig {
 | |
|   static {
 | |
|     __name(this, "GroupNodeConfig");
 | |
|   }
 | |
|   name;
 | |
|   nodeData;
 | |
|   inputCount;
 | |
|   oldToNewOutputMap;
 | |
|   newToOldOutputMap;
 | |
|   oldToNewInputMap;
 | |
|   oldToNewWidgetMap;
 | |
|   newToOldWidgetMap;
 | |
|   primitiveDefs;
 | |
|   widgetToPrimitive;
 | |
|   primitiveToWidget;
 | |
|   nodeInputs;
 | |
|   outputVisibility;
 | |
|   nodeDef;
 | |
|   inputs;
 | |
|   linksFrom;
 | |
|   linksTo;
 | |
|   externalFrom;
 | |
|   constructor(name, nodeData) {
 | |
|     this.name = name;
 | |
|     this.nodeData = nodeData;
 | |
|     this.getLinks();
 | |
|     this.inputCount = 0;
 | |
|     this.oldToNewOutputMap = {};
 | |
|     this.newToOldOutputMap = {};
 | |
|     this.oldToNewInputMap = {};
 | |
|     this.oldToNewWidgetMap = {};
 | |
|     this.newToOldWidgetMap = {};
 | |
|     this.primitiveDefs = {};
 | |
|     this.widgetToPrimitive = {};
 | |
|     this.primitiveToWidget = {};
 | |
|     this.nodeInputs = {};
 | |
|     this.outputVisibility = [];
 | |
|   }
 | |
|   async registerType(source = "workflow") {
 | |
|     this.nodeDef = {
 | |
|       output: [],
 | |
|       output_name: [],
 | |
|       output_is_list: [],
 | |
|       output_is_hidden: [],
 | |
|       name: source + "/" + this.name,
 | |
|       display_name: this.name,
 | |
|       category: "group nodes" + ("/" + source),
 | |
|       input: { required: {} },
 | |
|       [GROUP]: this
 | |
|     };
 | |
|     this.inputs = [];
 | |
|     const seenInputs = {};
 | |
|     const seenOutputs = {};
 | |
|     for (let i = 0; i < this.nodeData.nodes.length; i++) {
 | |
|       const node = this.nodeData.nodes[i];
 | |
|       node.index = i;
 | |
|       this.processNode(node, seenInputs, seenOutputs);
 | |
|     }
 | |
|     for (const p of this.#convertedToProcess) {
 | |
|       p();
 | |
|     }
 | |
|     this.#convertedToProcess = null;
 | |
|     await app.registerNodeDef("workflow/" + this.name, this.nodeDef);
 | |
|   }
 | |
|   getLinks() {
 | |
|     this.linksFrom = {};
 | |
|     this.linksTo = {};
 | |
|     this.externalFrom = {};
 | |
|     for (const l of this.nodeData.links) {
 | |
|       const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l;
 | |
|       if (sourceNodeId == null) continue;
 | |
|       if (!this.linksFrom[sourceNodeId]) {
 | |
|         this.linksFrom[sourceNodeId] = {};
 | |
|       }
 | |
|       if (!this.linksFrom[sourceNodeId][sourceNodeSlot]) {
 | |
|         this.linksFrom[sourceNodeId][sourceNodeSlot] = [];
 | |
|       }
 | |
|       this.linksFrom[sourceNodeId][sourceNodeSlot].push(l);
 | |
|       if (!this.linksTo[targetNodeId]) {
 | |
|         this.linksTo[targetNodeId] = {};
 | |
|       }
 | |
|       this.linksTo[targetNodeId][targetNodeSlot] = l;
 | |
|     }
 | |
|     if (this.nodeData.external) {
 | |
|       for (const ext2 of this.nodeData.external) {
 | |
|         if (!this.externalFrom[ext2[0]]) {
 | |
|           this.externalFrom[ext2[0]] = { [ext2[1]]: ext2[2] };
 | |
|         } else {
 | |
|           this.externalFrom[ext2[0]][ext2[1]] = ext2[2];
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   processNode(node, seenInputs, seenOutputs) {
 | |
|     const def = this.getNodeDef(node);
 | |
|     if (!def) return;
 | |
|     const inputs = { ...def.input?.required, ...def.input?.optional };
 | |
|     this.inputs.push(this.processNodeInputs(node, seenInputs, inputs));
 | |
|     if (def.output?.length) this.processNodeOutputs(node, seenOutputs, def);
 | |
|   }
 | |
|   getNodeDef(node) {
 | |
|     const def = globalDefs[node.type];
 | |
|     if (def) return def;
 | |
|     const linksFrom = this.linksFrom[node.index];
 | |
|     if (node.type === "PrimitiveNode") {
 | |
|       if (!linksFrom) return;
 | |
|       let type = linksFrom["0"][0][5];
 | |
|       if (type === "COMBO") {
 | |
|         const source = node.outputs[0].widget.name;
 | |
|         const fromTypeName = this.nodeData.nodes[linksFrom["0"][0][2]].type;
 | |
|         const fromType = globalDefs[fromTypeName];
 | |
|         const input = fromType.input.required[source] ?? fromType.input.optional[source];
 | |
|         type = input[0];
 | |
|       }
 | |
|       const def2 = this.primitiveDefs[node.index] = {
 | |
|         input: {
 | |
|           required: {
 | |
|             value: [type, {}]
 | |
|           }
 | |
|         },
 | |
|         output: [type],
 | |
|         output_name: [],
 | |
|         output_is_list: []
 | |
|       };
 | |
|       return def2;
 | |
|     } else if (node.type === "Reroute") {
 | |
|       const linksTo = this.linksTo[node.index];
 | |
|       if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) {
 | |
|         return null;
 | |
|       }
 | |
|       let config = {};
 | |
|       let rerouteType = "*";
 | |
|       if (linksFrom) {
 | |
|         for (const [, , id2, slot] of linksFrom["0"]) {
 | |
|           const node2 = this.nodeData.nodes[id2];
 | |
|           const input = node2.inputs[slot];
 | |
|           if (rerouteType === "*") {
 | |
|             rerouteType = input.type;
 | |
|           }
 | |
|           if (input.widget) {
 | |
|             const targetDef = globalDefs[node2.type];
 | |
|             const targetWidget = targetDef.input.required[input.widget.name] ?? targetDef.input.optional[input.widget.name];
 | |
|             const widget = [targetWidget[0], config];
 | |
|             const res = mergeIfValid(
 | |
|               {
 | |
|                 widget
 | |
|               },
 | |
|               targetWidget,
 | |
|               false,
 | |
|               null,
 | |
|               widget
 | |
|             );
 | |
|             config = res?.customConfig ?? config;
 | |
|           }
 | |
|         }
 | |
|       } else if (linksTo) {
 | |
|         const [id2, slot] = linksTo["0"];
 | |
|         rerouteType = this.nodeData.nodes[id2].outputs[slot].type;
 | |
|       } else {
 | |
|         for (const l of this.nodeData.links) {
 | |
|           if (l[2] === node.index) {
 | |
|             rerouteType = l[5];
 | |
|             break;
 | |
|           }
 | |
|         }
 | |
|         if (rerouteType === "*") {
 | |
|           const t = this.externalFrom[node.index]?.[0];
 | |
|           if (t) {
 | |
|             rerouteType = t;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       config.forceInput = true;
 | |
|       return {
 | |
|         input: {
 | |
|           required: {
 | |
|             [rerouteType]: [rerouteType, config]
 | |
|           }
 | |
|         },
 | |
|         output: [rerouteType],
 | |
|         output_name: [],
 | |
|         output_is_list: []
 | |
|       };
 | |
|     }
 | |
|     console.warn(
 | |
|       "Skipping virtual node " + node.type + " when building group node " + this.name
 | |
|     );
 | |
|   }
 | |
|   getInputConfig(node, inputName, seenInputs, config, extra) {
 | |
|     const customConfig = this.nodeData.config?.[node.index]?.input?.[inputName];
 | |
|     let name = customConfig?.name ?? node.inputs?.find((inp) => inp.name === inputName)?.label ?? inputName;
 | |
|     let key = name;
 | |
|     let prefix = "";
 | |
|     if (node.type === "PrimitiveNode" && node.title || name in seenInputs) {
 | |
|       prefix = `${node.title ?? node.type} `;
 | |
|       key = name = `${prefix}${inputName}`;
 | |
|       if (name in seenInputs) {
 | |
|         name = `${prefix}${seenInputs[name]} ${inputName}`;
 | |
|       }
 | |
|     }
 | |
|     seenInputs[key] = (seenInputs[key] ?? 1) + 1;
 | |
|     if (inputName === "seed" || inputName === "noise_seed") {
 | |
|       if (!extra) extra = {};
 | |
|       extra.control_after_generate = `${prefix}control_after_generate`;
 | |
|     }
 | |
|     if (config[0] === "IMAGEUPLOAD") {
 | |
|       if (!extra) extra = {};
 | |
|       extra.widget = this.oldToNewWidgetMap[node.index]?.[config[1]?.widget ?? "image"] ?? "image";
 | |
|     }
 | |
|     if (extra) {
 | |
|       config = [config[0], { ...config[1], ...extra }];
 | |
|     }
 | |
|     return { name, config, customConfig };
 | |
|   }
 | |
|   processWidgetInputs(inputs, node, inputNames, seenInputs) {
 | |
|     const slots = [];
 | |
|     const converted = /* @__PURE__ */ new Map();
 | |
|     const widgetMap = this.oldToNewWidgetMap[node.index] = {};
 | |
|     for (const inputName of inputNames) {
 | |
|       let widgetType = app.getWidgetType(inputs[inputName], inputName);
 | |
|       if (widgetType) {
 | |
|         const convertedIndex = node.inputs?.findIndex(
 | |
|           (inp) => inp.name === inputName && inp.widget?.name === inputName
 | |
|         );
 | |
|         if (convertedIndex > -1) {
 | |
|           converted.set(convertedIndex, inputName);
 | |
|           widgetMap[inputName] = null;
 | |
|         } else {
 | |
|           const { name, config } = this.getInputConfig(
 | |
|             node,
 | |
|             inputName,
 | |
|             seenInputs,
 | |
|             inputs[inputName]
 | |
|           );
 | |
|           this.nodeDef.input.required[name] = config;
 | |
|           widgetMap[inputName] = name;
 | |
|           this.newToOldWidgetMap[name] = { node, inputName };
 | |
|         }
 | |
|       } else {
 | |
|         slots.push(inputName);
 | |
|       }
 | |
|     }
 | |
|     return { converted, slots };
 | |
|   }
 | |
|   checkPrimitiveConnection(link, inputName, inputs) {
 | |
|     const sourceNode = this.nodeData.nodes[link[0]];
 | |
|     if (sourceNode.type === "PrimitiveNode") {
 | |
|       const [sourceNodeId, _, targetNodeId, __] = link;
 | |
|       const primitiveDef = this.primitiveDefs[sourceNodeId];
 | |
|       const targetWidget = inputs[inputName];
 | |
|       const primitiveConfig = primitiveDef.input.required.value;
 | |
|       const output = { widget: primitiveConfig };
 | |
|       const config = mergeIfValid(
 | |
|         output,
 | |
|         targetWidget,
 | |
|         false,
 | |
|         null,
 | |
|         primitiveConfig
 | |
|       );
 | |
|       primitiveConfig[1] = config?.customConfig ?? inputs[inputName][1] ? { ...inputs[inputName][1] } : {};
 | |
|       let name = this.oldToNewWidgetMap[sourceNodeId]["value"];
 | |
|       name = name.substr(0, name.length - 6);
 | |
|       primitiveConfig[1].control_after_generate = true;
 | |
|       primitiveConfig[1].control_prefix = name;
 | |
|       let toPrimitive = this.widgetToPrimitive[targetNodeId];
 | |
|       if (!toPrimitive) {
 | |
|         toPrimitive = this.widgetToPrimitive[targetNodeId] = {};
 | |
|       }
 | |
|       if (toPrimitive[inputName]) {
 | |
|         toPrimitive[inputName].push(sourceNodeId);
 | |
|       }
 | |
|       toPrimitive[inputName] = sourceNodeId;
 | |
|       let toWidget = this.primitiveToWidget[sourceNodeId];
 | |
|       if (!toWidget) {
 | |
|         toWidget = this.primitiveToWidget[sourceNodeId] = [];
 | |
|       }
 | |
|       toWidget.push({ nodeId: targetNodeId, inputName });
 | |
|     }
 | |
|   }
 | |
|   processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) {
 | |
|     this.nodeInputs[node.index] = {};
 | |
|     for (let i = 0; i < slots.length; i++) {
 | |
|       const inputName = slots[i];
 | |
|       if (linksTo[i]) {
 | |
|         this.checkPrimitiveConnection(linksTo[i], inputName, inputs);
 | |
|         continue;
 | |
|       }
 | |
|       const { name, config, customConfig } = this.getInputConfig(
 | |
|         node,
 | |
|         inputName,
 | |
|         seenInputs,
 | |
|         inputs[inputName]
 | |
|       );
 | |
|       this.nodeInputs[node.index][inputName] = name;
 | |
|       if (customConfig?.visible === false) continue;
 | |
|       this.nodeDef.input.required[name] = config;
 | |
|       inputMap[i] = this.inputCount++;
 | |
|     }
 | |
|   }
 | |
|   processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs) {
 | |
|     const convertedSlots = [...converted.keys()].sort().map((k) => converted.get(k));
 | |
|     for (let i = 0; i < convertedSlots.length; i++) {
 | |
|       const inputName = convertedSlots[i];
 | |
|       if (linksTo[slots.length + i]) {
 | |
|         this.checkPrimitiveConnection(
 | |
|           linksTo[slots.length + i],
 | |
|           inputName,
 | |
|           inputs
 | |
|         );
 | |
|         continue;
 | |
|       }
 | |
|       const { name, config } = this.getInputConfig(
 | |
|         node,
 | |
|         inputName,
 | |
|         seenInputs,
 | |
|         inputs[inputName],
 | |
|         {
 | |
|           defaultInput: true
 | |
|         }
 | |
|       );
 | |
|       this.nodeDef.input.required[name] = config;
 | |
|       this.newToOldWidgetMap[name] = { node, inputName };
 | |
|       if (!this.oldToNewWidgetMap[node.index]) {
 | |
|         this.oldToNewWidgetMap[node.index] = {};
 | |
|       }
 | |
|       this.oldToNewWidgetMap[node.index][inputName] = name;
 | |
|       inputMap[slots.length + i] = this.inputCount++;
 | |
|     }
 | |
|   }
 | |
|   #convertedToProcess = [];
 | |
|   processNodeInputs(node, seenInputs, inputs) {
 | |
|     const inputMapping = [];
 | |
|     const inputNames = Object.keys(inputs);
 | |
|     if (!inputNames.length) return;
 | |
|     const { converted, slots } = this.processWidgetInputs(
 | |
|       inputs,
 | |
|       node,
 | |
|       inputNames,
 | |
|       seenInputs
 | |
|     );
 | |
|     const linksTo = this.linksTo[node.index] ?? {};
 | |
|     const inputMap = this.oldToNewInputMap[node.index] = {};
 | |
|     this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs);
 | |
|     this.#convertedToProcess.push(
 | |
|       () => this.processConvertedWidgets(
 | |
|         inputs,
 | |
|         node,
 | |
|         slots,
 | |
|         converted,
 | |
|         linksTo,
 | |
|         inputMap,
 | |
|         seenInputs
 | |
|       )
 | |
|     );
 | |
|     return inputMapping;
 | |
|   }
 | |
|   processNodeOutputs(node, seenOutputs, def) {
 | |
|     const oldToNew = this.oldToNewOutputMap[node.index] = {};
 | |
|     for (let outputId = 0; outputId < def.output.length; outputId++) {
 | |
|       const linksFrom = this.linksFrom[node.index];
 | |
|       const hasLink = linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId];
 | |
|       const customConfig = this.nodeData.config?.[node.index]?.output?.[outputId];
 | |
|       const visible = customConfig?.visible ?? !hasLink;
 | |
|       this.outputVisibility.push(visible);
 | |
|       if (!visible) {
 | |
|         continue;
 | |
|       }
 | |
|       oldToNew[outputId] = this.nodeDef.output.length;
 | |
|       this.newToOldOutputMap[this.nodeDef.output.length] = {
 | |
|         node,
 | |
|         slot: outputId
 | |
|       };
 | |
|       this.nodeDef.output.push(def.output[outputId]);
 | |
|       this.nodeDef.output_is_list.push(def.output_is_list[outputId]);
 | |
|       let label = customConfig?.name;
 | |
|       if (!label) {
 | |
|         label = def.output_name?.[outputId] ?? def.output[outputId];
 | |
|         const output = node.outputs.find((o) => o.name === label);
 | |
|         if (output?.label) {
 | |
|           label = output.label;
 | |
|         }
 | |
|       }
 | |
|       let name = label;
 | |
|       if (name in seenOutputs) {
 | |
|         const prefix = `${node.title ?? node.type} `;
 | |
|         name = `${prefix}${label}`;
 | |
|         if (name in seenOutputs) {
 | |
|           name = `${prefix}${node.index} ${label}`;
 | |
|         }
 | |
|       }
 | |
|       seenOutputs[name] = 1;
 | |
|       this.nodeDef.output_name.push(name);
 | |
|     }
 | |
|   }
 | |
|   static async registerFromWorkflow(groupNodes, missingNodeTypes) {
 | |
|     const clean = app.clean;
 | |
|     app.clean = function() {
 | |
|       for (const g in groupNodes) {
 | |
|         try {
 | |
|           LiteGraph.unregisterNodeType("workflow/" + g);
 | |
|         } catch (error) {
 | |
|         }
 | |
|       }
 | |
|       app.clean = clean;
 | |
|     };
 | |
|     for (const g in groupNodes) {
 | |
|       const groupData = groupNodes[g];
 | |
|       let hasMissing = false;
 | |
|       for (const n of groupData.nodes) {
 | |
|         if (!(n.type in LiteGraph.registered_node_types)) {
 | |
|           missingNodeTypes.push({
 | |
|             type: n.type,
 | |
|             hint: ` (In group node 'workflow/${g}')`
 | |
|           });
 | |
|           missingNodeTypes.push({
 | |
|             type: "workflow/" + g,
 | |
|             action: {
 | |
|               text: "Remove from workflow",
 | |
|               callback: /* @__PURE__ */ __name((e) => {
 | |
|                 delete groupNodes[g];
 | |
|                 e.target.textContent = "Removed";
 | |
|                 e.target.style.pointerEvents = "none";
 | |
|                 e.target.style.opacity = 0.7;
 | |
|               }, "callback")
 | |
|             }
 | |
|           });
 | |
|           hasMissing = true;
 | |
|         }
 | |
|       }
 | |
|       if (hasMissing) continue;
 | |
|       const config = new GroupNodeConfig(g, groupData);
 | |
|       await config.registerType();
 | |
|     }
 | |
|   }
 | |
| }
 | |
| class GroupNodeHandler {
 | |
|   static {
 | |
|     __name(this, "GroupNodeHandler");
 | |
|   }
 | |
|   node;
 | |
|   groupData;
 | |
|   innerNodes;
 | |
|   constructor(node) {
 | |
|     this.node = node;
 | |
|     this.groupData = node.constructor?.nodeData?.[GROUP];
 | |
|     this.node.setInnerNodes = (innerNodes) => {
 | |
|       this.innerNodes = innerNodes;
 | |
|       for (let innerNodeIndex = 0; innerNodeIndex < this.innerNodes.length; innerNodeIndex++) {
 | |
|         const innerNode = this.innerNodes[innerNodeIndex];
 | |
|         for (const w of innerNode.widgets ?? []) {
 | |
|           if (w.type === "converted-widget") {
 | |
|             w.serializeValue = w.origSerializeValue;
 | |
|           }
 | |
|         }
 | |
|         innerNode.index = innerNodeIndex;
 | |
|         innerNode.getInputNode = (slot) => {
 | |
|           const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
 | |
|           if (externalSlot != null) {
 | |
|             return this.node.getInputNode(externalSlot);
 | |
|           }
 | |
|           const innerLink = this.groupData.linksTo[innerNode.index]?.[slot];
 | |
|           if (!innerLink) return null;
 | |
|           const inputNode = innerNodes[innerLink[0]];
 | |
|           if (inputNode.type === "PrimitiveNode") return null;
 | |
|           return inputNode;
 | |
|         };
 | |
|         innerNode.getInputLink = (slot) => {
 | |
|           const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
 | |
|           if (externalSlot != null) {
 | |
|             const linkId = this.node.inputs[externalSlot].link;
 | |
|             let link2 = app.graph.links[linkId];
 | |
|             link2 = {
 | |
|               ...link2,
 | |
|               target_id: innerNode.id,
 | |
|               target_slot: +slot
 | |
|             };
 | |
|             return link2;
 | |
|           }
 | |
|           let link = this.groupData.linksTo[innerNode.index]?.[slot];
 | |
|           if (!link) return null;
 | |
|           link = {
 | |
|             origin_id: innerNodes[link[0]].id,
 | |
|             origin_slot: link[1],
 | |
|             target_id: innerNode.id,
 | |
|             target_slot: +slot
 | |
|           };
 | |
|           return link;
 | |
|         };
 | |
|       }
 | |
|     };
 | |
|     this.node.updateLink = (link) => {
 | |
|       link = { ...link };
 | |
|       const output = this.groupData.newToOldOutputMap[link.origin_slot];
 | |
|       let innerNode = this.innerNodes[output.node.index];
 | |
|       let l;
 | |
|       while (innerNode?.type === "Reroute") {
 | |
|         l = innerNode.getInputLink(0);
 | |
|         innerNode = innerNode.getInputNode(0);
 | |
|       }
 | |
|       if (!innerNode) {
 | |
|         return null;
 | |
|       }
 | |
|       if (l && GroupNodeHandler.isGroupNode(innerNode)) {
 | |
|         return innerNode.updateLink(l);
 | |
|       }
 | |
|       link.origin_id = innerNode.id;
 | |
|       link.origin_slot = l?.origin_slot ?? output.slot;
 | |
|       return link;
 | |
|     };
 | |
|     this.node.getInnerNodes = () => {
 | |
|       if (!this.innerNodes) {
 | |
|         this.node.setInnerNodes(
 | |
|           this.groupData.nodeData.nodes.map((n, i) => {
 | |
|             const innerNode = LiteGraph.createNode(n.type);
 | |
|             innerNode.configure(n);
 | |
|             innerNode.id = `${this.node.id}:${i}`;
 | |
|             return innerNode;
 | |
|           })
 | |
|         );
 | |
|       }
 | |
|       this.updateInnerWidgets();
 | |
|       return this.innerNodes;
 | |
|     };
 | |
|     this.node.recreate = async () => {
 | |
|       const id2 = this.node.id;
 | |
|       const sz = this.node.size;
 | |
|       const nodes = this.node.convertToNodes();
 | |
|       const groupNode = LiteGraph.createNode(this.node.type);
 | |
|       groupNode.id = id2;
 | |
|       groupNode.setInnerNodes(nodes);
 | |
|       groupNode[GROUP].populateWidgets();
 | |
|       app.graph.add(groupNode);
 | |
|       groupNode.size = [
 | |
|         Math.max(groupNode.size[0], sz[0]),
 | |
|         Math.max(groupNode.size[1], sz[1])
 | |
|       ];
 | |
|       groupNode[GROUP].replaceNodes(nodes);
 | |
|       return groupNode;
 | |
|     };
 | |
|     this.node.convertToNodes = () => {
 | |
|       const addInnerNodes = /* @__PURE__ */ __name(() => {
 | |
|         const backup = localStorage.getItem("litegrapheditor_clipboard");
 | |
|         const c = { ...this.groupData.nodeData };
 | |
|         c.nodes = [...c.nodes];
 | |
|         const innerNodes = this.node.getInnerNodes();
 | |
|         let ids = [];
 | |
|         for (let i = 0; i < c.nodes.length; i++) {
 | |
|           let id2 = innerNodes?.[i]?.id;
 | |
|           if (id2 == null || isNaN(id2)) {
 | |
|             id2 = void 0;
 | |
|           } else {
 | |
|             ids.push(id2);
 | |
|           }
 | |
|           c.nodes[i] = { ...c.nodes[i], id: id2 };
 | |
|         }
 | |
|         localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(c));
 | |
|         app.canvas.pasteFromClipboard();
 | |
|         localStorage.setItem("litegrapheditor_clipboard", backup);
 | |
|         const [x, y] = this.node.pos;
 | |
|         let top;
 | |
|         let left;
 | |
|         const selectedIds2 = ids.length ? ids : Object.keys(app.canvas.selected_nodes);
 | |
|         const newNodes2 = [];
 | |
|         for (let i = 0; i < selectedIds2.length; i++) {
 | |
|           const id2 = selectedIds2[i];
 | |
|           const newNode = app.graph.getNodeById(id2);
 | |
|           const innerNode = innerNodes[i];
 | |
|           newNodes2.push(newNode);
 | |
|           if (left == null || newNode.pos[0] < left) {
 | |
|             left = newNode.pos[0];
 | |
|           }
 | |
|           if (top == null || newNode.pos[1] < top) {
 | |
|             top = newNode.pos[1];
 | |
|           }
 | |
|           if (!newNode.widgets) continue;
 | |
|           const map = this.groupData.oldToNewWidgetMap[innerNode.index];
 | |
|           if (map) {
 | |
|             const widgets = Object.keys(map);
 | |
|             for (const oldName of widgets) {
 | |
|               const newName = map[oldName];
 | |
|               if (!newName) continue;
 | |
|               const widgetIndex = this.node.widgets.findIndex(
 | |
|                 (w) => w.name === newName
 | |
|               );
 | |
|               if (widgetIndex === -1) continue;
 | |
|               if (innerNode.type === "PrimitiveNode") {
 | |
|                 for (let i2 = 0; i2 < newNode.widgets.length; i2++) {
 | |
|                   newNode.widgets[i2].value = this.node.widgets[widgetIndex + i2].value;
 | |
|                 }
 | |
|               } else {
 | |
|                 const outerWidget = this.node.widgets[widgetIndex];
 | |
|                 const newWidget = newNode.widgets.find(
 | |
|                   (w) => w.name === oldName
 | |
|                 );
 | |
|                 if (!newWidget) continue;
 | |
|                 newWidget.value = outerWidget.value;
 | |
|                 for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) {
 | |
|                   newWidget.linkedWidgets[w].value = outerWidget.linkedWidgets[w].value;
 | |
|                 }
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         for (const newNode of newNodes2) {
 | |
|           newNode.pos = [
 | |
|             newNode.pos[0] - (left - x),
 | |
|             newNode.pos[1] - (top - y)
 | |
|           ];
 | |
|         }
 | |
|         return { newNodes: newNodes2, selectedIds: selectedIds2 };
 | |
|       }, "addInnerNodes");
 | |
|       const reconnectInputs = /* @__PURE__ */ __name((selectedIds2) => {
 | |
|         for (const innerNodeIndex in this.groupData.oldToNewInputMap) {
 | |
|           const id2 = selectedIds2[innerNodeIndex];
 | |
|           const newNode = app.graph.getNodeById(id2);
 | |
|           const map = this.groupData.oldToNewInputMap[innerNodeIndex];
 | |
|           for (const innerInputId in map) {
 | |
|             const groupSlotId = map[innerInputId];
 | |
|             if (groupSlotId == null) continue;
 | |
|             const slot = node.inputs[groupSlotId];
 | |
|             if (slot.link == null) continue;
 | |
|             const link = app.graph.links[slot.link];
 | |
|             if (!link) continue;
 | |
|             const originNode = app.graph.getNodeById(link.origin_id);
 | |
|             originNode.connect(link.origin_slot, newNode, +innerInputId);
 | |
|           }
 | |
|         }
 | |
|       }, "reconnectInputs");
 | |
|       const reconnectOutputs = /* @__PURE__ */ __name((selectedIds2) => {
 | |
|         for (let groupOutputId = 0; groupOutputId < node.outputs?.length; groupOutputId++) {
 | |
|           const output = node.outputs[groupOutputId];
 | |
|           if (!output.links) continue;
 | |
|           const links = [...output.links];
 | |
|           for (const l of links) {
 | |
|             const slot = this.groupData.newToOldOutputMap[groupOutputId];
 | |
|             const link = app.graph.links[l];
 | |
|             const targetNode = app.graph.getNodeById(link.target_id);
 | |
|             const newNode = app.graph.getNodeById(selectedIds2[slot.node.index]);
 | |
|             newNode.connect(slot.slot, targetNode, link.target_slot);
 | |
|           }
 | |
|         }
 | |
|       }, "reconnectOutputs");
 | |
|       const { newNodes, selectedIds } = addInnerNodes();
 | |
|       reconnectInputs(selectedIds);
 | |
|       reconnectOutputs(selectedIds);
 | |
|       app.graph.remove(this.node);
 | |
|       return newNodes;
 | |
|     };
 | |
|     const getExtraMenuOptions = this.node.getExtraMenuOptions;
 | |
|     this.node.getExtraMenuOptions = function(_, options) {
 | |
|       getExtraMenuOptions?.apply(this, arguments);
 | |
|       let optionIndex = options.findIndex((o) => o.content === "Outputs");
 | |
|       if (optionIndex === -1) optionIndex = options.length;
 | |
|       else optionIndex++;
 | |
|       options.splice(
 | |
|         optionIndex,
 | |
|         0,
 | |
|         null,
 | |
|         {
 | |
|           content: "Convert to nodes",
 | |
|           callback: /* @__PURE__ */ __name(() => {
 | |
|             return this.convertToNodes();
 | |
|           }, "callback")
 | |
|         },
 | |
|         {
 | |
|           content: "Manage Group Node",
 | |
|           callback: /* @__PURE__ */ __name(() => {
 | |
|             new ManageGroupDialog(app).show(this.type);
 | |
|           }, "callback")
 | |
|         }
 | |
|       );
 | |
|     };
 | |
|     const onDrawTitleBox = this.node.onDrawTitleBox;
 | |
|     this.node.onDrawTitleBox = function(ctx, height, size, scale) {
 | |
|       onDrawTitleBox?.apply(this, arguments);
 | |
|       const fill = ctx.fillStyle;
 | |
|       ctx.beginPath();
 | |
|       ctx.rect(11, -height + 11, 2, 2);
 | |
|       ctx.rect(14, -height + 11, 2, 2);
 | |
|       ctx.rect(17, -height + 11, 2, 2);
 | |
|       ctx.rect(11, -height + 14, 2, 2);
 | |
|       ctx.rect(14, -height + 14, 2, 2);
 | |
|       ctx.rect(17, -height + 14, 2, 2);
 | |
|       ctx.rect(11, -height + 17, 2, 2);
 | |
|       ctx.rect(14, -height + 17, 2, 2);
 | |
|       ctx.rect(17, -height + 17, 2, 2);
 | |
|       ctx.fillStyle = this.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
 | |
|       ctx.fill();
 | |
|       ctx.fillStyle = fill;
 | |
|     };
 | |
|     const onDrawForeground = node.onDrawForeground;
 | |
|     const groupData = this.groupData.nodeData;
 | |
|     node.onDrawForeground = function(ctx) {
 | |
|       const r = onDrawForeground?.apply?.(this, arguments);
 | |
|       if (+app.runningNodeId === this.id && this.runningInternalNodeId !== null) {
 | |
|         const n = groupData.nodes[this.runningInternalNodeId];
 | |
|         if (!n) return;
 | |
|         const message = `Running ${n.title || n.type} (${this.runningInternalNodeId}/${groupData.nodes.length})`;
 | |
|         ctx.save();
 | |
|         ctx.font = "12px sans-serif";
 | |
|         const sz = ctx.measureText(message);
 | |
|         ctx.fillStyle = node.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
 | |
|         ctx.beginPath();
 | |
|         ctx.roundRect(
 | |
|           0,
 | |
|           -LiteGraph.NODE_TITLE_HEIGHT - 20,
 | |
|           sz.width + 12,
 | |
|           20,
 | |
|           5
 | |
|         );
 | |
|         ctx.fill();
 | |
|         ctx.fillStyle = "#fff";
 | |
|         ctx.fillText(message, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
 | |
|         ctx.restore();
 | |
|       }
 | |
|     };
 | |
|     const onExecutionStart = this.node.onExecutionStart;
 | |
|     this.node.onExecutionStart = function() {
 | |
|       this.resetExecution = true;
 | |
|       return onExecutionStart?.apply(this, arguments);
 | |
|     };
 | |
|     const self = this;
 | |
|     const onNodeCreated = this.node.onNodeCreated;
 | |
|     this.node.onNodeCreated = function() {
 | |
|       if (!this.widgets) {
 | |
|         return;
 | |
|       }
 | |
|       const config = self.groupData.nodeData.config;
 | |
|       if (config) {
 | |
|         for (const n in config) {
 | |
|           const inputs = config[n]?.input;
 | |
|           for (const w in inputs) {
 | |
|             if (inputs[w].visible !== false) continue;
 | |
|             const widgetName = self.groupData.oldToNewWidgetMap[n][w];
 | |
|             const widget = this.widgets.find((w2) => w2.name === widgetName);
 | |
|             if (widget) {
 | |
|               widget.type = "hidden";
 | |
|               widget.computeSize = () => [0, -4];
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return onNodeCreated?.apply(this, arguments);
 | |
|     };
 | |
|     function handleEvent(type, getId, getEvent) {
 | |
|       const handler = /* @__PURE__ */ __name(({ detail }) => {
 | |
|         const id2 = getId(detail);
 | |
|         if (!id2) return;
 | |
|         const node2 = app.graph.getNodeById(id2);
 | |
|         if (node2) return;
 | |
|         const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id2);
 | |
|         if (innerNodeIndex > -1) {
 | |
|           this.node.runningInternalNodeId = innerNodeIndex;
 | |
|           api.dispatchEvent(
 | |
|             new CustomEvent(type, {
 | |
|               detail: getEvent(detail, this.node.id + "", this.node)
 | |
|             })
 | |
|           );
 | |
|         }
 | |
|       }, "handler");
 | |
|       api.addEventListener(type, handler);
 | |
|       return handler;
 | |
|     }
 | |
|     __name(handleEvent, "handleEvent");
 | |
|     const executing = handleEvent.call(
 | |
|       this,
 | |
|       "executing",
 | |
|       (d) => d,
 | |
|       (d, id2, node2) => id2
 | |
|     );
 | |
|     const executed = handleEvent.call(
 | |
|       this,
 | |
|       "executed",
 | |
|       (d) => d?.display_node || d?.node,
 | |
|       (d, id2, node2) => ({
 | |
|         ...d,
 | |
|         node: id2,
 | |
|         display_node: id2,
 | |
|         merge: !node2.resetExecution
 | |
|       })
 | |
|     );
 | |
|     const onRemoved = node.onRemoved;
 | |
|     this.node.onRemoved = function() {
 | |
|       onRemoved?.apply(this, arguments);
 | |
|       api.removeEventListener("executing", executing);
 | |
|       api.removeEventListener("executed", executed);
 | |
|     };
 | |
|     this.node.refreshComboInNode = (defs) => {
 | |
|       for (const widgetName in this.groupData.newToOldWidgetMap) {
 | |
|         const widget = this.node.widgets.find((w) => w.name === widgetName);
 | |
|         if (widget?.type === "combo") {
 | |
|           const old = this.groupData.newToOldWidgetMap[widgetName];
 | |
|           const def = defs[old.node.type];
 | |
|           const input = def?.input?.required?.[old.inputName] ?? def?.input?.optional?.[old.inputName];
 | |
|           if (!input) continue;
 | |
|           widget.options.values = input[0];
 | |
|           if (old.inputName !== "image" && !widget.options.values.includes(widget.value)) {
 | |
|             widget.value = widget.options.values[0];
 | |
|             widget.callback(widget.value);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|   }
 | |
|   updateInnerWidgets() {
 | |
|     for (const newWidgetName in this.groupData.newToOldWidgetMap) {
 | |
|       const newWidget = this.node.widgets.find((w) => w.name === newWidgetName);
 | |
|       if (!newWidget) continue;
 | |
|       const newValue = newWidget.value;
 | |
|       const old = this.groupData.newToOldWidgetMap[newWidgetName];
 | |
|       let innerNode = this.innerNodes[old.node.index];
 | |
|       if (innerNode.type === "PrimitiveNode") {
 | |
|         innerNode.primitiveValue = newValue;
 | |
|         const primitiveLinked = this.groupData.primitiveToWidget[old.node.index];
 | |
|         for (const linked of primitiveLinked ?? []) {
 | |
|           const node = this.innerNodes[linked.nodeId];
 | |
|           const widget2 = node.widgets.find((w) => w.name === linked.inputName);
 | |
|           if (widget2) {
 | |
|             widget2.value = newValue;
 | |
|           }
 | |
|         }
 | |
|         continue;
 | |
|       } else if (innerNode.type === "Reroute") {
 | |
|         const rerouteLinks = this.groupData.linksFrom[old.node.index];
 | |
|         if (rerouteLinks) {
 | |
|           for (const [_, , targetNodeId, targetSlot] of rerouteLinks["0"]) {
 | |
|             const node = this.innerNodes[targetNodeId];
 | |
|             const input = node.inputs[targetSlot];
 | |
|             if (input.widget) {
 | |
|               const widget2 = node.widgets?.find(
 | |
|                 (w) => w.name === input.widget.name
 | |
|               );
 | |
|               if (widget2) {
 | |
|                 widget2.value = newValue;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       const widget = innerNode.widgets?.find((w) => w.name === old.inputName);
 | |
|       if (widget) {
 | |
|         widget.value = newValue;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   populatePrimitive(node, nodeId, oldName, i, linkedShift) {
 | |
|     const primitiveId = this.groupData.widgetToPrimitive[nodeId]?.[oldName];
 | |
|     if (primitiveId == null) return;
 | |
|     const targetWidgetName = this.groupData.oldToNewWidgetMap[primitiveId]["value"];
 | |
|     const targetWidgetIndex = this.node.widgets.findIndex(
 | |
|       (w) => w.name === targetWidgetName
 | |
|     );
 | |
|     if (targetWidgetIndex > -1) {
 | |
|       const primitiveNode = this.innerNodes[primitiveId];
 | |
|       let len = primitiveNode.widgets.length;
 | |
|       if (len - 1 !== this.node.widgets[targetWidgetIndex].linkedWidgets?.length) {
 | |
|         len = 1;
 | |
|       }
 | |
|       for (let i2 = 0; i2 < len; i2++) {
 | |
|         this.node.widgets[targetWidgetIndex + i2].value = primitiveNode.widgets[i2].value;
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
|   populateReroute(node, nodeId, map) {
 | |
|     if (node.type !== "Reroute") return;
 | |
|     const link = this.groupData.linksFrom[nodeId]?.[0]?.[0];
 | |
|     if (!link) return;
 | |
|     const [, , targetNodeId, targetNodeSlot] = link;
 | |
|     const targetNode = this.groupData.nodeData.nodes[targetNodeId];
 | |
|     const inputs = targetNode.inputs;
 | |
|     const targetWidget = inputs?.[targetNodeSlot]?.widget;
 | |
|     if (!targetWidget) return;
 | |
|     const offset = inputs.length - (targetNode.widgets_values?.length ?? 0);
 | |
|     const v = targetNode.widgets_values?.[targetNodeSlot - offset];
 | |
|     if (v == null) return;
 | |
|     const widgetName = Object.values(map)[0];
 | |
|     const widget = this.node.widgets.find((w) => w.name === widgetName);
 | |
|     if (widget) {
 | |
|       widget.value = v;
 | |
|     }
 | |
|   }
 | |
|   populateWidgets() {
 | |
|     if (!this.node.widgets) return;
 | |
|     for (let nodeId = 0; nodeId < this.groupData.nodeData.nodes.length; nodeId++) {
 | |
|       const node = this.groupData.nodeData.nodes[nodeId];
 | |
|       const map = this.groupData.oldToNewWidgetMap[nodeId] ?? {};
 | |
|       const widgets = Object.keys(map);
 | |
|       if (!node.widgets_values?.length) {
 | |
|         this.populateReroute(node, nodeId, map);
 | |
|         continue;
 | |
|       }
 | |
|       let linkedShift = 0;
 | |
|       for (let i = 0; i < widgets.length; i++) {
 | |
|         const oldName = widgets[i];
 | |
|         const newName = map[oldName];
 | |
|         const widgetIndex = this.node.widgets.findIndex(
 | |
|           (w) => w.name === newName
 | |
|         );
 | |
|         const mainWidget = this.node.widgets[widgetIndex];
 | |
|         if (this.populatePrimitive(node, nodeId, oldName, i, linkedShift) || widgetIndex === -1) {
 | |
|           const innerWidget = this.innerNodes[nodeId].widgets?.find(
 | |
|             (w) => w.name === oldName
 | |
|           );
 | |
|           linkedShift += innerWidget?.linkedWidgets?.length ?? 0;
 | |
|         }
 | |
|         if (widgetIndex === -1) {
 | |
|           continue;
 | |
|         }
 | |
|         mainWidget.value = node.widgets_values[i + linkedShift];
 | |
|         for (let w = 0; w < mainWidget.linkedWidgets?.length; w++) {
 | |
|           this.node.widgets[widgetIndex + w + 1].value = node.widgets_values[i + ++linkedShift];
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   replaceNodes(nodes) {
 | |
|     let top;
 | |
|     let left;
 | |
|     for (let i = 0; i < nodes.length; i++) {
 | |
|       const node = nodes[i];
 | |
|       if (left == null || node.pos[0] < left) {
 | |
|         left = node.pos[0];
 | |
|       }
 | |
|       if (top == null || node.pos[1] < top) {
 | |
|         top = node.pos[1];
 | |
|       }
 | |
|       this.linkOutputs(node, i);
 | |
|       app.graph.remove(node);
 | |
|     }
 | |
|     this.linkInputs();
 | |
|     this.node.pos = [left, top];
 | |
|   }
 | |
|   linkOutputs(originalNode, nodeId) {
 | |
|     if (!originalNode.outputs) return;
 | |
|     for (const output of originalNode.outputs) {
 | |
|       if (!output.links) continue;
 | |
|       const links = [...output.links];
 | |
|       for (const l of links) {
 | |
|         const link = app.graph.links[l];
 | |
|         if (!link) continue;
 | |
|         const targetNode = app.graph.getNodeById(link.target_id);
 | |
|         const newSlot = this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot];
 | |
|         if (newSlot != null) {
 | |
|           this.node.connect(newSlot, targetNode, link.target_slot);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   linkInputs() {
 | |
|     for (const link of this.groupData.nodeData.links ?? []) {
 | |
|       const [, originSlot, targetId, targetSlot, actualOriginId] = link;
 | |
|       const originNode = app.graph.getNodeById(actualOriginId);
 | |
|       if (!originNode) continue;
 | |
|       originNode.connect(
 | |
|         originSlot,
 | |
|         this.node.id,
 | |
|         this.groupData.oldToNewInputMap[targetId][targetSlot]
 | |
|       );
 | |
|     }
 | |
|   }
 | |
|   static getGroupData(node) {
 | |
|     return (node.nodeData ?? node.constructor?.nodeData)?.[GROUP];
 | |
|   }
 | |
|   static isGroupNode(node) {
 | |
|     return !!node.constructor?.nodeData?.[GROUP];
 | |
|   }
 | |
|   static async fromNodes(nodes) {
 | |
|     const builder = new GroupNodeBuilder(nodes);
 | |
|     const res = builder.build();
 | |
|     if (!res) return;
 | |
|     const { name, nodeData } = res;
 | |
|     const config = new GroupNodeConfig(name, nodeData);
 | |
|     await config.registerType();
 | |
|     const groupNode = LiteGraph.createNode(`workflow/${name}`);
 | |
|     groupNode.setInnerNodes(builder.nodes);
 | |
|     groupNode[GROUP].populateWidgets();
 | |
|     app.graph.add(groupNode);
 | |
|     groupNode[GROUP].replaceNodes(builder.nodes);
 | |
|     return groupNode;
 | |
|   }
 | |
| }
 | |
| function addConvertToGroupOptions() {
 | |
|   function addConvertOption(options, index) {
 | |
|     const selected = Object.values(app.canvas.selected_nodes ?? {});
 | |
|     const disabled = selected.length < 2 || selected.find((n) => GroupNodeHandler.isGroupNode(n));
 | |
|     options.splice(index + 1, null, {
 | |
|       content: `Convert to Group Node`,
 | |
|       disabled,
 | |
|       callback: /* @__PURE__ */ __name(async () => {
 | |
|         return await GroupNodeHandler.fromNodes(selected);
 | |
|       }, "callback")
 | |
|     });
 | |
|   }
 | |
|   __name(addConvertOption, "addConvertOption");
 | |
|   function addManageOption(options, index) {
 | |
|     const groups = app.graph.extra?.groupNodes;
 | |
|     const disabled = !groups || !Object.keys(groups).length;
 | |
|     options.splice(index + 1, null, {
 | |
|       content: `Manage Group Nodes`,
 | |
|       disabled,
 | |
|       callback: /* @__PURE__ */ __name(() => {
 | |
|         new ManageGroupDialog(app).show();
 | |
|       }, "callback")
 | |
|     });
 | |
|   }
 | |
|   __name(addManageOption, "addManageOption");
 | |
|   const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
 | |
|   LGraphCanvas.prototype.getCanvasMenuOptions = function() {
 | |
|     const options = getCanvasMenuOptions.apply(this, arguments);
 | |
|     const index = options.findIndex((o) => o?.content === "Add Group") + 1 || options.length;
 | |
|     addConvertOption(options, index);
 | |
|     addManageOption(options, index + 1);
 | |
|     return options;
 | |
|   };
 | |
|   const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions;
 | |
|   LGraphCanvas.prototype.getNodeMenuOptions = function(node) {
 | |
|     const options = getNodeMenuOptions.apply(this, arguments);
 | |
|     if (!GroupNodeHandler.isGroupNode(node)) {
 | |
|       const index = options.findIndex((o) => o?.content === "Outputs") + 1 || options.length - 1;
 | |
|       addConvertOption(options, index);
 | |
|     }
 | |
|     return options;
 | |
|   };
 | |
| }
 | |
| __name(addConvertToGroupOptions, "addConvertToGroupOptions");
 | |
| const id$3 = "Comfy.GroupNode";
 | |
| let globalDefs;
 | |
| const ext$1 = {
 | |
|   name: id$3,
 | |
|   setup() {
 | |
|     addConvertToGroupOptions();
 | |
|   },
 | |
|   async beforeConfigureGraph(graphData, missingNodeTypes) {
 | |
|     const nodes = graphData?.extra?.groupNodes;
 | |
|     if (nodes) {
 | |
|       await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes);
 | |
|     }
 | |
|   },
 | |
|   addCustomNodeDefs(defs) {
 | |
|     globalDefs = defs;
 | |
|   },
 | |
|   nodeCreated(node) {
 | |
|     if (GroupNodeHandler.isGroupNode(node)) {
 | |
|       node[GROUP] = new GroupNodeHandler(node);
 | |
|     }
 | |
|   },
 | |
|   async refreshComboInNodes(defs) {
 | |
|     Object.assign(globalDefs, defs);
 | |
|     const nodes = app.graph.extra?.groupNodes;
 | |
|     if (nodes) {
 | |
|       await GroupNodeConfig.registerFromWorkflow(nodes, {});
 | |
|     }
 | |
|   }
 | |
| };
 | |
| app.registerExtension(ext$1);
 | |
| window.comfyAPI = window.comfyAPI || {};
 | |
| window.comfyAPI.groupNode = window.comfyAPI.groupNode || {};
 | |
| window.comfyAPI.groupNode.GroupNodeConfig = GroupNodeConfig;
 | |
| window.comfyAPI.groupNode.GroupNodeHandler = GroupNodeHandler;
 | |
| function setNodeMode(node, mode) {
 | |
|   node.mode = mode;
 | |
|   node.graph.change();
 | |
| }
 | |
| __name(setNodeMode, "setNodeMode");
 | |
| function addNodesToGroup(group, nodes = []) {
 | |
|   var x1, y1, x2, y2;
 | |
|   var nx1, ny1, nx2, ny2;
 | |
|   var node;
 | |
|   x1 = y1 = x2 = y2 = -1;
 | |
|   nx1 = ny1 = nx2 = ny2 = -1;
 | |
|   for (var n of [group._nodes, nodes]) {
 | |
|     for (var i in n) {
 | |
|       node = n[i];
 | |
|       nx1 = node.pos[0];
 | |
|       ny1 = node.pos[1];
 | |
|       nx2 = node.pos[0] + node.size[0];
 | |
|       ny2 = node.pos[1] + node.size[1];
 | |
|       if (node.type != "Reroute") {
 | |
|         ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
 | |
|       }
 | |
|       if (node.flags?.collapsed) {
 | |
|         ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
 | |
|         if (node?._collapsed_width) {
 | |
|           nx2 = nx1 + Math.round(node._collapsed_width);
 | |
|         }
 | |
|       }
 | |
|       if (x1 == -1 || nx1 < x1) {
 | |
|         x1 = nx1;
 | |
|       }
 | |
|       if (y1 == -1 || ny1 < y1) {
 | |
|         y1 = ny1;
 | |
|       }
 | |
|       if (x2 == -1 || nx2 > x2) {
 | |
|         x2 = nx2;
 | |
|       }
 | |
|       if (y2 == -1 || ny2 > y2) {
 | |
|         y2 = ny2;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   var padding = 10;
 | |
|   y1 = y1 - Math.round(group.font_size * 1.4);
 | |
|   group.pos = [x1 - padding, y1 - padding];
 | |
|   group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
 | |
| }
 | |
| __name(addNodesToGroup, "addNodesToGroup");
 | |
| app.registerExtension({
 | |
|   name: "Comfy.GroupOptions",
 | |
|   setup() {
 | |
|     const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
 | |
|     LGraphCanvas.prototype.getCanvasMenuOptions = function() {
 | |
|       const options = orig.apply(this, arguments);
 | |
|       const group = this.graph.getGroupOnPos(
 | |
|         this.graph_mouse[0],
 | |
|         this.graph_mouse[1]
 | |
|       );
 | |
|       if (!group) {
 | |
|         options.push({
 | |
|           content: "Add Group For Selected Nodes",
 | |
|           disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
 | |
|           callback: /* @__PURE__ */ __name(() => {
 | |
|             const group2 = new LGraphGroup();
 | |
|             addNodesToGroup(group2, this.selected_nodes);
 | |
|             app.canvas.graph.add(group2);
 | |
|             this.graph.change();
 | |
|           }, "callback")
 | |
|         });
 | |
|         return options;
 | |
|       }
 | |
|       group.recomputeInsideNodes();
 | |
|       const nodesInGroup = group._nodes;
 | |
|       options.push({
 | |
|         content: "Add Selected Nodes To Group",
 | |
|         disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
 | |
|         callback: /* @__PURE__ */ __name(() => {
 | |
|           addNodesToGroup(group, this.selected_nodes);
 | |
|           this.graph.change();
 | |
|         }, "callback")
 | |
|       });
 | |
|       if (nodesInGroup.length === 0) {
 | |
|         return options;
 | |
|       } else {
 | |
|         options.push(null);
 | |
|       }
 | |
|       let allNodesAreSameMode = true;
 | |
|       for (let i = 1; i < nodesInGroup.length; i++) {
 | |
|         if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
 | |
|           allNodesAreSameMode = false;
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|       options.push({
 | |
|         content: "Fit Group To Nodes",
 | |
|         callback: /* @__PURE__ */ __name(() => {
 | |
|           addNodesToGroup(group);
 | |
|           this.graph.change();
 | |
|         }, "callback")
 | |
|       });
 | |
|       options.push({
 | |
|         content: "Select Nodes",
 | |
|         callback: /* @__PURE__ */ __name(() => {
 | |
|           this.selectNodes(nodesInGroup);
 | |
|           this.graph.change();
 | |
|           this.canvas.focus();
 | |
|         }, "callback")
 | |
|       });
 | |
|       if (allNodesAreSameMode) {
 | |
|         const mode = nodesInGroup[0].mode;
 | |
|         switch (mode) {
 | |
|           case 0:
 | |
|             options.push({
 | |
|               content: "Set Group Nodes to Never",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 2);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             options.push({
 | |
|               content: "Bypass Group Nodes",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 4);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             break;
 | |
|           case 2:
 | |
|             options.push({
 | |
|               content: "Set Group Nodes to Always",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 0);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             options.push({
 | |
|               content: "Bypass Group Nodes",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 4);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             break;
 | |
|           case 4:
 | |
|             options.push({
 | |
|               content: "Set Group Nodes to Always",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 0);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             options.push({
 | |
|               content: "Set Group Nodes to Never",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 2);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             break;
 | |
|           default:
 | |
|             options.push({
 | |
|               content: "Set Group Nodes to Always",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 0);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             options.push({
 | |
|               content: "Set Group Nodes to Never",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 2);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             options.push({
 | |
|               content: "Bypass Group Nodes",
 | |
|               callback: /* @__PURE__ */ __name(() => {
 | |
|                 for (const node of nodesInGroup) {
 | |
|                   setNodeMode(node, 4);
 | |
|                 }
 | |
|               }, "callback")
 | |
|             });
 | |
|             break;
 | |
|         }
 | |
|       } else {
 | |
|         options.push({
 | |
|           content: "Set Group Nodes to Always",
 | |
|           callback: /* @__PURE__ */ __name(() => {
 | |
|             for (const node of nodesInGroup) {
 | |
|               setNodeMode(node, 0);
 | |
|             }
 | |
|           }, "callback")
 | |
|         });
 | |
|         options.push({
 | |
|           content: "Set Group Nodes to Never",
 | |
|           callback: /* @__PURE__ */ __name(() => {
 | |
|             for (const node of nodesInGroup) {
 | |
|               setNodeMode(node, 2);
 | |
|             }
 | |
|           }, "callback")
 | |
|         });
 | |
|         options.push({
 | |
|           content: "Bypass Group Nodes",
 | |
|           callback: /* @__PURE__ */ __name(() => {
 | |
|             for (const node of nodesInGroup) {
 | |
|               setNodeMode(node, 4);
 | |
|             }
 | |
|           }, "callback")
 | |
|         });
 | |
|       }
 | |
|       return options;
 | |
|     };
 | |
|   }
 | |
| });
 | |
| const id$2 = "Comfy.InvertMenuScrolling";
 | |
| app.registerExtension({
 | |
|   name: id$2,
 | |
|   init() {
 | |
|     const ctxMenu = LiteGraph.ContextMenu;
 | |
|     const replace = /* @__PURE__ */ __name(() => {
 | |
|       LiteGraph.ContextMenu = function(values, options) {
 | |
|         options = options || {};
 | |
|         if (options.scroll_speed) {
 | |
|           options.scroll_speed *= -1;
 | |
|         } else {
 | |
|           options.scroll_speed = -0.1;
 | |
|         }
 | |
|         return ctxMenu.call(this, values, options);
 | |
|       };
 | |
|       LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
 | |
|     }, "replace");
 | |
|     app.ui.settings.addSetting({
 | |
|       id: id$2,
 | |
|       category: ["Comfy", "Graph", "InvertMenuScrolling"],
 | |
|       name: "Invert Context Menu Scrolling",
 | |
|       type: "boolean",
 | |
|       defaultValue: false,
 | |
|       onChange(value) {
 | |
|         if (value) {
 | |
|           replace();
 | |
|         } else {
 | |
|           LiteGraph.ContextMenu = ctxMenu;
 | |
|         }
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| });
 | |
| app.registerExtension({
 | |
|   name: "Comfy.Keybinds",
 | |
|   init() {
 | |
|     const keybindListener = /* @__PURE__ */ __name(async function(event) {
 | |
|       const modifierPressed = event.ctrlKey || event.metaKey;
 | |
|       if (modifierPressed && event.key === "Enter") {
 | |
|         if (event.altKey) {
 | |
|           await api.interrupt();
 | |
|           useToastStore().add({
 | |
|             severity: "info",
 | |
|             summary: "Interrupted",
 | |
|             detail: "Execution has been interrupted",
 | |
|             life: 1e3
 | |
|           });
 | |
|           return;
 | |
|         }
 | |
|         app.queuePrompt(event.shiftKey ? -1 : 0).then();
 | |
|         return;
 | |
|       }
 | |
|       const target = event.composedPath()[0];
 | |
|       if (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.tagName === "SPAN" && target.classList.contains("property_value")) {
 | |
|         return;
 | |
|       }
 | |
|       const modifierKeyIdMap = {
 | |
|         s: "#comfy-save-button",
 | |
|         o: "#comfy-file-input",
 | |
|         Backspace: "#comfy-clear-button",
 | |
|         d: "#comfy-load-default-button",
 | |
|         g: "#comfy-group-selected-nodes-button"
 | |
|       };
 | |
|       const modifierKeybindId = modifierKeyIdMap[event.key];
 | |
|       if (modifierPressed && modifierKeybindId) {
 | |
|         event.preventDefault();
 | |
|         const elem = document.querySelector(modifierKeybindId);
 | |
|         elem.click();
 | |
|         return;
 | |
|       }
 | |
|       if (event.ctrlKey || event.altKey || event.metaKey) {
 | |
|         return;
 | |
|       }
 | |
|       if (event.key === "Escape") {
 | |
|         const modals = document.querySelectorAll(".comfy-modal");
 | |
|         const modal = Array.from(modals).find(
 | |
|           (modal2) => window.getComputedStyle(modal2).getPropertyValue("display") !== "none"
 | |
|         );
 | |
|         if (modal) {
 | |
|           modal.style.display = "none";
 | |
|         }
 | |
|         ;
 | |
|         [...document.querySelectorAll("dialog")].forEach((d) => {
 | |
|           d.close();
 | |
|         });
 | |
|       }
 | |
|       const keyIdMap = {
 | |
|         q: ".queue-tab-button.side-bar-button",
 | |
|         h: ".queue-tab-button.side-bar-button",
 | |
|         r: "#comfy-refresh-button"
 | |
|       };
 | |
|       const buttonId = keyIdMap[event.key];
 | |
|       if (buttonId) {
 | |
|         const button = document.querySelector(buttonId);
 | |
|         button.click();
 | |
|       }
 | |
|     }, "keybindListener");
 | |
|     window.addEventListener("keydown", keybindListener, true);
 | |
|   }
 | |
| });
 | |
| const id$1 = "Comfy.LinkRenderMode";
 | |
| const ext = {
 | |
|   name: id$1,
 | |
|   async setup(app2) {
 | |
|     app2.ui.settings.addSetting({
 | |
|       id: id$1,
 | |
|       category: ["Comfy", "Graph", "LinkRenderMode"],
 | |
|       name: "Link Render Mode",
 | |
|       defaultValue: 2,
 | |
|       type: "combo",
 | |
|       // @ts-expect-error
 | |
|       options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
 | |
|         value: i,
 | |
|         text: m,
 | |
|         selected: i == app2.canvas.links_render_mode
 | |
|       })),
 | |
|       onChange(value) {
 | |
|         app2.canvas.links_render_mode = +value;
 | |
|         app2.graph.setDirtyCanvas(true);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| };
 | |
| app.registerExtension(ext);
 | |
| function dataURLToBlob(dataURL) {
 | |
|   const parts = dataURL.split(";base64,");
 | |
|   const contentType = parts[0].split(":")[1];
 | |
|   const byteString = atob(parts[1]);
 | |
|   const arrayBuffer = new ArrayBuffer(byteString.length);
 | |
|   const uint8Array = new Uint8Array(arrayBuffer);
 | |
|   for (let i = 0; i < byteString.length; i++) {
 | |
|     uint8Array[i] = byteString.charCodeAt(i);
 | |
|   }
 | |
|   return new Blob([arrayBuffer], { type: contentType });
 | |
| }
 | |
| __name(dataURLToBlob, "dataURLToBlob");
 | |
| function loadedImageToBlob(image) {
 | |
|   const canvas = document.createElement("canvas");
 | |
|   canvas.width = image.width;
 | |
|   canvas.height = image.height;
 | |
|   const ctx = canvas.getContext("2d");
 | |
|   ctx.drawImage(image, 0, 0);
 | |
|   const dataURL = canvas.toDataURL("image/png", 1);
 | |
|   const blob = dataURLToBlob(dataURL);
 | |
|   return blob;
 | |
| }
 | |
| __name(loadedImageToBlob, "loadedImageToBlob");
 | |
| function loadImage(imagePath) {
 | |
|   return new Promise((resolve, reject) => {
 | |
|     const image = new Image();
 | |
|     image.onload = function() {
 | |
|       resolve(image);
 | |
|     };
 | |
|     image.src = imagePath;
 | |
|   });
 | |
| }
 | |
| __name(loadImage, "loadImage");
 | |
| async function uploadMask(filepath, formData) {
 | |
|   await api.fetchApi("/upload/mask", {
 | |
|     method: "POST",
 | |
|     body: formData
 | |
|   }).then((response) => {
 | |
|   }).catch((error) => {
 | |
|     console.error("Error:", error);
 | |
|   });
 | |
|   ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]] = new Image();
 | |
|   ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src = api.apiURL(
 | |
|     "/view?" + new URLSearchParams(filepath).toString() + app.getPreviewFormatParam() + app.getRandParam()
 | |
|   );
 | |
|   if (ComfyApp.clipspace.images)
 | |
|     ComfyApp.clipspace.images[ComfyApp.clipspace["selectedIndex"]] = filepath;
 | |
|   ClipspaceDialog.invalidatePreview();
 | |
| }
 | |
| __name(uploadMask, "uploadMask");
 | |
| function prepare_mask(image, maskCanvas, maskCtx, maskColor) {
 | |
|   maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height);
 | |
|   const maskData = maskCtx.getImageData(
 | |
|     0,
 | |
|     0,
 | |
|     maskCanvas.width,
 | |
|     maskCanvas.height
 | |
|   );
 | |
|   for (let i = 0; i < maskData.data.length; i += 4) {
 | |
|     if (maskData.data[i + 3] == 255) maskData.data[i + 3] = 0;
 | |
|     else maskData.data[i + 3] = 255;
 | |
|     maskData.data[i] = maskColor.r;
 | |
|     maskData.data[i + 1] = maskColor.g;
 | |
|     maskData.data[i + 2] = maskColor.b;
 | |
|   }
 | |
|   maskCtx.globalCompositeOperation = "source-over";
 | |
|   maskCtx.putImageData(maskData, 0, 0);
 | |
| }
 | |
| __name(prepare_mask, "prepare_mask");
 | |
| class MaskEditorDialog extends ComfyDialog {
 | |
|   static {
 | |
|     __name(this, "MaskEditorDialog");
 | |
|   }
 | |
|   static instance = null;
 | |
|   static mousedown_x = null;
 | |
|   static mousedown_y = null;
 | |
|   brush;
 | |
|   maskCtx;
 | |
|   maskCanvas;
 | |
|   brush_size_slider;
 | |
|   brush_opacity_slider;
 | |
|   colorButton;
 | |
|   saveButton;
 | |
|   zoom_ratio;
 | |
|   pan_x;
 | |
|   pan_y;
 | |
|   imgCanvas;
 | |
|   last_display_style;
 | |
|   is_visible;
 | |
|   image;
 | |
|   handler_registered;
 | |
|   brush_slider_input;
 | |
|   cursorX;
 | |
|   cursorY;
 | |
|   mousedown_pan_x;
 | |
|   mousedown_pan_y;
 | |
|   last_pressure;
 | |
|   static getInstance() {
 | |
|     if (!MaskEditorDialog.instance) {
 | |
|       MaskEditorDialog.instance = new MaskEditorDialog();
 | |
|     }
 | |
|     return MaskEditorDialog.instance;
 | |
|   }
 | |
|   is_layout_created = false;
 | |
|   constructor() {
 | |
|     super();
 | |
|     this.element = $el("div.comfy-modal", { parent: document.body }, [
 | |
|       $el("div.comfy-modal-content", [...this.createButtons()])
 | |
|     ]);
 | |
|   }
 | |
|   createButtons() {
 | |
|     return [];
 | |
|   }
 | |
|   createButton(name, callback) {
 | |
|     var button = document.createElement("button");
 | |
|     button.style.pointerEvents = "auto";
 | |
|     button.innerText = name;
 | |
|     button.addEventListener("click", callback);
 | |
|     return button;
 | |
|   }
 | |
|   createLeftButton(name, callback) {
 | |
|     var button = this.createButton(name, callback);
 | |
|     button.style.cssFloat = "left";
 | |
|     button.style.marginRight = "4px";
 | |
|     return button;
 | |
|   }
 | |
|   createRightButton(name, callback) {
 | |
|     var button = this.createButton(name, callback);
 | |
|     button.style.cssFloat = "right";
 | |
|     button.style.marginLeft = "4px";
 | |
|     return button;
 | |
|   }
 | |
|   createLeftSlider(self, name, callback) {
 | |
|     const divElement = document.createElement("div");
 | |
|     divElement.id = "maskeditor-slider";
 | |
|     divElement.style.cssFloat = "left";
 | |
|     divElement.style.fontFamily = "sans-serif";
 | |
|     divElement.style.marginRight = "4px";
 | |
|     divElement.style.color = "var(--input-text)";
 | |
|     divElement.style.backgroundColor = "var(--comfy-input-bg)";
 | |
|     divElement.style.borderRadius = "8px";
 | |
|     divElement.style.borderColor = "var(--border-color)";
 | |
|     divElement.style.borderStyle = "solid";
 | |
|     divElement.style.fontSize = "15px";
 | |
|     divElement.style.height = "21px";
 | |
|     divElement.style.padding = "1px 6px";
 | |
|     divElement.style.display = "flex";
 | |
|     divElement.style.position = "relative";
 | |
|     divElement.style.top = "2px";
 | |
|     divElement.style.pointerEvents = "auto";
 | |
|     self.brush_slider_input = document.createElement("input");
 | |
|     self.brush_slider_input.setAttribute("type", "range");
 | |
|     self.brush_slider_input.setAttribute("min", "1");
 | |
|     self.brush_slider_input.setAttribute("max", "100");
 | |
|     self.brush_slider_input.setAttribute("value", "10");
 | |
|     const labelElement = document.createElement("label");
 | |
|     labelElement.textContent = name;
 | |
|     divElement.appendChild(labelElement);
 | |
|     divElement.appendChild(self.brush_slider_input);
 | |
|     self.brush_slider_input.addEventListener("change", callback);
 | |
|     return divElement;
 | |
|   }
 | |
|   createOpacitySlider(self, name, callback) {
 | |
|     const divElement = document.createElement("div");
 | |
|     divElement.id = "maskeditor-opacity-slider";
 | |
|     divElement.style.cssFloat = "left";
 | |
|     divElement.style.fontFamily = "sans-serif";
 | |
|     divElement.style.marginRight = "4px";
 | |
|     divElement.style.color = "var(--input-text)";
 | |
|     divElement.style.backgroundColor = "var(--comfy-input-bg)";
 | |
|     divElement.style.borderRadius = "8px";
 | |
|     divElement.style.borderColor = "var(--border-color)";
 | |
|     divElement.style.borderStyle = "solid";
 | |
|     divElement.style.fontSize = "15px";
 | |
|     divElement.style.height = "21px";
 | |
|     divElement.style.padding = "1px 6px";
 | |
|     divElement.style.display = "flex";
 | |
|     divElement.style.position = "relative";
 | |
|     divElement.style.top = "2px";
 | |
|     divElement.style.pointerEvents = "auto";
 | |
|     self.opacity_slider_input = document.createElement("input");
 | |
|     self.opacity_slider_input.setAttribute("type", "range");
 | |
|     self.opacity_slider_input.setAttribute("min", "0.1");
 | |
|     self.opacity_slider_input.setAttribute("max", "1.0");
 | |
|     self.opacity_slider_input.setAttribute("step", "0.01");
 | |
|     self.opacity_slider_input.setAttribute("value", "0.7");
 | |
|     const labelElement = document.createElement("label");
 | |
|     labelElement.textContent = name;
 | |
|     divElement.appendChild(labelElement);
 | |
|     divElement.appendChild(self.opacity_slider_input);
 | |
|     self.opacity_slider_input.addEventListener("input", callback);
 | |
|     return divElement;
 | |
|   }
 | |
|   setlayout(imgCanvas, maskCanvas) {
 | |
|     const self = this;
 | |
|     var bottom_panel = document.createElement("div");
 | |
|     bottom_panel.style.position = "absolute";
 | |
|     bottom_panel.style.bottom = "0px";
 | |
|     bottom_panel.style.left = "20px";
 | |
|     bottom_panel.style.right = "20px";
 | |
|     bottom_panel.style.height = "50px";
 | |
|     bottom_panel.style.pointerEvents = "none";
 | |
|     var brush = document.createElement("div");
 | |
|     brush.id = "brush";
 | |
|     brush.style.backgroundColor = "transparent";
 | |
|     brush.style.outline = "1px dashed black";
 | |
|     brush.style.boxShadow = "0 0 0 1px white";
 | |
|     brush.style.borderRadius = "50%";
 | |
|     brush.style.MozBorderRadius = "50%";
 | |
|     brush.style.WebkitBorderRadius = "50%";
 | |
|     brush.style.position = "absolute";
 | |
|     brush.style.zIndex = "8889";
 | |
|     brush.style.pointerEvents = "none";
 | |
|     this.brush = brush;
 | |
|     this.element.appendChild(imgCanvas);
 | |
|     this.element.appendChild(maskCanvas);
 | |
|     this.element.appendChild(bottom_panel);
 | |
|     document.body.appendChild(brush);
 | |
|     var clearButton = this.createLeftButton("Clear", () => {
 | |
|       self.maskCtx.clearRect(
 | |
|         0,
 | |
|         0,
 | |
|         self.maskCanvas.width,
 | |
|         self.maskCanvas.height
 | |
|       );
 | |
|     });
 | |
|     this.brush_size_slider = this.createLeftSlider(
 | |
|       self,
 | |
|       "Thickness",
 | |
|       (event) => {
 | |
|         self.brush_size = event.target.value;
 | |
|         self.updateBrushPreview(self);
 | |
|       }
 | |
|     );
 | |
|     this.brush_opacity_slider = this.createOpacitySlider(
 | |
|       self,
 | |
|       "Opacity",
 | |
|       (event) => {
 | |
|         self.brush_opacity = event.target.value;
 | |
|         if (self.brush_color_mode !== "negative") {
 | |
|           self.maskCanvas.style.opacity = self.brush_opacity.toString();
 | |
|         }
 | |
|       }
 | |
|     );
 | |
|     this.colorButton = this.createLeftButton(this.getColorButtonText(), () => {
 | |
|       if (self.brush_color_mode === "black") {
 | |
|         self.brush_color_mode = "white";
 | |
|       } else if (self.brush_color_mode === "white") {
 | |
|         self.brush_color_mode = "negative";
 | |
|       } else {
 | |
|         self.brush_color_mode = "black";
 | |
|       }
 | |
|       self.updateWhenBrushColorModeChanged();
 | |
|     });
 | |
|     var cancelButton = this.createRightButton("Cancel", () => {
 | |
|       document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
 | |
|       self.close();
 | |
|     });
 | |
|     this.saveButton = this.createRightButton("Save", () => {
 | |
|       document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown);
 | |
|       self.save();
 | |
|     });
 | |
|     this.element.appendChild(imgCanvas);
 | |
|     this.element.appendChild(maskCanvas);
 | |
|     this.element.appendChild(bottom_panel);
 | |
|     bottom_panel.appendChild(clearButton);
 | |
|     bottom_panel.appendChild(this.saveButton);
 | |
|     bottom_panel.appendChild(cancelButton);
 | |
|     bottom_panel.appendChild(this.brush_size_slider);
 | |
|     bottom_panel.appendChild(this.brush_opacity_slider);
 | |
|     bottom_panel.appendChild(this.colorButton);
 | |
|     imgCanvas.style.position = "absolute";
 | |
|     maskCanvas.style.position = "absolute";
 | |
|     imgCanvas.style.top = "200";
 | |
|     imgCanvas.style.left = "0";
 | |
|     maskCanvas.style.top = imgCanvas.style.top;
 | |
|     maskCanvas.style.left = imgCanvas.style.left;
 | |
|     const maskCanvasStyle = this.getMaskCanvasStyle();
 | |
|     maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
 | |
|     maskCanvas.style.opacity = maskCanvasStyle.opacity.toString();
 | |
|   }
 | |
|   async show() {
 | |
|     this.zoom_ratio = 1;
 | |
|     this.pan_x = 0;
 | |
|     this.pan_y = 0;
 | |
|     if (!this.is_layout_created) {
 | |
|       const imgCanvas = document.createElement("canvas");
 | |
|       const maskCanvas = document.createElement("canvas");
 | |
|       imgCanvas.id = "imageCanvas";
 | |
|       maskCanvas.id = "maskCanvas";
 | |
|       this.setlayout(imgCanvas, maskCanvas);
 | |
|       this.imgCanvas = imgCanvas;
 | |
|       this.maskCanvas = maskCanvas;
 | |
|       this.maskCtx = maskCanvas.getContext("2d", { willReadFrequently: true });
 | |
|       this.setEventHandler(maskCanvas);
 | |
|       this.is_layout_created = true;
 | |
|       const self = this;
 | |
|       const observer = new MutationObserver(function(mutations) {
 | |
|         mutations.forEach(function(mutation) {
 | |
|           if (mutation.type === "attributes" && mutation.attributeName === "style") {
 | |
|             if (self.last_display_style && self.last_display_style != "none" && self.element.style.display == "none") {
 | |
|               self.brush.style.display = "none";
 | |
|               ComfyApp.onClipspaceEditorClosed();
 | |
|             }
 | |
|             self.last_display_style = self.element.style.display;
 | |
|           }
 | |
|         });
 | |
|       });
 | |
|       const config = { attributes: true };
 | |
|       observer.observe(this.element, config);
 | |
|     }
 | |
|     document.addEventListener("keydown", MaskEditorDialog.handleKeyDown);
 | |
|     if (ComfyApp.clipspace_return_node) {
 | |
|       this.saveButton.innerText = "Save to node";
 | |
|     } else {
 | |
|       this.saveButton.innerText = "Save";
 | |
|     }
 | |
|     this.saveButton.disabled = false;
 | |
|     this.element.style.display = "block";
 | |
|     this.element.style.width = "85%";
 | |
|     this.element.style.margin = "0 7.5%";
 | |
|     this.element.style.height = "100vh";
 | |
|     this.element.style.top = "50%";
 | |
|     this.element.style.left = "42%";
 | |
|     this.element.style.zIndex = "8888";
 | |
|     await this.setImages(this.imgCanvas);
 | |
|     this.is_visible = true;
 | |
|   }
 | |
|   isOpened() {
 | |
|     return this.element.style.display == "block";
 | |
|   }
 | |
|   invalidateCanvas(orig_image, mask_image) {
 | |
|     this.imgCanvas.width = orig_image.width;
 | |
|     this.imgCanvas.height = orig_image.height;
 | |
|     this.maskCanvas.width = orig_image.width;
 | |
|     this.maskCanvas.height = orig_image.height;
 | |
|     let imgCtx = this.imgCanvas.getContext("2d", { willReadFrequently: true });
 | |
|     let maskCtx = this.maskCanvas.getContext("2d", {
 | |
|       willReadFrequently: true
 | |
|     });
 | |
|     imgCtx.drawImage(orig_image, 0, 0, orig_image.width, orig_image.height);
 | |
|     prepare_mask(mask_image, this.maskCanvas, maskCtx, this.getMaskColor());
 | |
|   }
 | |
|   async setImages(imgCanvas) {
 | |
|     let self = this;
 | |
|     const imgCtx = imgCanvas.getContext("2d", { willReadFrequently: true });
 | |
|     const maskCtx = this.maskCtx;
 | |
|     const maskCanvas = this.maskCanvas;
 | |
|     imgCtx.clearRect(0, 0, this.imgCanvas.width, this.imgCanvas.height);
 | |
|     maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
 | |
|     const filepath = ComfyApp.clipspace.images;
 | |
|     const alpha_url = new URL(
 | |
|       ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src
 | |
|     );
 | |
|     alpha_url.searchParams.delete("channel");
 | |
|     alpha_url.searchParams.delete("preview");
 | |
|     alpha_url.searchParams.set("channel", "a");
 | |
|     let mask_image = await loadImage(alpha_url);
 | |
|     const rgb_url = new URL(
 | |
|       ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src
 | |
|     );
 | |
|     rgb_url.searchParams.delete("channel");
 | |
|     rgb_url.searchParams.set("channel", "rgb");
 | |
|     this.image = new Image();
 | |
|     this.image.onload = function() {
 | |
|       maskCanvas.width = self.image.width;
 | |
|       maskCanvas.height = self.image.height;
 | |
|       self.invalidateCanvas(self.image, mask_image);
 | |
|       self.initializeCanvasPanZoom();
 | |
|     };
 | |
|     this.image.src = rgb_url.toString();
 | |
|   }
 | |
|   initializeCanvasPanZoom() {
 | |
|     let drawWidth = this.image.width;
 | |
|     let drawHeight = this.image.height;
 | |
|     let width = this.element.clientWidth;
 | |
|     let height = this.element.clientHeight;
 | |
|     if (this.image.width > width) {
 | |
|       drawWidth = width;
 | |
|       drawHeight = drawWidth / this.image.width * this.image.height;
 | |
|     }
 | |
|     if (drawHeight > height) {
 | |
|       drawHeight = height;
 | |
|       drawWidth = drawHeight / this.image.height * this.image.width;
 | |
|     }
 | |
|     this.zoom_ratio = drawWidth / this.image.width;
 | |
|     const canvasX = (width - drawWidth) / 2;
 | |
|     const canvasY = (height - drawHeight) / 2;
 | |
|     this.pan_x = canvasX;
 | |
|     this.pan_y = canvasY;
 | |
|     this.invalidatePanZoom();
 | |
|   }
 | |
|   invalidatePanZoom() {
 | |
|     let raw_width = this.image.width * this.zoom_ratio;
 | |
|     let raw_height = this.image.height * this.zoom_ratio;
 | |
|     if (this.pan_x + raw_width < 10) {
 | |
|       this.pan_x = 10 - raw_width;
 | |
|     }
 | |
|     if (this.pan_y + raw_height < 10) {
 | |
|       this.pan_y = 10 - raw_height;
 | |
|     }
 | |
|     let width = `${raw_width}px`;
 | |
|     let height = `${raw_height}px`;
 | |
|     let left = `${this.pan_x}px`;
 | |
|     let top = `${this.pan_y}px`;
 | |
|     this.maskCanvas.style.width = width;
 | |
|     this.maskCanvas.style.height = height;
 | |
|     this.maskCanvas.style.left = left;
 | |
|     this.maskCanvas.style.top = top;
 | |
|     this.imgCanvas.style.width = width;
 | |
|     this.imgCanvas.style.height = height;
 | |
|     this.imgCanvas.style.left = left;
 | |
|     this.imgCanvas.style.top = top;
 | |
|   }
 | |
|   setEventHandler(maskCanvas) {
 | |
|     const self = this;
 | |
|     if (!this.handler_registered) {
 | |
|       maskCanvas.addEventListener("contextmenu", (event) => {
 | |
|         event.preventDefault();
 | |
|       });
 | |
|       this.element.addEventListener(
 | |
|         "wheel",
 | |
|         (event) => this.handleWheelEvent(self, event)
 | |
|       );
 | |
|       this.element.addEventListener(
 | |
|         "pointermove",
 | |
|         (event) => this.pointMoveEvent(self, event)
 | |
|       );
 | |
|       this.element.addEventListener(
 | |
|         "touchmove",
 | |
|         (event) => this.pointMoveEvent(self, event)
 | |
|       );
 | |
|       this.element.addEventListener("dragstart", (event) => {
 | |
|         if (event.ctrlKey) {
 | |
|           event.preventDefault();
 | |
|         }
 | |
|       });
 | |
|       maskCanvas.addEventListener(
 | |
|         "pointerdown",
 | |
|         (event) => this.handlePointerDown(self, event)
 | |
|       );
 | |
|       maskCanvas.addEventListener(
 | |
|         "pointermove",
 | |
|         (event) => this.draw_move(self, event)
 | |
|       );
 | |
|       maskCanvas.addEventListener(
 | |
|         "touchmove",
 | |
|         (event) => this.draw_move(self, event)
 | |
|       );
 | |
|       maskCanvas.addEventListener("pointerover", (event) => {
 | |
|         this.brush.style.display = "block";
 | |
|       });
 | |
|       maskCanvas.addEventListener("pointerleave", (event) => {
 | |
|         this.brush.style.display = "none";
 | |
|       });
 | |
|       document.addEventListener("pointerup", MaskEditorDialog.handlePointerUp);
 | |
|       this.handler_registered = true;
 | |
|     }
 | |
|   }
 | |
|   getMaskCanvasStyle() {
 | |
|     if (this.brush_color_mode === "negative") {
 | |
|       return {
 | |
|         mixBlendMode: "difference",
 | |
|         opacity: "1"
 | |
|       };
 | |
|     } else {
 | |
|       return {
 | |
|         mixBlendMode: "initial",
 | |
|         opacity: this.brush_opacity
 | |
|       };
 | |
|     }
 | |
|   }
 | |
|   getMaskColor() {
 | |
|     if (this.brush_color_mode === "black") {
 | |
|       return { r: 0, g: 0, b: 0 };
 | |
|     }
 | |
|     if (this.brush_color_mode === "white") {
 | |
|       return { r: 255, g: 255, b: 255 };
 | |
|     }
 | |
|     if (this.brush_color_mode === "negative") {
 | |
|       return { r: 255, g: 255, b: 255 };
 | |
|     }
 | |
|     return { r: 0, g: 0, b: 0 };
 | |
|   }
 | |
|   getMaskFillStyle() {
 | |
|     const maskColor = this.getMaskColor();
 | |
|     return "rgb(" + maskColor.r + "," + maskColor.g + "," + maskColor.b + ")";
 | |
|   }
 | |
|   getColorButtonText() {
 | |
|     let colorCaption = "unknown";
 | |
|     if (this.brush_color_mode === "black") {
 | |
|       colorCaption = "black";
 | |
|     } else if (this.brush_color_mode === "white") {
 | |
|       colorCaption = "white";
 | |
|     } else if (this.brush_color_mode === "negative") {
 | |
|       colorCaption = "negative";
 | |
|     }
 | |
|     return "Color: " + colorCaption;
 | |
|   }
 | |
|   updateWhenBrushColorModeChanged() {
 | |
|     this.colorButton.innerText = this.getColorButtonText();
 | |
|     const maskCanvasStyle = this.getMaskCanvasStyle();
 | |
|     this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode;
 | |
|     this.maskCanvas.style.opacity = maskCanvasStyle.opacity.toString();
 | |
|     const maskColor = this.getMaskColor();
 | |
|     const maskData = this.maskCtx.getImageData(
 | |
|       0,
 | |
|       0,
 | |
|       this.maskCanvas.width,
 | |
|       this.maskCanvas.height
 | |
|     );
 | |
|     for (let i = 0; i < maskData.data.length; i += 4) {
 | |
|       maskData.data[i] = maskColor.r;
 | |
|       maskData.data[i + 1] = maskColor.g;
 | |
|       maskData.data[i + 2] = maskColor.b;
 | |
|     }
 | |
|     this.maskCtx.putImageData(maskData, 0, 0);
 | |
|   }
 | |
|   brush_opacity = 0.7;
 | |
|   brush_size = 10;
 | |
|   brush_color_mode = "black";
 | |
|   drawing_mode = false;
 | |
|   lastx = -1;
 | |
|   lasty = -1;
 | |
|   lasttime = 0;
 | |
|   static handleKeyDown(event) {
 | |
|     const self = MaskEditorDialog.instance;
 | |
|     if (event.key === "]") {
 | |
|       self.brush_size = Math.min(self.brush_size + 2, 100);
 | |
|       self.brush_slider_input.value = self.brush_size;
 | |
|     } else if (event.key === "[") {
 | |
|       self.brush_size = Math.max(self.brush_size - 2, 1);
 | |
|       self.brush_slider_input.value = self.brush_size;
 | |
|     } else if (event.key === "Enter") {
 | |
|       self.save();
 | |
|     }
 | |
|     self.updateBrushPreview(self);
 | |
|   }
 | |
|   static handlePointerUp(event) {
 | |
|     event.preventDefault();
 | |
|     this.mousedown_x = null;
 | |
|     this.mousedown_y = null;
 | |
|     MaskEditorDialog.instance.drawing_mode = false;
 | |
|   }
 | |
|   updateBrushPreview(self) {
 | |
|     const brush = self.brush;
 | |
|     var centerX = self.cursorX;
 | |
|     var centerY = self.cursorY;
 | |
|     brush.style.width = self.brush_size * 2 * this.zoom_ratio + "px";
 | |
|     brush.style.height = self.brush_size * 2 * this.zoom_ratio + "px";
 | |
|     brush.style.left = centerX - self.brush_size * this.zoom_ratio + "px";
 | |
|     brush.style.top = centerY - self.brush_size * this.zoom_ratio + "px";
 | |
|   }
 | |
|   handleWheelEvent(self, event) {
 | |
|     event.preventDefault();
 | |
|     if (event.ctrlKey) {
 | |
|       if (event.deltaY < 0) {
 | |
|         this.zoom_ratio = Math.min(10, this.zoom_ratio + 0.2);
 | |
|       } else {
 | |
|         this.zoom_ratio = Math.max(0.2, this.zoom_ratio - 0.2);
 | |
|       }
 | |
|       this.invalidatePanZoom();
 | |
|     } else {
 | |
|       if (event.deltaY < 0) this.brush_size = Math.min(this.brush_size + 2, 100);
 | |
|       else this.brush_size = Math.max(this.brush_size - 2, 1);
 | |
|       this.brush_slider_input.value = this.brush_size.toString();
 | |
|       this.updateBrushPreview(this);
 | |
|     }
 | |
|   }
 | |
|   pointMoveEvent(self, event) {
 | |
|     this.cursorX = event.pageX;
 | |
|     this.cursorY = event.pageY;
 | |
|     self.updateBrushPreview(self);
 | |
|     if (event.ctrlKey) {
 | |
|       event.preventDefault();
 | |
|       self.pan_move(self, event);
 | |
|     }
 | |
|     let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;
 | |
|     if (event.shiftKey && left_button_down) {
 | |
|       self.drawing_mode = false;
 | |
|       const y = event.clientY;
 | |
|       let delta = (self.zoom_lasty - y) * 5e-3;
 | |
|       self.zoom_ratio = Math.max(
 | |
|         Math.min(10, self.last_zoom_ratio - delta),
 | |
|         0.2
 | |
|       );
 | |
|       this.invalidatePanZoom();
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
|   pan_move(self, event) {
 | |
|     if (event.buttons == 1) {
 | |
|       if (MaskEditorDialog.mousedown_x) {
 | |
|         let deltaX = MaskEditorDialog.mousedown_x - event.clientX;
 | |
|         let deltaY = MaskEditorDialog.mousedown_y - event.clientY;
 | |
|         self.pan_x = this.mousedown_pan_x - deltaX;
 | |
|         self.pan_y = this.mousedown_pan_y - deltaY;
 | |
|         self.invalidatePanZoom();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   draw_move(self, event) {
 | |
|     if (event.ctrlKey || event.shiftKey) {
 | |
|       return;
 | |
|     }
 | |
|     event.preventDefault();
 | |
|     this.cursorX = event.pageX;
 | |
|     this.cursorY = event.pageY;
 | |
|     self.updateBrushPreview(self);
 | |
|     let left_button_down = window.TouchEvent && event instanceof TouchEvent || event.buttons == 1;
 | |
|     let right_button_down = [2, 5, 32].includes(event.buttons);
 | |
|     if (!event.altKey && left_button_down) {
 | |
|       var diff = performance.now() - self.lasttime;
 | |
|       const maskRect = self.maskCanvas.getBoundingClientRect();
 | |
|       var x = event.offsetX;
 | |
|       var y = event.offsetY;
 | |
|       if (event.offsetX == null) {
 | |
|         x = event.targetTouches[0].clientX - maskRect.left;
 | |
|       }
 | |
|       if (event.offsetY == null) {
 | |
|         y = event.targetTouches[0].clientY - maskRect.top;
 | |
|       }
 | |
|       x /= self.zoom_ratio;
 | |
|       y /= self.zoom_ratio;
 | |
|       var brush_size = this.brush_size;
 | |
|       if (event instanceof PointerEvent && event.pointerType == "pen") {
 | |
|         brush_size *= event.pressure;
 | |
|         this.last_pressure = event.pressure;
 | |
|       } else if (window.TouchEvent && event instanceof TouchEvent && diff < 20) {
 | |
|         brush_size *= this.last_pressure;
 | |
|       } else {
 | |
|         brush_size = this.brush_size;
 | |
|       }
 | |
|       if (diff > 20 && !this.drawing_mode)
 | |
|         requestAnimationFrame(() => {
 | |
|           self.maskCtx.beginPath();
 | |
|           self.maskCtx.fillStyle = this.getMaskFillStyle();
 | |
|           self.maskCtx.globalCompositeOperation = "source-over";
 | |
|           self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
 | |
|           self.maskCtx.fill();
 | |
|           self.lastx = x;
 | |
|           self.lasty = y;
 | |
|         });
 | |
|       else
 | |
|         requestAnimationFrame(() => {
 | |
|           self.maskCtx.beginPath();
 | |
|           self.maskCtx.fillStyle = this.getMaskFillStyle();
 | |
|           self.maskCtx.globalCompositeOperation = "source-over";
 | |
|           var dx = x - self.lastx;
 | |
|           var dy = y - self.lasty;
 | |
|           var distance = Math.sqrt(dx * dx + dy * dy);
 | |
|           var directionX = dx / distance;
 | |
|           var directionY = dy / distance;
 | |
|           for (var i = 0; i < distance; i += 5) {
 | |
|             var px = self.lastx + directionX * i;
 | |
|             var py = self.lasty + directionY * i;
 | |
|             self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
 | |
|             self.maskCtx.fill();
 | |
|           }
 | |
|           self.lastx = x;
 | |
|           self.lasty = y;
 | |
|         });
 | |
|       self.lasttime = performance.now();
 | |
|     } else if (event.altKey && left_button_down || right_button_down) {
 | |
|       const maskRect = self.maskCanvas.getBoundingClientRect();
 | |
|       const x2 = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
 | |
|       const y2 = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
 | |
|       var brush_size = this.brush_size;
 | |
|       if (event instanceof PointerEvent && event.pointerType == "pen") {
 | |
|         brush_size *= event.pressure;
 | |
|         this.last_pressure = event.pressure;
 | |
|       } else if (window.TouchEvent && event instanceof TouchEvent && diff < 20) {
 | |
|         brush_size *= this.last_pressure;
 | |
|       } else {
 | |
|         brush_size = this.brush_size;
 | |
|       }
 | |
|       if (diff > 20 && !this.drawing_mode)
 | |
|         requestAnimationFrame(() => {
 | |
|           self.maskCtx.beginPath();
 | |
|           self.maskCtx.globalCompositeOperation = "destination-out";
 | |
|           self.maskCtx.arc(x2, y2, brush_size, 0, Math.PI * 2, false);
 | |
|           self.maskCtx.fill();
 | |
|           self.lastx = x2;
 | |
|           self.lasty = y2;
 | |
|         });
 | |
|       else
 | |
|         requestAnimationFrame(() => {
 | |
|           self.maskCtx.beginPath();
 | |
|           self.maskCtx.globalCompositeOperation = "destination-out";
 | |
|           var dx = x2 - self.lastx;
 | |
|           var dy = y2 - self.lasty;
 | |
|           var distance = Math.sqrt(dx * dx + dy * dy);
 | |
|           var directionX = dx / distance;
 | |
|           var directionY = dy / distance;
 | |
|           for (var i = 0; i < distance; i += 5) {
 | |
|             var px = self.lastx + directionX * i;
 | |
|             var py = self.lasty + directionY * i;
 | |
|             self.maskCtx.arc(px, py, brush_size, 0, Math.PI * 2, false);
 | |
|             self.maskCtx.fill();
 | |
|           }
 | |
|           self.lastx = x2;
 | |
|           self.lasty = y2;
 | |
|         });
 | |
|       self.lasttime = performance.now();
 | |
|     }
 | |
|   }
 | |
|   handlePointerDown(self, event) {
 | |
|     if (event.ctrlKey) {
 | |
|       if (event.buttons == 1) {
 | |
|         MaskEditorDialog.mousedown_x = event.clientX;
 | |
|         MaskEditorDialog.mousedown_y = event.clientY;
 | |
|         this.mousedown_pan_x = this.pan_x;
 | |
|         this.mousedown_pan_y = this.pan_y;
 | |
|       }
 | |
|       return;
 | |
|     }
 | |
|     var brush_size = this.brush_size;
 | |
|     if (event instanceof PointerEvent && event.pointerType == "pen") {
 | |
|       brush_size *= event.pressure;
 | |
|       this.last_pressure = event.pressure;
 | |
|     }
 | |
|     if ([0, 2, 5].includes(event.button)) {
 | |
|       self.drawing_mode = true;
 | |
|       event.preventDefault();
 | |
|       if (event.shiftKey) {
 | |
|         self.zoom_lasty = event.clientY;
 | |
|         self.last_zoom_ratio = self.zoom_ratio;
 | |
|         return;
 | |
|       }
 | |
|       const maskRect = self.maskCanvas.getBoundingClientRect();
 | |
|       const x = (event.offsetX || event.targetTouches[0].clientX - maskRect.left) / self.zoom_ratio;
 | |
|       const y = (event.offsetY || event.targetTouches[0].clientY - maskRect.top) / self.zoom_ratio;
 | |
|       self.maskCtx.beginPath();
 | |
|       if (!event.altKey && event.button == 0) {
 | |
|         self.maskCtx.fillStyle = this.getMaskFillStyle();
 | |
|         self.maskCtx.globalCompositeOperation = "source-over";
 | |
|       } else {
 | |
|         self.maskCtx.globalCompositeOperation = "destination-out";
 | |
|       }
 | |
|       self.maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
 | |
|       self.maskCtx.fill();
 | |
|       self.lastx = x;
 | |
|       self.lasty = y;
 | |
|       self.lasttime = performance.now();
 | |
|     }
 | |
|   }
 | |
|   async save() {
 | |
|     const backupCanvas = document.createElement("canvas");
 | |
|     const backupCtx = backupCanvas.getContext("2d", {
 | |
|       willReadFrequently: true
 | |
|     });
 | |
|     backupCanvas.width = this.image.width;
 | |
|     backupCanvas.height = this.image.height;
 | |
|     backupCtx.clearRect(0, 0, backupCanvas.width, backupCanvas.height);
 | |
|     backupCtx.drawImage(
 | |
|       this.maskCanvas,
 | |
|       0,
 | |
|       0,
 | |
|       this.maskCanvas.width,
 | |
|       this.maskCanvas.height,
 | |
|       0,
 | |
|       0,
 | |
|       backupCanvas.width,
 | |
|       backupCanvas.height
 | |
|     );
 | |
|     const backupData = backupCtx.getImageData(
 | |
|       0,
 | |
|       0,
 | |
|       backupCanvas.width,
 | |
|       backupCanvas.height
 | |
|     );
 | |
|     for (let i = 0; i < backupData.data.length; i += 4) {
 | |
|       if (backupData.data[i + 3] == 255) backupData.data[i + 3] = 0;
 | |
|       else backupData.data[i + 3] = 255;
 | |
|       backupData.data[i] = 0;
 | |
|       backupData.data[i + 1] = 0;
 | |
|       backupData.data[i + 2] = 0;
 | |
|     }
 | |
|     backupCtx.globalCompositeOperation = "source-over";
 | |
|     backupCtx.putImageData(backupData, 0, 0);
 | |
|     const formData = new FormData();
 | |
|     const filename = "clipspace-mask-" + performance.now() + ".png";
 | |
|     const item = {
 | |
|       filename,
 | |
|       subfolder: "clipspace",
 | |
|       type: "input"
 | |
|     };
 | |
|     if (ComfyApp.clipspace.images) ComfyApp.clipspace.images[0] = item;
 | |
|     if (ComfyApp.clipspace.widgets) {
 | |
|       const index = ComfyApp.clipspace.widgets.findIndex(
 | |
|         (obj) => obj.name === "image"
 | |
|       );
 | |
|       if (index >= 0) ComfyApp.clipspace.widgets[index].value = item;
 | |
|     }
 | |
|     const dataURL = backupCanvas.toDataURL();
 | |
|     const blob = dataURLToBlob(dataURL);
 | |
|     let original_url = new URL(this.image.src);
 | |
|     const original_ref = {
 | |
|       filename: original_url.searchParams.get("filename")
 | |
|     };
 | |
|     let original_subfolder = original_url.searchParams.get("subfolder");
 | |
|     if (original_subfolder) original_ref.subfolder = original_subfolder;
 | |
|     let original_type = original_url.searchParams.get("type");
 | |
|     if (original_type) original_ref.type = original_type;
 | |
|     formData.append("image", blob, filename);
 | |
|     formData.append("original_ref", JSON.stringify(original_ref));
 | |
|     formData.append("type", "input");
 | |
|     formData.append("subfolder", "clipspace");
 | |
|     this.saveButton.innerText = "Saving...";
 | |
|     this.saveButton.disabled = true;
 | |
|     await uploadMask(item, formData);
 | |
|     ComfyApp.onClipspaceEditorSave();
 | |
|     this.close();
 | |
|   }
 | |
| }
 | |
| app.registerExtension({
 | |
|   name: "Comfy.MaskEditor",
 | |
|   init(app2) {
 | |
|     ComfyApp.open_maskeditor = function() {
 | |
|       const dlg = MaskEditorDialog.getInstance();
 | |
|       if (!dlg.isOpened()) {
 | |
|         dlg.show();
 | |
|       }
 | |
|     };
 | |
|     const context_predicate = /* @__PURE__ */ __name(() => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0, "context_predicate");
 | |
|     ClipspaceDialog.registerButton(
 | |
|       "MaskEditor",
 | |
|       context_predicate,
 | |
|       ComfyApp.open_maskeditor
 | |
|     );
 | |
|   }
 | |
| });
 | |
| const id = "Comfy.NodeTemplates";
 | |
| const file = "comfy.templates.json";
 | |
| class ManageTemplates extends ComfyDialog {
 | |
|   static {
 | |
|     __name(this, "ManageTemplates");
 | |
|   }
 | |
|   templates;
 | |
|   draggedEl;
 | |
|   saveVisualCue;
 | |
|   emptyImg;
 | |
|   importInput;
 | |
|   constructor() {
 | |
|     super();
 | |
|     this.load().then((v) => {
 | |
|       this.templates = v;
 | |
|     });
 | |
|     this.element.classList.add("comfy-manage-templates");
 | |
|     this.draggedEl = null;
 | |
|     this.saveVisualCue = null;
 | |
|     this.emptyImg = new Image();
 | |
|     this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
 | |
|     this.importInput = $el("input", {
 | |
|       type: "file",
 | |
|       accept: ".json",
 | |
|       multiple: true,
 | |
|       style: { display: "none" },
 | |
|       parent: document.body,
 | |
|       onchange: /* @__PURE__ */ __name(() => this.importAll(), "onchange")
 | |
|     });
 | |
|   }
 | |
|   createButtons() {
 | |
|     const btns = super.createButtons();
 | |
|     btns[0].textContent = "Close";
 | |
|     btns[0].onclick = (e) => {
 | |
|       clearTimeout(this.saveVisualCue);
 | |
|       this.close();
 | |
|     };
 | |
|     btns.unshift(
 | |
|       $el("button", {
 | |
|         type: "button",
 | |
|         textContent: "Export",
 | |
|         onclick: /* @__PURE__ */ __name(() => this.exportAll(), "onclick")
 | |
|       })
 | |
|     );
 | |
|     btns.unshift(
 | |
|       $el("button", {
 | |
|         type: "button",
 | |
|         textContent: "Import",
 | |
|         onclick: /* @__PURE__ */ __name(() => {
 | |
|           this.importInput.click();
 | |
|         }, "onclick")
 | |
|       })
 | |
|     );
 | |
|     return btns;
 | |
|   }
 | |
|   async load() {
 | |
|     let templates = [];
 | |
|     if (app.storageLocation === "server") {
 | |
|       if (app.isNewUserSession) {
 | |
|         const json = localStorage.getItem(id);
 | |
|         if (json) {
 | |
|           templates = JSON.parse(json);
 | |
|         }
 | |
|         await api.storeUserData(file, json, { stringify: false });
 | |
|       } else {
 | |
|         const res = await api.getUserData(file);
 | |
|         if (res.status === 200) {
 | |
|           try {
 | |
|             templates = await res.json();
 | |
|           } catch (error) {
 | |
|           }
 | |
|         } else if (res.status !== 404) {
 | |
|           console.error(res.status + " " + res.statusText);
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       const json = localStorage.getItem(id);
 | |
|       if (json) {
 | |
|         templates = JSON.parse(json);
 | |
|       }
 | |
|     }
 | |
|     return templates ?? [];
 | |
|   }
 | |
|   async store() {
 | |
|     if (app.storageLocation === "server") {
 | |
|       const templates = JSON.stringify(this.templates, void 0, 4);
 | |
|       localStorage.setItem(id, templates);
 | |
|       try {
 | |
|         await api.storeUserData(file, templates, { stringify: false });
 | |
|       } catch (error) {
 | |
|         console.error(error);
 | |
|         alert(error.message);
 | |
|       }
 | |
|     } else {
 | |
|       localStorage.setItem(id, JSON.stringify(this.templates));
 | |
|     }
 | |
|   }
 | |
|   async importAll() {
 | |
|     for (const file2 of this.importInput.files) {
 | |
|       if (file2.type === "application/json" || file2.name.endsWith(".json")) {
 | |
|         const reader = new FileReader();
 | |
|         reader.onload = async () => {
 | |
|           const importFile = JSON.parse(reader.result);
 | |
|           if (importFile?.templates) {
 | |
|             for (const template of importFile.templates) {
 | |
|               if (template?.name && template?.data) {
 | |
|                 this.templates.push(template);
 | |
|               }
 | |
|             }
 | |
|             await this.store();
 | |
|           }
 | |
|         };
 | |
|         await reader.readAsText(file2);
 | |
|       }
 | |
|     }
 | |
|     this.importInput.value = null;
 | |
|     this.close();
 | |
|   }
 | |
|   exportAll() {
 | |
|     if (this.templates.length == 0) {
 | |
|       alert("No templates to export.");
 | |
|       return;
 | |
|     }
 | |
|     const json = JSON.stringify({ templates: this.templates }, null, 2);
 | |
|     const blob = new Blob([json], { type: "application/json" });
 | |
|     const url = URL.createObjectURL(blob);
 | |
|     const a = $el("a", {
 | |
|       href: url,
 | |
|       download: "node_templates.json",
 | |
|       style: { display: "none" },
 | |
|       parent: document.body
 | |
|     });
 | |
|     a.click();
 | |
|     setTimeout(function() {
 | |
|       a.remove();
 | |
|       window.URL.revokeObjectURL(url);
 | |
|     }, 0);
 | |
|   }
 | |
|   show() {
 | |
|     super.show(
 | |
|       $el(
 | |
|         "div",
 | |
|         {},
 | |
|         this.templates.flatMap((t, i) => {
 | |
|           let nameInput;
 | |
|           return [
 | |
|             $el(
 | |
|               "div",
 | |
|               {
 | |
|                 dataset: { id: i.toString() },
 | |
|                 className: "templateManagerRow",
 | |
|                 style: {
 | |
|                   display: "grid",
 | |
|                   gridTemplateColumns: "1fr auto",
 | |
|                   border: "1px dashed transparent",
 | |
|                   gap: "5px",
 | |
|                   backgroundColor: "var(--comfy-menu-bg)"
 | |
|                 },
 | |
|                 ondragstart: /* @__PURE__ */ __name((e) => {
 | |
|                   this.draggedEl = e.currentTarget;
 | |
|                   e.currentTarget.style.opacity = "0.6";
 | |
|                   e.currentTarget.style.border = "1px dashed yellow";
 | |
|                   e.dataTransfer.effectAllowed = "move";
 | |
|                   e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
 | |
|                 }, "ondragstart"),
 | |
|                 ondragend: /* @__PURE__ */ __name((e) => {
 | |
|                   e.target.style.opacity = "1";
 | |
|                   e.currentTarget.style.border = "1px dashed transparent";
 | |
|                   e.currentTarget.removeAttribute("draggable");
 | |
|                   this.element.querySelectorAll(".templateManagerRow").forEach((el, i2) => {
 | |
|                     var prev_i = Number.parseInt(el.dataset.id);
 | |
|                     if (el == this.draggedEl && prev_i != i2) {
 | |
|                       this.templates.splice(
 | |
|                         i2,
 | |
|                         0,
 | |
|                         this.templates.splice(prev_i, 1)[0]
 | |
|                       );
 | |
|                     }
 | |
|                     el.dataset.id = i2.toString();
 | |
|                   });
 | |
|                   this.store();
 | |
|                 }, "ondragend"),
 | |
|                 ondragover: /* @__PURE__ */ __name((e) => {
 | |
|                   e.preventDefault();
 | |
|                   if (e.currentTarget == this.draggedEl) return;
 | |
|                   let rect = e.currentTarget.getBoundingClientRect();
 | |
|                   if (e.clientY > rect.top + rect.height / 2) {
 | |
|                     e.currentTarget.parentNode.insertBefore(
 | |
|                       this.draggedEl,
 | |
|                       e.currentTarget.nextSibling
 | |
|                     );
 | |
|                   } else {
 | |
|                     e.currentTarget.parentNode.insertBefore(
 | |
|                       this.draggedEl,
 | |
|                       e.currentTarget
 | |
|                     );
 | |
|                   }
 | |
|                 }, "ondragover")
 | |
|               },
 | |
|               [
 | |
|                 $el(
 | |
|                   "label",
 | |
|                   {
 | |
|                     textContent: "Name: ",
 | |
|                     style: {
 | |
|                       cursor: "grab"
 | |
|                     },
 | |
|                     onmousedown: /* @__PURE__ */ __name((e) => {
 | |
|                       if (e.target.localName == "label")
 | |
|                         e.currentTarget.parentNode.draggable = "true";
 | |
|                     }, "onmousedown")
 | |
|                   },
 | |
|                   [
 | |
|                     $el("input", {
 | |
|                       value: t.name,
 | |
|                       dataset: { name: t.name },
 | |
|                       style: {
 | |
|                         transitionProperty: "background-color",
 | |
|                         transitionDuration: "0s"
 | |
|                       },
 | |
|                       onchange: /* @__PURE__ */ __name((e) => {
 | |
|                         clearTimeout(this.saveVisualCue);
 | |
|                         var el = e.target;
 | |
|                         var row = el.parentNode.parentNode;
 | |
|                         this.templates[row.dataset.id].name = el.value.trim() || "untitled";
 | |
|                         this.store();
 | |
|                         el.style.backgroundColor = "rgb(40, 95, 40)";
 | |
|                         el.style.transitionDuration = "0s";
 | |
|                         this.saveVisualCue = setTimeout(function() {
 | |
|                           el.style.transitionDuration = ".7s";
 | |
|                           el.style.backgroundColor = "var(--comfy-input-bg)";
 | |
|                         }, 15);
 | |
|                       }, "onchange"),
 | |
|                       onkeypress: /* @__PURE__ */ __name((e) => {
 | |
|                         var el = e.target;
 | |
|                         clearTimeout(this.saveVisualCue);
 | |
|                         el.style.transitionDuration = "0s";
 | |
|                         el.style.backgroundColor = "var(--comfy-input-bg)";
 | |
|                       }, "onkeypress"),
 | |
|                       $: /* @__PURE__ */ __name((el) => nameInput = el, "$")
 | |
|                     })
 | |
|                   ]
 | |
|                 ),
 | |
|                 $el("div", {}, [
 | |
|                   $el("button", {
 | |
|                     textContent: "Export",
 | |
|                     style: {
 | |
|                       fontSize: "12px",
 | |
|                       fontWeight: "normal"
 | |
|                     },
 | |
|                     onclick: /* @__PURE__ */ __name((e) => {
 | |
|                       const json = JSON.stringify({ templates: [t] }, null, 2);
 | |
|                       const blob = new Blob([json], {
 | |
|                         type: "application/json"
 | |
|                       });
 | |
|                       const url = URL.createObjectURL(blob);
 | |
|                       const a = $el("a", {
 | |
|                         href: url,
 | |
|                         download: (nameInput.value || t.name) + ".json",
 | |
|                         style: { display: "none" },
 | |
|                         parent: document.body
 | |
|                       });
 | |
|                       a.click();
 | |
|                       setTimeout(function() {
 | |
|                         a.remove();
 | |
|                         window.URL.revokeObjectURL(url);
 | |
|                       }, 0);
 | |
|                     }, "onclick")
 | |
|                   }),
 | |
|                   $el("button", {
 | |
|                     textContent: "Delete",
 | |
|                     style: {
 | |
|                       fontSize: "12px",
 | |
|                       color: "red",
 | |
|                       fontWeight: "normal"
 | |
|                     },
 | |
|                     onclick: /* @__PURE__ */ __name((e) => {
 | |
|                       const item = e.target.parentNode.parentNode;
 | |
|                       item.parentNode.removeChild(item);
 | |
|                       this.templates.splice(item.dataset.id * 1, 1);
 | |
|                       this.store();
 | |
|                       var that = this;
 | |
|                       setTimeout(function() {
 | |
|                         that.element.querySelectorAll(".templateManagerRow").forEach((el, i2) => {
 | |
|                           el.dataset.id = i2.toString();
 | |
|                         });
 | |
|                       }, 0);
 | |
|                     }, "onclick")
 | |
|                   })
 | |
|                 ])
 | |
|               ]
 | |
|             )
 | |
|           ];
 | |
|         })
 | |
|       )
 | |
|     );
 | |
|   }
 | |
| }
 | |
| app.registerExtension({
 | |
|   name: id,
 | |
|   setup() {
 | |
|     const manage = new ManageTemplates();
 | |
|     const clipboardAction = /* @__PURE__ */ __name(async (cb) => {
 | |
|       const old = localStorage.getItem("litegrapheditor_clipboard");
 | |
|       await cb();
 | |
|       localStorage.setItem("litegrapheditor_clipboard", old);
 | |
|     }, "clipboardAction");
 | |
|     const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
 | |
|     LGraphCanvas.prototype.getCanvasMenuOptions = function() {
 | |
|       const options = orig.apply(this, arguments);
 | |
|       options.push(null);
 | |
|       options.push({
 | |
|         content: `Save Selected as Template`,
 | |
|         disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
 | |
|         callback: /* @__PURE__ */ __name(() => {
 | |
|           const name = prompt("Enter name");
 | |
|           if (!name?.trim()) return;
 | |
|           clipboardAction(() => {
 | |
|             app.canvas.copyToClipboard();
 | |
|             let data = localStorage.getItem("litegrapheditor_clipboard");
 | |
|             data = JSON.parse(data);
 | |
|             const nodeIds = Object.keys(app.canvas.selected_nodes);
 | |
|             for (let i = 0; i < nodeIds.length; i++) {
 | |
|               const node = app.graph.getNodeById(nodeIds[i]);
 | |
|               const nodeData = node?.constructor.nodeData;
 | |
|               let groupData = GroupNodeHandler.getGroupData(node);
 | |
|               if (groupData) {
 | |
|                 groupData = groupData.nodeData;
 | |
|                 if (!data.groupNodes) {
 | |
|                   data.groupNodes = {};
 | |
|                 }
 | |
|                 data.groupNodes[nodeData.name] = groupData;
 | |
|                 data.nodes[i].type = nodeData.name;
 | |
|               }
 | |
|             }
 | |
|             manage.templates.push({
 | |
|               name,
 | |
|               data: JSON.stringify(data)
 | |
|             });
 | |
|             manage.store();
 | |
|           });
 | |
|         }, "callback")
 | |
|       });
 | |
|       const subItems = manage.templates.map((t) => {
 | |
|         return {
 | |
|           content: t.name,
 | |
|           callback: /* @__PURE__ */ __name(() => {
 | |
|             clipboardAction(async () => {
 | |
|               const data = JSON.parse(t.data);
 | |
|               await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
 | |
|               localStorage.setItem("litegrapheditor_clipboard", t.data);
 | |
|               app.canvas.pasteFromClipboard();
 | |
|             });
 | |
|           }, "callback")
 | |
|         };
 | |
|       });
 | |
|       subItems.push(null, {
 | |
|         content: "Manage",
 | |
|         callback: /* @__PURE__ */ __name(() => manage.show(), "callback")
 | |
|       });
 | |
|       options.push({
 | |
|         content: "Node Templates",
 | |
|         submenu: {
 | |
|           options: subItems
 | |
|         }
 | |
|       });
 | |
|       return options;
 | |
|     };
 | |
|   }
 | |
| });
 | |
| app.registerExtension({
 | |
|   name: "Comfy.NoteNode",
 | |
|   registerCustomNodes() {
 | |
|     class NoteNode extends LGraphNode {
 | |
|       static {
 | |
|         __name(this, "NoteNode");
 | |
|       }
 | |
|       static category;
 | |
|       color = LGraphCanvas.node_colors.yellow.color;
 | |
|       bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
 | |
|       groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
 | |
|       isVirtualNode;
 | |
|       collapsable;
 | |
|       title_mode;
 | |
|       constructor(title) {
 | |
|         super(title);
 | |
|         if (!this.properties) {
 | |
|           this.properties = { text: "" };
 | |
|         }
 | |
|         ComfyWidgets.STRING(
 | |
|           // Should we extends LGraphNode?  Yesss
 | |
|           this,
 | |
|           "",
 | |
|           // @ts-expect-error
 | |
|           ["", { default: this.properties.text, multiline: true }],
 | |
|           app
 | |
|         );
 | |
|         this.serialize_widgets = true;
 | |
|         this.isVirtualNode = true;
 | |
|       }
 | |
|     }
 | |
|     LiteGraph.registerNodeType(
 | |
|       "Note",
 | |
|       Object.assign(NoteNode, {
 | |
|         title_mode: LiteGraph.NORMAL_TITLE,
 | |
|         title: "Note",
 | |
|         collapsable: true
 | |
|       })
 | |
|     );
 | |
|     NoteNode.category = "utils";
 | |
|   }
 | |
| });
 | |
| app.registerExtension({
 | |
|   name: "Comfy.RerouteNode",
 | |
|   registerCustomNodes(app2) {
 | |
|     class RerouteNode extends LGraphNode {
 | |
|       static {
 | |
|         __name(this, "RerouteNode");
 | |
|       }
 | |
|       static category;
 | |
|       static defaultVisibility = false;
 | |
|       constructor(title) {
 | |
|         super(title);
 | |
|         if (!this.properties) {
 | |
|           this.properties = {};
 | |
|         }
 | |
|         this.properties.showOutputText = RerouteNode.defaultVisibility;
 | |
|         this.properties.horizontal = false;
 | |
|         this.addInput("", "*");
 | |
|         this.addOutput(this.properties.showOutputText ? "*" : "", "*");
 | |
|         this.onAfterGraphConfigured = function() {
 | |
|           requestAnimationFrame(() => {
 | |
|             this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
 | |
|           });
 | |
|         };
 | |
|         this.onConnectionsChange = function(type, index, connected, link_info) {
 | |
|           this.applyOrientation();
 | |
|           if (connected && type === LiteGraph.OUTPUT) {
 | |
|             const types = new Set(
 | |
|               this.outputs[0].links.map((l) => app2.graph.links[l].type).filter((t) => t !== "*")
 | |
|             );
 | |
|             if (types.size > 1) {
 | |
|               const linksToDisconnect = [];
 | |
|               for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
 | |
|                 const linkId = this.outputs[0].links[i];
 | |
|                 const link = app2.graph.links[linkId];
 | |
|                 linksToDisconnect.push(link);
 | |
|               }
 | |
|               for (const link of linksToDisconnect) {
 | |
|                 const node = app2.graph.getNodeById(link.target_id);
 | |
|                 node.disconnectInput(link.target_slot);
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|           let currentNode = this;
 | |
|           let updateNodes = [];
 | |
|           let inputType = null;
 | |
|           let inputNode = null;
 | |
|           while (currentNode) {
 | |
|             updateNodes.unshift(currentNode);
 | |
|             const linkId = currentNode.inputs[0].link;
 | |
|             if (linkId !== null) {
 | |
|               const link = app2.graph.links[linkId];
 | |
|               if (!link) return;
 | |
|               const node = app2.graph.getNodeById(link.origin_id);
 | |
|               const type2 = node.constructor.type;
 | |
|               if (type2 === "Reroute") {
 | |
|                 if (node === this) {
 | |
|                   currentNode.disconnectInput(link.target_slot);
 | |
|                   currentNode = null;
 | |
|                 } else {
 | |
|                   currentNode = node;
 | |
|                 }
 | |
|               } else {
 | |
|                 inputNode = currentNode;
 | |
|                 inputType = node.outputs[link.origin_slot]?.type ?? null;
 | |
|                 break;
 | |
|               }
 | |
|             } else {
 | |
|               currentNode = null;
 | |
|               break;
 | |
|             }
 | |
|           }
 | |
|           const nodes = [this];
 | |
|           let outputType = null;
 | |
|           while (nodes.length) {
 | |
|             currentNode = nodes.pop();
 | |
|             const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
 | |
|             if (outputs.length) {
 | |
|               for (const linkId of outputs) {
 | |
|                 const link = app2.graph.links[linkId];
 | |
|                 if (!link) continue;
 | |
|                 const node = app2.graph.getNodeById(link.target_id);
 | |
|                 const type2 = node.constructor.type;
 | |
|                 if (type2 === "Reroute") {
 | |
|                   nodes.push(node);
 | |
|                   updateNodes.push(node);
 | |
|                 } else {
 | |
|                   const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
 | |
|                   if (inputType && inputType !== "*" && nodeOutType !== inputType) {
 | |
|                     node.disconnectInput(link.target_slot);
 | |
|                   } else {
 | |
|                     outputType = nodeOutType;
 | |
|                   }
 | |
|                 }
 | |
|               }
 | |
|             } else {
 | |
|             }
 | |
|           }
 | |
|           const displayType = inputType || outputType || "*";
 | |
|           const color = LGraphCanvas.link_type_colors[displayType];
 | |
|           let widgetConfig;
 | |
|           let targetWidget;
 | |
|           let widgetType;
 | |
|           for (const node of updateNodes) {
 | |
|             node.outputs[0].type = inputType || "*";
 | |
|             node.__outputType = displayType;
 | |
|             node.outputs[0].name = node.properties.showOutputText ? displayType : "";
 | |
|             node.size = node.computeSize();
 | |
|             node.applyOrientation();
 | |
|             for (const l of node.outputs[0].links || []) {
 | |
|               const link = app2.graph.links[l];
 | |
|               if (link) {
 | |
|                 link.color = color;
 | |
|                 if (app2.configuringGraph) continue;
 | |
|                 const targetNode = app2.graph.getNodeById(link.target_id);
 | |
|                 const targetInput = targetNode.inputs?.[link.target_slot];
 | |
|                 if (targetInput?.widget) {
 | |
|                   const config = getWidgetConfig(targetInput);
 | |
|                   if (!widgetConfig) {
 | |
|                     widgetConfig = config[1] ?? {};
 | |
|                     widgetType = config[0];
 | |
|                   }
 | |
|                   if (!targetWidget) {
 | |
|                     targetWidget = targetNode.widgets?.find(
 | |
|                       (w) => w.name === targetInput.widget.name
 | |
|                     );
 | |
|                   }
 | |
|                   const merged = mergeIfValid(targetInput, [
 | |
|                     config[0],
 | |
|                     widgetConfig
 | |
|                   ]);
 | |
|                   if (merged.customConfig) {
 | |
|                     widgetConfig = merged.customConfig;
 | |
|                   }
 | |
|                 }
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|           for (const node of updateNodes) {
 | |
|             if (widgetConfig && outputType) {
 | |
|               node.inputs[0].widget = { name: "value" };
 | |
|               setWidgetConfig(
 | |
|                 node.inputs[0],
 | |
|                 [widgetType ?? displayType, widgetConfig],
 | |
|                 targetWidget
 | |
|               );
 | |
|             } else {
 | |
|               setWidgetConfig(node.inputs[0], null);
 | |
|             }
 | |
|           }
 | |
|           if (inputNode) {
 | |
|             const link = app2.graph.links[inputNode.inputs[0].link];
 | |
|             if (link) {
 | |
|               link.color = color;
 | |
|             }
 | |
|           }
 | |
|         };
 | |
|         this.clone = function() {
 | |
|           const cloned = RerouteNode.prototype.clone.apply(this);
 | |
|           cloned.removeOutput(0);
 | |
|           cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
 | |
|           cloned.size = cloned.computeSize();
 | |
|           return cloned;
 | |
|         };
 | |
|         this.isVirtualNode = true;
 | |
|       }
 | |
|       getExtraMenuOptions(_, options) {
 | |
|         options.unshift(
 | |
|           {
 | |
|             content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
 | |
|             callback: /* @__PURE__ */ __name(() => {
 | |
|               this.properties.showOutputText = !this.properties.showOutputText;
 | |
|               if (this.properties.showOutputText) {
 | |
|                 this.outputs[0].name = this.__outputType || this.outputs[0].type;
 | |
|               } else {
 | |
|                 this.outputs[0].name = "";
 | |
|               }
 | |
|               this.size = this.computeSize();
 | |
|               this.applyOrientation();
 | |
|               app2.graph.setDirtyCanvas(true, true);
 | |
|             }, "callback")
 | |
|           },
 | |
|           {
 | |
|             content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default",
 | |
|             callback: /* @__PURE__ */ __name(() => {
 | |
|               RerouteNode.setDefaultTextVisibility(
 | |
|                 !RerouteNode.defaultVisibility
 | |
|               );
 | |
|             }, "callback")
 | |
|           },
 | |
|           {
 | |
|             // naming is inverted with respect to LiteGraphNode.horizontal
 | |
|             // LiteGraphNode.horizontal == true means that
 | |
|             // each slot in the inputs and outputs are laid out horizontally,
 | |
|             // which is the opposite of the visual orientation of the inputs and outputs as a node
 | |
|             content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
 | |
|             callback: /* @__PURE__ */ __name(() => {
 | |
|               this.properties.horizontal = !this.properties.horizontal;
 | |
|               this.applyOrientation();
 | |
|             }, "callback")
 | |
|           }
 | |
|         );
 | |
|       }
 | |
|       applyOrientation() {
 | |
|         this.horizontal = this.properties.horizontal;
 | |
|         if (this.horizontal) {
 | |
|           this.inputs[0].pos = [this.size[0] / 2, 0];
 | |
|         } else {
 | |
|           delete this.inputs[0].pos;
 | |
|         }
 | |
|         app2.graph.setDirtyCanvas(true, true);
 | |
|       }
 | |
|       computeSize() {
 | |
|         return [
 | |
|           this.properties.showOutputText && this.outputs && this.outputs.length ? Math.max(
 | |
|             75,
 | |
|             LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40
 | |
|           ) : 75,
 | |
|           26
 | |
|         ];
 | |
|       }
 | |
|       static setDefaultTextVisibility(visible) {
 | |
|         RerouteNode.defaultVisibility = visible;
 | |
|         if (visible) {
 | |
|           localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
 | |
|         } else {
 | |
|           delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     RerouteNode.setDefaultTextVisibility(
 | |
|       !!localStorage["Comfy.RerouteNode.DefaultVisibility"]
 | |
|     );
 | |
|     LiteGraph.registerNodeType(
 | |
|       "Reroute",
 | |
|       Object.assign(RerouteNode, {
 | |
|         title_mode: LiteGraph.NO_TITLE,
 | |
|         title: "Reroute",
 | |
|         collapsable: false
 | |
|       })
 | |
|     );
 | |
|     RerouteNode.category = "utils";
 | |
|   }
 | |
| });
 | |
| app.registerExtension({
 | |
|   name: "Comfy.SaveImageExtraOutput",
 | |
|   async beforeRegisterNodeDef(nodeType, nodeData, app2) {
 | |
|     if (nodeData.name === "SaveImage" || nodeData.name === "SaveAnimatedWEBP") {
 | |
|       const onNodeCreated = nodeType.prototype.onNodeCreated;
 | |
|       nodeType.prototype.onNodeCreated = function() {
 | |
|         const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : void 0;
 | |
|         const widget = this.widgets.find((w) => w.name === "filename_prefix");
 | |
|         widget.serializeValue = () => {
 | |
|           return applyTextReplacements(app2, widget.value);
 | |
|         };
 | |
|         return r;
 | |
|       };
 | |
|     } else {
 | |
|       const onNodeCreated = nodeType.prototype.onNodeCreated;
 | |
|       nodeType.prototype.onNodeCreated = function() {
 | |
|         const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : void 0;
 | |
|         if (!this.properties || !("Node name for S&R" in this.properties)) {
 | |
|           this.addProperty("Node name for S&R", this.constructor.type, "string");
 | |
|         }
 | |
|         return r;
 | |
|       };
 | |
|     }
 | |
|   }
 | |
| });
 | |
| let touchZooming;
 | |
| let touchCount = 0;
 | |
| app.registerExtension({
 | |
|   name: "Comfy.SimpleTouchSupport",
 | |
|   setup() {
 | |
|     let zoomPos;
 | |
|     let touchTime;
 | |
|     let lastTouch;
 | |
|     function getMultiTouchPos(e) {
 | |
|       return Math.hypot(
 | |
|         e.touches[0].clientX - e.touches[1].clientX,
 | |
|         e.touches[0].clientY - e.touches[1].clientY
 | |
|       );
 | |
|     }
 | |
|     __name(getMultiTouchPos, "getMultiTouchPos");
 | |
|     app.canvasEl.addEventListener(
 | |
|       "touchstart",
 | |
|       (e) => {
 | |
|         touchCount++;
 | |
|         lastTouch = null;
 | |
|         if (e.touches?.length === 1) {
 | |
|           touchTime = /* @__PURE__ */ new Date();
 | |
|           lastTouch = e.touches[0];
 | |
|         } else {
 | |
|           touchTime = null;
 | |
|           if (e.touches?.length === 2) {
 | |
|             zoomPos = getMultiTouchPos(e);
 | |
|             app.canvas.pointer_is_down = false;
 | |
|           }
 | |
|         }
 | |
|       },
 | |
|       true
 | |
|     );
 | |
|     app.canvasEl.addEventListener("touchend", (e) => {
 | |
|       touchZooming = false;
 | |
|       touchCount = e.touches?.length ?? touchCount - 1;
 | |
|       if (touchTime && !e.touches?.length) {
 | |
|         if ((/* @__PURE__ */ new Date()).getTime() - touchTime > 600) {
 | |
|           try {
 | |
|             e.constructor = CustomEvent;
 | |
|           } catch (error) {
 | |
|           }
 | |
|           e.clientX = lastTouch.clientX;
 | |
|           e.clientY = lastTouch.clientY;
 | |
|           app.canvas.pointer_is_down = true;
 | |
|           app.canvas._mousedown_callback(e);
 | |
|         }
 | |
|         touchTime = null;
 | |
|       }
 | |
|     });
 | |
|     app.canvasEl.addEventListener(
 | |
|       "touchmove",
 | |
|       (e) => {
 | |
|         touchTime = null;
 | |
|         if (e.touches?.length === 2) {
 | |
|           app.canvas.pointer_is_down = false;
 | |
|           touchZooming = true;
 | |
|           LiteGraph.closeAllContextMenus();
 | |
|           app.canvas.search_box?.close();
 | |
|           const newZoomPos = getMultiTouchPos(e);
 | |
|           const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
 | |
|           const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
 | |
|           let scale = app.canvas.ds.scale;
 | |
|           const diff = zoomPos - newZoomPos;
 | |
|           if (diff > 0.5) {
 | |
|             scale *= 1 / 1.07;
 | |
|           } else if (diff < -0.5) {
 | |
|             scale *= 1.07;
 | |
|           }
 | |
|           app.canvas.ds.changeScale(scale, [midX, midY]);
 | |
|           app.canvas.setDirty(true, true);
 | |
|           zoomPos = newZoomPos;
 | |
|         }
 | |
|       },
 | |
|       true
 | |
|     );
 | |
|   }
 | |
| });
 | |
| const processMouseDown = LGraphCanvas.prototype.processMouseDown;
 | |
| LGraphCanvas.prototype.processMouseDown = function(e) {
 | |
|   if (touchZooming || touchCount) {
 | |
|     return;
 | |
|   }
 | |
|   return processMouseDown.apply(this, arguments);
 | |
| };
 | |
| const processMouseMove = LGraphCanvas.prototype.processMouseMove;
 | |
| LGraphCanvas.prototype.processMouseMove = function(e) {
 | |
|   if (touchZooming || touchCount > 1) {
 | |
|     return;
 | |
|   }
 | |
|   return processMouseMove.apply(this, arguments);
 | |
| };
 | |
| app.registerExtension({
 | |
|   name: "Comfy.SlotDefaults",
 | |
|   suggestionsNumber: null,
 | |
|   init() {
 | |
|     LiteGraph.search_filter_enabled = true;
 | |
|     LiteGraph.middle_click_slot_add_default_node = true;
 | |
|     this.suggestionsNumber = app.ui.settings.addSetting({
 | |
|       id: "Comfy.NodeSuggestions.number",
 | |
|       category: ["Comfy", "Node Search Box", "NodeSuggestions"],
 | |
|       name: "Number of nodes suggestions",
 | |
|       tooltip: "Only for litegraph searchbox/context menu",
 | |
|       type: "slider",
 | |
|       attrs: {
 | |
|         min: 1,
 | |
|         max: 100,
 | |
|         step: 1
 | |
|       },
 | |
|       defaultValue: 5,
 | |
|       onChange: /* @__PURE__ */ __name((newVal, oldVal) => {
 | |
|         this.setDefaults(newVal);
 | |
|       }, "onChange")
 | |
|     });
 | |
|   },
 | |
|   slot_types_default_out: {},
 | |
|   slot_types_default_in: {},
 | |
|   async beforeRegisterNodeDef(nodeType, nodeData, app2) {
 | |
|     var nodeId = nodeData.name;
 | |
|     var inputs = [];
 | |
|     inputs = nodeData["input"]["required"];
 | |
|     for (const inputKey in inputs) {
 | |
|       var input = inputs[inputKey];
 | |
|       if (typeof input[0] !== "string") continue;
 | |
|       var type = input[0];
 | |
|       if (type in ComfyWidgets) {
 | |
|         var customProperties = input[1];
 | |
|         if (!customProperties?.forceInput) continue;
 | |
|       }
 | |
|       if (!(type in this.slot_types_default_out)) {
 | |
|         this.slot_types_default_out[type] = ["Reroute"];
 | |
|       }
 | |
|       if (this.slot_types_default_out[type].includes(nodeId)) continue;
 | |
|       this.slot_types_default_out[type].push(nodeId);
 | |
|       const lowerType = type.toLocaleLowerCase();
 | |
|       if (!(lowerType in LiteGraph.registered_slot_in_types)) {
 | |
|         LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
 | |
|       }
 | |
|       LiteGraph.registered_slot_in_types[lowerType].nodes.push(
 | |
|         nodeType.comfyClass
 | |
|       );
 | |
|     }
 | |
|     var outputs = nodeData["output"];
 | |
|     for (const key in outputs) {
 | |
|       var type = outputs[key];
 | |
|       if (!(type in this.slot_types_default_in)) {
 | |
|         this.slot_types_default_in[type] = ["Reroute"];
 | |
|       }
 | |
|       this.slot_types_default_in[type].push(nodeId);
 | |
|       if (!(type in LiteGraph.registered_slot_out_types)) {
 | |
|         LiteGraph.registered_slot_out_types[type] = { nodes: [] };
 | |
|       }
 | |
|       LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
 | |
|       if (!LiteGraph.slot_types_out.includes(type)) {
 | |
|         LiteGraph.slot_types_out.push(type);
 | |
|       }
 | |
|     }
 | |
|     var maxNum = this.suggestionsNumber.value;
 | |
|     this.setDefaults(maxNum);
 | |
|   },
 | |
|   setDefaults(maxNum) {
 | |
|     LiteGraph.slot_types_default_out = {};
 | |
|     LiteGraph.slot_types_default_in = {};
 | |
|     for (const type in this.slot_types_default_out) {
 | |
|       LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum);
 | |
|     }
 | |
|     for (const type in this.slot_types_default_in) {
 | |
|       LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum);
 | |
|     }
 | |
|   }
 | |
| });
 | |
| function roundVectorToGrid(vec) {
 | |
|   vec[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE);
 | |
|   vec[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE);
 | |
|   return vec;
 | |
| }
 | |
| __name(roundVectorToGrid, "roundVectorToGrid");
 | |
| app.registerExtension({
 | |
|   name: "Comfy.SnapToGrid",
 | |
|   init() {
 | |
|     app.ui.settings.addSetting({
 | |
|       id: "Comfy.SnapToGrid.GridSize",
 | |
|       category: ["Comfy", "Graph", "GridSize"],
 | |
|       name: "Snap to grid size",
 | |
|       type: "slider",
 | |
|       attrs: {
 | |
|         min: 1,
 | |
|         max: 500
 | |
|       },
 | |
|       tooltip: "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
 | |
|       defaultValue: LiteGraph.CANVAS_GRID_SIZE,
 | |
|       onChange(value) {
 | |
|         LiteGraph.CANVAS_GRID_SIZE = +value;
 | |
|       }
 | |
|     });
 | |
|     const onNodeMoved = app.canvas.onNodeMoved;
 | |
|     app.canvas.onNodeMoved = function(node) {
 | |
|       const r = onNodeMoved?.apply(this, arguments);
 | |
|       if (app.shiftDown) {
 | |
|         for (const id2 in this.selected_nodes) {
 | |
|           this.selected_nodes[id2].alignToGrid();
 | |
|         }
 | |
|       }
 | |
|       return r;
 | |
|     };
 | |
|     const onNodeAdded = app.graph.onNodeAdded;
 | |
|     app.graph.onNodeAdded = function(node) {
 | |
|       const onResize = node.onResize;
 | |
|       node.onResize = function() {
 | |
|         if (app.shiftDown) {
 | |
|           roundVectorToGrid(node.size);
 | |
|         }
 | |
|         return onResize?.apply(this, arguments);
 | |
|       };
 | |
|       return onNodeAdded?.apply(this, arguments);
 | |
|     };
 | |
|     const origDrawNode = LGraphCanvas.prototype.drawNode;
 | |
|     LGraphCanvas.prototype.drawNode = function(node, ctx) {
 | |
|       if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
 | |
|         const [x, y] = roundVectorToGrid([...node.pos]);
 | |
|         const shiftX = x - node.pos[0];
 | |
|         let shiftY = y - node.pos[1];
 | |
|         let w, h;
 | |
|         if (node.flags.collapsed) {
 | |
|           w = node._collapsed_width;
 | |
|           h = LiteGraph.NODE_TITLE_HEIGHT;
 | |
|           shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
 | |
|         } else {
 | |
|           w = node.size[0];
 | |
|           h = node.size[1];
 | |
|           let titleMode = node.constructor.title_mode;
 | |
|           if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) {
 | |
|             h += LiteGraph.NODE_TITLE_HEIGHT;
 | |
|             shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
 | |
|           }
 | |
|         }
 | |
|         const f = ctx.fillStyle;
 | |
|         ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
 | |
|         ctx.fillRect(shiftX, shiftY, w, h);
 | |
|         ctx.fillStyle = f;
 | |
|       }
 | |
|       return origDrawNode.apply(this, arguments);
 | |
|     };
 | |
|     let selectedAndMovingGroup = null;
 | |
|     const groupMove = LGraphGroup.prototype.move;
 | |
|     LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
 | |
|       const v = groupMove.apply(this, arguments);
 | |
|       if (!selectedAndMovingGroup && app.canvas.selected_group === this && (deltax || deltay)) {
 | |
|         selectedAndMovingGroup = this;
 | |
|       }
 | |
|       if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
 | |
|         this.recomputeInsideNodes();
 | |
|         for (const node of this._nodes) {
 | |
|           node.alignToGrid();
 | |
|         }
 | |
|         LGraphNode.prototype.alignToGrid.apply(this);
 | |
|       }
 | |
|       return v;
 | |
|     };
 | |
|     const drawGroups = LGraphCanvas.prototype.drawGroups;
 | |
|     LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
 | |
|       if (this.selected_group && app.shiftDown) {
 | |
|         if (this.selected_group_resizing) {
 | |
|           roundVectorToGrid(this.selected_group.size);
 | |
|         } else if (selectedAndMovingGroup) {
 | |
|           const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
 | |
|           const f = ctx.fillStyle;
 | |
|           const s = ctx.strokeStyle;
 | |
|           ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
 | |
|           ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
 | |
|           ctx.rect(x, y, ...selectedAndMovingGroup.size);
 | |
|           ctx.fill();
 | |
|           ctx.stroke();
 | |
|           ctx.fillStyle = f;
 | |
|           ctx.strokeStyle = s;
 | |
|         }
 | |
|       } else if (!this.selected_group) {
 | |
|         selectedAndMovingGroup = null;
 | |
|       }
 | |
|       return drawGroups.apply(this, arguments);
 | |
|     };
 | |
|     const onGroupAdd = LGraphCanvas.onGroupAdd;
 | |
|     LGraphCanvas.onGroupAdd = function() {
 | |
|       const v = onGroupAdd.apply(app.canvas, arguments);
 | |
|       if (app.shiftDown) {
 | |
|         const lastGroup = app.graph._groups[app.graph._groups.length - 1];
 | |
|         if (lastGroup) {
 | |
|           roundVectorToGrid(lastGroup.pos);
 | |
|           roundVectorToGrid(lastGroup.size);
 | |
|         }
 | |
|       }
 | |
|       return v;
 | |
|     };
 | |
|   }
 | |
| });
 | |
| app.registerExtension({
 | |
|   name: "Comfy.UploadImage",
 | |
|   async beforeRegisterNodeDef(nodeType, nodeData, app2) {
 | |
|     if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
 | |
|       nodeData.input.required.upload = ["IMAGEUPLOAD"];
 | |
|     }
 | |
|   }
 | |
| });
 | |
| const WEBCAM_READY = Symbol();
 | |
| app.registerExtension({
 | |
|   name: "Comfy.WebcamCapture",
 | |
|   getCustomWidgets(app2) {
 | |
|     return {
 | |
|       WEBCAM(node, inputName) {
 | |
|         let res;
 | |
|         node[WEBCAM_READY] = new Promise((resolve) => res = resolve);
 | |
|         const container = document.createElement("div");
 | |
|         container.style.background = "rgba(0,0,0,0.25)";
 | |
|         container.style.textAlign = "center";
 | |
|         const video = document.createElement("video");
 | |
|         video.style.height = video.style.width = "100%";
 | |
|         const loadVideo = /* @__PURE__ */ __name(async () => {
 | |
|           try {
 | |
|             const stream = await navigator.mediaDevices.getUserMedia({
 | |
|               video: true,
 | |
|               audio: false
 | |
|             });
 | |
|             container.replaceChildren(video);
 | |
|             setTimeout(() => res(video), 500);
 | |
|             video.addEventListener("loadedmetadata", () => res(video), false);
 | |
|             video.srcObject = stream;
 | |
|             video.play();
 | |
|           } catch (error) {
 | |
|             const label = document.createElement("div");
 | |
|             label.style.color = "red";
 | |
|             label.style.overflow = "auto";
 | |
|             label.style.maxHeight = "100%";
 | |
|             label.style.whiteSpace = "pre-wrap";
 | |
|             if (window.isSecureContext) {
 | |
|               label.textContent = "Unable to load webcam, please ensure access is granted:\n" + error.message;
 | |
|             } else {
 | |
|               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;
 | |
|             }
 | |
|             container.replaceChildren(label);
 | |
|           }
 | |
|         }, "loadVideo");
 | |
|         loadVideo();
 | |
|         return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
 | |
|       }
 | |
|     };
 | |
|   },
 | |
|   nodeCreated(node) {
 | |
|     if (node.type, node.constructor.comfyClass !== "WebcamCapture") return;
 | |
|     let video;
 | |
|     const camera = node.widgets.find((w2) => w2.name === "image");
 | |
|     const w = node.widgets.find((w2) => w2.name === "width");
 | |
|     const h = node.widgets.find((w2) => w2.name === "height");
 | |
|     const captureOnQueue = node.widgets.find(
 | |
|       (w2) => w2.name === "capture_on_queue"
 | |
|     );
 | |
|     const canvas = document.createElement("canvas");
 | |
|     const capture = /* @__PURE__ */ __name(() => {
 | |
|       canvas.width = w.value;
 | |
|       canvas.height = h.value;
 | |
|       const ctx = canvas.getContext("2d");
 | |
|       ctx.drawImage(video, 0, 0, w.value, h.value);
 | |
|       const data = canvas.toDataURL("image/png");
 | |
|       const img = new Image();
 | |
|       img.onload = () => {
 | |
|         node.imgs = [img];
 | |
|         app.graph.setDirtyCanvas(true);
 | |
|         requestAnimationFrame(() => {
 | |
|           node.setSizeForImage?.();
 | |
|         });
 | |
|       };
 | |
|       img.src = data;
 | |
|     }, "capture");
 | |
|     const btn = node.addWidget(
 | |
|       "button",
 | |
|       "waiting for camera...",
 | |
|       "capture",
 | |
|       capture
 | |
|     );
 | |
|     btn.disabled = true;
 | |
|     btn.serializeValue = () => void 0;
 | |
|     camera.serializeValue = async () => {
 | |
|       if (captureOnQueue.value) {
 | |
|         capture();
 | |
|       } else if (!node.imgs?.length) {
 | |
|         const err = `No webcam image captured`;
 | |
|         alert(err);
 | |
|         throw new Error(err);
 | |
|       }
 | |
|       const blob = await new Promise((r) => canvas.toBlob(r));
 | |
|       const name = `${+/* @__PURE__ */ new Date()}.png`;
 | |
|       const file2 = new File([blob], name);
 | |
|       const body = new FormData();
 | |
|       body.append("image", file2);
 | |
|       body.append("subfolder", "webcam");
 | |
|       body.append("type", "temp");
 | |
|       const resp = await api.fetchApi("/upload/image", {
 | |
|         method: "POST",
 | |
|         body
 | |
|       });
 | |
|       if (resp.status !== 200) {
 | |
|         const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
 | |
|         alert(err);
 | |
|         throw new Error(err);
 | |
|       }
 | |
|       return `webcam/${name} [temp]`;
 | |
|     };
 | |
|     node[WEBCAM_READY].then((v) => {
 | |
|       video = v;
 | |
|       if (!w.value) {
 | |
|         w.value = video.videoWidth || 640;
 | |
|         h.value = video.videoHeight || 480;
 | |
|       }
 | |
|       btn.disabled = false;
 | |
|       btn.label = "capture";
 | |
|     });
 | |
|   }
 | |
| });
 | |
| function splitFilePath(path) {
 | |
|   const folder_separator = path.lastIndexOf("/");
 | |
|   if (folder_separator === -1) {
 | |
|     return ["", path];
 | |
|   }
 | |
|   return [
 | |
|     path.substring(0, folder_separator),
 | |
|     path.substring(folder_separator + 1)
 | |
|   ];
 | |
| }
 | |
| __name(splitFilePath, "splitFilePath");
 | |
| function getResourceURL(subfolder, filename, type = "input") {
 | |
|   const params = [
 | |
|     "filename=" + encodeURIComponent(filename),
 | |
|     "type=" + type,
 | |
|     "subfolder=" + subfolder,
 | |
|     app.getRandParam().substring(1)
 | |
|   ].join("&");
 | |
|   return `/view?${params}`;
 | |
| }
 | |
| __name(getResourceURL, "getResourceURL");
 | |
| async function uploadFile(audioWidget, audioUIWidget, file2, updateNode, pasted = false) {
 | |
|   try {
 | |
|     const body = new FormData();
 | |
|     body.append("image", file2);
 | |
|     if (pasted) body.append("subfolder", "pasted");
 | |
|     const resp = await api.fetchApi("/upload/image", {
 | |
|       method: "POST",
 | |
|       body
 | |
|     });
 | |
|     if (resp.status === 200) {
 | |
|       const data = await resp.json();
 | |
|       let path = data.name;
 | |
|       if (data.subfolder) path = data.subfolder + "/" + path;
 | |
|       if (!audioWidget.options.values.includes(path)) {
 | |
|         audioWidget.options.values.push(path);
 | |
|       }
 | |
|       if (updateNode) {
 | |
|         audioUIWidget.element.src = api.apiURL(
 | |
|           getResourceURL(...splitFilePath(path))
 | |
|         );
 | |
|         audioWidget.value = path;
 | |
|       }
 | |
|     } else {
 | |
|       alert(resp.status + " - " + resp.statusText);
 | |
|     }
 | |
|   } catch (error) {
 | |
|     alert(error);
 | |
|   }
 | |
| }
 | |
| __name(uploadFile, "uploadFile");
 | |
| app.registerExtension({
 | |
|   name: "Comfy.AudioWidget",
 | |
|   async beforeRegisterNodeDef(nodeType, nodeData) {
 | |
|     if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) {
 | |
|       nodeData.input.required.audioUI = ["AUDIO_UI"];
 | |
|     }
 | |
|   },
 | |
|   getCustomWidgets() {
 | |
|     return {
 | |
|       AUDIO_UI(node, inputName) {
 | |
|         const audio = document.createElement("audio");
 | |
|         audio.controls = true;
 | |
|         audio.classList.add("comfy-audio");
 | |
|         audio.setAttribute("name", "media");
 | |
|         const audioUIWidget = node.addDOMWidget(
 | |
|           inputName,
 | |
|           /* name=*/
 | |
|           "audioUI",
 | |
|           audio
 | |
|         );
 | |
|         audioUIWidget.serialize = false;
 | |
|         const isOutputNode = node.constructor.nodeData.output_node;
 | |
|         if (isOutputNode) {
 | |
|           audioUIWidget.element.classList.add("empty-audio-widget");
 | |
|           const onExecuted = node.onExecuted;
 | |
|           node.onExecuted = function(message) {
 | |
|             onExecuted?.apply(this, arguments);
 | |
|             const audios = message.audio;
 | |
|             if (!audios) return;
 | |
|             const audio2 = audios[0];
 | |
|             audioUIWidget.element.src = api.apiURL(
 | |
|               getResourceURL(audio2.subfolder, audio2.filename, audio2.type)
 | |
|             );
 | |
|             audioUIWidget.element.classList.remove("empty-audio-widget");
 | |
|           };
 | |
|         }
 | |
|         return { widget: audioUIWidget };
 | |
|       }
 | |
|     };
 | |
|   },
 | |
|   onNodeOutputsUpdated(nodeOutputs) {
 | |
|     for (const [nodeId, output] of Object.entries(nodeOutputs)) {
 | |
|       const node = app.graph.getNodeById(nodeId);
 | |
|       if ("audio" in output) {
 | |
|         const audioUIWidget = node.widgets.find(
 | |
|           (w) => w.name === "audioUI"
 | |
|         );
 | |
|         const audio = output.audio[0];
 | |
|         audioUIWidget.element.src = api.apiURL(
 | |
|           getResourceURL(audio.subfolder, audio.filename, audio.type)
 | |
|         );
 | |
|         audioUIWidget.element.classList.remove("empty-audio-widget");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| });
 | |
| app.registerExtension({
 | |
|   name: "Comfy.UploadAudio",
 | |
|   async beforeRegisterNodeDef(nodeType, nodeData) {
 | |
|     if (nodeData?.input?.required?.audio?.[1]?.audio_upload === true) {
 | |
|       nodeData.input.required.upload = ["AUDIOUPLOAD"];
 | |
|     }
 | |
|   },
 | |
|   getCustomWidgets() {
 | |
|     return {
 | |
|       AUDIOUPLOAD(node, inputName) {
 | |
|         const audioWidget = node.widgets.find(
 | |
|           (w) => w.name === "audio"
 | |
|         );
 | |
|         const audioUIWidget = node.widgets.find(
 | |
|           (w) => w.name === "audioUI"
 | |
|         );
 | |
|         const onAudioWidgetUpdate = /* @__PURE__ */ __name(() => {
 | |
|           audioUIWidget.element.src = api.apiURL(
 | |
|             getResourceURL(...splitFilePath(audioWidget.value))
 | |
|           );
 | |
|         }, "onAudioWidgetUpdate");
 | |
|         if (audioWidget.value) {
 | |
|           onAudioWidgetUpdate();
 | |
|         }
 | |
|         audioWidget.callback = onAudioWidgetUpdate;
 | |
|         const onGraphConfigured = node.onGraphConfigured;
 | |
|         node.onGraphConfigured = function() {
 | |
|           onGraphConfigured?.apply(this, arguments);
 | |
|           if (audioWidget.value) {
 | |
|             onAudioWidgetUpdate();
 | |
|           }
 | |
|         };
 | |
|         const fileInput = document.createElement("input");
 | |
|         fileInput.type = "file";
 | |
|         fileInput.accept = "audio/*";
 | |
|         fileInput.style.display = "none";
 | |
|         fileInput.onchange = () => {
 | |
|           if (fileInput.files.length) {
 | |
|             uploadFile(audioWidget, audioUIWidget, fileInput.files[0], true);
 | |
|           }
 | |
|         };
 | |
|         const uploadWidget = node.addWidget(
 | |
|           "button",
 | |
|           inputName,
 | |
|           /* value=*/
 | |
|           "",
 | |
|           () => {
 | |
|             fileInput.click();
 | |
|           }
 | |
|         );
 | |
|         uploadWidget.label = "choose file to upload";
 | |
|         uploadWidget.serialize = false;
 | |
|         return { widget: uploadWidget };
 | |
|       }
 | |
|     };
 | |
|   }
 | |
| });
 | |
| //# sourceMappingURL=index-CrROdkG4.js.map
 | 
