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, u as useToastStore, i as LGraphGroup } from "./index-CI3N807S.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(() => { var group2 = new LiteGraph.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(Number.parseInt(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(Number.parseInt(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-BD-Ia1C4.js.map