|
var __defProp = Object.defineProperty; |
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); |
|
import { C as ComfyDialog, $ as $el, a as ComfyApp, b as app, L as LGraphCanvas, c as LiteGraph, d as LGraphNode, e as applyTextReplacements, f as ComfyWidgets, g as addValueControlWidgets, D as DraggableList, h as api, u as useToastStore, i as LGraphGroup } from "./index-CI3N807S.js"; |
|
class ClipspaceDialog extends ComfyDialog { |
|
static { |
|
__name(this, "ClipspaceDialog"); |
|
} |
|
static items = []; |
|
static instance = null; |
|
static registerButton(name, contextPredicate, callback) { |
|
const item = $el("button", { |
|
type: "button", |
|
textContent: name, |
|
contextPredicate, |
|
onclick: callback |
|
}); |
|
ClipspaceDialog.items.push(item); |
|
} |
|
static invalidatePreview() { |
|
if (ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) { |
|
const img_preview = document.getElementById( |
|
"clipspace_preview" |
|
); |
|
if (img_preview) { |
|
img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src; |
|
img_preview.style.maxHeight = "100%"; |
|
img_preview.style.maxWidth = "100%"; |
|
} |
|
} |
|
} |
|
static invalidate() { |
|
if (ClipspaceDialog.instance) { |
|
const self = ClipspaceDialog.instance; |
|
const children = $el("div.comfy-modal-content", [ |
|
self.createImgSettings(), |
|
...self.createButtons() |
|
]); |
|
if (self.element) { |
|
self.element.removeChild(self.element.firstChild); |
|
self.element.appendChild(children); |
|
} else { |
|
self.element = $el("div.comfy-modal", { parent: document.body }, [ |
|
children |
|
]); |
|
} |
|
if (self.element.children[0].children.length <= 1) { |
|
self.element.children[0].appendChild( |
|
$el("p", {}, [ |
|
"Unable to find the features to edit content of a format stored in the current Clipspace." |
|
]) |
|
); |
|
} |
|
ClipspaceDialog.invalidatePreview(); |
|
} |
|
} |
|
constructor() { |
|
super(); |
|
} |
|
createButtons() { |
|
const buttons = []; |
|
for (let idx in ClipspaceDialog.items) { |
|
const item = ClipspaceDialog.items[idx]; |
|
if (!item.contextPredicate || item.contextPredicate()) |
|
buttons.push(ClipspaceDialog.items[idx]); |
|
} |
|
buttons.push( |
|
$el("button", { |
|
type: "button", |
|
textContent: "Close", |
|
onclick: __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: __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: __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: __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: __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", |
|
|
|
CLIP_VISION: "#A8DADC", |
|
|
|
CLIP_VISION_OUTPUT: "#ad7452", |
|
|
|
CONDITIONING: "#FFA931", |
|
|
|
CONTROL_NET: "#6EE7B7", |
|
|
|
IMAGE: "#64B5F6", |
|
|
|
LATENT: "#FF9CF9", |
|
|
|
MASK: "#81C784", |
|
|
|
MODEL: "#B39DDB", |
|
|
|
STYLE_MODEL: "#C2FFAE", |
|
|
|
VAE: "#FF6E6E", |
|
|
|
NOISE: "#B0B0B0", |
|
|
|
GUIDER: "#66FFFF", |
|
|
|
SAMPLER: "#ECB4B4", |
|
|
|
SIGMAS: "#CDFFCD", |
|
|
|
TAESD: "#DCC274" |
|
|
|
}, |
|
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", |
|
|
|
CLIP_VISION: "#5C6BC0", |
|
|
|
CLIP_VISION_OUTPUT: "#8D6E63", |
|
|
|
CONDITIONING: "#EF5350", |
|
|
|
CONTROL_NET: "#66BB6A", |
|
|
|
IMAGE: "#42A5F5", |
|
|
|
LATENT: "#AB47BC", |
|
|
|
MASK: "#9CCC65", |
|
|
|
MODEL: "#7E57C2", |
|
|
|
STYLE_MODEL: "#D4E157", |
|
|
|
VAE: "#FF7043" |
|
|
|
}, |
|
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", |
|
|
|
CLIP_VISION: "#6c71c4", |
|
|
|
CLIP_VISION_OUTPUT: "#859900", |
|
|
|
CONDITIONING: "#d33682", |
|
|
|
CONTROL_NET: "#d1ffd7", |
|
|
|
IMAGE: "#5940bb", |
|
|
|
LATENT: "#268bd2", |
|
|
|
MASK: "#CCC9E7", |
|
|
|
MODEL: "#dc322f", |
|
|
|
STYLE_MODEL: "#1a998a", |
|
|
|
UPSCALE_MODEL: "#054A29", |
|
|
|
VAE: "#facfad" |
|
|
|
}, |
|
litegraph_base: { |
|
NODE_TITLE_COLOR: "#fdf6e3", |
|
|
|
NODE_SELECTED_TITLE_COLOR: "#A9D400", |
|
NODE_TEXT_SIZE: 14, |
|
NODE_TEXT_COLOR: "#657b83", |
|
|
|
NODE_SUBTEXT_SIZE: 12, |
|
NODE_DEFAULT_COLOR: "#094656", |
|
NODE_DEFAULT_BGCOLOR: "#073642", |
|
|
|
NODE_DEFAULT_BOXCOLOR: "#839496", |
|
|
|
NODE_DEFAULT_SHAPE: "box", |
|
NODE_BOX_OUTLINE_COLOR: "#fdf6e3", |
|
|
|
DEFAULT_SHADOW_COLOR: "rgba(0,0,0,0.5)", |
|
DEFAULT_GROUP_FONT: 24, |
|
WIDGET_BGCOLOR: "#002b36", |
|
|
|
WIDGET_OUTLINE_COLOR: "#839496", |
|
|
|
WIDGET_TEXT_COLOR: "#fdf6e3", |
|
|
|
WIDGET_SECONDARY_TEXT_COLOR: "#93a1a1", |
|
|
|
LINK_COLOR: "#2aa198", |
|
|
|
EVENT_LINK_COLOR: "#268bd2", |
|
|
|
CONNECTING_LINK_COLOR: "#859900" |
|
|
|
}, |
|
comfy_base: { |
|
"fg-color": "#fdf6e3", |
|
|
|
"bg-color": "#002b36", |
|
|
|
"comfy-menu-bg": "#073642", |
|
|
|
"comfy-input-bg": "#002b36", |
|
|
|
"input-text": "#93a1a1", |
|
|
|
"descrip-text": "#586e75", |
|
|
|
"drag-text": "#839496", |
|
|
|
"error-text": "#dc322f", |
|
|
|
"border-color": "#657b83", |
|
|
|
"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 = __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 = __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 = __name(() => { |
|
return app.ui.settings.getSettingValue(idCustomColorPalettes, {}); |
|
}, "getCustomColorPalettes"); |
|
const setCustomColorPalettes = __name((customColorPalettes) => { |
|
return app.ui.settings.setSettingValue( |
|
idCustomColorPalettes, |
|
customColorPalettes |
|
); |
|
}, "setCustomColorPalettes"); |
|
const addCustomColorPalette = __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 = __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 = __name(async (colorPalette) => { |
|
colorPalette = await completeColorPalette(colorPalette); |
|
if (colorPalette.colors) { |
|
if (colorPalette.colors.node_slot) { |
|
Object.assign( |
|
|
|
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 = __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 = __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: __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: __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: __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: __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: __name(() => { |
|
fileInput.click(); |
|
}, "onclick") |
|
}), |
|
$el("input", { |
|
type: "button", |
|
value: "Template", |
|
onclick: __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: __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 = __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 = new Set([ |
|
...Object.keys(config1[1] ?? {}), |
|
...Object.keys(config2[1] ?? {}) |
|
]); |
|
let customConfig; |
|
const getCustomConfig = __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: __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: __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: __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: __name((e) => { |
|
this.storeModification({ |
|
section, |
|
prop, |
|
value: { name: e.target.value } |
|
}); |
|
}, "onchange") |
|
}), |
|
$el("label", { textContent: "Visible" }, [ |
|
$el("input", { |
|
type: "checkbox", |
|
checked, |
|
disabled: !checkable, |
|
onchange: __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: __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: __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: __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: __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: __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 = __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 = __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 = 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: __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 = __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 = __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 = __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: __name(() => { |
|
return this.convertToNodes(); |
|
}, "callback") |
|
}, |
|
{ |
|
content: "Manage Group Node", |
|
callback: __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 = __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: __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: __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: __name(() => { |
|
var group2 = new LiteGraph.LGraphGroup(); |
|
addNodesToGroup(group2, this.selected_nodes); |
|
app.canvas.graph.add(group2); |
|
this.graph.change(); |
|
}, "callback") |
|
}); |
|
return options; |
|
} |
|
group.recomputeInsideNodes(); |
|
const nodesInGroup = group._nodes; |
|
options.push({ |
|
content: "Add Selected Nodes To Group", |
|
disabled: !Object.keys(app.canvas.selected_nodes || {}).length, |
|
callback: __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: __name(() => { |
|
addNodesToGroup(group); |
|
this.graph.change(); |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Select Nodes", |
|
callback: __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: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 2); |
|
} |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Bypass Group Nodes", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 4); |
|
} |
|
}, "callback") |
|
}); |
|
break; |
|
case 2: |
|
options.push({ |
|
content: "Set Group Nodes to Always", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 0); |
|
} |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Bypass Group Nodes", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 4); |
|
} |
|
}, "callback") |
|
}); |
|
break; |
|
case 4: |
|
options.push({ |
|
content: "Set Group Nodes to Always", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 0); |
|
} |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Set Group Nodes to Never", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 2); |
|
} |
|
}, "callback") |
|
}); |
|
break; |
|
default: |
|
options.push({ |
|
content: "Set Group Nodes to Always", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 0); |
|
} |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Set Group Nodes to Never", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 2); |
|
} |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Bypass Group Nodes", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 4); |
|
} |
|
}, "callback") |
|
}); |
|
break; |
|
} |
|
} else { |
|
options.push({ |
|
content: "Set Group Nodes to Always", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 0); |
|
} |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Set Group Nodes to Never", |
|
callback: __name(() => { |
|
for (const node of nodesInGroup) { |
|
setNodeMode(node, 2); |
|
} |
|
}, "callback") |
|
}); |
|
options.push({ |
|
content: "Bypass Group Nodes", |
|
callback: __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 = __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 = __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", |
|
|
|
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 = __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: __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: __name(() => this.exportAll(), "onclick") |
|
}) |
|
); |
|
btns.unshift( |
|
$el("button", { |
|
type: "button", |
|
textContent: "Import", |
|
onclick: __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: __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: __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: __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: __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: __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: __name((e) => { |
|
var el = e.target; |
|
clearTimeout(this.saveVisualCue); |
|
el.style.transitionDuration = "0s"; |
|
el.style.backgroundColor = "var(--comfy-input-bg)"; |
|
}, "onkeypress"), |
|
$: __name((el) => nameInput = el, "$") |
|
}) |
|
] |
|
), |
|
$el("div", {}, [ |
|
$el("button", { |
|
textContent: "Export", |
|
style: { |
|
fontSize: "12px", |
|
fontWeight: "normal" |
|
}, |
|
onclick: __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: __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 = __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: __name(() => { |
|
const name = prompt("Enter name"); |
|
if (!name?.trim()) return; |
|
clipboardAction(() => { |
|
app.canvas.copyToClipboard(); |
|
let data = localStorage.getItem("litegrapheditor_clipboard"); |
|
data = JSON.parse(data); |
|
const nodeIds = Object.keys(app.canvas.selected_nodes); |
|
for (let i = 0; i < nodeIds.length; i++) { |
|
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i])); |
|
const nodeData = node?.constructor.nodeData; |
|
let groupData = GroupNodeHandler.getGroupData(node); |
|
if (groupData) { |
|
groupData = groupData.nodeData; |
|
if (!data.groupNodes) { |
|
data.groupNodes = {}; |
|
} |
|
data.groupNodes[nodeData.name] = groupData; |
|
data.nodes[i].type = nodeData.name; |
|
} |
|
} |
|
manage.templates.push({ |
|
name, |
|
data: JSON.stringify(data) |
|
}); |
|
manage.store(); |
|
}); |
|
}, "callback") |
|
}); |
|
const subItems = manage.templates.map((t) => { |
|
return { |
|
content: t.name, |
|
callback: __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: __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( |
|
|
|
this, |
|
"", |
|
|
|
["", { 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: __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: __name(() => { |
|
RerouteNode.setDefaultTextVisibility( |
|
!RerouteNode.defaultVisibility |
|
); |
|
}, "callback") |
|
}, |
|
{ |
|
|
|
|
|
|
|
|
|
content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"), |
|
callback: __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 = 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 (( 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: __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 = __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 = __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, |
|
|
|
"audioUI", |
|
audio |
|
); |
|
audioUIWidget.serialize = false; |
|
const isOutputNode = node.constructor.nodeData.output_node; |
|
if (isOutputNode) { |
|
audioUIWidget.element.classList.add("empty-audio-widget"); |
|
const onExecuted = node.onExecuted; |
|
node.onExecuted = function(message) { |
|
onExecuted?.apply(this, arguments); |
|
const audios = message.audio; |
|
if (!audios) return; |
|
const audio2 = audios[0]; |
|
audioUIWidget.element.src = api.apiURL( |
|
getResourceURL(audio2.subfolder, audio2.filename, audio2.type) |
|
); |
|
audioUIWidget.element.classList.remove("empty-audio-widget"); |
|
}; |
|
} |
|
return { widget: audioUIWidget }; |
|
} |
|
}; |
|
}, |
|
onNodeOutputsUpdated(nodeOutputs) { |
|
for (const [nodeId, output] of Object.entries(nodeOutputs)) { |
|
const node = app.graph.getNodeById(Number.parseInt(nodeId)); |
|
if ("audio" in output) { |
|
const audioUIWidget = node.widgets.find( |
|
(w) => w.name === "audioUI" |
|
); |
|
const audio = output.audio[0]; |
|
audioUIWidget.element.src = api.apiURL( |
|
getResourceURL(audio.subfolder, audio.filename, audio.type) |
|
); |
|
audioUIWidget.element.classList.remove("empty-audio-widget"); |
|
} |
|
} |
|
} |
|
}); |
|
app.registerExtension({ |
|
name: "Comfy.UploadAudio", |
|
async beforeRegisterNodeDef(nodeType, nodeData) { |
|
if (nodeData?.input?.required?.audio?.[1]?.audio_upload === true) { |
|
nodeData.input.required.upload = ["AUDIOUPLOAD"]; |
|
} |
|
}, |
|
getCustomWidgets() { |
|
return { |
|
AUDIOUPLOAD(node, inputName) { |
|
const audioWidget = node.widgets.find( |
|
(w) => w.name === "audio" |
|
); |
|
const audioUIWidget = node.widgets.find( |
|
(w) => w.name === "audioUI" |
|
); |
|
const onAudioWidgetUpdate = __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, |
|
|
|
"", |
|
() => { |
|
fileInput.click(); |
|
} |
|
); |
|
uploadWidget.label = "choose file to upload"; |
|
uploadWidget.serialize = false; |
|
return { widget: uploadWidget }; |
|
} |
|
}; |
|
} |
|
}); |
|
|
|
|