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