diff --git a/web/extensions/core/clipspace.js b/web/extensions/core/clipspace.js deleted file mode 100644 index e376a02f7..000000000 --- a/web/extensions/core/clipspace.js +++ /dev/null @@ -1,166 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { ComfyApp } from "../../scripts/app.js"; - -export class ClipspaceDialog extends ComfyDialog { - static items = []; - static instance = null; - - static registerButton(name, contextPredicate, callback) { - const item = - $el("button", { - type: "button", - textContent: name, - contextPredicate: 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; - // allow reconstruct controls when copying from non-image to image content. - const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]); - - if(self.element) { - // update - self.element.removeChild(self.element.firstChild); - self.element.appendChild(children); - } - else { - // new - 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(self) { - 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: () => { this.close(); } - }) - ); - - 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:(event) => { - ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex; - ClipspaceDialog.invalidatePreview(); - } }, 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:(event) => { - ComfyApp.clipspace['img_paste_mode'] = event.target.value; - } }, - [ - $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:() => false},[]) ]); - - 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:() => false}); - } - else - return []; - } - - show() { - const img_preview = document.getElementById("clipspace_preview"); - ClipspaceDialog.invalidate(); - - this.element.style.display = "block"; - } -} - -app.registerExtension({ - name: "Comfy.Clipspace", - init(app) { - app.openClipspace = - function () { - if(!ClipspaceDialog.instance) { - ClipspaceDialog.instance = new ClipspaceDialog(app); - ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate; - } - - if(ComfyApp.clipspace) { - ClipspaceDialog.instance.show(); - } - else - app.ui.dialog.show("Clipspace is Empty!"); - }; - } -}); \ No newline at end of file diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js deleted file mode 100644 index 245f3b93e..000000000 --- a/web/extensions/core/colorPalette.js +++ /dev/null @@ -1,785 +0,0 @@ -import {app} from "../../scripts/app.js"; -import {$el} from "../../scripts/ui.js"; - -// Manage color palettes - -const colorPalettes = { - "dark": { - "id": "dark", - "name": "Dark (Default)", - "colors": { - "node_slot": { - "CLIP": "#FFD500", // bright yellow - "CLIP_VISION": "#A8DADC", // light blue-gray - "CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange - "CONDITIONING": "#FFA931", // vibrant orange-yellow - "CONTROL_NET": "#6EE7B7", // soft mint green - "IMAGE": "#64B5F6", // bright sky blue - "LATENT": "#FF9CF9", // light pink-purple - "MASK": "#81C784", // muted green - "MODEL": "#B39DDB", // light lavender-purple - "STYLE_MODEL": "#C2FFAE", // light green-yellow - "VAE": "#FF6E6E", // bright red - "NOISE": "#B0B0B0", // gray - "GUIDER": "#66FFFF", // cyan - "SAMPLER": "#ECB4B4", // very soft red - "SIGMAS": "#CDFFCD", // soft lime green - "TAESD": "#DCC274", // cheesecake - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=", - "CLEAR_BACKGROUND_COLOR": "#222", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#FFF", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#AAA", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#333", - "NODE_DEFAULT_BGCOLOR": "#353535", - "NODE_DEFAULT_BOXCOLOR": "#666", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#FFF", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - - "WIDGET_BGCOLOR": "#222", - "WIDGET_OUTLINE_COLOR": "#666", - "WIDGET_TEXT_COLOR": "#DDD", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA", - }, - "comfy_base": { - "fg-color": "#fff", - "bg-color": "#202020", - "comfy-menu-bg": "#353535", - "comfy-input-bg": "#222", - "input-text": "#ddd", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#4e4e4e", - "tr-even-bg-color": "#222", - "tr-odd-bg-color": "#353535", - "content-bg": "#4e4e4e", - "content-fg": "#fff", - "content-hover-bg": "#222", - "content-hover-fg": "#fff" - } - }, - }, - "light": { - "id": "light", - "name": "Light", - "colors": { - "node_slot": { - "CLIP": "#FFA726", // orange - "CLIP_VISION": "#5C6BC0", // indigo - "CLIP_VISION_OUTPUT": "#8D6E63", // brown - "CONDITIONING": "#EF5350", // red - "CONTROL_NET": "#66BB6A", // green - "IMAGE": "#42A5F5", // blue - "LATENT": "#AB47BC", // purple - "MASK": "#9CCC65", // light green - "MODEL": "#7E57C2", // deep purple - "STYLE_MODEL": "#D4E157", // lime - "VAE": "#FF7043", // deep orange - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==", - "CLEAR_BACKGROUND_COLOR": "lightgray", - "NODE_TITLE_COLOR": "#222", - "NODE_SELECTED_TITLE_COLOR": "#000", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#444", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#F7F7F7", - "NODE_DEFAULT_BGCOLOR": "#F5F5F5", - "NODE_DEFAULT_BOXCOLOR": "#CCC", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#000", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)", - "DEFAULT_GROUP_FONT": 24, - - "WIDGET_BGCOLOR": "#D4D4D4", - "WIDGET_OUTLINE_COLOR": "#999", - "WIDGET_TEXT_COLOR": "#222", - "WIDGET_SECONDARY_TEXT_COLOR": "#555", - - "LINK_COLOR": "#4CAF50", - "EVENT_LINK_COLOR": "#FF9800", - "CONNECTING_LINK_COLOR": "#2196F3", - }, - "comfy_base": { - "fg-color": "#222", - "bg-color": "#DDD", - "comfy-menu-bg": "#F5F5F5", - "comfy-input-bg": "#C9C9C9", - "input-text": "#222", - "descrip-text": "#444", - "drag-text": "#555", - "error-text": "#F44336", - "border-color": "#888", - "tr-even-bg-color": "#f9f9f9", - "tr-odd-bg-color": "#fff", - "content-bg": "#e0e0e0", - "content-fg": "#222", - "content-hover-bg": "#adadad", - "content-hover-fg": "#222" - } - }, - }, - "solarized": { - "id": "solarized", - "name": "Solarized", - "colors": { - "node_slot": { - "CLIP": "#2AB7CA", // light blue - "CLIP_VISION": "#6c71c4", // blue violet - "CLIP_VISION_OUTPUT": "#859900", // olive green - "CONDITIONING": "#d33682", // magenta - "CONTROL_NET": "#d1ffd7", // light mint green - "IMAGE": "#5940bb", // deep blue violet - "LATENT": "#268bd2", // blue - "MASK": "#CCC9E7", // light purple-gray - "MODEL": "#dc322f", // red - "STYLE_MODEL": "#1a998a", // teal - "UPSCALE_MODEL": "#054A29", // dark green - "VAE": "#facfad", // light pink-orange - }, - "litegraph_base": { - "NODE_TITLE_COLOR": "#fdf6e3", // Base3 - "NODE_SELECTED_TITLE_COLOR": "#A9D400", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#657b83", // Base00 - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#094656", - "NODE_DEFAULT_BGCOLOR": "#073642", // Base02 - "NODE_DEFAULT_BOXCOLOR": "#839496", // Base0 - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#fdf6e3", // Base3 - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - - "WIDGET_BGCOLOR": "#002b36", // Base03 - "WIDGET_OUTLINE_COLOR": "#839496", // Base0 - "WIDGET_TEXT_COLOR": "#fdf6e3", // Base3 - "WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", // Base1 - - "LINK_COLOR": "#2aa198", // Solarized Cyan - "EVENT_LINK_COLOR": "#268bd2", // Solarized Blue - "CONNECTING_LINK_COLOR": "#859900", // Solarized Green - }, - "comfy_base": { - "fg-color": "#fdf6e3", // Base3 - "bg-color": "#002b36", // Base03 - "comfy-menu-bg": "#073642", // Base02 - "comfy-input-bg": "#002b36", // Base03 - "input-text": "#93a1a1", // Base1 - "descrip-text": "#586e75", // Base01 - "drag-text": "#839496", // Base0 - "error-text": "#dc322f", // Solarized Red - "border-color": "#657b83", // Base00 - "tr-even-bg-color": "#002b36", - "tr-odd-bg-color": "#073642", - "content-bg": "#657b83", - "content-fg": "#fdf6e3", - "content-hover-bg": "#002b36", - "content-hover-fg": "#fdf6e3" - } - }, - }, - "arc": { - "id": "arc", - "name": "Arc", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==", - "CLEAR_BACKGROUND_COLOR": "#2b2f38", - "NODE_TITLE_COLOR": "#b2b7bd", - "NODE_SELECTED_TITLE_COLOR": "#FFF", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#AAA", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#2b2f38", - "NODE_DEFAULT_BGCOLOR": "#242730", - "NODE_DEFAULT_BOXCOLOR": "#6e7581", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#FFF", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 22, - "WIDGET_BGCOLOR": "#2b2f38", - "WIDGET_OUTLINE_COLOR": "#6e7581", - "WIDGET_TEXT_COLOR": "#DDD", - "WIDGET_SECONDARY_TEXT_COLOR": "#b2b7bd", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#fff", - "bg-color": "#2b2f38", - "comfy-menu-bg": "#242730", - "comfy-input-bg": "#2b2f38", - "input-text": "#ddd", - "descrip-text": "#b2b7bd", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#6e7581", - "tr-even-bg-color": "#2b2f38", - "tr-odd-bg-color": "#242730", - "content-bg": "#6e7581", - "content-fg": "#fff", - "content-hover-bg": "#2b2f38", - "content-hover-fg": "#fff" - } - }, - }, - "nord": { - "id": "nord", - "name": "Nord", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC", - "CLEAR_BACKGROUND_COLOR": "#212732", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#e5eaf0", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#bcc2c8", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#2e3440", - "NODE_DEFAULT_BGCOLOR": "#161b22", - "NODE_DEFAULT_BOXCOLOR": "#545d70", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#e5eaf0", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - "WIDGET_BGCOLOR": "#2e3440", - "WIDGET_OUTLINE_COLOR": "#545d70", - "WIDGET_TEXT_COLOR": "#bcc2c8", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#e5eaf0", - "bg-color": "#2e3440", - "comfy-menu-bg": "#161b22", - "comfy-input-bg": "#2e3440", - "input-text": "#bcc2c8", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#545d70", - "tr-even-bg-color": "#2e3440", - "tr-odd-bg-color": "#161b22", - "content-bg": "#545d70", - "content-fg": "#e5eaf0", - "content-hover-bg": "#2e3440", - "content-hover-fg": "#e5eaf0" - } - }, - }, - "github": { - "id": "github", - "name": "Github", - "colors": { - "node_slot": { - "BOOLEAN": "", - "CLIP": "#eacb8b", - "CLIP_VISION": "#A8DADC", - "CLIP_VISION_OUTPUT": "#ad7452", - "CONDITIONING": "#cf876f", - "CONTROL_NET": "#00d78d", - "CONTROL_NET_WEIGHTS": "", - "FLOAT": "", - "GLIGEN": "", - "IMAGE": "#80a1c0", - "IMAGEUPLOAD": "", - "INT": "", - "LATENT": "#b38ead", - "LATENT_KEYFRAME": "", - "MASK": "#a3bd8d", - "MODEL": "#8978a7", - "SAMPLER": "", - "SIGMAS": "", - "STRING": "", - "STYLE_MODEL": "#C2FFAE", - "T2I_ADAPTER_WEIGHTS": "", - "TAESD": "#DCC274", - "TIMESTEP_KEYFRAME": "", - "UPSCALE_MODEL": "", - "VAE": "#be616b" - }, - "litegraph_base": { - "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==", - "CLEAR_BACKGROUND_COLOR": "#040506", - "NODE_TITLE_COLOR": "#999", - "NODE_SELECTED_TITLE_COLOR": "#e5eaf0", - "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#bcc2c8", - "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#161b22", - "NODE_DEFAULT_BGCOLOR": "#13171d", - "NODE_DEFAULT_BOXCOLOR": "#30363d", - "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#e5eaf0", - "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", - "DEFAULT_GROUP_FONT": 24, - "WIDGET_BGCOLOR": "#161b22", - "WIDGET_OUTLINE_COLOR": "#30363d", - "WIDGET_TEXT_COLOR": "#bcc2c8", - "WIDGET_SECONDARY_TEXT_COLOR": "#999", - "LINK_COLOR": "#9A9", - "EVENT_LINK_COLOR": "#A86", - "CONNECTING_LINK_COLOR": "#AFA" - }, - "comfy_base": { - "fg-color": "#e5eaf0", - "bg-color": "#161b22", - "comfy-menu-bg": "#13171d", - "comfy-input-bg": "#161b22", - "input-text": "#bcc2c8", - "descrip-text": "#999", - "drag-text": "#ccc", - "error-text": "#ff4444", - "border-color": "#30363d", - "tr-even-bg-color": "#161b22", - "tr-odd-bg-color": "#13171d", - "content-bg": "#30363d", - "content-fg": "#e5eaf0", - "content-hover-bg": "#161b22", - "content-hover-fg": "#e5eaf0" - } - }, - } -}; - -const id = "Comfy.ColorPalette"; -const idCustomColorPalettes = "Comfy.CustomColorPalettes"; -const defaultColorPaletteId = "dark"; -const els = {} -// const ctxMenu = LiteGraph.ContextMenu; -app.registerExtension({ - name: id, - addCustomNodeDefs(node_defs) { - const sortObjectKeys = (unordered) => { - return Object.keys(unordered).sort().reduce((obj, key) => { - obj[key] = unordered[key]; - return obj; - }, {}); - }; - - 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"] !== undefined) { - 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; - } - - 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; - } - - const getColorPaletteTemplate = async () => { - let colorPalette = { - "id": "my_color_palette_unique_id", - "name": "My Color Palette", - "colors": { - "node_slot": {}, - "litegraph_base": {}, - "comfy_base": {} - } - }; - - // Copy over missing keys from default color palette - 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); - }; - - const getCustomColorPalettes = () => { - return app.ui.settings.getSettingValue(idCustomColorPalettes, {}); - }; - - const setCustomColorPalettes = (customColorPalettes) => { - return app.ui.settings.setSettingValue(idCustomColorPalettes, customColorPalettes); - }; - - const addCustomColorPalette = 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); - }; - - const deleteCustomColorPalette = async (colorPaletteId) => { - const customColorPalettes = getCustomColorPalettes(); - delete customColorPalettes[colorPaletteId]; - setCustomColorPalettes(customColorPalettes); - - for (const option of els.select.childNodes) { - if (option.value === defaultColorPaletteId) { - option.selected = true; - } - - if (option.value === "custom_" + colorPaletteId) { - els.select.removeChild(option); - } - } - - setColorPalette(defaultColorPaletteId); - await loadColorPalette(getColorPalette()); - }; - - const loadColorPalette = async (colorPalette) => { - colorPalette = await completeColorPalette(colorPalette); - if (colorPalette.colors) { - // Sets the colors of node slots and links - 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); - } - // Sets the colors of the LiteGraph objects - if (colorPalette.colors.litegraph_base) { - // Everything updates correctly in the loop, except the Node Title and Link Color for some reason - 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]; - } - } - } - // Sets the color of ComfyUI elements - 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); - } - }; - - const getColorPalette = (colorPaletteId) => { - if (!colorPaletteId) { - colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - } - - if (colorPaletteId.startsWith("custom_")) { - colorPaletteId = colorPaletteId.substr(7); - let customColorPalettes = getCustomColorPalettes(); - if (customColorPalettes[colorPaletteId]) { - return customColorPalettes[colorPaletteId]; - } - } - - return colorPalettes[colorPaletteId]; - }; - - const setColorPalette = (colorPaletteId) => { - app.ui.settings.setSettingValue(id, colorPaletteId); - }; - - const fileInput = $el("input", { - type: "file", - accept: ".json", - style: {display: "none"}, - parent: document.body, - onchange: () => { - const file = fileInput.files[0]; - if (file.type === "application/json" || file.name.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - await addCustomColorPalette(JSON.parse(reader.result)); - }; - reader.readAsText(file); - } - }, - }); - - app.ui.settings.addSetting({ - id, - name: "Color Palette", - type: (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: (e) => { - setter(e.target.value); - } - }, options) - - return $el("tr", [ - $el("td", [ - $el("label", { - for: id.replaceAll(".", "-"), - textContent: "Color palette", - }), - ]), - $el("td", [ - els.select, - $el("div", { - style: { - display: "grid", - gap: "4px", - gridAutoFlow: "column", - }, - }, [ - $el("input", { - type: "button", - value: "Export", - onclick: async () => { - const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId); - const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId)); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - 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); - }, - }), - $el("input", { - type: "button", - value: "Import", - onclick: () => { - fileInput.click(); - } - }), - $el("input", { - type: "button", - value: "Template", - onclick: async () => { - const colorPalette = await getColorPaletteTemplate(); - const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string - 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); - } - }), - $el("input", { - type: "button", - value: "Delete", - onclick: async () => { - let colorPaletteId = app.ui.settings.getSettingValue(id, 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); - } - }), - ]), - ]), - ]) - }, - 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 === undefined || CLEAR_BACKGROUND_COLOR === undefined) { - 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); - }, - }); - }, -}); diff --git a/web/extensions/core/contextMenuFilter.js b/web/extensions/core/contextMenuFilter.js deleted file mode 100644 index 0a305391a..000000000 --- a/web/extensions/core/contextMenuFilter.js +++ /dev/null @@ -1,148 +0,0 @@ -import {app} from "../../scripts/app.js"; - -// Adds filtering to combo context menus - -const ext = { - name: "Comfy.ContextMenuFilter", - init() { - const ctxMenu = LiteGraph.ContextMenu; - - LiteGraph.ContextMenu = function (values, options) { - const ctx = ctxMenu.call(this, values, options); - - // If we are a dark menu (only used for combo boxes) then add a filter input - if (options?.className === "dark" && values?.length > 10) { - const filter = document.createElement("input"); - filter.classList.add("comfy-context-menu-filter"); - filter.placeholder = "Filter list"; - this.root.prepend(filter); - - const items = Array.from(this.root.querySelectorAll(".litemenu-entry")); - let displayedItems = [...items]; - let itemCount = displayedItems.length; - - // We must request an animation frame for the current node of the active canvas to update. - 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(); - - // Apply highlighting to the selected item - 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"); - } - - const positionList = () => { - const rect = this.root.getBoundingClientRect(); - - // If the top is off-screen then shift the element with scaling applied - if (rect.top < 0) { - const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight; - const shift = (this.root.clientHeight * scale) / 2; - this.root.style.top = -shift + "px"; - } - } - - // Arrow up/down to select items - 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": - this.close(); - break; - } - }); - - filter.addEventListener("input", () => { - // Hide all items that don't match our filter - const term = filter.value.toLocaleLowerCase(); - // When filtering, recompute which items are visible for arrow up/down and maintain selection. - 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 we have an event then we can try and position the list under the source - if (options.event) { - let top = options.event.clientY - 10; - - const bodyRect = document.body.getBoundingClientRect(); - const rootRect = this.root.getBoundingClientRect(); - if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) { - top = Math.max(0, bodyRect.height - rootRect.height - 10); - } - - this.root.style.top = top + "px"; - positionList(); - } - }); - - requestAnimationFrame(() => { - // Focus the filter box when opening - filter.focus(); - - positionList(); - }); - }) - } - - return ctx; - }; - - LiteGraph.ContextMenu.prototype = ctxMenu.prototype; - }, -} - -app.registerExtension(ext); diff --git a/web/extensions/core/dynamicPrompts.js b/web/extensions/core/dynamicPrompts.js deleted file mode 100644 index 7417361ba..000000000 --- a/web/extensions/core/dynamicPrompts.js +++ /dev/null @@ -1,48 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Allows for simple dynamic prompt replacement -// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued. - -/* - * Strips C-style line and block comments from a string - */ -function stripComments(str) { - return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,''); -} - -app.registerExtension({ - name: "Comfy.DynamicPrompts", - nodeCreated(node) { - if (node.widgets) { - // Locate dynamic prompt text widgets - // Include any widgets with dynamicPrompts set to true, and customtext - const widgets = node.widgets.filter( - (n) => n.dynamicPrompts - ); - for (const widget of widgets) { - // Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node - widget.serializeValue = (workflowNode, widgetIndex) => { - let prompt = stripComments(widget.value); - while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) { - const startIndex = prompt.replace("\\{", "00").indexOf("{"); - const endIndex = prompt.replace("\\}", "00").indexOf("}"); - - const optionsString = prompt.substring(startIndex + 1, endIndex); - const options = optionsString.split("|"); - - const randomIndex = Math.floor(Math.random() * options.length); - const randomOption = options[randomIndex]; - - prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1); - } - - // Overwrite the value in the serialized workflow pnginfo - if (workflowNode?.widgets_values) - workflowNode.widgets_values[widgetIndex] = prompt; - - return prompt; - }; - } - } - }, -}); diff --git a/web/extensions/core/editAttention.js b/web/extensions/core/editAttention.js deleted file mode 100644 index 6792b2357..000000000 --- a/web/extensions/core/editAttention.js +++ /dev/null @@ -1,144 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys - -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; - if (newWeight < 0) return "0"; - return String(Number(newWeight.toFixed(10))); - } - - function findNearestEnclosure(text, cursorPos) { - let start = cursorPos, end = cursorPos; - let openCount = 0, closeCount = 0; - - // Find opening parenthesis before cursor - 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; - - // Find closing parenthesis after cursor - 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: end }; - } - - 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; - } - }; - - 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 there is no selection, attempt to find the nearest enclosure, or select the current word - if (!selectedText) { - const nearestEnclosure = findNearestEnclosure(inputField.value, start); - if (nearestEnclosure) { - start = nearestEnclosure.start; - end = nearestEnclosure.end; - selectedText = inputField.value.substring(start, end); - } else { - // Select the current word, find the start and end of the word - const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t"; - - 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 the selection ends with a space, remove it - if (selectedText[selectedText.length - 1] === " ") { - selectedText = selectedText.substring(0, selectedText.length - 1); - end -= 1; - } - - // If there are parentheses left and right of the selection, select them - if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") { - start -= 1; - end += 1; - selectedText = inputField.value.substring(start, end); - } - - // If the selection is not enclosed in parentheses, add them - if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") { - selectedText = `(${selectedText})`; - } - - // If the selection does not have a weight, add a weight of 1.0 - selectedText = addWeightToParentheses(selectedText); - - // Increment the weight - 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.setRangeText(updatedText, start, end, "select"); - } - window.addEventListener("keydown", editAttention); - }, -}); diff --git a/web/extensions/core/groupNode.js b/web/extensions/core/groupNode.js deleted file mode 100644 index 163e42b89..000000000 --- a/web/extensions/core/groupNode.js +++ /dev/null @@ -1,1281 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; -import { mergeIfValid } from "./widgetInputs.js"; -import { ManageGroupDialog } from "./groupNodeManage.js"; - -const GROUP = Symbol(); - -const Workflow = { - InUse: { - Free: 0, - Registered: 1, - InWorkflow: 2, - }, - isInUseGroupNode(name) { - const id = `workflow/${name}`; - // Check if lready registered/in use in this workflow - if (app.graph.extra?.groupNodes?.[name]) { - if (app.graph._nodes.find((n) => n.type === id)) { - 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 { - constructor(nodes) { - this.nodes = nodes; - } - - build() { - const name = this.getName(); - if (!name) return; - - // Sort the nodes so they are in execution order - // this allows for widgets to be in the correct order when reconstructing - 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() { - // Gets the builders nodes in graph execution order - 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 = (config) => { - // Store link types for dynamically typed nodes e.g. reroutes - for (const link of config.links) { - const origin = app.graph.getNodeById(link[4]); - const type = origin.outputs[link[1]].type; - link.push(type); - } - }; - - const storeExternalLinks = (config) => { - // Store any external links to the group in the config so when rebuilding we add extra slots - 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]); - } - } - } - }; - - // Use the built in copyToClipboard function to generate the node data we need - 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); - } - } -} - -export class GroupNodeConfig { - 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 = {}; - - // Extract links for easy lookup - for (const l of this.nodeData.links) { - const [sourceNodeId, sourceNodeSlot, targetNodeId, targetNodeSlot] = l; - - // Skip links outside the copy config - 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 ext of this.nodeData.external) { - if (!this.externalFrom[ext[0]]) { - this.externalFrom[ext[0]] = { [ext[1]]: ext[2] }; - } else { - this.externalFrom[ext[0]][ext[1]] = ext[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") { - // Skip as its not linked - if (!linksFrom) return; - - let type = linksFrom["0"][0][5]; - if (type === "COMBO") { - // Use the array items - 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 def = (this.primitiveDefs[node.index] = { - input: { - required: { - value: [type, {}], - }, - }, - output: [type], - output_name: [], - output_is_list: [], - }); - return def; - } else if (node.type === "Reroute") { - const linksTo = this.linksTo[node.index]; - if (linksTo && linksFrom && !this.externalFrom[node.index]?.[0]) { - // Being used internally - return null; - } - - let config = {}; - let rerouteType = "*"; - if (linksFrom) { - for (const [, , id, slot] of linksFrom["0"]) { - const node = this.nodeData.nodes[id]; - const input = node.inputs[slot]; - if (rerouteType === "*") { - rerouteType = input.type; - } - if (input.widget) { - const targetDef = globalDefs[node.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 [id, slot] = linksTo["0"]; - rerouteType = this.nodeData.nodes[id].outputs[slot].type; - } else { - // Reroute used as a pipe - for (const l of this.nodeData.links) { - if (l[2] === node.index) { - rerouteType = l[5]; - break; - } - } - if (rerouteType === "*") { - // Check for an external link - 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 = ""; - // Special handling for primitive to include the title if it is set rather than just "value" - 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) { - // This widget has been converted to a widget - // We need to store this in the correct position so link ids line up - converted.set(convertedIndex, inputName); - widgetMap[inputName] = null; - } else { - // Normal widget - 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 { - // Normal input - slots.push(inputName); - } - } - return { converted, slots }; - } - - checkPrimitiveConnection(link, inputName, inputs) { - const sourceNode = this.nodeData.nodes[link[0]]; - if (sourceNode.type === "PrimitiveNode") { - // Merge link configurations - 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); - // This input is linked so we can skip it - 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) { - // Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up - 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); - // This input is linked so we can skip it - 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); - - // Converted inputs have to be processed after all other nodes as they'll be at the end of the list - this.#convertedToProcess.push(() => this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs)); - - return inputMapping; - } - - processNodeOutputs(node, seenOutputs, def) { - const oldToNew = (this.oldToNewOutputMap[node.index] = {}); - - // Add outputs - for (let outputId = 0; outputId < def.output.length; outputId++) { - const linksFrom = this.linksFrom[node.index]; - // If this output is linked internally we flag it to hide - 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) { - // Find missing node types - 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: (e) => { - delete groupNodes[g]; - e.target.textContent = "Removed"; - e.target.style.pointerEvents = "none"; - e.target.style.opacity = 0.7; - }, - }, - }); - - hasMissing = true; - } - } - - if (hasMissing) continue; - - const config = new GroupNodeConfig(g, groupData); - await config.registerType(); - } - } -} - -export class GroupNodeHandler { - node; - groupData; - - 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) => { - // Check if this input is internal or external - const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; - if (externalSlot != null) { - return this.node.getInputNode(externalSlot); - } - - // Internal link - const innerLink = this.groupData.linksTo[innerNode.index]?.[slot]; - if (!innerLink) return null; - - const inputNode = innerNodes[innerLink[0]]; - // Primitives will already apply their values - if (inputNode.type === "PrimitiveNode") return null; - - return inputNode; - }; - - innerNode.getInputLink = (slot) => { - const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot]; - if (externalSlot != null) { - // The inner node is connected via the group node inputs - const linkId = this.node.inputs[externalSlot].link; - let link = app.graph.links[linkId]; - - // Use the outer link, but update the target to the inner node - link = { - ...link, - target_id: innerNode.id, - target_slot: +slot, - }; - return link; - } - - let link = this.groupData.linksTo[innerNode.index]?.[slot]; - if (!link) return null; - // Use the inner link, but update the origin node to be inner node id - link = { - origin_id: innerNodes[link[0]].id, - origin_slot: link[1], - target_id: innerNode.id, - target_slot: +slot, - }; - return link; - }; - } - }; - - this.node.updateLink = (link) => { - // Replace the group node reference with the internal node - 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 id = this.node.id; - const sz = this.node.size; - const nodes = this.node.convertToNodes(); - - const groupNode = LiteGraph.createNode(this.node.type); - groupNode.id = id; - - // Reuse the existing nodes for this instance - 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])]; - - // Remove all converted nodes and relink them - groupNode[GROUP].replaceNodes(nodes); - return groupNode; - }; - - this.node.convertToNodes = () => { - const addInnerNodes = () => { - const backup = localStorage.getItem("litegrapheditor_clipboard"); - // Clone the node data so we dont mutate it for other nodes - 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 id = innerNodes?.[i]?.id; - // Use existing IDs if they are set on the inner nodes - if (id == null || isNaN(id)) { - id = undefined; - } else { - ids.push(id); - } - c.nodes[i] = { ...c.nodes[i], id }; - } - 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; - // Configure nodes with current widget data - const selectedIds = ids.length ? ids : Object.keys(app.canvas.selected_nodes); - const newNodes = []; - for (let i = 0; i < selectedIds.length; i++) { - const id = selectedIds[i]; - const newNode = app.graph.getNodeById(id); - const innerNode = innerNodes[i]; - newNodes.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; - - // Populate the main and any linked widgets - if (innerNode.type === "PrimitiveNode") { - for (let i = 0; i < newNode.widgets.length; i++) { - newNode.widgets[i].value = this.node.widgets[widgetIndex + i].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; - } - } - } - } - } - - // Shift each node - for (const newNode of newNodes) { - newNode.pos = [newNode.pos[0] - (left - x), newNode.pos[1] - (top - y)]; - } - - return { newNodes, selectedIds }; - }; - - const reconnectInputs = (selectedIds) => { - for (const innerNodeIndex in this.groupData.oldToNewInputMap) { - const id = selectedIds[innerNodeIndex]; - const newNode = app.graph.getNodeById(id); - 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; - // connect this node output to the input of another node - const originNode = app.graph.getNodeById(link.origin_id); - originNode.connect(link.origin_slot, newNode, +innerInputId); - } - } - }; - - const reconnectOutputs = (selectedIds) => { - 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(selectedIds[slot.node.index]); - newNode.connect(slot.slot, targetNode, link.target_slot); - } - } - }; - - 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: () => { - return this.convertToNodes(); - }, - }, - { - content: "Manage Group Node", - callback: () => { - new ManageGroupDialog(app).show(this.type); - }, - } - ); - }; - - // Draw custom collapse icon to identity this as a group - 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; - }; - - // Draw progress label - 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(); - } - }; - - // Flag this node as needing to be reset - 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((w) => w.name === widgetName); - if (widget) { - widget.type = "hidden"; - widget.computeSize = () => [0, -4]; - } - } - } - } - - return onNodeCreated?.apply(this, arguments); - }; - - function handleEvent(type, getId, getEvent) { - const handler = ({ detail }) => { - const id = getId(detail); - if (!id) return; - const node = app.graph.getNodeById(id); - if (node) return; - - const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == id); - if (innerNodeIndex > -1) { - this.node.runningInternalNodeId = innerNodeIndex; - api.dispatchEvent(new CustomEvent(type, { detail: getEvent(detail, this.node.id + "", this.node) })); - } - }; - api.addEventListener(type, handler); - return handler; - } - - const executing = handleEvent.call( - this, - "executing", - (d) => d, - (d, id, node) => id - ); - - const executed = handleEvent.call( - this, - "executed", - (d) => d?.display_node, - (d, id, node) => ({ ...d, node: id, display_node: id, merge: !node.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) => { - // Update combo widget options - 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 widget = node.widgets.find((w) => w.name === linked.inputName); - - if (widget) { - widget.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 widget = node.widgets?.find((w) => w.name === input.widget.name); - if (widget) { - widget.value = newValue; - } - } - } - } - } - - const widget = innerNode.widgets?.find((w) => w.name === old.inputName); - if (widget) { - widget.value = newValue; - } - } - } - - populatePrimitive(node, nodeId, oldName, i, linkedShift) { - // Converted widget, populate primitive if linked - 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) { - // Fallback handling for if some reason the primitive has a different number of widgets - // we dont want to overwrite random widgets, better to leave blank - len = 1; - } - for (let i = 0; i < len; i++) { - this.node.widgets[targetWidgetIndex + i].value = primitiveNode.widgets[i].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) { - // special handling for populating values into reroutes - // this allows primitives connect to them to pick up the correct value - 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) { - // Find the inner widget and shift by the number of linked widgets as they will have been removed too - const innerWidget = this.innerNodes[nodeId].widgets?.find((w) => w.name === oldName); - linkedShift += innerWidget?.linkedWidgets?.length ?? 0; - } - if (widgetIndex === -1) { - continue; - } - - // Populate the main and any linked widget - 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; - // Clone the links as they'll be changed if we reconnect - 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; // this node is in the group - 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) { - // Process the nodes into the stored workflow group node data - const builder = new GroupNodeBuilder(nodes); - const res = builder.build(); - if (!res) return; - - const { name, nodeData } = res; - - // Convert this data into a LG node definition and register it - const config = new GroupNodeConfig(name, nodeData); - await config.registerType(); - - const groupNode = LiteGraph.createNode(`workflow/${name}`); - // Reuse the existing nodes for this instance - groupNode.setInnerNodes(builder.nodes); - groupNode[GROUP].populateWidgets(); - app.graph.add(groupNode); - - // Remove all converted nodes and relink them - 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: async () => { - return await GroupNodeHandler.fromNodes(selected); - }, - }); - } - - 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: () => { - new ManageGroupDialog(app).show(); - }, - }); - } - - // Add to canvas - 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; - }; - - // Add to nodes - 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; - }; -} - -const id = "Comfy.GroupNode"; -let globalDefs; -const ext = { - name: id, - setup() { - addConvertToGroupOptions(); - }, - async beforeConfigureGraph(graphData, missingNodeTypes) { - const nodes = graphData?.extra?.groupNodes; - if (nodes) { - await GroupNodeConfig.registerFromWorkflow(nodes, missingNodeTypes); - } - }, - addCustomNodeDefs(defs) { - // Store this so we can mutate it later with group nodes - globalDefs = defs; - }, - nodeCreated(node) { - if (GroupNodeHandler.isGroupNode(node)) { - node[GROUP] = new GroupNodeHandler(node); - } - }, - async refreshComboInNodes(defs) { - // Re-register group nodes so new ones are created with the correct options - Object.assign(globalDefs, defs); - const nodes = app.graph.extra?.groupNodes; - if (nodes) { - await GroupNodeConfig.registerFromWorkflow(nodes, {}); - } - } -}; - -app.registerExtension(ext); \ No newline at end of file diff --git a/web/extensions/core/groupNodeManage.css b/web/extensions/core/groupNodeManage.css deleted file mode 100644 index 5470ecb5e..000000000 --- a/web/extensions/core/groupNodeManage.css +++ /dev/null @@ -1,149 +0,0 @@ -.comfy-group-manage { - background: var(--bg-color); - color: var(--fg-color); - padding: 0; - font-family: Arial, Helvetica, sans-serif; - border-color: black; - margin: 20vh auto; - max-height: 60vh; -} -.comfy-group-manage-outer { - max-height: 60vh; - min-width: 500px; - display: flex; - flex-direction: column; -} -.comfy-group-manage-outer > header { - display: flex; - align-items: center; - gap: 10px; - justify-content: space-between; - background: var(--comfy-menu-bg); - padding: 15px 20px; -} -.comfy-group-manage-outer > header select { - background: var(--comfy-input-bg); - border: 1px solid var(--border-color); - color: var(--input-text); - padding: 5px 10px; - border-radius: 5px; -} -.comfy-group-manage h2 { - margin: 0; - font-weight: normal; -} -.comfy-group-manage main { - display: flex; - overflow: hidden; -} -.comfy-group-manage .drag-handle { - font-weight: bold; -} -.comfy-group-manage-list { - border-right: 1px solid var(--comfy-menu-bg); -} -.comfy-group-manage-list ul { - margin: 40px 0 0; - padding: 0; - list-style: none; -} -.comfy-group-manage-list-items { - max-height: calc(100% - 40px); - overflow-y: scroll; - overflow-x: hidden; -} -.comfy-group-manage-list li { - display: flex; - padding: 10px 20px 10px 10px; - cursor: pointer; - align-items: center; - gap: 5px; -} -.comfy-group-manage-list div { - display: flex; - flex-direction: column; -} -.comfy-group-manage-list li:not(.selected):hover div { - text-decoration: underline; -} -.comfy-group-manage-list li.selected { - background: var(--border-color); -} -.comfy-group-manage-list li span { - opacity: 0.7; - font-size: smaller; -} -.comfy-group-manage-node { - flex: auto; - background: var(--border-color); - display: flex; - flex-direction: column; -} -.comfy-group-manage-node > div { - overflow: auto; -} -.comfy-group-manage-node header { - display: flex; - background: var(--bg-color); - height: 40px; -} -.comfy-group-manage-node header a { - text-align: center; - flex: auto; - border-right: 1px solid var(--comfy-menu-bg); - border-bottom: 1px solid var(--comfy-menu-bg); - padding: 10px; - cursor: pointer; - font-size: 15px; -} -.comfy-group-manage-node header a:last-child { - border-right: none; -} -.comfy-group-manage-node header a:not(.active):hover { - text-decoration: underline; -} -.comfy-group-manage-node header a.active { - background: var(--border-color); - border-bottom: none; -} -.comfy-group-manage-node-page { - display: none; - overflow: auto; -} -.comfy-group-manage-node-page.active { - display: block; -} -.comfy-group-manage-node-page div { - padding: 10px; - display: flex; - align-items: center; - gap: 10px; -} -.comfy-group-manage-node-page input { - border: none; - color: var(--input-text); - background: var(--comfy-input-bg); - padding: 5px 10px; -} -.comfy-group-manage-node-page input[type="text"] { - flex: auto; -} -.comfy-group-manage-node-page label { - display: flex; - gap: 5px; - align-items: center; -} -.comfy-group-manage footer { - border-top: 1px solid var(--comfy-menu-bg); - padding: 10px; - display: flex; - gap: 10px; -} -.comfy-group-manage footer button { - font-size: 14px; - padding: 5px 10px; - border-radius: 0; -} -.comfy-group-manage footer button:first-child { - margin-right: auto; -} diff --git a/web/extensions/core/groupNodeManage.js b/web/extensions/core/groupNodeManage.js deleted file mode 100644 index 1ab338386..000000000 --- a/web/extensions/core/groupNodeManage.js +++ /dev/null @@ -1,422 +0,0 @@ -import { $el, ComfyDialog } from "../../scripts/ui.js"; -import { DraggableList } from "../../scripts/ui/draggableList.js"; -import { addStylesheet } from "../../scripts/utils.js"; -import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js"; - -addStylesheet(import.meta.url); - -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; -} - -export class ManageGroupDialog extends ComfyDialog { - /** @type { Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}> } */ - tabs = {}; - /** @type { number | null | undefined } */ - selectedNodeIndex; - /** @type { keyof ManageGroupDialog["tabs"] } */ - selectedTab = "Inputs"; - /** @type { string | undefined } */ - selectedGroup; - - /** @type { Record>> } */ - modifications = {}; - - get selectedNodeInnerIndex() { - return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex; - } - - constructor(app) { - super(); - this.app = app; - 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: () => { - this.changeNode(i); - }, - }, - [ - $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({ nodeIndex, section, prop, value }) { - 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: (e) => { - this.storeModification({ section, prop, value: { name: e.target.value } }); - }, - }), - $el("label", { textContent: "Visible" }, [ - $el("input", { - type: "checkbox", - checked, - disabled: !checkable, - onchange: (e) => { - this.storeModification({ section, prop, value: { visible: !!e.target.checked } }); - }, - }), - ]), - ]); - } - - 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((type, slot) => { - const groupOutputIndex = groupOutputs?.[slot]; - const oldName = innerNodeDef.output_name?.[slot] ?? type; - 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: () => { - this.changeTab(name); - }, - textContent: name, - }), - page, - }; - return p; - }, {}); - - const outer = $el("div.comfy-group-manage-outer", [ - $el("header", [ - $el("h2", "Group Nodes"), - $el( - "select", - { - onchange: (e) => { - this.changeGroup(e.target.value); - }, - }, - 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: (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(); - }, - }, - "Delete Group Node" - ), - $el( - "button.comfy-btn", - { - onclick: async () => { - let nodesByType; - let recreateNodes = []; - const types = {}; - for (const g in this.modifications) { - const type = app.graph.extra.groupNodes[g]; - let config = (type.config ??= {}); - - let nodeMods = this.modifications[g]?.nodes; - if (nodeMods) { - const keys = Object.keys(nodeMods); - if (nodeMods[keys[0]][ORDER]) { - // If any node is reordered, they will all need sequencing - const orderedNodes = []; - const orderedMods = {}; - const orderedConfig = {}; - - for (const n of keys) { - const order = nodeMods[n][ORDER].order; - orderedNodes[order] = type.nodes[+n]; - orderedMods[order] = nodeMods[n]; - orderedNodes[order].index = order; - } - - // Rewrite links - for (const l of type.links) { - if (l[0] != null) l[0] = type.nodes[l[0]].index; - if (l[2] != null) l[2] = type.nodes[l[2]].index; - } - - // Rewrite externals - if (type.external) { - for (const ext of type.external) { - ext[0] = type.nodes[ext[0]]; - } - } - - // Rewrite modifications - for (const id of keys) { - if (config[id]) { - orderedConfig[type.nodes[id].index] = config[id]; - } - delete config[id]; - } - - type.nodes = orderedNodes; - nodeMods = orderedMods; - type.config = config = orderedConfig; - } - - merge(config, nodeMods); - } - - types[g] = type; - - 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); - }, - }, - "Save" - ), - $el("button.comfy-btn", { onclick: () => this.element.close() }, "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(); - }); - } -} \ No newline at end of file diff --git a/web/extensions/core/groupOptions.js b/web/extensions/core/groupOptions.js deleted file mode 100644 index 5dd21e730..000000000 --- a/web/extensions/core/groupOptions.js +++ /dev/null @@ -1,259 +0,0 @@ -import {app} from "../../scripts/app.js"; - -function setNodeMode(node, mode) { - node.mode = mode; - node.graph.change(); -} - -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]; -} - -app.registerExtension({ - name: "Comfy.GroupOptions", - setup() { - const orig = LGraphCanvas.prototype.getCanvasMenuOptions; - // graph_mouse - 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: () => { - var group = new LiteGraph.LGraphGroup(); - addNodesToGroup(group, this.selected_nodes) - app.canvas.graph.add(group); - this.graph.change(); - } - }); - - return options; - } - - // Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date - group.recomputeInsideNodes(); - const nodesInGroup = group._nodes; - - options.push({ - content: "Add Selected Nodes To Group", - disabled: !Object.keys(app.canvas.selected_nodes || {}).length, - callback: () => { - addNodesToGroup(group, this.selected_nodes) - this.graph.change(); - } - }); - - // No nodes in group, return default options - if (nodesInGroup.length === 0) { - return options; - } else { - // Add a separator between the default options and the group options - options.push(null); - } - - // Check if all nodes are the same mode - 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: () => { - addNodesToGroup(group) - this.graph.change(); - } - }); - - options.push({ - content: "Select Nodes", - callback: () => { - this.selectNodes(nodesInGroup); - this.graph.change(); - this.canvas.focus(); - } - }); - - // Modes - // 0: Always - // 1: On Event - // 2: Never - // 3: On Trigger - // 4: Bypass - // If all nodes are the same mode, add a menu option to change the mode - if (allNodesAreSameMode) { - const mode = nodesInGroup[0].mode; - switch (mode) { - case 0: - // All nodes are always, option to disable, and bypass - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - case 2: - // All nodes are never, option to enable, and bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - case 4: - // All nodes are bypass, option to enable, and disable - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - break; - default: - // All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - break; - } - } else { - // Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass - options.push({ - content: "Set Group Nodes to Always", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 0); - } - } - }); - options.push({ - content: "Set Group Nodes to Never", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 2); - } - } - }); - options.push({ - content: "Bypass Group Nodes", - callback: () => { - for (const node of nodesInGroup) { - setNodeMode(node, 4); - } - } - }); - } - - return options - } - } -}); diff --git a/web/extensions/core/invertMenuScrolling.js b/web/extensions/core/invertMenuScrolling.js deleted file mode 100644 index 98a1786ab..000000000 --- a/web/extensions/core/invertMenuScrolling.js +++ /dev/null @@ -1,36 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Inverts the scrolling of context menus - -const id = "Comfy.InvertMenuScrolling"; -app.registerExtension({ - name: id, - init() { - const ctxMenu = LiteGraph.ContextMenu; - const replace = () => { - 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; - }; - app.ui.settings.addSetting({ - id, - name: "Invert Menu Scrolling", - type: "boolean", - defaultValue: false, - onChange(value) { - if (value) { - replace(); - } else { - LiteGraph.ContextMenu = ctxMenu; - } - }, - }); - }, -}); diff --git a/web/extensions/core/keybinds.js b/web/extensions/core/keybinds.js deleted file mode 100644 index ac367c116..000000000 --- a/web/extensions/core/keybinds.js +++ /dev/null @@ -1,69 +0,0 @@ -import {app} from "../../scripts/app.js"; - -app.registerExtension({ - name: "Comfy.Keybinds", - init() { - const keybindListener = function (event) { - const modifierPressed = event.ctrlKey || event.metaKey; - - // Queue prompt using ctrl or command + enter - if (modifierPressed && event.key === "Enter") { - app.queuePrompt(event.shiftKey ? -1 : 0).then(); - return; - } - - const target = event.composedPath()[0]; - if (["INPUT", "TEXTAREA"].includes(target.tagName)) { - return; - } - - const modifierKeyIdMap = { - s: "#comfy-save-button", - o: "#comfy-file-input", - Backspace: "#comfy-clear-button", - d: "#comfy-load-default-button", - }; - - const modifierKeybindId = modifierKeyIdMap[event.key]; - if (modifierPressed && modifierKeybindId) { - event.preventDefault(); - - const elem = document.querySelector(modifierKeybindId); - elem.click(); - return; - } - - // Finished Handling all modifier keybinds, now handle the rest - if (event.ctrlKey || event.altKey || event.metaKey) { - return; - } - - // Close out of modals using escape - if (event.key === "Escape") { - const modals = document.querySelectorAll(".comfy-modal"); - const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none"); - if (modal) { - modal.style.display = "none"; - } - - [...document.querySelectorAll("dialog")].forEach(d => { - d.close(); - }); - } - - const keyIdMap = { - q: "#comfy-view-queue-button", - h: "#comfy-view-history-button", - r: "#comfy-refresh-button", - }; - - const buttonId = keyIdMap[event.key]; - if (buttonId) { - const button = document.querySelector(buttonId); - button.click(); - } - } - - window.addEventListener("keydown", keybindListener, true); - } -}); diff --git a/web/extensions/core/linkRenderMode.js b/web/extensions/core/linkRenderMode.js deleted file mode 100644 index fb4df4234..000000000 --- a/web/extensions/core/linkRenderMode.js +++ /dev/null @@ -1,25 +0,0 @@ -import { app } from "../../scripts/app.js"; - -const id = "Comfy.LinkRenderMode"; -const ext = { - name: id, - async setup(app) { - app.ui.settings.addSetting({ - id, - name: "Link Render Mode", - defaultValue: 2, - type: "combo", - options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({ - value: i, - text: m, - selected: i == app.canvas.links_render_mode, - })), - onChange(value) { - app.canvas.links_render_mode = +value; - app.graph.setDirtyCanvas(true); - }, - }); - }, -}; - -app.registerExtension(ext); diff --git a/web/extensions/core/maskeditor.js b/web/extensions/core/maskeditor.js deleted file mode 100644 index 36f7496e7..000000000 --- a/web/extensions/core/maskeditor.js +++ /dev/null @@ -1,967 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { ComfyApp } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js" -import { ClipspaceDialog } from "./clipspace.js"; - -// Helper function to convert a data URL to a Blob object -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 }); -} - -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; -} - -function loadImage(imagePath) { - return new Promise((resolve, reject) => { - const image = new Image(); - - image.onload = function() { - resolve(image); - }; - - image.src = imagePath; - }); -} - -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(); -} - -function prepare_mask(image, maskCanvas, maskCtx, maskColor) { - // paste mask data into alpha channel - maskCtx.drawImage(image, 0, 0, maskCanvas.width, maskCanvas.height); - const maskData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); - - // invert mask - 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); -} - -class MaskEditorDialog extends ComfyDialog { - static instance = null; - - static getInstance() { - if(!MaskEditorDialog.instance) { - MaskEditorDialog.instance = new MaskEditorDialog(app); - } - - 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; - - // If it is specified as relative, using it only as a hidden placeholder for padding is recommended - // to prevent anomalies where it exceeds a certain size and goes outside of the window. - 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, null, null); - }); - - 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; - } - }); - - 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("mouseup", MaskEditorDialog.handleMouseUp); - document.removeEventListener("keydown", MaskEditorDialog.handleKeyDown); - self.close(); - }); - - this.saveButton = this.createRightButton("Save", () => { - document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp); - 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; - } - - async show() { - this.zoom_ratio = 1.0; - this.pan_x = 0; - this.pan_y = 0; - - if(!this.is_layout_created) { - // layout - const imgCanvas = document.createElement('canvas'); - const maskCanvas = document.createElement('canvas'); - - imgCanvas.id = "imageCanvas"; - maskCanvas.id = "maskCanvas"; - - this.setlayout(imgCanvas, maskCanvas); - - // prepare content - this.imgCanvas = imgCanvas; - this.maskCanvas = maskCanvas; - this.maskCtx = maskCanvas.getContext('2d', {willReadFrequently: true }); - - this.setEventHandler(maskCanvas); - - this.is_layout_created = true; - - // replacement of onClose hook since close is not real close - 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') { - document.removeEventListener("mouseup", MaskEditorDialog.handleMouseUp); - self.brush.style.display = "none"; - ComfyApp.onClipspaceEditorClosed(); - } - - self.last_display_style = self.element.style.display; - } - }); - }); - - const config = { attributes: true }; - observer.observe(this.element, config); - } - - // The keydown event needs to be reconfigured when closing the dialog as it gets removed. - 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; // NOTE: alert dialog must be high priority. - - 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); - - // image load - 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); - - // original image load - 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; - } - - initializeCanvasPanZoom() { - // set initialize - 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") { - // negative effect only works with white color - 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(); - - // update mask canvas css styles - - const maskCanvasStyle = this.getMaskCanvasStyle(); - this.maskCanvas.style.mixBlendMode = maskCanvasStyle.mixBlendMode; - this.maskCanvas.style.opacity = maskCanvasStyle.opacity; - - // update mask canvas rgb colors - - 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) { - // zoom canvas - if(event.deltaY < 0) { - this.zoom_ratio = Math.min(10.0, this.zoom_ratio+0.2); - } - else { - this.zoom_ratio = Math.max(0.2, this.zoom_ratio-0.2); - } - - this.invalidatePanZoom(); - } - else { - // adjust brush size - 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; - - 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)*0.005; - self.zoom_ratio = Math.max(Math.min(10.0, self.last_zoom_ratio - delta), 0.2); - - this.invalidatePanZoom(); - return; - } - } - - pan_move(self, event) { - if(event.buttons == 1) { - if(this.mousedown_x) { - let deltaX = this.mousedown_x - event.clientX; - let deltaY = this.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){ - // The firing interval of PointerEvents in Pen is unreliable, so it is supplemented by TouchEvents. - 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 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; - - 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 && !drawing_mode) // cannot tracking drawing_mode for touch event - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - 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; - }); - else - requestAnimationFrame(() => { - self.maskCtx.beginPath(); - self.maskCtx.globalCompositeOperation = "destination-out"; - - 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(); - } - } - - handlePointerDown(self, event) { - if(event.ctrlKey) { - if (event.buttons == 1) { - this.mousedown_x = event.clientX; - this.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); - - // paste mask data into alpha channel - const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height); - - // refine mask image - 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": 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(app) { - ComfyApp.open_maskeditor = - function () { - const dlg = MaskEditorDialog.getInstance(); - if(!dlg.isOpened()) { - dlg.show(); - } - }; - - const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0 - ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor); - } -}); diff --git a/web/extensions/core/nodeTemplates.js b/web/extensions/core/nodeTemplates.js deleted file mode 100644 index 9350ba654..000000000 --- a/web/extensions/core/nodeTemplates.js +++ /dev/null @@ -1,412 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; -import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js"; - -// Adds the ability to save and add multiple nodes as a template -// To save: -// Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes) -// Right click the canvas -// Save Node Template -> give it a name -// -// To add: -// Right click the canvas -// Node templates -> click the one to add -// -// To delete/rename: -// Right click the canvas -// Node templates -> Manage -// -// To rearrange: -// Open the manage dialog and Drag and drop elements using the "Name:" label as handle - -const id = "Comfy.NodeTemplates"; -const file = "comfy.templates.json"; - -class ManageTemplates extends ComfyDialog { - 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: () => this.importAll(), - }); - } - - 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: () => this.exportAll(), - }) - ); - btns.unshift( - $el("button", { - type: "button", - textContent: "Import", - onclick: () => { - this.importInput.click(); - }, - }) - ); - return btns; - } - - async load() { - let templates = []; - if (app.storageLocation === "server") { - if (app.isNewUserSession) { - // New user so migrate existing templates - 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, undefined, 4); - localStorage.setItem(id, templates); // Backwards compatibility - 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 file of this.importInput.files) { - if (file.type === "application/json" || file.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(file); - } - } - - 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); // convert the data to a JSON string - 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() { - // Show list of template names + delete button - super.show( - $el( - "div", - {}, - this.templates.flatMap((t,i) => { - let nameInput; - return [ - $el( - "div", - { - dataset: { id: i }, - className: "tempateManagerRow", - style: { - display: "grid", - gridTemplateColumns: "1fr auto", - border: "1px dashed transparent", - gap: "5px", - backgroundColor: "var(--comfy-menu-bg)" - }, - ondragstart: (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); - }, - ondragend: (e) => { - e.target.style.opacity = "1"; - e.currentTarget.style.border = "1px dashed transparent"; - e.currentTarget.removeAttribute("draggable"); - - // rearrange the elements - this.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { - var prev_i = el.dataset.id; - - if ( el == this.draggedEl && prev_i != i ) { - this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]); - } - el.dataset.id = i; - }); - this.store(); - }, - ondragover: (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); - } - } - }, - [ - $el( - "label", - { - textContent: "Name: ", - style: { - cursor: "grab", - }, - onmousedown: (e) => { - // enable dragging only from the label - if (e.target.localName == 'label') - e.currentTarget.parentNode.draggable = 'true'; - } - }, - [ - $el("input", { - value: t.name, - dataset: { name: t.name }, - style: { - transitionProperty: 'background-color', - transitionDuration: '0s', - }, - onchange: (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); - }, - onkeypress: (e) => { - var el = e.target; - clearTimeout(this.saveVisualCue); - el.style.transitionDuration = '0s'; - el.style.backgroundColor = 'var(--comfy-input-bg)'; - }, - $: (el) => (nameInput = el), - }) - ] - ), - $el( - "div", - {}, - [ - $el("button", { - textContent: "Export", - style: { - fontSize: "12px", - fontWeight: "normal", - }, - onclick: (e) => { - const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string - 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); - }, - }), - $el("button", { - textContent: "Delete", - style: { - fontSize: "12px", - color: "red", - fontWeight: "normal", - }, - onclick: (e) => { - const item = e.target.parentNode.parentNode; - item.parentNode.removeChild(item); - this.templates.splice(item.dataset.id*1, 1); - this.store(); - // update the rows index, setTimeout ensures that the list is updated - var that = this; - setTimeout(function (){ - that.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { - el.dataset.id = i; - }); - }, 0); - }, - }), - ] - ), - ] - ) - ]; - }) - ) - ); - } -} - -app.registerExtension({ - name: id, - setup() { - const manage = new ManageTemplates(); - - const clipboardAction = async (cb) => { - // We use the clipboard functions but dont want to overwrite the current user clipboard - // Restore it after we've run our callback - const old = localStorage.getItem("litegrapheditor_clipboard"); - await cb(); - localStorage.setItem("litegrapheditor_clipboard", old); - }; - - 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: () => { - const name = prompt("Enter name"); - if (!name?.trim()) return; - - clipboardAction(() => { - app.canvas.copyToClipboard(); - let data = localStorage.getItem("litegrapheditor_clipboard"); - data = JSON.parse(data); - const nodeIds = Object.keys(app.canvas.selected_nodes); - for (let i = 0; i < nodeIds.length; i++) { - const node = app.graph.getNodeById(nodeIds[i]); - const nodeData = node?.constructor.nodeData; - - let groupData = GroupNodeHandler.getGroupData(node); - if (groupData) { - groupData = groupData.nodeData; - if (!data.groupNodes) { - data.groupNodes = {}; - } - data.groupNodes[nodeData.name] = groupData; - data.nodes[i].type = nodeData.name; - } - } - - manage.templates.push({ - name, - data: JSON.stringify(data), - }); - manage.store(); - }); - }, - }); - - // Map each template to a menu item - const subItems = manage.templates.map((t) => { - return { - content: t.name, - callback: () => { - clipboardAction(async () => { - const data = JSON.parse(t.data); - await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}); - localStorage.setItem("litegrapheditor_clipboard", t.data); - app.canvas.pasteFromClipboard(); - }); - }, - }; - }); - - subItems.push(null, { - content: "Manage", - callback: () => manage.show(), - }); - - options.push({ - content: "Node Templates", - submenu: { - options: subItems, - }, - }); - - return options; - }; - }, -}); diff --git a/web/extensions/core/noteNode.js b/web/extensions/core/noteNode.js deleted file mode 100644 index 8d89054e9..000000000 --- a/web/extensions/core/noteNode.js +++ /dev/null @@ -1,41 +0,0 @@ -import {app} from "../../scripts/app.js"; -import {ComfyWidgets} from "../../scripts/widgets.js"; -// Node that add notes to your project - -app.registerExtension({ - name: "Comfy.NoteNode", - registerCustomNodes() { - class NoteNode { - color=LGraphCanvas.node_colors.yellow.color; - bgcolor=LGraphCanvas.node_colors.yellow.bgcolor; - groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; - constructor() { - if (!this.properties) { - this.properties = {}; - this.properties.text=""; - } - - ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app) - - this.serialize_widgets = true; - this.isVirtualNode = true; - - } - - - } - - // Load default visibility - - LiteGraph.registerNodeType( - "Note", - Object.assign(NoteNode, { - title_mode: LiteGraph.NORMAL_TITLE, - title: "Note", - collapsable: true, - }) - ); - - NoteNode.category = "utils"; - }, -}); diff --git a/web/extensions/core/rerouteNode.js b/web/extensions/core/rerouteNode.js deleted file mode 100644 index 4feff91e5..000000000 --- a/web/extensions/core/rerouteNode.js +++ /dev/null @@ -1,274 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs.js"; - -// Node that allows you to redirect connections for cleaner graphs - -app.registerExtension({ - name: "Comfy.RerouteNode", - registerCustomNodes(app) { - class RerouteNode { - constructor() { - 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(); - - // Prevent multiple connections to different types when we have no input - if (connected && type === LiteGraph.OUTPUT) { - // Ignore wildcard nodes as these will be updated to real types - const types = new Set(this.outputs[0].links.map((l) => app.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 = app.graph.links[linkId]; - linksToDisconnect.push(link); - } - for (const link of linksToDisconnect) { - const node = app.graph.getNodeById(link.target_id); - node.disconnectInput(link.target_slot); - } - } - } - - // Find root input - 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 = app.graph.links[linkId]; - if (!link) return; - const node = app.graph.getNodeById(link.origin_id); - const type = node.constructor.type; - if (type === "Reroute") { - if (node === this) { - // We've found a circle - currentNode.disconnectInput(link.target_slot); - currentNode = null; - } else { - // Move the previous node - currentNode = node; - } - } else { - // We've found the end - inputNode = currentNode; - inputType = node.outputs[link.origin_slot]?.type ?? null; - break; - } - } else { - // This path has no input node - currentNode = null; - break; - } - } - - // Find all outputs - 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 = app.graph.links[linkId]; - - // When disconnecting sometimes the link is still registered - if (!link) continue; - - const node = app.graph.getNodeById(link.target_id); - const type = node.constructor.type; - - if (type === "Reroute") { - // Follow reroute nodes - nodes.push(node); - updateNodes.push(node); - } else { - // We've found an output - 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) { - // The output doesnt match our input so disconnect it - node.disconnectInput(link.target_slot); - } else { - outputType = nodeOutType; - } - } - } - } else { - // No more outputs for this path - } - } - - const displayType = inputType || outputType || "*"; - const color = LGraphCanvas.link_type_colors[displayType]; - - let widgetConfig; - let targetWidget; - let widgetType; - // Update the types of each node - for (const node of updateNodes) { - // If we dont have an input type we are always wildcard but we'll show the output type - // This lets you change the output link to a different type and all nodes will update - 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 = app.graph.links[l]; - if (link) { - link.color = color; - - if (app.configuringGraph) continue; - const targetNode = app.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 = app.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 node is purely frontend and does not impact the resulting prompt so should not be serialized - this.isVirtualNode = true; - } - - getExtraMenuOptions(_, options) { - options.unshift( - { - content: (this.properties.showOutputText ? "Hide" : "Show") + " Type", - callback: () => { - 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(); - app.graph.setDirtyCanvas(true, true); - }, - }, - { - content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default", - callback: () => { - RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility); - }, - }, - { - // naming is inverted with respect to LiteGraphNode.horizontal - // LiteGraphNode.horizontal == true means that - // each slot in the inputs and outputs are layed out horizontally, - // which is the opposite of the visual orientation of the inputs and outputs as a node - content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"), - callback: () => { - this.properties.horizontal = !this.properties.horizontal; - this.applyOrientation(); - }, - } - ); - } - applyOrientation() { - this.horizontal = this.properties.horizontal; - if (this.horizontal) { - // we correct the input position, because LiteGraphNode.horizontal - // doesn't account for title presence - // which reroute nodes don't have - this.inputs[0].pos = [this.size[0] / 2, 0]; - } else { - delete this.inputs[0].pos; - } - app.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"]; - } - } - } - - // Load default visibility - RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]); - - LiteGraph.registerNodeType( - "Reroute", - Object.assign(RerouteNode, { - title_mode: LiteGraph.NO_TITLE, - title: "Reroute", - collapsable: false, - }) - ); - - RerouteNode.category = "utils"; - }, -}); diff --git a/web/extensions/core/saveImageExtraOutput.js b/web/extensions/core/saveImageExtraOutput.js deleted file mode 100644 index a0506b43b..000000000 --- a/web/extensions/core/saveImageExtraOutput.js +++ /dev/null @@ -1,35 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { applyTextReplacements } from "../../scripts/utils.js"; -// Use widget values and dates in output filenames - -app.registerExtension({ - name: "Comfy.SaveImageExtraOutput", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "SaveImage") { - const onNodeCreated = nodeType.prototype.onNodeCreated; - // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - const widget = this.widgets.find((w) => w.name === "filename_prefix"); - widget.serializeValue = () => { - return applyTextReplacements(app, widget.value); - }; - - return r; - }; - } else { - // When any other node is created add a property to alias the node - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - - if (!this.properties || !("Node name for S&R" in this.properties)) { - this.addProperty("Node name for S&R", this.constructor.type, "string"); - } - - return r; - }; - } - }, -}); diff --git a/web/extensions/core/simpleTouchSupport.js b/web/extensions/core/simpleTouchSupport.js deleted file mode 100644 index 041fc2c4c..000000000 --- a/web/extensions/core/simpleTouchSupport.js +++ /dev/null @@ -1,102 +0,0 @@ -import { app } from "../../scripts/app.js"; - -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); - } - - app.canvasEl.addEventListener( - "touchstart", - (e) => { - touchCount++; - lastTouch = null; - if (e.touches?.length === 1) { - // Store start time for press+hold for context menu - touchTime = new Date(); - lastTouch = e.touches[0]; - } else { - touchTime = null; - if (e.touches?.length === 2) { - // Store center pos for zoom - 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() - touchTime > 600) { - try { - // hack to get litegraph to use this event - 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); -}; diff --git a/web/extensions/core/slotDefaults.js b/web/extensions/core/slotDefaults.js deleted file mode 100644 index 718d25405..000000000 --- a/web/extensions/core/slotDefaults.js +++ /dev/null @@ -1,91 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; -// Adds defaults for quickly adding nodes with middle click on the input/output - -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", - name: "Number of nodes suggestions", - type: "slider", - attrs: { - min: 1, - max: 100, - step: 1, - }, - defaultValue: 5, - onChange: (newVal, oldVal) => { - this.setDefaults(newVal); - } - }); - }, - slot_types_default_out: {}, - slot_types_default_in: {}, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - var nodeId = nodeData.name; - var inputs = []; - inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs - 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; //ignore widgets that don't force input - } - - 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); - - // Input types have to be stored as lower case - // Store each node that can handle this input type - 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"];// ["Reroute", "Primitive"]; primitive doesn't always work :'() - } - - this.slot_types_default_in[type].push(nodeId); - - // Store each node that can handle this output type - 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); - } - } -}); diff --git a/web/extensions/core/snapToGrid.js b/web/extensions/core/snapToGrid.js deleted file mode 100644 index aac017748..000000000 --- a/web/extensions/core/snapToGrid.js +++ /dev/null @@ -1,171 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Shift + drag/resize to snap to grid - -/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */ -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; -} - -app.registerExtension({ - name: "Comfy.SnapToGrid", - init() { - // Add setting to control grid size - app.ui.settings.addSetting({ - id: "Comfy.SnapToGrid.GridSize", - name: "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; - }, - }); - - // After moving a node, if the shift key is down align it to grid - const onNodeMoved = app.canvas.onNodeMoved; - app.canvas.onNodeMoved = function (node) { - const r = onNodeMoved?.apply(this, arguments); - - if (app.shiftDown) { - // Ensure all selected nodes are realigned - for (const id in this.selected_nodes) { - this.selected_nodes[id].alignToGrid(); - } - } - - return r; - }; - - // When a node is added, add a resize handler to it so we can fix align the size with the grid - 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); - }; - - // Draw a preview of where the node will go if holding shift and the node is selected - 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); - }; - - - - /** - * The currently moving, selected group only. Set after the `selected_group` has actually started - * moving. - */ - let selectedAndMovingGroup = null; - - /** - * Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups` - * below) as well as handle the last move call from LiteGraph's `processMouseUp`. - */ - const groupMove = LGraphGroup.prototype.move; - LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { - const v = groupMove.apply(this, arguments); - // When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group` - // too eagerly and we don't want to behave like we're moving until we get a delta. - if (!selectedAndMovingGroup && app.canvas.selected_group === this && (deltax || deltay)) { - selectedAndMovingGroup = this; - } - - // LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want - // to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging` - // has been set to `false`. Essentially, this check here is the equivilant to calling an - // `LGraphGroup.prototype.onNodeMoved` if it had existed. - if (app.canvas.last_mouse_dragging === false && app.shiftDown) { - // After moving a group (while app.shiftDown), snap all the child nodes and, finally, - // align the group itself. - this.recomputeInsideNodes(); - for (const node of this._nodes) { - node.alignToGrid(); - } - LGraphNode.prototype.alignToGrid.apply(this); - } - return v; - }; - - /** - * Handles drawing a group when, snapping the size when one is actively being resized tracking and/or - * drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for - * both. - */ - 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); - }; - - - /** Handles adding a group in a snapping-enabled state. */ - 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; - }; - }, -}); diff --git a/web/extensions/core/tooltips.js b/web/extensions/core/tooltips.js deleted file mode 100644 index 08ab05efa..000000000 --- a/web/extensions/core/tooltips.js +++ /dev/null @@ -1,122 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { $el } from "../../scripts/ui.js"; - -// Adds support for tooltips - -function getHoveredWidget() { - if (!app) { - return; - } - - const node = app.canvas.node_over; - if (!node.widgets) return; - - const graphPos = app.canvas.graph_mouse; - - const x = graphPos[0] - node.pos[0]; - const y = graphPos[1] - node.pos[1]; - - for (const w of node.widgets) { - let widgetWidth, widgetHeight; - if (w.computeSize) { - const sz = w.computeSize(); - widgetWidth = sz[0]; - widgetHeight = sz[1]; - } else { - widgetWidth = w.width || node.size[0]; - widgetHeight = LiteGraph.NODE_WIDGET_HEIGHT; - } - - if (w.last_y !== undefined && x >= 6 && x <= widgetWidth - 12 && y >= w.last_y && y <= w.last_y + widgetHeight) { - return w; - } - } -} - -app.registerExtension({ - name: "Comfy.Tooltips", - setup() { - const tooltipEl = $el("div.comfy-graph-tooltip", { - parent: document.body, - }); - let idleTimeout; - - const hideTooltip = () => { - tooltipEl.style.display = "none"; - }; - const showTooltip = (tooltip) => { - if (!tooltip) return; - - tooltipEl.textContent = tooltip; - tooltipEl.style.display = "block"; - tooltipEl.style.left = app.canvas.mouse[0] + "px"; - tooltipEl.style.top = app.canvas.mouse[1] + "px"; - const rect = tooltipEl.getBoundingClientRect(); - if (rect.right > window.innerWidth) { - tooltipEl.style.left = app.canvas.mouse[0] - rect.width + "px"; - } - - if (rect.top < 0) { - tooltipEl.style.top = app.canvas.mouse[1] + rect.height + "px"; - } - }; - const getInputTooltip = (nodeData, name) => { - const inputDef = nodeData.input?.required?.[name] ?? nodeData.input?.optional?.[name]; - return inputDef?.[1]?.tooltip; - }; - const onIdle = () => { - const { canvas } = app; - const node = canvas.node_over; - if (!node) return; - - const nodeData = node.constructor.nodeData ?? {}; - - if (node.constructor.title_mode !== LiteGraph.NO_TITLE && canvas.graph_mouse[1] < node.pos[1]) { - return showTooltip(nodeData.description); - } - - if (node.flags?.collapsed) return; - - const inputSlot = canvas.isOverNodeInput(node, canvas.graph_mouse[0], canvas.graph_mouse[1], [0, 0]); - if (inputSlot !== -1) { - const inputName = node.inputs[inputSlot].name; - return showTooltip(getInputTooltip(nodeData, inputName)); - } - - const outputSlot = canvas.isOverNodeOutput(node, canvas.graph_mouse[0], canvas.graph_mouse[1], [0, 0]); - if (outputSlot !== -1) { - return showTooltip(nodeData.output_tooltips?.[outputSlot]); - } - - const widget = getHoveredWidget(); - // Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these - if (widget && !widget.element) { - return showTooltip(widget.tooltip ?? getInputTooltip(nodeData, widget.name)); - } - }; - - const onMouseMove = (e) => { - hideTooltip(); - clearTimeout(idleTimeout); - - if(e.target.nodeName !== "CANVAS") return - idleTimeout = setTimeout(onIdle, 500); - }; - - app.ui.settings.addSetting({ - id: "Comfy.EnableTooltips", - name: "Enable Tooltips", - type: "boolean", - defaultValue: true, - onChange(value) { - if (value) { - window.addEventListener("mousemove", onMouseMove); - window.addEventListener("click", hideTooltip); - } else { - window.removeEventListener("mousemove", onMouseMove); - window.removeEventListener("click", hideTooltip); - } - }, - }); - }, -}); diff --git a/web/extensions/core/uploadAudio.js b/web/extensions/core/uploadAudio.js deleted file mode 100644 index 9dfa029bf..000000000 --- a/web/extensions/core/uploadAudio.js +++ /dev/null @@ -1,186 +0,0 @@ -import { app } from "../../scripts/app.js" -import { api } from "../../scripts/api.js" - -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) - ] -} - -function getResourceURL(subfolder, filename, type = "input") { - const params = [ - "filename=" + encodeURIComponent(filename), - "type=" + type, - "subfolder=" + subfolder, - app.getRandParam().substring(1) - ].join("&") - - return `/view?${params}` -} - -async function uploadFile( - audioWidget, - audioUIWidget, - file, - updateNode, - pasted = false -) { - try { - // Wrap file in formdata so it includes filename - const body = new FormData() - body.append("image", file) - 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() - // Add the file to the dropdown list and update the widget value - 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) - } -} - -// AudioWidget MUST be registered first, as AUDIOUPLOAD depends on AUDIO_UI to be -// present. -app.registerExtension({ - name: "Comfy.AudioWidget", - async beforeRegisterNodeDef(nodeType, nodeData) { - if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) { - nodeData.input.required.audioUI = ["AUDIO_UI"] - } - }, - getCustomWidgets() { - return { - AUDIO_UI(node, inputName) { - const audio = document.createElement("audio") - audio.controls = true - audio.classList.add("comfy-audio") - audio.setAttribute("name", "media") - - const audioUIWidget = node.addDOMWidget( - inputName, - /* name=*/ "audioUI", - audio - ) - // @ts-ignore - // TODO: Sort out the DOMWidget type. - audioUIWidget.serialize = false - - const isOutputNode = node.constructor.nodeData.output_node - if (isOutputNode) { - // Hide the audio widget when there is no audio initially. - audioUIWidget.element.classList.add("empty-audio-widget") - // Populate the audio widget UI on node execution. - const onExecuted = node.onExecuted - node.onExecuted = function(message) { - onExecuted?.apply(this, arguments) - const audios = message.audio - if (!audios) return - const audio = audios[0] - audioUIWidget.element.src = api.apiURL( - getResourceURL(audio.subfolder, audio.filename, audio.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) { - // The widget that allows user to select file. - const audioWidget = node.widgets.find(w => w.name === "audio") - const audioUIWidget = node.widgets.find(w => w.name === "audioUI") - - const onAudioWidgetUpdate = () => { - audioUIWidget.element.src = api.apiURL( - getResourceURL(...splitFilePath(audioWidget.value)) - ) - } - // Initially load default audio file to audioUIWidget. - if (audioWidget.value) { - onAudioWidgetUpdate() - } - audioWidget.callback = onAudioWidgetUpdate - - // Load saved audio file widget values if restoring from workflow - 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) - } - } - // The widget to pop up the upload dialog. - const uploadWidget = node.addWidget( - "button", - inputName, - /* value=*/ "", - () => { - fileInput.click() - } - ) - uploadWidget.label = "choose file to upload" - uploadWidget.serialize = false - - return { widget: uploadWidget } - } - } - } -}) diff --git a/web/extensions/core/uploadImage.js b/web/extensions/core/uploadImage.js deleted file mode 100644 index 530c4599e..000000000 --- a/web/extensions/core/uploadImage.js +++ /dev/null @@ -1,12 +0,0 @@ -import { app } from "../../scripts/app.js"; - -// Adds an upload button to the nodes - -app.registerExtension({ - name: "Comfy.UploadImage", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData?.input?.required?.image?.[1]?.image_upload === true) { - nodeData.input.required.upload = ["IMAGEUPLOAD"]; - } - }, -}); diff --git a/web/extensions/core/webcamCapture.js b/web/extensions/core/webcamCapture.js deleted file mode 100644 index dd5725bd4..000000000 --- a/web/extensions/core/webcamCapture.js +++ /dev/null @@ -1,126 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; - -const WEBCAM_READY = Symbol(); - -app.registerExtension({ - name: "Comfy.WebcamCapture", - getCustomWidgets(app) { - 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 = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); - container.replaceChildren(video); - - setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes? - 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(); - - return { widget: node.addDOMWidget(inputName, "WEBCAM", container) }; - }, - }; - }, - nodeCreated(node) { - if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return; - - let video; - const camera = node.widgets.find((w) => w.name === "image"); - const w = node.widgets.find((w) => w.name === "width"); - const h = node.widgets.find((w) => w.name === "height"); - const captureOnQueue = node.widgets.find((w) => w.name === "capture_on_queue"); - - const canvas = document.createElement("canvas"); - - const capture = () => { - 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; - }; - - const btn = node.addWidget("button", "waiting for camera...", "capture", capture); - btn.disabled = true; - btn.serializeValue = () => undefined; - - camera.serializeValue = async () => { - if (captureOnQueue.value) { - capture(); - } else if (!node.imgs?.length) { - const err = `No webcam image captured`; - alert(err); - throw new Error(err); - } - - // Upload image to temp storage - const blob = await new Promise((r) => canvas.toBlob(r)); - const name = `${+new Date()}.png`; - const file = new File([blob], name); - const body = new FormData(); - body.append("image", file); - 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 width isnt specified then use video output resolution - if (!w.value) { - w.value = video.videoWidth || 640; - h.value = video.videoHeight || 480; - } - btn.disabled = false; - btn.label = "capture"; - }); - }, -}); diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js deleted file mode 100644 index 88f1f9d8a..000000000 --- a/web/extensions/core/widgetInputs.js +++ /dev/null @@ -1,800 +0,0 @@ -import { ComfyWidgets, addValueControlWidgets } from "../../scripts/widgets.js"; -import { app } from "../../scripts/app.js"; -import { applyTextReplacements } from "../../scripts/utils.js"; - -const CONVERTED_TYPE = "converted-widget"; -const VALID_TYPES = ["STRING", "combo", "number", "toggle", "BOOLEAN"]; -const CONFIG = Symbol(); -const GET_CONFIG = Symbol(); -const TARGET = Symbol(); // Used for reroutes to specify the real target widget - -export function getWidgetConfig(slot) { - return slot.widget[CONFIG] ?? slot.widget[GET_CONFIG](); -} - -function getConfig(widgetName) { - const { nodeData } = this.constructor; - return nodeData?.input?.required[widgetName] ?? nodeData?.input?.optional?.[widgetName]; -} - -function isConvertableWidget(widget, config) { - return (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput; -} - -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]; // -4 is due to the gap litegraph adds between widgets automatically - widget.type = CONVERTED_TYPE + suffix; - widget.serializeValue = () => { - // Prevent serializing the widget if we have no input linked - if (!node.inputs) { - return undefined; - } - let node_input = node.inputs.find((i) => i.widget?.name === widget.name); - - if (!node_input || !node_input.link) { - return undefined; - } - return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; - }; - - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - hideWidget(node, w, ":" + widget.name); - } - } -} - -function showWidget(widget) { - widget.type = widget.origType; - widget.computeSize = widget.origComputeSize; - widget.serializeValue = widget.origSerializeValue; - - delete widget.origType; - delete widget.origComputeSize; - delete widget.origSerializeValue; - - // Hide any linked widgets, e.g. seed+seedControl - if (widget.linkedWidgets) { - for (const w of widget.linkedWidgets) { - showWidget(w); - } - } -} - -function convertToInput(node, widget, config) { - hideWidget(node, widget); - - const { type } = getWidgetType(config); - - // Add input and store widget config for creating on primitive node - const sz = node.size; - node.addInput(widget.name, type, { - widget: { name: widget.name, [GET_CONFIG]: () => config }, - }); - - for (const widget of node.widgets) { - widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; - } - - // Restore original size but grow if needed - node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); -} - -function convertToWidget(node, widget) { - showWidget(widget); - const sz = node.size; - node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)); - - for (const widget of node.widgets) { - widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT; - } - - // Restore original size but grow if needed - node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); -} - -function getWidgetType(config) { - // Special handling for COMBO so we restrict links based on the entries - let type = config[0]; - if (type instanceof Array) { - type = "COMBO"; - } - return { type }; -} - -function isValidCombo(combo, obj) { - // New input isnt a combo - if (!(obj instanceof Array)) { - console.log(`connection rejected: tried to connect combo to ${obj}`); - return false; - } - // New imput combo has a different size - if (combo.length !== obj.length) { - console.log(`connection rejected: combo lists dont match`); - return false; - } - // New input combo has different elements - if (combo.find((v, i) => obj[i] !== v)) { - console.log(`connection rejected: combo lists dont match`); - return false; - } - - return true; -} - -export 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 (originNode.type === "PrimitiveNode") { - if (config) { - originNode.recreateWidget(); - } else if(!app.configuringGraph) { - originNode.disconnectOutput(0); - originNode.onLastDisconnect(); - } - } - } - } -} - -export 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 false; - } else if (config1[0] !== config2[0]) { - // Types dont match - console.log(`connection rejected: types dont match`, config1[0], config2[0]); - return false; - } - - const keys = new Set([...Object.keys(config1[1] ?? {}), ...Object.keys(config2[1] ?? {})]); - - let customConfig; - const getCustomConfig = () => { - if (!customConfig) { - if (typeof structuredClone === "undefined") { - customConfig = JSON.parse(JSON.stringify(config1[1] ?? {})); - } else { - customConfig = structuredClone(config1[1] ?? {}); - } - } - return customConfig; - }; - - 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 false; - } - 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 false; - } - getCustomConfig()[k] = v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2); - continue; - } else if (k === "step") { - let step; - if (v1 == null) { - // No current step - step = v2; - } else if (v2 == null) { - // No new step - step = v1; - } else { - if (v1 < v2) { - // Ensure v1 is larger for the mod - const a = v2; - v2 = v1; - v1 = a; - } - if (v1 % v2) { - console.log("connection rejected: steps not divisible", "current:", v1, "new:", v2); - return false; - } - - step = v1; - } - - getCustomConfig()[k] = step; - continue; - } - } - - console.log(`connection rejected: config ${k} values dont match`, v1, v2); - return false; - } - } - - if (customConfig || forceUpdate) { - if (customConfig) { - output.widget[CONFIG] = [config1[0], customConfig]; - } - - const widget = recreateWidget?.call(this); - // When deleting a node this can be null - 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 }; -} - -let useConversionSubmenusSetting; -app.registerExtension({ - name: "Comfy.WidgetInputs", - init() { - useConversionSubmenusSetting = app.ui.settings.addSetting({ - id: "Comfy.NodeInputConversionSubmenus", - name: "Node widget/input conversion sub-menus", - tooltip: "In the node context menu, place the entries that convert between input/widget in sub-menus.", - type: "boolean", - defaultValue: true, - }); - }, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - // Add menu options to conver to/from widgets - const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; - nodeType.prototype.convertWidgetToInput = function (widget) { - const config = getConfig.call(this, widget.name) ?? [widget.type, widget.options || {}]; - if (!isConvertableWidget(widget, config)) return false; - convertToInput(this, widget, config); - return true; - }; - nodeType.prototype.getExtraMenuOptions = function (_, options) { - const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined; - - 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: () => convertToWidget(this, w), - }); - } else { - const config = getConfig.call(this, w.name) ?? [w.type, w.options || {}]; - if (isConvertableWidget(w, config)) { - toInput.push({ - content: `Convert ${w.name} to input`, - callback: () => convertToInput(this, w, config), - }); - } - } - } - - //Convert.. main menu - 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); - } - - // Cleanup old widget config - if (input.widget.config) { - if (input.widget.config[0] instanceof Array) { - // If we are an old converted combo then replace the input type and the stored link data - input.type = "COMBO"; - - const link = app.graph.links[input.link]; - if (link) { - link.type = input.type; - } - } - delete input.widget.config; - } - - const w = this.widgets.find((w) => w.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) : undefined; - - // When node is created, convert any force/default inputs - if (!app.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) : undefined; - if (!app.configuringGraph && this.inputs) { - // On copy + paste of nodes, ensure that widget configs are set up - 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((w) => w.name === input.widget.name); - if (w) { - hideWidget(this, w); - } - } - } - } - - return r; - }; - - function isNodeAtPos(pos) { - for (const n of app.graph._nodes) { - if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) { - return true; - } - } - return false; - } - - // Double click a widget input to automatically attach a primitive - const origOnInputDblClick = nodeType.prototype.onInputDblClick; - const ignoreDblClick = Symbol(); - nodeType.prototype.onInputDblClick = function (slot) { - const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined; - - const input = this.inputs[slot]; - if (!input.widget || !input[ignoreDblClick]) { - // Not a widget input or already handled input - if (!(input.type in ComfyWidgets) && !(input.widget[GET_CONFIG]?.()?.[0] instanceof Array)) { - return r; //also Not a ComfyWidgets input or combo (do nothing) - } - } - - // Create a primitive node - const node = LiteGraph.createNode("PrimitiveNode"); - app.graph.add(node); - - // Calculate a position that wont directly overlap another 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; - - // Prevent adding duplicates due to triple clicking - input[ignoreDblClick] = true; - setTimeout(() => { - delete input[ignoreDblClick]; - }, 300); - - return r; - }; - - // Prevent connecting COMBO lists to converted inputs that dont match types - const onConnectInput = nodeType.prototype.onConnectInput; - nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { - const v = onConnectInput?.(this, arguments); - // Not a combo, ignore - if (type !== "COMBO") return v; - // Primitive output, allow that to handle - if (originNode.outputs[originSlot].widget) return v; - - // Ensure target is also a combo - const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0]; - if (!targetCombo || !(targetCombo instanceof Array)) return v; - - // Check they match - const originConfig = originNode.constructor?.nodeData?.output?.[originSlot]; - if (!originConfig || !isValidCombo(targetCombo, originConfig)) { - return false; - } - - return v; - }; - }, - registerCustomNodes() { - const replacePropertyName = "Run widget replace on values"; - class PrimitiveNode { - constructor() { - 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 links = []; - 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") { - links = links.concat(get_links(n)); - } else { - links.push(l); - } - } - return 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 each output link copy our value over the original widget value - 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; - - // Populate widget values from config data - 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]; - } - } - } - - // Merge values if required - this.#mergeWidgetConfig(); - } - } - - onConnectionsChange(_, index, connected) { - if (app.configuringGraph) { - // Dont run while the graph is still setting up - return; - } - - const links = this.outputs[0].links; - if (connected) { - if (links?.length && !this.widgets?.length) { - this.#onFirstConnection(); - } - } else { - // We may have removed a link that caused the constraints to change - this.#mergeWidgetConfig(); - - if (!links?.length) { - this.onLastDisconnect(); - } - } - } - - onConnectOutput(slot, type, input, target_node, target_slot) { - // Fires before the link is made allowing us to reject it if it isn't valid - // No widget, we cant connect - if (!input.widget) { - if (!(input.type in ComfyWidgets)) return false; - } - - if (this.outputs[slot].links?.length) { - const valid = this.#isValidConnection(input); - if (valid) { - // On connect of additional outputs, copy our value to their widget - this.applyToGraph([{ target_id: target_node.id, target_slot }]); - } - return valid; - } - } - - #onFirstConnection(recreating) { - // First connection can fire before the graph is ready on initial load so random things can be missing - 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, {}] }; //fake widget - } else { - widget = input.widget; - } - - const config = widget[GET_CONFIG]?.(); - if (!config) return; - - const { type } = getWidgetType(config); - // Update our output to restrict to the widget type - 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"; - } - - 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, undefined, inputData); - let filter = this.widgets_values?.[2]; - if (filter && this.widgets.length === 3) { - this.widgets[2].value = filter; - } - } - - // Restore any saved control values - 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]; - } - } - - // When our value changes, update other widgets to reflect our changes - // e.g. so LoadImage shows correct image - const callback = widget.callback; - const self = this; - widget.callback = function () { - const r = callback ? callback.apply(this, arguments) : undefined; - self.applyToGraph(); - return r; - }; - - if (!recreating) { - // Grow our node if required - 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() { - // Merge widget configs if the node has multiple outputs - 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) { - // Copy the widget options from the source - 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; // Can be null when removing a node - - const theirNode = app.graph.getNodeById(link.target_id); - const theirInput = theirNode.inputs[link.target_slot]; - - // Call is valid connection so it can merge the configs when validating - this.#isValidConnection(theirInput, hasConfig); - } - } - - #isValidConnection(input, forceUpdate) { - // Only allow connections where the configs match - const output = this.outputs[0]; - const config2 = input.widget[GET_CONFIG](); - return !!mergeIfValid.call(this, output, config2, forceUpdate, this.recreateWidget); - } - - #removeWidgets() { - if (this.widgets) { - // Allow widgets to cleanup - for (const w of this.widgets) { - if (w.onRemove) { - w.onRemove(); - } - } - - // Temporarily store the current values in case the node is being recreated - // e.g. by group node conversion - 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() { - // We cant remove + re-add the output here as if you drag a link over the same link - // it removes, then re-adds, causing it to break - this.outputs[0].type = "*"; - this.outputs[0].name = "connect to widget input"; - delete this.outputs[0].widget; - - this.#removeWidgets(); - } - } - - LiteGraph.registerNodeType( - "PrimitiveNode", - Object.assign(PrimitiveNode, { - title: "Primitive", - }) - ); - PrimitiveNode.category = "utils"; - }, -}); diff --git a/web/extensions/logging.js.example b/web/extensions/logging.js.example deleted file mode 100644 index d015096a2..000000000 --- a/web/extensions/logging.js.example +++ /dev/null @@ -1,55 +0,0 @@ -import { app } from "../scripts/app.js"; - -const ext = { - // Unique name for the extension - name: "Example.LoggingExtension", - async init(app) { - // Any initial setup to run as soon as the page loads - console.log("[logging]", "extension init"); - }, - async setup(app) { - // Any setup to run after the app is created - console.log("[logging]", "extension setup"); - }, - async addCustomNodeDefs(defs, app) { - // Add custom node definitions - // These definitions will be configured and registered automatically - // defs is a lookup core nodes, add yours into this - console.log("[logging]", "add custom node definitions", "current nodes:", Object.keys(defs)); - }, - async getCustomWidgets(app) { - // Return custom widget types - // See ComfyWidgets for widget examples - console.log("[logging]", "provide custom widgets"); - }, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - // Run custom logic before a node definition is registered with the graph - console.log("[logging]", "before register node: ", nodeType, nodeData); - - // This fires for every node definition so only log once - delete ext.beforeRegisterNodeDef; - }, - async registerCustomNodes(app) { - // Register any custom node implementations here allowing for more flexability than a custom node def - console.log("[logging]", "register custom nodes"); - }, - loadedGraphNode(node, app) { - // Fires for each node when loading/dragging/etc a workflow json or png - // If you break something in the backend and want to patch workflows in the frontend - // This is the place to do this - console.log("[logging]", "loaded graph node: ", node); - - // This fires for every node on each load so only log once - delete ext.loadedGraphNode; - }, - nodeCreated(node, app) { - // Fires every time a node is constructed - // You can modify widgets/add handlers/etc here - console.log("[logging]", "node created: ", node); - - // This fires for every node so only log once - delete ext.nodeCreated; - } -}; - -app.registerExtension(ext); diff --git a/web/fonts/materialdesignicons-webfont.woff2 b/web/fonts/materialdesignicons-webfont.woff2 deleted file mode 100644 index 8c69b85f6..000000000 Binary files a/web/fonts/materialdesignicons-webfont.woff2 and /dev/null differ diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 5ebf03c01..000000000 --- a/web/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - ComfyUI - - - - - - - - - - - - - diff --git a/web/jsconfig.json b/web/jsconfig.json deleted file mode 100644 index cd55a0cc7..000000000 --- a/web/jsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "/*": ["./*"] - }, - "lib": ["DOM", "ES2022", "DOM.Iterable"], - "target": "ES2015", - "module": "es2020" - }, - "include": ["."] -} diff --git a/web/lib/litegraph.core.js b/web/lib/litegraph.core.js deleted file mode 100644 index 427a62b59..000000000 --- a/web/lib/litegraph.core.js +++ /dev/null @@ -1,14424 +0,0 @@ -//packer version - - -(function(global) { - // ************************************************************* - // LiteGraph CLASS ******* - // ************************************************************* - - /** - * The Global Scope. It contains all the registered node classes. - * - * @class LiteGraph - * @constructor - */ - - var LiteGraph = (global.LiteGraph = { - VERSION: 0.4, - - CANVAS_GRID_SIZE: 10, - - NODE_TITLE_HEIGHT: 30, - NODE_TITLE_TEXT_Y: 20, - NODE_SLOT_HEIGHT: 20, - NODE_WIDGET_HEIGHT: 20, - NODE_WIDTH: 140, - NODE_MIN_WIDTH: 50, - NODE_COLLAPSED_RADIUS: 10, - NODE_COLLAPSED_WIDTH: 80, - 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", - - MAX_NUMBER_OF_NODES: 10000, //avoid infinite loops - DEFAULT_POSITION: [100, 100], //default node position - VALID_SHAPES: ["default", "box", "round", "card"], //,"circle" - - //shapes are used for nodes but also for slots - BOX_SHAPE: 1, - ROUND_SHAPE: 2, - CIRCLE_SHAPE: 3, - CARD_SHAPE: 4, - ARROW_SHAPE: 5, - GRID_SHAPE: 6, // intended for slot arrays - - //enums - INPUT: 1, - OUTPUT: 2, - - EVENT: -1, //for outputs - ACTION: -1, //for inputs - - NODE_MODES: ["Always", "On Event", "Never", "On Trigger"], // helper, will add "On Request" and more in the future - NODE_MODES_COLORS:["#666","#422","#333","#224","#626"], // use with node_box_coloured_by_mode - ALWAYS: 0, - ON_EVENT: 1, - NEVER: 2, - ON_TRIGGER: 3, - - UP: 1, - DOWN: 2, - LEFT: 3, - RIGHT: 4, - CENTER: 5, - - LINK_RENDER_MODES: ["Straight", "Linear", "Spline"], // helper - STRAIGHT_LINK: 0, - LINEAR_LINK: 1, - SPLINE_LINK: 2, - - NORMAL_TITLE: 0, - NO_TITLE: 1, - TRANSPARENT_TITLE: 2, - AUTOHIDE_TITLE: 3, - VERTICAL_LAYOUT: "vertical", // arrange nodes vertically - - proxy: null, //used to redirect calls - node_images_path: "", - - debug: false, - catch_exceptions: true, - throw_errors: true, - allow_scripts: false, //if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits - registered_node_types: {}, //nodetypes by string - node_types_by_file_extension: {}, //used for dropping files in the canvas - Nodes: {}, //node types by classname - Globals: {}, //used to store vars between graphs - - searchbox_extras: {}, //used to add extra features to the search box - auto_sort_node_types: false, // [true!] If set to true, will automatically sort node types / categories in the context menus - - node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback - node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback - - dialog_close_on_mouse_leave: false, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false - dialog_close_on_mouse_leave_delay: 500, - - shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys - click_do_break_link_to: false, // [false!]prefer false, way too easy to break links - - search_hide_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false - search_filter_enabled: false, // [true!] enable filtering slots type in the search widget, !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out] - search_show_all_on_open: true, // [true!] opens the results list when opening the search widget - - auto_load_slot_types: false, // [if want false, use true, run, get vars values to be statically set, than disable] nodes types and nodeclass association with node types need to be calculated, if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out] - - // set these values if not using auto_load_slot_types - registered_slot_in_types: {}, // slot types for nodeclass - registered_slot_out_types: {}, // slot types for nodeclass - slot_types_in: [], // slot types IN - slot_types_out: [], // slot types OUT - slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search - slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search - - alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node - - do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this - - allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one - - middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel) - - release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults - - pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) - // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary) - - ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes - - // if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers. - // use this if you must have node IDs that are unique across all graphs and subgraphs. - use_uuids: false, - - /** - * Register a node class so it can be listed when the user wants to create a new one - * @method registerNodeType - * @param {String} type name of the node and path - * @param {Class} base_class class containing the structure of a node - */ - - registerNodeType: function(type, base_class) { - if (!base_class.prototype) { - throw "Cannot register a simple object, it must be a class with a prototype"; - } - base_class.type = type; - - if (LiteGraph.debug) { - console.log("Node registered: " + type); - } - - const classname = base_class.name; - - const pos = type.lastIndexOf("/"); - base_class.category = type.substring(0, pos); - - if (!base_class.title) { - base_class.title = classname; - } - - //extend class - for (var i in LGraphNode.prototype) { - if (!base_class.prototype[i]) { - base_class.prototype[i] = LGraphNode.prototype[i]; - } - } - - const prev = this.registered_node_types[type]; - if(prev) { - console.log("replacing node type: " + type); - } - if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) { - Object.defineProperty(base_class.prototype, "shape", { - set: function(v) { - switch (v) { - case "default": - delete this._shape; - break; - case "box": - this._shape = LiteGraph.BOX_SHAPE; - break; - case "round": - this._shape = LiteGraph.ROUND_SHAPE; - break; - case "circle": - this._shape = LiteGraph.CIRCLE_SHAPE; - break; - case "card": - this._shape = LiteGraph.CARD_SHAPE; - break; - default: - this._shape = v; - } - }, - get: function() { - return this._shape; - }, - enumerable: true, - configurable: true - }); - - - //used to know which nodes to create when dragging files to the canvas - if (base_class.supported_extensions) { - for (let i in base_class.supported_extensions) { - const ext = base_class.supported_extensions[i]; - if(ext && ext.constructor === String) { - this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class; - } - } - } - } - - this.registered_node_types[type] = base_class; - if (base_class.constructor.name) { - this.Nodes[classname] = base_class; - } - if (LiteGraph.onNodeTypeRegistered) { - LiteGraph.onNodeTypeRegistered(type, base_class); - } - if (prev && LiteGraph.onNodeTypeReplaced) { - LiteGraph.onNodeTypeReplaced(type, base_class, prev); - } - - //warnings - if (base_class.prototype.onPropertyChange) { - console.warn( - "LiteGraph node class " + - type + - " has onPropertyChange method, it must be called onPropertyChanged with d at the end" - ); - } - - // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types - if (this.auto_load_slot_types) { - new base_class(base_class.title || "tmpnode"); - } - }, - - /** - * removes a node type from the system - * @method unregisterNodeType - * @param {String|Object} type name of the node or the node constructor itself - */ - unregisterNodeType: function(type) { - const base_class = - type.constructor === String - ? this.registered_node_types[type] - : type; - if (!base_class) { - throw "node type not found: " + type; - } - delete this.registered_node_types[base_class.type]; - if (base_class.constructor.name) { - delete this.Nodes[base_class.constructor.name]; - } - }, - - /** - * Save a slot type and his node - * @method registerSlotType - * @param {String|Object} type name of the node or the node constructor itself - * @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, .. - */ - registerNodeAndSlotType: function(type, slot_type, out){ - out = out || false; - const base_class = - type.constructor === String && - this.registered_node_types[type] !== "anonymous" - ? this.registered_node_types[type] - : type; - - const class_type = base_class.constructor.type; - - let allTypes = []; - if (typeof slot_type === "string") { - allTypes = slot_type.split(","); - } else if (slot_type == this.EVENT || slot_type == this.ACTION) { - allTypes = ["_event_"]; - } else { - allTypes = ["*"]; - } - - for (let i = 0; i < allTypes.length; ++i) { - let slotType = allTypes[i]; - if (slotType === "") { - slotType = "*"; - } - const registerTo = out - ? "registered_slot_out_types" - : "registered_slot_in_types"; - if (this[registerTo][slotType] === undefined) { - this[registerTo][slotType] = { nodes: [] }; - } - if (!this[registerTo][slotType].nodes.includes(class_type)) { - this[registerTo][slotType].nodes.push(class_type); - } - - // check if is a new type - if (!out) { - if (!this.slot_types_in.includes(slotType.toLowerCase())) { - this.slot_types_in.push(slotType.toLowerCase()); - this.slot_types_in.sort(); - } - } else { - if (!this.slot_types_out.includes(slotType.toLowerCase())) { - this.slot_types_out.push(slotType.toLowerCase()); - this.slot_types_out.sort(); - } - } - } - }, - - /** - * Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. - * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. - * @method wrapFunctionAsNode - * @param {String} name node name with namespace (p.e.: 'math/sum') - * @param {Function} func - * @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type - * @param {String} return_type [optional] string with the return type, otherwise it will be generic - * @param {Object} properties [optional] properties to be configurable - */ - wrapFunctionAsNode: function( - name, - func, - param_types, - return_type, - properties - ) { - var params = Array(func.length); - var code = ""; - var names = LiteGraph.getParameterNames(func); - for (var i = 0; i < names.length; ++i) { - code += - "this.addInput('" + - names[i] + - "'," + - (param_types && param_types[i] - ? "'" + param_types[i] + "'" - : "0") + - ");\n"; - } - code += - "this.addOutput('out'," + - (return_type ? "'" + return_type + "'" : 0) + - ");\n"; - if (properties) { - code += - "this.properties = " + JSON.stringify(properties) + ";\n"; - } - var classobj = Function(code); - classobj.title = name.split("/").pop(); - classobj.desc = "Generated from " + func.name; - classobj.prototype.onExecute = function onExecute() { - for (var i = 0; i < params.length; ++i) { - params[i] = this.getInputData(i); - } - var r = func.apply(this, params); - this.setOutputData(0, r); - }; - this.registerNodeType(name, classobj); - }, - - /** - * Removes all previously registered node's types - */ - clearRegisteredTypes: function() { - this.registered_node_types = {}; - this.node_types_by_file_extension = {}; - this.Nodes = {}; - this.searchbox_extras = {}; - }, - - /** - * Adds this method to all nodetypes, existing and to be created - * (You can add it to LGraphNode.prototype but then existing node types wont have it) - * @method addNodeMethod - * @param {Function} func - */ - addNodeMethod: function(name, func) { - LGraphNode.prototype[name] = func; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if (type.prototype[name]) { - type.prototype["_" + name] = type.prototype[name]; - } //keep old in case of replacing - type.prototype[name] = func; - } - }, - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @method createNode - * @param {String} type full name of the node class. p.e. "math/sin" - * @param {String} name a name to distinguish from other nodes - * @param {Object} options to set options - */ - - createNode: function(type, title, options) { - var base_class = this.registered_node_types[type]; - if (!base_class) { - if (LiteGraph.debug) { - console.log( - 'GraphNode type "' + type + '" not registered.' - ); - } - return null; - } - - var prototype = base_class.prototype || base_class; - - title = title || base_class.title || type; - - var node = null; - - if (LiteGraph.catch_exceptions) { - try { - node = new base_class(title); - } catch (err) { - console.error(err); - return null; - } - } else { - node = new base_class(title); - } - - node.type = type; - - if (!node.title && title) { - node.title = title; - } - if (!node.properties) { - node.properties = {}; - } - if (!node.properties_info) { - node.properties_info = []; - } - if (!node.flags) { - node.flags = {}; - } - if (!node.size) { - node.size = node.computeSize(); - //call onresize? - } - if (!node.pos) { - node.pos = LiteGraph.DEFAULT_POSITION.concat(); - } - if (!node.mode) { - node.mode = LiteGraph.ALWAYS; - } - - //extra options - if (options) { - for (var i in options) { - node[i] = options[i]; - } - } - - // callback - if ( node.onNodeCreated ) { - node.onNodeCreated(); - } - - return node; - }, - - /** - * Returns a registered node type with a given name - * @method getNodeType - * @param {String} type full name of the node class. p.e. "math/sin" - * @return {Class} the node class - */ - getNodeType: function(type) { - return this.registered_node_types[type]; - }, - - /** - * Returns a list of node types matching one category - * @method getNodeType - * @param {String} category category name - * @return {Array} array with all the node classes - */ - - getNodeTypesInCategory: function(category, filter) { - var r = []; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if (type.filter != filter) { - continue; - } - - if (category == "") { - if (type.category == null) { - r.push(type); - } - } else if (type.category == category) { - r.push(type); - } - } - - if (this.auto_sort_node_types) { - r.sort(function(a,b){return a.title.localeCompare(b.title)}); - } - - return r; - }, - - /** - * Returns a list with all the node type categories - * @method getNodeTypesCategories - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the names of the categories - */ - getNodeTypesCategories: function( filter ) { - var categories = { "": 1 }; - for (var i in this.registered_node_types) { - var type = this.registered_node_types[i]; - if ( type.category && !type.skip_list ) - { - if(type.filter != filter) - continue; - categories[type.category] = 1; - } - } - var result = []; - for (var i in categories) { - result.push(i); - } - return this.auto_sort_node_types ? result.sort() : result; - }, - - //debug purposes: reloads all the js scripts that matches a wildcard - reloadNodes: function(folder_wildcard) { - var tmp = document.getElementsByTagName("script"); - //weird, this array changes by its own, so we use a copy - var script_files = []; - for (var i=0; i < tmp.length; i++) { - script_files.push(tmp[i]); - } - - var docHeadObj = document.getElementsByTagName("head")[0]; - folder_wildcard = document.location.href + folder_wildcard; - - for (var i=0; i < script_files.length; i++) { - var src = script_files[i].src; - if ( - !src || - src.substr(0, folder_wildcard.length) != folder_wildcard - ) { - continue; - } - - try { - if (LiteGraph.debug) { - console.log("Reloading: " + src); - } - var dynamicScript = document.createElement("script"); - dynamicScript.type = "text/javascript"; - dynamicScript.src = src; - docHeadObj.appendChild(dynamicScript); - docHeadObj.removeChild(script_files[i]); - } catch (err) { - if (LiteGraph.throw_errors) { - throw err; - } - if (LiteGraph.debug) { - console.log("Error while reloading " + src); - } - } - } - - if (LiteGraph.debug) { - console.log("Nodes reloaded"); - } - }, - - //separated just to improve if it doesn't work - cloneObject: function(obj, target) { - if (obj == null) { - return null; - } - var r = JSON.parse(JSON.stringify(obj)); - if (!target) { - return r; - } - - for (var i in r) { - target[i] = r[i]; - } - return target; - }, - - /* - * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 - */ - uuidv4: function() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,a=>(a^Math.random()*16>>a/4).toString(16)); - }, - - /** - * Returns if the types of two slots are compatible (taking into account wildcards, etc) - * @method isValidConnection - * @param {String} type_a - * @param {String} type_b - * @return {Boolean} true if they can be connected - */ - isValidConnection: function(type_a, type_b) { - if (type_a=="" || type_a==="*") type_a = 0; - if (type_b=="" || type_b==="*") type_b = 0; - if ( - !type_a //generic output - || !type_b // generic input - || type_a == type_b //same type (is valid for triggers) - || (type_a == LiteGraph.EVENT && type_b == LiteGraph.ACTION) - ) { - return true; - } - - // Enforce string type to handle toLowerCase call (-1 number not ok) - type_a = String(type_a); - type_b = String(type_b); - type_a = type_a.toLowerCase(); - type_b = type_b.toLowerCase(); - - // For nodes supporting multiple connection types - if (type_a.indexOf(",") == -1 && type_b.indexOf(",") == -1) { - return type_a == type_b; - } - - // Check all permutations to see if one is valid - var supported_types_a = type_a.split(","); - var supported_types_b = type_b.split(","); - for (var i = 0; i < supported_types_a.length; ++i) { - for (var j = 0; j < supported_types_b.length; ++j) { - if(this.isValidConnection(supported_types_a[i],supported_types_b[j])){ - //if (supported_types_a[i] == supported_types_b[j]) { - return true; - } - } - } - - return false; - }, - - /** - * Register a string in the search box so when the user types it it will recommend this node - * @method registerSearchboxExtra - * @param {String} node_type the node recommended - * @param {String} description text to show next to it - * @param {Object} data it could contain info of how the node should be configured - * @return {Boolean} true if they can be connected - */ - registerSearchboxExtra: function(node_type, description, data) { - this.searchbox_extras[description.toLowerCase()] = { - type: node_type, - desc: description, - data: data - }; - }, - - /** - * Wrapper to load files (from url using fetch or from file using FileReader) - * @method fetchFile - * @param {String|File|Blob} url the url of the file (or the file itself) - * @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob" - * @param {Function} on_complete callback(data) - * @param {Function} on_error in case of an error - * @return {FileReader|Promise} returns the object used to - */ - fetchFile: function( url, type, on_complete, on_error ) { - var that = this; - if(!url) - return null; - - type = type || "text"; - if( url.constructor === String ) - { - if (url.substr(0, 4) == "http" && LiteGraph.proxy) { - url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3); - } - return fetch(url) - .then(function(response) { - if(!response.ok) - throw new Error("File not found"); //it will be catch below - if(type == "arraybuffer") - return response.arrayBuffer(); - else if(type == "text" || type == "string") - return response.text(); - else if(type == "json") - return response.json(); - else if(type == "blob") - return response.blob(); - }) - .then(function(data) { - if(on_complete) - on_complete(data); - }) - .catch(function(error) { - console.error("error fetching file:",url); - if(on_error) - on_error(error); - }); - } - else if( url.constructor === File || url.constructor === Blob) - { - var reader = new FileReader(); - reader.onload = function(e) - { - var v = e.target.result; - if( type == "json" ) - v = JSON.parse(v); - if(on_complete) - on_complete(v); - } - if(type == "arraybuffer") - return reader.readAsArrayBuffer(url); - else if(type == "text" || type == "json") - return reader.readAsText(url); - else if(type == "blob") - return reader.readAsBinaryString(url); - } - return null; - } - }); - - //timer that works everywhere - if (typeof performance != "undefined") { - LiteGraph.getTime = performance.now.bind(performance); - } else if (typeof Date != "undefined" && Date.now) { - LiteGraph.getTime = Date.now.bind(Date); - } else if (typeof process != "undefined") { - LiteGraph.getTime = function() { - var t = process.hrtime(); - return t[0] * 0.001 + t[1] * 1e-6; - }; - } else { - LiteGraph.getTime = function getTime() { - return new Date().getTime(); - }; - } - - //********************************************************************************* - // LGraph CLASS - //********************************************************************************* - - /** - * LGraph is the class that contain a full graph. We instantiate one and add nodes to it, and then we can run the execution loop. - * supported callbacks: - + onNodeAdded: when a new node is added to the graph - + onNodeRemoved: when a node inside this graph is removed - + onNodeConnectionChange: some connection has changed in the graph (connected or disconnected) - * - * @class LGraph - * @constructor - * @param {Object} o data from previous serialization [optional] - */ - - function LGraph(o) { - if (LiteGraph.debug) { - console.log("Graph created"); - } - this.list_of_graphcanvas = null; - this.clear(); - - if (o) { - this.configure(o); - } - } - - global.LGraph = LiteGraph.LGraph = LGraph; - - //default supported types - LGraph.supported_types = ["number", "string", "boolean"]; - - //used to know which types of connections support this graph (some graphs do not allow certain types) - LGraph.prototype.getSupportedTypes = function() { - return this.supported_types || LGraph.supported_types; - }; - - LGraph.STATUS_STOPPED = 1; - LGraph.STATUS_RUNNING = 2; - - /** - * Removes all nodes from this graph - * @method clear - */ - - LGraph.prototype.clear = function() { - this.stop(); - this.status = LGraph.STATUS_STOPPED; - - this.last_node_id = 0; - this.last_link_id = 0; - - this._version = -1; //used to detect changes - - //safe clear - if (this._nodes) { - for (var i = 0; i < this._nodes.length; ++i) { - var node = this._nodes[i]; - if (node.onRemoved) { - node.onRemoved(); - } - } - } - - //nodes - this._nodes = []; - this._nodes_by_id = {}; - this._nodes_in_order = []; //nodes sorted in execution order - this._nodes_executable = null; //nodes that contain onExecute sorted in execution order - - //other scene stuff - this._groups = []; - - //links - this.links = {}; //container with all the links - - //iterations - this.iteration = 0; - - //custom data - this.config = {}; - this.vars = {}; - this.extra = {}; //to store custom data - - //timing - this.globaltime = 0; - this.runningtime = 0; - this.fixedtime = 0; - this.fixedtime_lapse = 0.01; - this.elapsed_time = 0.01; - this.last_update_time = 0; - this.starttime = 0; - - this.catch_errors = true; - - this.nodes_executing = []; - this.nodes_actioning = []; - this.nodes_executedAction = []; - - //subgraph_data - this.inputs = {}; - this.outputs = {}; - - //notify canvas to redraw - this.change(); - - this.sendActionToCanvas("clear"); - }; - - /** - * Attach Canvas to this graph - * @method attachCanvas - * @param {GraphCanvas} graph_canvas - */ - - LGraph.prototype.attachCanvas = function(graphcanvas) { - if (graphcanvas.constructor != LGraphCanvas) { - throw "attachCanvas expects a LGraphCanvas instance"; - } - if (graphcanvas.graph && graphcanvas.graph != this) { - graphcanvas.graph.detachCanvas(graphcanvas); - } - - graphcanvas.graph = this; - - if (!this.list_of_graphcanvas) { - this.list_of_graphcanvas = []; - } - this.list_of_graphcanvas.push(graphcanvas); - }; - - /** - * Detach Canvas from this graph - * @method detachCanvas - * @param {GraphCanvas} graph_canvas - */ - LGraph.prototype.detachCanvas = function(graphcanvas) { - if (!this.list_of_graphcanvas) { - return; - } - - var pos = this.list_of_graphcanvas.indexOf(graphcanvas); - if (pos == -1) { - return; - } - graphcanvas.graph = null; - this.list_of_graphcanvas.splice(pos, 1); - }; - - /** - * Starts running this graph every interval milliseconds. - * @method start - * @param {number} interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate - */ - - LGraph.prototype.start = function(interval) { - if (this.status == LGraph.STATUS_RUNNING) { - return; - } - this.status = LGraph.STATUS_RUNNING; - - if (this.onPlayEvent) { - this.onPlayEvent(); - } - - this.sendEventToAllNodes("onStart"); - - //launch - this.starttime = LiteGraph.getTime(); - this.last_update_time = this.starttime; - interval = interval || 0; - var that = this; - - //execute once per frame - if ( interval == 0 && typeof window != "undefined" && window.requestAnimationFrame ) { - function on_frame() { - if (that.execution_timer_id != -1) { - return; - } - window.requestAnimationFrame(on_frame); - if(that.onBeforeStep) - that.onBeforeStep(); - that.runStep(1, !that.catch_errors); - if(that.onAfterStep) - that.onAfterStep(); - } - this.execution_timer_id = -1; - on_frame(); - } else { //execute every 'interval' ms - this.execution_timer_id = setInterval(function() { - //execute - if(that.onBeforeStep) - that.onBeforeStep(); - that.runStep(1, !that.catch_errors); - if(that.onAfterStep) - that.onAfterStep(); - }, interval); - } - }; - - /** - * Stops the execution loop of the graph - * @method stop execution - */ - - LGraph.prototype.stop = function() { - if (this.status == LGraph.STATUS_STOPPED) { - return; - } - - this.status = LGraph.STATUS_STOPPED; - - if (this.onStopEvent) { - this.onStopEvent(); - } - - if (this.execution_timer_id != null) { - if (this.execution_timer_id != -1) { - clearInterval(this.execution_timer_id); - } - this.execution_timer_id = null; - } - - this.sendEventToAllNodes("onStop"); - }; - - /** - * Run N steps (cycles) of the graph - * @method runStep - * @param {number} num number of steps to run, default is 1 - * @param {Boolean} do_not_catch_errors [optional] if you want to try/catch errors - * @param {number} limit max number of nodes to execute (used to execute from start to a node) - */ - - LGraph.prototype.runStep = function(num, do_not_catch_errors, limit ) { - num = num || 1; - - var start = LiteGraph.getTime(); - this.globaltime = 0.001 * (start - this.starttime); - - var nodes = this._nodes_executable - ? this._nodes_executable - : this._nodes; - if (!nodes) { - return; - } - - limit = limit || nodes.length; - - if (do_not_catch_errors) { - //iterations - for (var i = 0; i < num; i++) { - for (var j = 0; j < limit; ++j) { - var node = nodes[j]; - if (node.mode == LiteGraph.ALWAYS && node.onExecute) { - //wrap node.onExecute(); - node.doExecute(); - } - } - - this.fixedtime += this.fixedtime_lapse; - if (this.onExecuteStep) { - this.onExecuteStep(); - } - } - - if (this.onAfterExecute) { - this.onAfterExecute(); - } - } else { - try { - //iterations - for (var i = 0; i < num; i++) { - for (var j = 0; j < limit; ++j) { - var node = nodes[j]; - if (node.mode == LiteGraph.ALWAYS && node.onExecute) { - node.onExecute(); - } - } - - this.fixedtime += this.fixedtime_lapse; - if (this.onExecuteStep) { - this.onExecuteStep(); - } - } - - if (this.onAfterExecute) { - this.onAfterExecute(); - } - this.errors_in_execution = false; - } catch (err) { - this.errors_in_execution = true; - if (LiteGraph.throw_errors) { - throw err; - } - if (LiteGraph.debug) { - console.log("Error during execution: " + err); - } - this.stop(); - } - } - - var now = LiteGraph.getTime(); - var elapsed = now - start; - if (elapsed == 0) { - elapsed = 1; - } - this.execution_time = 0.001 * elapsed; - this.globaltime += 0.001 * elapsed; - this.iteration += 1; - this.elapsed_time = (now - this.last_update_time) * 0.001; - this.last_update_time = now; - this.nodes_executing = []; - this.nodes_actioning = []; - this.nodes_executedAction = []; - }; - - /** - * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than - * nodes with only inputs. - * @method updateExecutionOrder - */ - LGraph.prototype.updateExecutionOrder = function() { - this._nodes_in_order = this.computeExecutionOrder(false); - this._nodes_executable = []; - for (var i = 0; i < this._nodes_in_order.length; ++i) { - if (this._nodes_in_order[i].onExecute) { - this._nodes_executable.push(this._nodes_in_order[i]); - } - } - }; - - //This is more internal, it computes the executable nodes in order and returns it - LGraph.prototype.computeExecutionOrder = function( - only_onExecute, - set_level - ) { - var L = []; - var S = []; - var M = {}; - var visited_links = {}; //to avoid repeating links - var remaining_links = {}; //to a - - //search for the nodes without inputs (starting nodes) - for (var i = 0, l = this._nodes.length; i < l; ++i) { - var node = this._nodes[i]; - if (only_onExecute && !node.onExecute) { - continue; - } - - M[node.id] = node; //add to pending nodes - - var num = 0; //num of input connections - if (node.inputs) { - for (var j = 0, l2 = node.inputs.length; j < l2; j++) { - if (node.inputs[j] && node.inputs[j].link != null) { - num += 1; - } - } - } - - if (num == 0) { - //is a starting node - S.push(node); - if (set_level) { - node._level = 1; - } - } //num of input links - else { - if (set_level) { - node._level = 0; - } - remaining_links[node.id] = num; - } - } - - while (true) { - if (S.length == 0) { - break; - } - - //get an starting node - var node = S.shift(); - L.push(node); //add to ordered list - delete M[node.id]; //remove from the pending nodes - - if (!node.outputs) { - continue; - } - - //for every output - for (var i = 0; i < node.outputs.length; i++) { - var output = node.outputs[i]; - //not connected - if ( - output == null || - output.links == null || - output.links.length == 0 - ) { - continue; - } - - //for every connection - for (var j = 0; j < output.links.length; j++) { - var link_id = output.links[j]; - var link = this.links[link_id]; - if (!link) { - continue; - } - - //already visited link (ignore it) - if (visited_links[link.id]) { - continue; - } - - var target_node = this.getNodeById(link.target_id); - if (target_node == null) { - visited_links[link.id] = true; - continue; - } - - if ( - set_level && - (!target_node._level || - target_node._level <= node._level) - ) { - target_node._level = node._level + 1; - } - - visited_links[link.id] = true; //mark as visited - remaining_links[target_node.id] -= 1; //reduce the number of links remaining - if (remaining_links[target_node.id] == 0) { - S.push(target_node); - } //if no more links, then add to starters array - } - } - } - - //the remaining ones (loops) - for (var i in M) { - L.push(M[i]); - } - - if (L.length != this._nodes.length && LiteGraph.debug) { - console.warn("something went wrong, nodes missing"); - } - - var l = L.length; - - //save order number in the node - for (var i = 0; i < l; ++i) { - L[i].order = i; - } - - //sort now by priority - L = L.sort(function(A, B) { - var Ap = A.constructor.priority || A.priority || 0; - var Bp = B.constructor.priority || B.priority || 0; - if (Ap == Bp) { - //if same priority, sort by order - return A.order - B.order; - } - return Ap - Bp; //sort by priority - }); - - //save order number in the node, again... - for (var i = 0; i < l; ++i) { - L[i].order = i; - } - - return L; - }; - - /** - * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. - * It doesn't include the node itself - * @method getAncestors - * @return {Array} an array with all the LGraphNodes that affect this node, in order of execution - */ - LGraph.prototype.getAncestors = function(node) { - var ancestors = []; - var pending = [node]; - var visited = {}; - - while (pending.length) { - var current = pending.shift(); - if (!current.inputs) { - continue; - } - if (!visited[current.id] && current != node) { - visited[current.id] = true; - ancestors.push(current); - } - - for (var i = 0; i < current.inputs.length; ++i) { - var input = current.getInputNode(i); - if (input && ancestors.indexOf(input) == -1) { - pending.push(input); - } - } - } - - ancestors.sort(function(a, b) { - return a.order - b.order; - }); - return ancestors; - }; - - /** - * Positions every node in a more readable manner - * @method arrange - */ - LGraph.prototype.arrange = function (margin, layout) { - margin = margin || 100; - - const nodes = this.computeExecutionOrder(false, true); - const columns = []; - for (let i = 0; i < nodes.length; ++i) { - const node = nodes[i]; - const col = node._level || 1; - if (!columns[col]) { - columns[col] = []; - } - columns[col].push(node); - } - - let x = margin; - - for (let i = 0; i < columns.length; ++i) { - const column = columns[i]; - if (!column) { - continue; - } - let max_size = 100; - let y = margin + LiteGraph.NODE_TITLE_HEIGHT; - for (let j = 0; j < column.length; ++j) { - const node = column[j]; - node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x; - node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y; - const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0; - if (node.size[max_size_index] > max_size) { - max_size = node.size[max_size_index]; - } - const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1; - y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT; - } - x += max_size + margin; - } - - this.setDirtyCanvas(true, true); - }; - - /** - * Returns the amount of time the graph has been running in milliseconds - * @method getTime - * @return {number} number of milliseconds the graph has been running - */ - LGraph.prototype.getTime = function() { - return this.globaltime; - }; - - /** - * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant - * @method getFixedTime - * @return {number} number of milliseconds the graph has been running - */ - - LGraph.prototype.getFixedTime = function() { - return this.fixedtime; - }; - - /** - * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct - * if the nodes are using graphical actions - * @method getElapsedTime - * @return {number} number of milliseconds it took the last cycle - */ - - LGraph.prototype.getElapsedTime = function() { - return this.elapsed_time; - }; - - /** - * Sends an event to all the nodes, useful to trigger stuff - * @method sendEventToAllNodes - * @param {String} eventname the name of the event (function to be called) - * @param {Array} params parameters in array format - */ - LGraph.prototype.sendEventToAllNodes = function(eventname, params, mode) { - mode = mode || LiteGraph.ALWAYS; - - var nodes = this._nodes_in_order ? this._nodes_in_order : this._nodes; - if (!nodes) { - return; - } - - for (var j = 0, l = nodes.length; j < l; ++j) { - var node = nodes[j]; - - if ( - node.constructor === LiteGraph.Subgraph && - eventname != "onExecute" - ) { - if (node.mode == mode) { - node.sendEventToAllNodes(eventname, params, mode); - } - continue; - } - - if (!node[eventname] || node.mode != mode) { - continue; - } - if (params === undefined) { - node[eventname](); - } else if (params && params.constructor === Array) { - node[eventname].apply(node, params); - } else { - node[eventname](params); - } - } - }; - - LGraph.prototype.sendActionToCanvas = function(action, params) { - if (!this.list_of_graphcanvas) { - return; - } - - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var c = this.list_of_graphcanvas[i]; - if (c[action]) { - c[action].apply(c, params); - } - } - }; - - /** - * Adds a new node instance to this graph - * @method add - * @param {LGraphNode} node the instance of the node - */ - - LGraph.prototype.add = function(node, skip_compute_order) { - if (!node) { - return; - } - - //groups - if (node.constructor === LGraphGroup) { - this._groups.push(node); - this.setDirtyCanvas(true); - this.change(); - node.graph = this; - this._version++; - return; - } - - //nodes - if (node.id != -1 && this._nodes_by_id[node.id] != null) { - console.warn( - "LiteGraph: there is already a node with this ID, changing it" - ); - if (LiteGraph.use_uuids) { - node.id = LiteGraph.uuidv4(); - } - else { - node.id = ++this.last_node_id; - } - } - - if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) { - throw "LiteGraph: max number of nodes in a graph reached"; - } - - //give him an id - if (LiteGraph.use_uuids) { - if (node.id == null || node.id == -1) - node.id = LiteGraph.uuidv4(); - } - else { - if (node.id == null || node.id == -1) { - node.id = ++this.last_node_id; - } else if (this.last_node_id < node.id) { - this.last_node_id = node.id; - } - } - - node.graph = this; - this._version++; - - this._nodes.push(node); - this._nodes_by_id[node.id] = node; - - if (node.onAdded) { - node.onAdded(this); - } - - if (this.config.align_to_grid) { - node.alignToGrid(); - } - - if (!skip_compute_order) { - this.updateExecutionOrder(); - } - - if (this.onNodeAdded) { - this.onNodeAdded(node); - } - - this.setDirtyCanvas(true); - this.change(); - - return node; //to chain actions - }; - - /** - * Removes a node from the graph - * @method remove - * @param {LGraphNode} node the instance of the node - */ - - LGraph.prototype.remove = function(node) { - if (node.constructor === LiteGraph.LGraphGroup) { - var index = this._groups.indexOf(node); - if (index != -1) { - this._groups.splice(index, 1); - } - node.graph = null; - this._version++; - this.setDirtyCanvas(true, true); - this.change(); - return; - } - - if (this._nodes_by_id[node.id] == null) { - return; - } //not found - - if (node.ignore_remove) { - return; - } //cannot be removed - - this.beforeChange(); //sure? - almost sure is wrong - - //disconnect inputs - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - if (slot.link != null) { - node.disconnectInput(i); - } - } - } - - //disconnect outputs - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - if (slot.links != null && slot.links.length) { - node.disconnectOutput(i); - } - } - } - - //node.id = -1; //why? - - //callback - if (node.onRemoved) { - node.onRemoved(); - } - - node.graph = null; - this._version++; - - //remove from canvas render - if (this.list_of_graphcanvas) { - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var canvas = this.list_of_graphcanvas[i]; - if (canvas.selected_nodes[node.id]) { - delete canvas.selected_nodes[node.id]; - } - if (canvas.node_dragged == node) { - canvas.node_dragged = null; - } - } - } - - //remove from containers - var pos = this._nodes.indexOf(node); - if (pos != -1) { - this._nodes.splice(pos, 1); - } - delete this._nodes_by_id[node.id]; - - if (this.onNodeRemoved) { - this.onNodeRemoved(node); - } - - //close panels - this.sendActionToCanvas("checkPanels"); - - this.setDirtyCanvas(true, true); - this.afterChange(); //sure? - almost sure is wrong - this.change(); - - this.updateExecutionOrder(); - }; - - /** - * Returns a node by its id. - * @method getNodeById - * @param {Number} id - */ - - LGraph.prototype.getNodeById = function(id) { - if (id == null) { - return null; - } - return this._nodes_by_id[id]; - }; - - /** - * Returns a list of nodes that matches a class - * @method findNodesByClass - * @param {Class} classObject the class itself (not an string) - * @return {Array} a list with all the nodes of this type - */ - LGraph.prototype.findNodesByClass = function(classObject, result) { - result = result || []; - result.length = 0; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].constructor === classObject) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns a list of nodes that matches a type - * @method findNodesByType - * @param {String} type the name of the node type - * @return {Array} a list with all the nodes of this type - */ - LGraph.prototype.findNodesByType = function(type, result) { - var type = type.toLowerCase(); - result = result || []; - result.length = 0; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].type.toLowerCase() == type) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns the first node that matches a name in its title - * @method findNodeByTitle - * @param {String} name the name of the node to search - * @return {Node} the node or null - */ - LGraph.prototype.findNodeByTitle = function(title) { - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].title == title) { - return this._nodes[i]; - } - } - return null; - }; - - /** - * Returns a list of nodes that matches a name - * @method findNodesByTitle - * @param {String} name the name of the node to search - * @return {Array} a list with all the nodes with this name - */ - LGraph.prototype.findNodesByTitle = function(title) { - var result = []; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - if (this._nodes[i].title == title) { - result.push(this._nodes[i]); - } - } - return result; - }; - - /** - * Returns the top-most node in this position of the canvas - * @method getNodeOnPos - * @param {number} x the x coordinate in canvas space - * @param {number} y the y coordinate in canvas space - * @param {Array} nodes_list a list with all the nodes to search from, by default is all the nodes in the graph - * @return {LGraphNode} the node at this position or null - */ - LGraph.prototype.getNodeOnPos = function(x, y, nodes_list, margin) { - nodes_list = nodes_list || this._nodes; - var nRet = null; - for (var i = nodes_list.length - 1; i >= 0; i--) { - var n = nodes_list[i]; - var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE; - if (n.isPointInside(x, y, margin, skip_title)) { - // check for lesser interest nodes (TODO check for overlapping, use the top) - /*if (typeof n == "LGraphGroup"){ - nRet = n; - }else{*/ - return n; - /*}*/ - } - } - return nRet; - }; - - /** - * Returns the top-most group in that position - * @method getGroupOnPos - * @param {number} x the x coordinate in canvas space - * @param {number} y the y coordinate in canvas space - * @return {LGraphGroup} the group or null - */ - LGraph.prototype.getGroupOnPos = function(x, y) { - for (var i = this._groups.length - 1; i >= 0; i--) { - var g = this._groups[i]; - if (g.isPointInside(x, y, 2, true)) { - return g; - } - } - return null; - }; - - /** - * Checks that the node type matches the node type registered, used when replacing a nodetype by a newer version during execution - * this replaces the ones using the old version with the new version - * @method checkNodeTypes - */ - LGraph.prototype.checkNodeTypes = function() { - var changes = false; - for (var i = 0; i < this._nodes.length; i++) { - var node = this._nodes[i]; - var ctor = LiteGraph.registered_node_types[node.type]; - if (node.constructor == ctor) { - continue; - } - console.log("node being replaced by newer version: " + node.type); - var newnode = LiteGraph.createNode(node.type); - changes = true; - this._nodes[i] = newnode; - newnode.configure(node.serialize()); - newnode.graph = this; - this._nodes_by_id[newnode.id] = newnode; - if (node.inputs) { - newnode.inputs = node.inputs.concat(); - } - if (node.outputs) { - newnode.outputs = node.outputs.concat(); - } - } - this.updateExecutionOrder(); - }; - - // ********** GLOBALS ***************** - - LGraph.prototype.onAction = function(action, param, options) { - this._input_nodes = this.findNodesByClass( - LiteGraph.GraphInput, - this._input_nodes - ); - for (var i = 0; i < this._input_nodes.length; ++i) { - var node = this._input_nodes[i]; - if (node.properties.name != action) { - continue; - } - //wrap node.onAction(action, param); - node.actionDo(action, param, options); - break; - } - }; - - LGraph.prototype.trigger = function(action, param) { - if (this.onTrigger) { - this.onTrigger(action, param); - } - }; - - /** - * Tell this graph it has a global graph input of this type - * @method addGlobalInput - * @param {String} name - * @param {String} type - * @param {*} value [optional] - */ - LGraph.prototype.addInput = function(name, type, value) { - var input = this.inputs[name]; - if (input) { - //already exist - return; - } - - this.beforeChange(); - this.inputs[name] = { name: name, type: type, value: value }; - this._version++; - this.afterChange(); - - if (this.onInputAdded) { - this.onInputAdded(name, type); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Assign a data to the global graph input - * @method setGlobalInputData - * @param {String} name - * @param {*} data - */ - LGraph.prototype.setInputData = function(name, data) { - var input = this.inputs[name]; - if (!input) { - return; - } - input.value = data; - }; - - /** - * Returns the current value of a global graph input - * @method getInputData - * @param {String} name - * @return {*} the data - */ - LGraph.prototype.getInputData = function(name) { - var input = this.inputs[name]; - if (!input) { - return null; - } - return input.value; - }; - - /** - * Changes the name of a global graph input - * @method renameInput - * @param {String} old_name - * @param {String} new_name - */ - LGraph.prototype.renameInput = function(old_name, name) { - if (name == old_name) { - return; - } - - if (!this.inputs[old_name]) { - return false; - } - - if (this.inputs[name]) { - console.error("there is already one input with that name"); - return false; - } - - this.inputs[name] = this.inputs[old_name]; - delete this.inputs[old_name]; - this._version++; - - if (this.onInputRenamed) { - this.onInputRenamed(old_name, name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Changes the type of a global graph input - * @method changeInputType - * @param {String} name - * @param {String} type - */ - LGraph.prototype.changeInputType = function(name, type) { - if (!this.inputs[name]) { - return false; - } - - if ( - this.inputs[name].type && - String(this.inputs[name].type).toLowerCase() == - String(type).toLowerCase() - ) { - return; - } - - this.inputs[name].type = type; - this._version++; - if (this.onInputTypeChanged) { - this.onInputTypeChanged(name, type); - } - }; - - /** - * Removes a global graph input - * @method removeInput - * @param {String} name - * @param {String} type - */ - LGraph.prototype.removeInput = function(name) { - if (!this.inputs[name]) { - return false; - } - - delete this.inputs[name]; - this._version++; - - if (this.onInputRemoved) { - this.onInputRemoved(name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - return true; - }; - - /** - * Creates a global graph output - * @method addOutput - * @param {String} name - * @param {String} type - * @param {*} value - */ - LGraph.prototype.addOutput = function(name, type, value) { - this.outputs[name] = { name: name, type: type, value: value }; - this._version++; - - if (this.onOutputAdded) { - this.onOutputAdded(name, type); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Assign a data to the global output - * @method setOutputData - * @param {String} name - * @param {String} value - */ - LGraph.prototype.setOutputData = function(name, value) { - var output = this.outputs[name]; - if (!output) { - return; - } - output.value = value; - }; - - /** - * Returns the current value of a global graph output - * @method getOutputData - * @param {String} name - * @return {*} the data - */ - LGraph.prototype.getOutputData = function(name) { - var output = this.outputs[name]; - if (!output) { - return null; - } - return output.value; - }; - - /** - * Renames a global graph output - * @method renameOutput - * @param {String} old_name - * @param {String} new_name - */ - LGraph.prototype.renameOutput = function(old_name, name) { - if (!this.outputs[old_name]) { - return false; - } - - if (this.outputs[name]) { - console.error("there is already one output with that name"); - return false; - } - - this.outputs[name] = this.outputs[old_name]; - delete this.outputs[old_name]; - this._version++; - - if (this.onOutputRenamed) { - this.onOutputRenamed(old_name, name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - }; - - /** - * Changes the type of a global graph output - * @method changeOutputType - * @param {String} name - * @param {String} type - */ - LGraph.prototype.changeOutputType = function(name, type) { - if (!this.outputs[name]) { - return false; - } - - if ( - this.outputs[name].type && - String(this.outputs[name].type).toLowerCase() == - String(type).toLowerCase() - ) { - return; - } - - this.outputs[name].type = type; - this._version++; - if (this.onOutputTypeChanged) { - this.onOutputTypeChanged(name, type); - } - }; - - /** - * Removes a global graph output - * @method removeOutput - * @param {String} name - */ - LGraph.prototype.removeOutput = function(name) { - if (!this.outputs[name]) { - return false; - } - delete this.outputs[name]; - this._version++; - - if (this.onOutputRemoved) { - this.onOutputRemoved(name); - } - - if (this.onInputsOutputsChange) { - this.onInputsOutputsChange(); - } - return true; - }; - - LGraph.prototype.triggerInput = function(name, value) { - var nodes = this.findNodesByTitle(name); - for (var i = 0; i < nodes.length; ++i) { - nodes[i].onTrigger(value); - } - }; - - LGraph.prototype.setCallback = function(name, func) { - var nodes = this.findNodesByTitle(name); - for (var i = 0; i < nodes.length; ++i) { - nodes[i].setTrigger(func); - } - }; - - //used for undo, called before any change is made to the graph - LGraph.prototype.beforeChange = function(info) { - if (this.onBeforeChange) { - this.onBeforeChange(this,info); - } - this.sendActionToCanvas("onBeforeChange", this); - }; - - //used to resend actions, called after any change is made to the graph - LGraph.prototype.afterChange = function(info) { - if (this.onAfterChange) { - this.onAfterChange(this,info); - } - this.sendActionToCanvas("onAfterChange", this); - }; - - LGraph.prototype.connectionChange = function(node, link_info) { - this.updateExecutionOrder(); - if (this.onConnectionChange) { - this.onConnectionChange(node); - } - this._version++; - this.sendActionToCanvas("onConnectionChange"); - }; - - /** - * returns if the graph is in live mode - * @method isLive - */ - - LGraph.prototype.isLive = function() { - if (!this.list_of_graphcanvas) { - return false; - } - - for (var i = 0; i < this.list_of_graphcanvas.length; ++i) { - var c = this.list_of_graphcanvas[i]; - if (c.live_mode) { - return true; - } - } - return false; - }; - - /** - * clears the triggered slot animation in all links (stop visual animation) - * @method clearTriggeredSlots - */ - LGraph.prototype.clearTriggeredSlots = function() { - for (var i in this.links) { - var link_info = this.links[i]; - if (!link_info) { - continue; - } - if (link_info._last_time) { - link_info._last_time = 0; - } - } - }; - - /* Called when something visually changed (not the graph!) */ - LGraph.prototype.change = function() { - if (LiteGraph.debug) { - console.log("Graph changed"); - } - this.sendActionToCanvas("setDirty", [true, true]); - if (this.on_change) { - this.on_change(this); - } - }; - - LGraph.prototype.setDirtyCanvas = function(fg, bg) { - this.sendActionToCanvas("setDirty", [fg, bg]); - }; - - /** - * Destroys a link - * @method removeLink - * @param {Number} link_id - */ - LGraph.prototype.removeLink = function(link_id) { - var link = this.links[link_id]; - if (!link) { - return; - } - var node = this.getNodeById(link.target_id); - if (node) { - node.disconnectInput(link.target_slot); - } - }; - - //save and recover app state *************************************** - /** - * Creates a Object containing all the info about this graph, it can be serialized - * @method serialize - * @return {Object} value of the node - */ - LGraph.prototype.serialize = function() { - var nodes_info = []; - for (var i = 0, l = this._nodes.length; i < l; ++i) { - nodes_info.push(this._nodes[i].serialize()); - } - - //pack link info into a non-verbose format - var links = []; - for (var i in this.links) { - //links is an OBJECT - var link = this.links[i]; - if (!link.serialize) { - //weird bug I havent solved yet - console.warn( - "weird LLink bug, link info is not a LLink but a regular object" - ); - var link2 = new LLink(); - for (var j in link) { - link2[j] = link[j]; - } - this.links[i] = link2; - link = link2; - } - - links.push(link.serialize()); - } - - var groups_info = []; - for (var i = 0; i < this._groups.length; ++i) { - groups_info.push(this._groups[i].serialize()); - } - - var data = { - last_node_id: this.last_node_id, - last_link_id: this.last_link_id, - nodes: nodes_info, - links: links, - groups: groups_info, - config: this.config, - extra: this.extra, - version: LiteGraph.VERSION - }; - - if(this.onSerialize) - this.onSerialize(data); - - return data; - }; - - /** - * Configure a graph from a JSON string - * @method configure - * @param {String} str configure a graph from a JSON string - * @param {Boolean} returns if there was any error parsing - */ - LGraph.prototype.configure = function(data, keep_old) { - if (!data) { - return; - } - - if (!keep_old) { - this.clear(); - } - - var nodes = data.nodes; - - //decode links info (they are very verbose) - if (data.links && data.links.constructor === Array) { - var links = []; - for (var i = 0; i < data.links.length; ++i) { - var link_data = data.links[i]; - if(!link_data) //weird bug - { - console.warn("serialized graph link data contains errors, skipping."); - continue; - } - var link = new LLink(); - link.configure(link_data); - links[link.id] = link; - } - data.links = links; - } - - //copy all stored fields - for (var i in data) { - if(i == "nodes" || i == "groups" ) //links must be accepted - continue; - this[i] = data[i]; - } - - var error = false; - - //create nodes - this._nodes = []; - if (nodes) { - for (var i = 0, l = nodes.length; i < l; ++i) { - var n_info = nodes[i]; //stored info - var node = LiteGraph.createNode(n_info.type, n_info.title); - if (!node) { - if (LiteGraph.debug) { - console.log( - "Node not found or has errors: " + n_info.type - ); - } - - //in case of error we create a replacement node to avoid losing info - node = new LGraphNode(); - node.last_serialization = n_info; - node.has_errors = true; - error = true; - //continue; - } - - node.id = n_info.id; //id it or it will create a new id - this.add(node, true); //add before configure, otherwise configure cannot create links - } - - //configure nodes afterwards so they can reach each other - for (var i = 0, l = nodes.length; i < l; ++i) { - var n_info = nodes[i]; - var node = this.getNodeById(n_info.id); - if (node) { - node.configure(n_info); - } - } - } - - //groups - this._groups.length = 0; - if (data.groups) { - for (var i = 0; i < data.groups.length; ++i) { - var group = new LiteGraph.LGraphGroup(); - group.configure(data.groups[i]); - this.add(group); - } - } - - this.updateExecutionOrder(); - - this.extra = data.extra || {}; - - if(this.onConfigure) - this.onConfigure(data); - - this._version++; - this.setDirtyCanvas(true, true); - return error; - }; - - LGraph.prototype.load = function(url, callback) { - var that = this; - - //from file - if(url.constructor === File || url.constructor === Blob) - { - var reader = new FileReader(); - reader.addEventListener('load', function(event) { - var data = JSON.parse(event.target.result); - that.configure(data); - if(callback) - callback(); - }); - - reader.readAsText(url); - return; - } - - //is a string, then an URL - var req = new XMLHttpRequest(); - req.open("GET", url, true); - req.send(null); - req.onload = function(oEvent) { - if (req.status !== 200) { - console.error("Error loading graph:", req.status, req.response); - return; - } - var data = JSON.parse( req.response ); - that.configure(data); - if(callback) - callback(); - }; - req.onerror = function(err) { - console.error("Error loading graph:", err); - }; - }; - - LGraph.prototype.onNodeTrace = function(node, msg, color) { - //TODO - }; - - //this is the class in charge of storing link information - function LLink(id, type, origin_id, origin_slot, target_id, target_slot) { - this.id = id; - this.type = type; - this.origin_id = origin_id; - this.origin_slot = origin_slot; - this.target_id = target_id; - this.target_slot = target_slot; - - this._data = null; - this._pos = new Float32Array(2); //center - } - - LLink.prototype.configure = function(o) { - if (o.constructor === Array) { - this.id = o[0]; - this.origin_id = o[1]; - this.origin_slot = o[2]; - this.target_id = o[3]; - this.target_slot = o[4]; - this.type = o[5]; - } else { - this.id = o.id; - this.type = o.type; - this.origin_id = o.origin_id; - this.origin_slot = o.origin_slot; - this.target_id = o.target_id; - this.target_slot = o.target_slot; - } - }; - - LLink.prototype.serialize = function() { - return [ - this.id, - this.origin_id, - this.origin_slot, - this.target_id, - this.target_slot, - this.type - ]; - }; - - LiteGraph.LLink = LLink; - - // ************************************************************* - // Node CLASS ******* - // ************************************************************* - - /* - title: string - pos: [x,y] - size: [x,y] - - input|output: every connection - + { name:string, type:string, pos: [x,y]=Optional, direction: "input"|"output", links: Array }); - - general properties: - + clip_area: if you render outside the node, it will be clipped - + unsafe_execution: not allowed for safe execution - + skip_repeated_outputs: when adding new outputs, it wont show if there is one already connected - + resizable: if set to false it wont be resizable with the mouse - + horizontal: slots are distributed horizontally - + widgets_start_y: widgets start at y distance from the top of the node - - flags object: - + collapsed: if it is collapsed - - supported callbacks: - + onAdded: when added to graph (warning: this is called BEFORE the node is configured when loading) - + onRemoved: when removed from graph - + onStart: when the graph starts playing - + onStop: when the graph stops playing - + onDrawForeground: render the inside widgets inside the node - + onDrawBackground: render the background area inside the node (only in edit mode) - + onMouseDown - + onMouseMove - + onMouseUp - + onMouseEnter - + onMouseLeave - + onExecute: execute the node - + onPropertyChanged: when a property is changed in the panel (return true to skip default behaviour) - + onGetInputs: returns an array of possible inputs - + onGetOutputs: returns an array of possible outputs - + onBounding: in case this node has a bigger bounding than the node itself (the callback receives the bounding as [x,y,w,h]) - + onDblClick: double clicked in the node - + onInputDblClick: input slot double clicked (can be used to automatically create a node connected) - + onOutputDblClick: output slot double clicked (can be used to automatically create a node connected) - + onConfigure: called after the node has been configured - + onSerialize: to add extra info when serializing (the callback receives the object that should be filled with the data) - + onSelected - + onDeselected - + onDropItem : DOM item dropped over the node - + onDropFile : file dropped over the node - + onConnectInput : if returns false the incoming connection will be canceled - + onConnectionsChange : a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info ) - + onAction: action slot triggered - + getExtraMenuOptions: to add option to context menu -*/ - - /** - * Base Class for all the node type classes - * @class LGraphNode - * @param {String} name a name for the node - */ - - function LGraphNode(title) { - this._ctor(title); - } - - global.LGraphNode = LiteGraph.LGraphNode = LGraphNode; - - LGraphNode.prototype._ctor = function(title) { - this.title = title || "Unnamed"; - this.size = [LiteGraph.NODE_WIDTH, 60]; - this.graph = null; - - this._pos = new Float32Array(10, 10); - - Object.defineProperty(this, "pos", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() { - return this._pos; - }, - enumerable: true - }); - - if (LiteGraph.use_uuids) { - this.id = LiteGraph.uuidv4(); - } - else { - this.id = -1; //not know till not added - } - this.type = null; - - //inputs available: array of inputs - this.inputs = []; - this.outputs = []; - this.connections = []; - - //local data - this.properties = {}; //for the values - this.properties_info = []; //for the info - - this.flags = {}; - }; - - /** - * configure a node from an object containing the serialized info - * @method configure - */ - LGraphNode.prototype.configure = function(info) { - if (this.graph) { - this.graph._version++; - } - for (var j in info) { - if (j == "properties") { - //i don't want to clone properties, I want to reuse the old container - for (var k in info.properties) { - this.properties[k] = info.properties[k]; - if (this.onPropertyChanged) { - this.onPropertyChanged( k, info.properties[k] ); - } - } - continue; - } - - if (info[j] == null) { - continue; - } else if (typeof info[j] == "object") { - //object - if (this[j] && this[j].configure) { - this[j].configure(info[j]); - } else { - this[j] = LiteGraph.cloneObject(info[j], this[j]); - } - } //value - else { - this[j] = info[j]; - } - } - - if (!info.title) { - this.title = this.constructor.title; - } - - if (this.inputs) { - for (var i = 0; i < this.inputs.length; ++i) { - var input = this.inputs[i]; - var link_info = this.graph ? this.graph.links[input.link] : null; - if (this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated - - if( this.onInputAdded ) - this.onInputAdded(input); - - } - } - - if (this.outputs) { - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; - if (!output.links) { - continue; - } - for (var j = 0; j < output.links.length; ++j) { - var link_info = this.graph ? this.graph.links[output.links[j]] : null; - if (this.onConnectionsChange) - this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated - } - - if( this.onOutputAdded ) - this.onOutputAdded(output); - } - } - - if( this.widgets ) - { - for (var i = 0; i < this.widgets.length; ++i) - { - var w = this.widgets[i]; - if(!w) - continue; - if(w.options && w.options.property && (this.properties[ w.options.property ] != undefined)) - w.value = JSON.parse( JSON.stringify( this.properties[ w.options.property ] ) ); - } - if (info.widgets_values) { - for (var i = 0; i < info.widgets_values.length; ++i) { - if (this.widgets[i]) { - this.widgets[i].value = info.widgets_values[i]; - } - } - } - } - - if (this.onConfigure) { - this.onConfigure(info); - } - }; - - /** - * serialize the content - * @method serialize - */ - - LGraphNode.prototype.serialize = function() { - //create serialization object - var o = { - id: this.id, - type: this.type, - pos: this.pos, - size: this.size, - flags: LiteGraph.cloneObject(this.flags), - order: this.order, - mode: this.mode - }; - - //special case for when there were errors - if (this.constructor === LGraphNode && this.last_serialization) { - return this.last_serialization; - } - - if (this.inputs) { - o.inputs = this.inputs; - } - - if (this.outputs) { - //clear outputs last data (because data in connections is never serialized but stored inside the outputs info) - for (var i = 0; i < this.outputs.length; i++) { - delete this.outputs[i]._data; - } - o.outputs = this.outputs; - } - - if (this.title && this.title != this.constructor.title) { - o.title = this.title; - } - - if (this.properties) { - o.properties = LiteGraph.cloneObject(this.properties); - } - - if (this.widgets && this.serialize_widgets) { - o.widgets_values = []; - for (var i = 0; i < this.widgets.length; ++i) { - if(this.widgets[i]) - o.widgets_values[i] = this.widgets[i].value; - else - o.widgets_values[i] = null; - } - } - - if (!o.type) { - o.type = this.constructor.type; - } - - if (this.color) { - o.color = this.color; - } - if (this.bgcolor) { - o.bgcolor = this.bgcolor; - } - if (this.boxcolor) { - o.boxcolor = this.boxcolor; - } - if (this.shape) { - o.shape = this.shape; - } - - if (this.onSerialize) { - if (this.onSerialize(o)) { - console.warn( - "node onSerialize shouldnt return anything, data should be stored in the object pass in the first parameter" - ); - } - } - - return o; - }; - - /* Creates a clone of this node */ - LGraphNode.prototype.clone = function() { - var node = LiteGraph.createNode(this.type); - if (!node) { - return null; - } - - //we clone it because serialize returns shared containers - var data = LiteGraph.cloneObject(this.serialize()); - - //remove links - if (data.inputs) { - for (var i = 0; i < data.inputs.length; ++i) { - data.inputs[i].link = null; - } - } - - if (data.outputs) { - for (var i = 0; i < data.outputs.length; ++i) { - if (data.outputs[i].links) { - data.outputs[i].links.length = 0; - } - } - } - - delete data["id"]; - - if (LiteGraph.use_uuids) { - data["id"] = LiteGraph.uuidv4() - } - - //remove links - node.configure(data); - - return node; - }; - - /** - * serialize and stringify - * @method toString - */ - - LGraphNode.prototype.toString = function() { - return JSON.stringify(this.serialize()); - }; - //LGraphNode.prototype.deserialize = function(info) {} //this cannot be done from within, must be done in LiteGraph - - /** - * get the title string - * @method getTitle - */ - - LGraphNode.prototype.getTitle = function() { - return this.title || this.constructor.title; - }; - - /** - * sets the value of a property - * @method setProperty - * @param {String} name - * @param {*} value - */ - LGraphNode.prototype.setProperty = function(name, value) { - if (!this.properties) { - this.properties = {}; - } - if( value === this.properties[name] ) - return; - var prev_value = this.properties[name]; - this.properties[name] = value; - if (this.onPropertyChanged) { - if( this.onPropertyChanged(name, value, prev_value) === false ) //abort change - this.properties[name] = prev_value; - } - if(this.widgets) //widgets could be linked to properties - for(var i = 0; i < this.widgets.length; ++i) - { - var w = this.widgets[i]; - if(!w) - continue; - if(w.options.property == name) - { - w.value = value; - break; - } - } - }; - - // Execution ************************* - /** - * sets the output data - * @method setOutputData - * @param {number} slot - * @param {*} data - */ - LGraphNode.prototype.setOutputData = function(slot, data) { - if (!this.outputs) { - return; - } - - //this maybe slow and a niche case - //if(slot && slot.constructor === String) - // slot = this.findOutputSlot(slot); - - if (slot == -1 || slot >= this.outputs.length) { - return; - } - - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } - - //store data in the output itself in case we want to debug - output_info._data = data; - - //if there are connections, pass the data to the connections - if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - var link = this.graph.links[link_id]; - if(link) - link.data = data; - } - } - }; - - /** - * sets the output data type, useful when you want to be able to overwrite the data type - * @method setOutputDataType - * @param {number} slot - * @param {String} datatype - */ - LGraphNode.prototype.setOutputDataType = function(slot, type) { - if (!this.outputs) { - return; - } - if (slot == -1 || slot >= this.outputs.length) { - return; - } - var output_info = this.outputs[slot]; - if (!output_info) { - return; - } - //store data in the output itself in case we want to debug - output_info.type = type; - - //if there are connections, pass the data to the connections - if (this.outputs[slot].links) { - for (var i = 0; i < this.outputs[slot].links.length; i++) { - var link_id = this.outputs[slot].links[i]; - this.graph.links[link_id].type = type; - } - } - }; - - /** - * Retrieves the input data (data traveling through the connection) from one slot - * @method getInputData - * @param {number} slot - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns undefined - */ - LGraphNode.prototype.getInputData = function(slot, force_update) { - if (!this.inputs) { - return; - } //undefined; - - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return; - } - - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } - - if (!force_update) { - return link.data; - } - - //special case: used to extract data from the incoming connection before the graph has been executed - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.data; - } - - if (node.updateOutputData) { - node.updateOutputData(link.origin_slot); - } else if (node.onExecute) { - node.onExecute(); - } - - return link.data; - }; - - /** - * Retrieves the input data type (in case this supports multiple input types) - * @method getInputDataType - * @param {number} slot - * @return {String} datatype in string format - */ - LGraphNode.prototype.getInputDataType = function(slot) { - if (!this.inputs) { - return null; - } //undefined; - - if (slot >= this.inputs.length || this.inputs[slot].link == null) { - return null; - } - var link_id = this.inputs[slot].link; - var link = this.graph.links[link_id]; - if (!link) { - //bug: weird case but it happens sometimes - return null; - } - var node = this.graph.getNodeById(link.origin_id); - if (!node) { - return link.type; - } - var output_info = node.outputs[link.origin_slot]; - if (output_info) { - return output_info.type; - } - return null; - }; - - /** - * Retrieves the input data from one slot using its name instead of slot number - * @method getInputDataByName - * @param {String} slot_name - * @param {boolean} force_update if set to true it will force the connected node of this slot to output data into this link - * @return {*} data or if it is not connected returns null - */ - LGraphNode.prototype.getInputDataByName = function( - slot_name, - force_update - ) { - var slot = this.findInputSlot(slot_name); - if (slot == -1) { - return null; - } - return this.getInputData(slot, force_update); - }; - - /** - * tells you if there is a connection in one input slot - * @method isInputConnected - * @param {number} slot - * @return {boolean} - */ - LGraphNode.prototype.isInputConnected = function(slot) { - if (!this.inputs) { - return false; - } - return slot < this.inputs.length && this.inputs[slot].link != null; - }; - - /** - * tells you info about an input connection (which node, type, etc) - * @method getInputInfo - * @param {number} slot - * @return {Object} object or null { link: id, name: string, type: string or 0 } - */ - LGraphNode.prototype.getInputInfo = function(slot) { - if (!this.inputs) { - return null; - } - if (slot < this.inputs.length) { - return this.inputs[slot]; - } - return null; - }; - - /** - * Returns the link info in the connection of an input slot - * @method getInputLink - * @param {number} slot - * @return {LLink} object or null - */ - LGraphNode.prototype.getInputLink = function(slot) { - if (!this.inputs) { - return null; - } - if (slot < this.inputs.length) { - var slot_info = this.inputs[slot]; - return this.graph.links[ slot_info.link ]; - } - return null; - }; - - /** - * returns the node connected in the input slot - * @method getInputNode - * @param {number} slot - * @return {LGraphNode} node or null - */ - LGraphNode.prototype.getInputNode = function(slot) { - if (!this.inputs) { - return null; - } - if (slot >= this.inputs.length) { - return null; - } - var input = this.inputs[slot]; - if (!input || input.link === null) { - return null; - } - var link_info = this.graph.links[input.link]; - if (!link_info) { - return null; - } - return this.graph.getNodeById(link_info.origin_id); - }; - - /** - * returns the value of an input with this name, otherwise checks if there is a property with that name - * @method getInputOrProperty - * @param {string} name - * @return {*} value - */ - LGraphNode.prototype.getInputOrProperty = function(name) { - if (!this.inputs || !this.inputs.length) { - return this.properties ? this.properties[name] : null; - } - - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input_info = this.inputs[i]; - if (name == input_info.name && input_info.link != null) { - var link = this.graph.links[input_info.link]; - if (link) { - return link.data; - } - } - } - return this.properties[name]; - }; - - /** - * tells you the last output data that went in that slot - * @method getOutputData - * @param {number} slot - * @return {Object} object or null - */ - LGraphNode.prototype.getOutputData = function(slot) { - if (!this.outputs) { - return null; - } - if (slot >= this.outputs.length) { - return null; - } - - var info = this.outputs[slot]; - return info._data; - }; - - /** - * tells you info about an output connection (which node, type, etc) - * @method getOutputInfo - * @param {number} slot - * @return {Object} object or null { name: string, type: string, links: [ ids of links in number ] } - */ - LGraphNode.prototype.getOutputInfo = function(slot) { - if (!this.outputs) { - return null; - } - if (slot < this.outputs.length) { - return this.outputs[slot]; - } - return null; - }; - - /** - * tells you if there is a connection in one output slot - * @method isOutputConnected - * @param {number} slot - * @return {boolean} - */ - LGraphNode.prototype.isOutputConnected = function(slot) { - if (!this.outputs) { - return false; - } - return ( - slot < this.outputs.length && - this.outputs[slot].links && - this.outputs[slot].links.length - ); - }; - - /** - * tells you if there is any connection in the output slots - * @method isAnyOutputConnected - * @return {boolean} - */ - LGraphNode.prototype.isAnyOutputConnected = function() { - if (!this.outputs) { - return false; - } - for (var i = 0; i < this.outputs.length; ++i) { - if (this.outputs[i].links && this.outputs[i].links.length) { - return true; - } - } - return false; - }; - - /** - * retrieves all the nodes connected to this output slot - * @method getOutputNodes - * @param {number} slot - * @return {array} - */ - LGraphNode.prototype.getOutputNodes = function(slot) { - if (!this.outputs || this.outputs.length == 0) { - return null; - } - - if (slot >= this.outputs.length) { - return null; - } - - var output = this.outputs[slot]; - if (!output.links || output.links.length == 0) { - return null; - } - - var r = []; - for (var i = 0; i < output.links.length; i++) { - var link_id = output.links[i]; - var link = this.graph.links[link_id]; - if (link) { - var target_node = this.graph.getNodeById(link.target_id); - if (target_node) { - r.push(target_node); - } - } - } - return r; - }; - - LGraphNode.prototype.addOnTriggerInput = function(){ - var trigS = this.findInputSlot("onTrigger"); - if (trigS == -1){ //!trigS || - var input = this.addInput("onTrigger", LiteGraph.EVENT, {optional: true, nameLocked: true}); - return this.findInputSlot("onTrigger"); - } - return trigS; - } - - LGraphNode.prototype.addOnExecutedOutput = function(){ - var trigS = this.findOutputSlot("onExecuted"); - if (trigS == -1){ //!trigS || - var output = this.addOutput("onExecuted", LiteGraph.ACTION, {optional: true, nameLocked: true}); - return this.findOutputSlot("onExecuted"); - } - return trigS; - } - - LGraphNode.prototype.onAfterExecuteNode = function(param, options){ - var trigS = this.findOutputSlot("onExecuted"); - if (trigS != -1){ - - //console.debug(this.id+":"+this.order+" triggering slot onAfterExecute"); - //console.debug(param); - //console.debug(options); - this.triggerSlot(trigS, param, null, options); - - } - } - - LGraphNode.prototype.changeMode = function(modeTo){ - switch(modeTo){ - case LiteGraph.ON_EVENT: - // this.addOnExecutedOutput(); - break; - - case LiteGraph.ON_TRIGGER: - this.addOnTriggerInput(); - this.addOnExecutedOutput(); - break; - - case LiteGraph.NEVER: - break; - - case LiteGraph.ALWAYS: - break; - - case LiteGraph.ON_REQUEST: - break; - - default: - return false; - break; - } - this.mode = modeTo; - return true; - }; - - /** - * Triggers the node code execution, place a boolean/counter to mark the node as being executed - * @method execute - * @param {*} param - * @param {*} options - */ - LGraphNode.prototype.doExecute = function(param, options) { - options = options || {}; - if (this.onExecute){ - - // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id+"_exec_"+Math.floor(Math.random()*9999); - - this.graph.nodes_executing[this.id] = true; //.push(this.id); - - this.onExecute(param, options); - - this.graph.nodes_executing[this.id] = false; //.pop(); - - // save execution/action ref - this.exec_version = this.graph.iteration; - if(options && options.action_call){ - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; - } - } - this.execute_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); // callback - }; - - /** - * Triggers an action, wrapped by logics to control execution flow - * @method actionDo - * @param {String} action name - * @param {*} param - */ - LGraphNode.prototype.actionDo = function(action, param, options) { - options = options || {}; - if (this.onAction){ - - // enable this to give the event an ID - if (!options.action_call) options.action_call = this.id+"_"+(action?action:"action")+"_"+Math.floor(Math.random()*9999); - - this.graph.nodes_actioning[this.id] = (action?action:"actioning"); //.push(this.id); - - this.onAction(action, param, options); - - this.graph.nodes_actioning[this.id] = false; //.pop(); - - // save execution/action ref - if(options && options.action_call){ - this.action_call = options.action_call; // if (param) - this.graph.nodes_executedAction[this.id] = options.action_call; - } - } - this.action_triggered = 2; // the nFrames it will be used (-- each step), means "how old" is the event - if(this.onAfterExecuteNode) this.onAfterExecuteNode(param, options); - }; - - /** - * Triggers an event in this node, this will trigger any output with the same name - * @method trigger - * @param {String} event name ( "on_play", ... ) if action is equivalent to false then the event is send to all - * @param {*} param - */ - LGraphNode.prototype.trigger = function(action, param, options) { - if (!this.outputs || !this.outputs.length) { - return; - } - - if (this.graph) - this.graph._last_trigger_time = LiteGraph.getTime(); - - for (var i = 0; i < this.outputs.length; ++i) { - var output = this.outputs[i]; - if ( !output || output.type !== LiteGraph.EVENT || (action && output.name != action) ) - continue; - this.triggerSlot(i, param, null, options); - } - }; - - /** - * Triggers a slot event in this node: cycle output slots and launch execute/action on connected nodes - * @method triggerSlot - * @param {Number} slot the index of the output slot - * @param {*} param - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ - LGraphNode.prototype.triggerSlot = function(slot, param, link_id, options) { - options = options || {}; - if (!this.outputs) { - return; - } - - if(slot == null) - { - console.error("slot must be a number"); - return; - } - - if(slot.constructor !== Number) - console.warn("slot must be a number, use node.trigger('name') if you want to use a string"); - - var output = this.outputs[slot]; - if (!output) { - return; - } - - var links = output.links; - if (!links || !links.length) { - return; - } - - if (this.graph) { - this.graph._last_trigger_time = LiteGraph.getTime(); - } - - //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = LiteGraph.getTime(); - var node = this.graph.getNodeById(link_info.target_id); - if (!node) { - //node not found? - continue; - } - - //used to mark events in graph - var target_connection = node.inputs[link_info.target_slot]; - - if (node.mode === LiteGraph.ON_TRIGGER) - { - // generate unique trigger ID if not present - if (!options.action_call) options.action_call = this.id+"_trigg_"+Math.floor(Math.random()*9999); - if (node.onExecute) { - // -- wrapping node.onExecute(param); -- - node.doExecute(param, options); - } - } - else if (node.onAction) { - // generate unique action ID if not present - if (!options.action_call) options.action_call = this.id+"_act_"+Math.floor(Math.random()*9999); - //pass the action name - var target_connection = node.inputs[link_info.target_slot]; - // wrap node.onAction(target_connection.name, param); - node.actionDo(target_connection.name, param, options); - } - } - }; - - /** - * clears the trigger slot animation - * @method clearTriggeredSlot - * @param {Number} slot the index of the output slot - * @param {Number} link_id [optional] in case you want to trigger and specific output link in a slot - */ - LGraphNode.prototype.clearTriggeredSlot = function(slot, link_id) { - if (!this.outputs) { - return; - } - - var output = this.outputs[slot]; - if (!output) { - return; - } - - var links = output.links; - if (!links || !links.length) { - return; - } - - //for every link attached here - for (var k = 0; k < links.length; ++k) { - var id = links[k]; - if (link_id != null && link_id != id) { - //to skip links - continue; - } - var link_info = this.graph.links[links[k]]; - if (!link_info) { - //not connected - continue; - } - link_info._last_time = 0; - } - }; - - /** - * changes node size and triggers callback - * @method setSize - * @param {vec2} size - */ - LGraphNode.prototype.setSize = function(size) - { - this.size = size; - if(this.onResize) - this.onResize(this.size); - } - - /** - * add a new property to this node - * @method addProperty - * @param {string} name - * @param {*} default_value - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of the property (like values, etc) - */ - LGraphNode.prototype.addProperty = function( - name, - default_value, - type, - extra_info - ) { - var o = { name: name, type: type, default_value: default_value }; - if (extra_info) { - for (var i in extra_info) { - o[i] = extra_info[i]; - } - } - if (!this.properties_info) { - this.properties_info = []; - } - this.properties_info.push(o); - if (!this.properties) { - this.properties = {}; - } - this.properties[name] = default_value; - return o; - }; - - //connections - - /** - * add a new output slot to use in this node - * @method addOutput - * @param {string} name - * @param {string} type string defining the output type ("vec3","number",...) - * @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc) - */ - LGraphNode.prototype.addOutput = function(name, type, extra_info) { - var output = { name: name, type: type, links: null }; - if (extra_info) { - for (var i in extra_info) { - output[i] = extra_info[i]; - } - } - - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(output); - if (this.onOutputAdded) { - this.onOutputAdded(output); - } - - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true); - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - return output; - }; - - /** - * add a new output slot to use in this node - * @method addOutputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - LGraphNode.prototype.addOutputs = function(array) { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; - if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; - } - } - - if (!this.outputs) { - this.outputs = []; - } - this.outputs.push(o); - if (this.onOutputAdded) { - this.onOutputAdded(o); - } - - if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,info[1],true); - - } - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - }; - - /** - * remove an existing output slot - * @method removeOutput - * @param {number} slot - */ - LGraphNode.prototype.removeOutput = function(slot) { - this.disconnectOutput(slot); - this.outputs.splice(slot, 1); - for (var i = slot; i < this.outputs.length; ++i) { - if (!this.outputs[i] || !this.outputs[i].links) { - continue; - } - var links = this.outputs[i].links; - for (var j = 0; j < links.length; ++j) { - var link = this.graph.links[links[j]]; - if (!link) { - continue; - } - link.origin_slot -= 1; - } - } - - this.setSize( this.computeSize() ); - if (this.onOutputRemoved) { - this.onOutputRemoved(slot); - } - this.setDirtyCanvas(true, true); - }; - - /** - * add a new input slot to use in this node - * @method addInput - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...), it its a generic one use 0 - * @param {Object} extra_info this can be used to have special properties of an input (label, color, position, etc) - */ - LGraphNode.prototype.addInput = function(name, type, extra_info) { - type = type || 0; - var input = { name: name, type: type, link: null }; - if (extra_info) { - for (var i in extra_info) { - input[i] = extra_info[i]; - } - } - - if (!this.inputs) { - this.inputs = []; - } - - this.inputs.push(input); - this.setSize( this.computeSize() ); - - if (this.onInputAdded) { - this.onInputAdded(input); - } - - LiteGraph.registerNodeAndSlotType(this,type); - - this.setDirtyCanvas(true, true); - return input; - }; - - /** - * add several new input slots in this node - * @method addInputs - * @param {Array} array of triplets like [[name,type,extra_info],[...]] - */ - LGraphNode.prototype.addInputs = function(array) { - for (var i = 0; i < array.length; ++i) { - var info = array[i]; - var o = { name: info[0], type: info[1], link: null }; - if (array[2]) { - for (var j in info[2]) { - o[j] = info[2][j]; - } - } - - if (!this.inputs) { - this.inputs = []; - } - this.inputs.push(o); - if (this.onInputAdded) { - this.onInputAdded(o); - } - - LiteGraph.registerNodeAndSlotType(this,info[1]); - } - - this.setSize( this.computeSize() ); - this.setDirtyCanvas(true, true); - }; - - /** - * remove an existing input slot - * @method removeInput - * @param {number} slot - */ - LGraphNode.prototype.removeInput = function(slot) { - this.disconnectInput(slot); - var slot_info = this.inputs.splice(slot, 1); - for (var i = slot; i < this.inputs.length; ++i) { - if (!this.inputs[i]) { - continue; - } - var link = this.graph.links[this.inputs[i].link]; - if (!link) { - continue; - } - link.target_slot -= 1; - } - this.setSize( this.computeSize() ); - if (this.onInputRemoved) { - this.onInputRemoved(slot, slot_info[0] ); - } - this.setDirtyCanvas(true, true); - }; - - /** - * add an special connection to this node (used for special kinds of graphs) - * @method addConnection - * @param {string} name - * @param {string} type string defining the input type ("vec3","number",...) - * @param {[x,y]} pos position of the connection inside the node - * @param {string} direction if is input or output - */ - LGraphNode.prototype.addConnection = function(name, type, pos, direction) { - var o = { - name: name, - type: type, - pos: pos, - direction: direction, - links: null - }; - this.connections.push(o); - return o; - }; - - /** - * computes the minimum size of a node according to its inputs and output slots - * @method computeSize - * @param {vec2} minHeight - * @return {vec2} the total size - */ - LGraphNode.prototype.computeSize = function(out) { - if (this.constructor.size) { - return this.constructor.size.concat(); - } - - var rows = Math.max( - this.inputs ? this.inputs.length : 1, - this.outputs ? this.outputs.length : 1 - ); - var size = out || new Float32Array([0, 0]); - rows = Math.max(rows, 1); - var font_size = LiteGraph.NODE_TEXT_SIZE; //although it should be graphcanvas.inner_text_font size - - var title_width = compute_text_size(this.title); - var input_width = 0; - var output_width = 0; - - if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - var text = input.label || input.name || ""; - var text_width = compute_text_size(text); - if (input_width < text_width) { - input_width = text_width; - } - } - } - - if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - var text = output.label || output.name || ""; - var text_width = compute_text_size(text); - if (output_width < text_width) { - output_width = text_width; - } - } - } - - size[0] = Math.max(input_width + output_width + 10, title_width); - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH); - if (this.widgets && this.widgets.length) { - size[0] = Math.max(size[0], LiteGraph.NODE_WIDTH * 1.5); - } - - size[1] = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; - - var widgets_height = 0; - if (this.widgets && this.widgets.length) { - for (var i = 0, l = this.widgets.length; i < l; ++i) { - if (this.widgets[i].computeSize) - widgets_height += this.widgets[i].computeSize(size[0])[1] + 4; - else - widgets_height += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - widgets_height += 8; - } - - //compute height using widgets height - if( this.widgets_up ) - size[1] = Math.max( size[1], widgets_height ); - else if( this.widgets_start_y != null ) - size[1] = Math.max( size[1], widgets_height + this.widgets_start_y ); - else - size[1] += widgets_height; - - function compute_text_size(text) { - if (!text) { - return 0; - } - return font_size * text.length * 0.6; - } - - if ( - this.constructor.min_height && - size[1] < this.constructor.min_height - ) { - size[1] = this.constructor.min_height; - } - - size[1] += 6; //margin - - return size; - }; - - LGraphNode.prototype.inResizeCorner = function(canvasX, canvasY) { - var rows = this.outputs ? this.outputs.length : 1; - var outputs_offset = (this.constructor.slot_start_y || 0) + rows * LiteGraph.NODE_SLOT_HEIGHT; - return isInsideRectangle(canvasX, - canvasY, - this.pos[0] + this.size[0] - 15, - this.pos[1] + Math.max(this.size[1] - 15, outputs_offset), - 20, - 20 - ); - } - - /** - * returns all the info available about a property of this node. - * - * @method getPropertyInfo - * @param {String} property name of the property - * @return {Object} the object with all the available info - */ - LGraphNode.prototype.getPropertyInfo = function( property ) - { - var info = null; - - //there are several ways to define info about a property - //legacy mode - if (this.properties_info) { - for (var i = 0; i < this.properties_info.length; ++i) { - if (this.properties_info[i].name == property) { - info = this.properties_info[i]; - break; - } - } - } - //litescene mode using the constructor - if(this.constructor["@" + property]) - info = this.constructor["@" + property]; - - if(this.constructor.widgets_info && this.constructor.widgets_info[property]) - info = this.constructor.widgets_info[property]; - - //litescene mode using the constructor - if (!info && this.onGetPropertyInfo) { - info = this.onGetPropertyInfo(property); - } - - if (!info) - info = {}; - if(!info.type) - info.type = typeof this.properties[property]; - if(info.widget == "combo") - info.type = "enum"; - - return info; - } - - /** - * Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties - * - * @method addWidget - * @param {String} type the widget type (could be "number","string","combo" - * @param {String} name the text to show on the widget - * @param {String} value the default value - * @param {Function|String} callback function to call when it changes (optionally, it can be the name of the property to modify) - * @param {Object} options the object that contains special properties of this widget - * @return {Object} the created widget object - */ - LGraphNode.prototype.addWidget = function( type, name, value, callback, options ) - { - if (!this.widgets) { - this.widgets = []; - } - - if(!options && callback && callback.constructor === Object) - { - options = callback; - callback = null; - } - - if(options && options.constructor === String) //options can be the property name - options = { property: options }; - - if(callback && callback.constructor === String) //callback can be the property name - { - if(!options) - options = {}; - options.property = callback; - callback = null; - } - - if(callback && callback.constructor !== Function) - { - console.warn("addWidget: callback must be a function"); - callback = null; - } - - var w = { - type: type.toLowerCase(), - name: name, - value: value, - callback: callback, - options: options || {} - }; - - if (w.options.y !== undefined) { - w.y = w.options.y; - } - - if (!callback && !w.options.callback && !w.options.property) { - console.warn("LiteGraph addWidget(...) without a callback or property assigned"); - } - if (type == "combo" && !w.options.values) { - throw "LiteGraph addWidget('combo',...) requires to pass values in options: { values:['red','blue'] }"; - } - this.widgets.push(w); - this.setSize( this.computeSize() ); - return w; - }; - - LGraphNode.prototype.addCustomWidget = function(custom_widget) { - if (!this.widgets) { - this.widgets = []; - } - this.widgets.push(custom_widget); - return custom_widget; - }; - - /** - * returns the bounding of the object, used for rendering purposes - * @method getBounding - * @param out {Float32Array[4]?} [optional] a place to store the output, to free garbage - * @param compute_outer {boolean?} [optional] set to true to include the shadow and connection points in the bounding calculation - * @return {Float32Array[4]} the bounding box in format of [topleft_cornerx, topleft_cornery, width, height] - */ - LGraphNode.prototype.getBounding = function(out, compute_outer) { - out = out || new Float32Array(4); - const nodePos = this.pos; - const isCollapsed = this.flags.collapsed; - const nodeSize = this.size; - - let left_offset = 0; - // 1 offset due to how nodes are rendered - let right_offset = 1 ; - let top_offset = 0; - let bottom_offset = 0; - - if (compute_outer) { - // 4 offset for collapsed node connection points - left_offset = 4; - // 6 offset for right shadow and collapsed node connection points - right_offset = 6 + left_offset; - // 4 offset for collapsed nodes top connection points - top_offset = 4; - // 5 offset for bottom shadow and collapsed node connection points - bottom_offset = 5 + top_offset; - } - - out[0] = nodePos[0] - left_offset; - out[1] = nodePos[1] - LiteGraph.NODE_TITLE_HEIGHT - top_offset; - out[2] = isCollapsed ? - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + right_offset : - nodeSize[0] + right_offset; - out[3] = isCollapsed ? - LiteGraph.NODE_TITLE_HEIGHT + bottom_offset : - nodeSize[1] + LiteGraph.NODE_TITLE_HEIGHT + bottom_offset; - - if (this.onBounding) { - this.onBounding(out); - } - return out; - }; - - /** - * checks if a point is inside the shape of a node - * @method isPointInside - * @param {number} x - * @param {number} y - * @return {boolean} - */ - LGraphNode.prototype.isPointInside = function(x, y, margin, skip_title) { - margin = margin || 0; - - var margin_top = this.graph && this.graph.isLive() ? 0 : LiteGraph.NODE_TITLE_HEIGHT; - if (skip_title) { - margin_top = 0; - } - if (this.flags && this.flags.collapsed) { - //if ( distance([x,y], [this.pos[0] + this.size[0]*0.5, this.pos[1] + this.size[1]*0.5]) < LiteGraph.NODE_COLLAPSED_RADIUS) - if ( - isInsideRectangle( - x, - y, - this.pos[0] - margin, - this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT - margin, - (this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH) + - 2 * margin, - LiteGraph.NODE_TITLE_HEIGHT + 2 * margin - ) - ) { - return true; - } - } else if ( - this.pos[0] - 4 - margin < x && - this.pos[0] + this.size[0] + 4 + margin > x && - this.pos[1] - margin_top - margin < y && - this.pos[1] + this.size[1] + margin > y - ) { - return true; - } - return false; - }; - - /** - * checks if a point is inside a node slot, and returns info about which slot - * @method getSlotInPosition - * @param {number} x - * @param {number} y - * @return {Object} if found the object contains { input|output: slot object, slot: number, link_pos: [x,y] } - */ - LGraphNode.prototype.getSlotInPosition = function(x, y) { - //search for inputs - var link_pos = new Float32Array(2); - if (this.inputs) { - for (var i = 0, l = this.inputs.length; i < l; ++i) { - var input = this.inputs[i]; - this.getConnectionPos(true, i, link_pos); - if ( - isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - ) - ) { - return { input: input, slot: i, link_pos: link_pos }; - } - } - } - - if (this.outputs) { - for (var i = 0, l = this.outputs.length; i < l; ++i) { - var output = this.outputs[i]; - this.getConnectionPos(false, i, link_pos); - if ( - isInsideRectangle( - x, - y, - link_pos[0] - 10, - link_pos[1] - 5, - 20, - 10 - ) - ) { - return { output: output, slot: i, link_pos: link_pos }; - } - } - } - - return null; - }; - - /** - * returns the input slot with a given name (used for dynamic slots), -1 if not found - * @method findInputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findInputSlot = function(name, returnObj) { - if (!this.inputs) { - return -1; - } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (name == this.inputs[i].name) { - return !returnObj ? i : this.inputs[i]; - } - } - return -1; - }; - - /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @method findOutputSlot - * @param {string} name the name of the slot - * @param {boolean} returnObj if the obj itself wanted - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findOutputSlot = function(name, returnObj) { - returnObj = returnObj || false; - if (!this.outputs) { - return -1; - } - for (var i = 0, l = this.outputs.length; i < l; ++i) { - if (name == this.outputs[i].name) { - return !returnObj ? i : this.outputs[i]; - } - } - return -1; - }; - - // TODO refactor: USE SINGLE findInput/findOutput functions! :: merge options - - /** - * returns the first free input slot - * @method findInputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findInputSlotFree = function(optsIn) { - var optsIn = optsIn || {}; - var optsDef = {returnObj: false - ,typesNotAccepted: [] - }; - var opts = Object.assign(optsDef,optsIn); - if (!this.inputs) { - return -1; - } - for (var i = 0, l = this.inputs.length; i < l; ++i) { - if (this.inputs[i].link && this.inputs[i].link != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.inputs[i].type)){ - continue; - } - return !opts.returnObj ? i : this.inputs[i]; - } - return -1; - }; - - /** - * returns the first output slot free - * @method findOutputSlotFree - * @param {object} options - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findOutputSlotFree = function(optsIn) { - var optsIn = optsIn || {}; - var optsDef = { returnObj: false - ,typesNotAccepted: [] - }; - var opts = Object.assign(optsDef,optsIn); - if (!this.outputs) { - return -1; - } - for (var i = 0, l = this.outputs.length; i < l; ++i) { - if (this.outputs[i].links && this.outputs[i].links != null) { - continue; - } - if (opts.typesNotAccepted && opts.typesNotAccepted.includes && opts.typesNotAccepted.includes(this.outputs[i].type)){ - continue; - } - return !opts.returnObj ? i : this.outputs[i]; - } - return -1; - }; - - /** - * findSlotByType for INPUTS - */ - LGraphNode.prototype.findInputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(true, type, returnObj, preferFreeSlot, doNotUseOccupied); - }; - - /** - * findSlotByType for OUTPUTS - */ - LGraphNode.prototype.findOutputSlotByType = function(type, returnObj, preferFreeSlot, doNotUseOccupied) { - return this.findSlotByType(false, type, returnObj, preferFreeSlot, doNotUseOccupied); - }; - - /** - * returns the output (or input) slot with a given type, -1 if not found - * @method findSlotByType - * @param {boolean} input uise inputs instead of outputs - * @param {string} type the type of the slot - * @param {boolean} returnObj if the obj itself wanted - * @param {boolean} preferFreeSlot if we want a free slot (if not found, will return the first of the type anyway) - * @return {number_or_object} the slot (-1 if not found) - */ - LGraphNode.prototype.findSlotByType = function(input, type, returnObj, preferFreeSlot, doNotUseOccupied) { - input = input || false; - returnObj = returnObj || false; - preferFreeSlot = preferFreeSlot || false; - doNotUseOccupied = doNotUseOccupied || false; - var aSlots = input ? this.inputs : this.outputs; - if (!aSlots) { - return -1; - } - // !! empty string type is considered 0, * !! - if (type == "" || type == "*") type = 0; - for (var i = 0, l = aSlots.length; i < l; ++i) { - var tFound = false; - var aSource = (type+"").toLowerCase().split(","); - var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type; - aDest = (aDest+"").toLowerCase().split(","); - for(var sI=0;sI= 0 && target_slot !== null){ - //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot) - return this.connect(slot, target_node, target_slot); - }else{ - //console.log("type "+target_slotType+" not found or not free?") - if (opts.createEventInCase && target_slotType == LiteGraph.EVENT){ - // WILL CREATE THE onTrigger IN SLOT - //console.debug("connect WILL CREATE THE onTrigger "+target_slotType+" to "+target_node); - return this.connect(slot, target_node, -1); - } - // connect to the first general output slot if not found a specific type and - if (opts.generalTypeInCase){ - var target_slot = target_node.findInputSlotByType(0, false, true, true); - //console.debug("connect TO a general type (*, 0), if not found the specific type ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); - if (target_slot >= 0){ - return this.connect(slot, target_node, target_slot); - } - } - // connect to the first free input slot if not found a specific type and this output is general - if (opts.firstFreeIfOutputGeneralInCase && (target_slotType == 0 || target_slotType == "*" || target_slotType == "")){ - var target_slot = target_node.findInputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); - //console.debug("connect TO TheFirstFREE ",target_slotType," to ",target_node,"RES_SLOT:",target_slot); - if (target_slot >= 0){ - return this.connect(slot, target_node, target_slot); - } - } - - console.debug("no way to connect type: ",target_slotType," to targetNODE ",target_node); - //TODO filter - - return null; - } - } - - /** - * connect this node input to the output of another node BY TYPE - * @method connectByType - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {string} target_type the output slot type of the target node - * @return {Object} the link_info is created, otherwise null - */ - LGraphNode.prototype.connectByTypeOutput = function(slot, source_node, source_slotType, optsIn) { - var optsIn = optsIn || {}; - var optsDef = { createEventInCase: true - ,firstFreeIfInputGeneralInCase: true - ,generalTypeInCase: true - }; - var opts = Object.assign(optsDef,optsIn); - if (source_node && source_node.constructor === Number) { - source_node = this.graph.getNodeById(source_node); - } - var source_slot = source_node.findOutputSlotByType(source_slotType, false, true); - if (source_slot >= 0 && source_slot !== null){ - //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot) - return source_node.connect(source_slot, this, slot); - }else{ - - // connect to the first general output slot if not found a specific type and - if (opts.generalTypeInCase){ - var source_slot = source_node.findOutputSlotByType(0, false, true, true); - if (source_slot >= 0){ - return source_node.connect(source_slot, this, slot); - } - } - - if (opts.createEventInCase && source_slotType == LiteGraph.EVENT){ - // WILL CREATE THE onExecuted OUT SLOT - if (LiteGraph.do_add_triggers_slots){ - var source_slot = source_node.addOnExecutedOutput(); - return source_node.connect(source_slot, this, slot); - } - } - // connect to the first free output slot if not found a specific type and this input is general - if (opts.firstFreeIfInputGeneralInCase && (source_slotType == 0 || source_slotType == "*" || source_slotType == "")){ - var source_slot = source_node.findOutputSlotFree({typesNotAccepted: [LiteGraph.EVENT] }); - if (source_slot >= 0){ - return source_node.connect(source_slot, this, slot); - } - } - - console.debug("no way to connect byOUT type: ",source_slotType," to sourceNODE ",source_node); - //TODO filter - - //console.log("type OUT! "+source_slotType+" not found or not free?") - return null; - } - } - - /** - * connect this node output to the input of another node - * @method connect - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} node the target node - * @param {number_or_string} target_slot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) - * @return {Object} the link_info is created, otherwise null - */ - LGraphNode.prototype.connect = function(slot, target_node, target_slot) { - target_slot = target_slot || 0; - - if (!this.graph) { - //could be connected before adding it to a graph - console.log( - "Connect: Error, node doesn't belong to any graph. Nodes must be added first to a graph before connecting them." - ); //due to link ids being associated with graphs - return null; - } - - //seek for the output slot - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return null; - } - } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return null; - } - - if (target_node && target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); - } - if (!target_node) { - throw "target node is null"; - } - - //avoid loopback - if (target_node == this) { - return null; - } - - //you can specify the slot by name - if (target_slot.constructor === String) { - target_slot = target_node.findInputSlot(target_slot); - if (target_slot == -1) { - if (LiteGraph.debug) { - console.log( - "Connect: Error, no slot of name " + target_slot - ); - } - return null; - } - } else if (target_slot === LiteGraph.EVENT) { - - if (LiteGraph.do_add_triggers_slots){ - //search for first slot with event? :: NO this is done outside - //console.log("Connect: Creating triggerEvent"); - // force mode - target_node.changeMode(LiteGraph.ON_TRIGGER); - target_slot = target_node.findInputSlot("onTrigger"); - }else{ - return null; // -- break -- - } - } else if ( - !target_node.inputs || - target_slot >= target_node.inputs.length - ) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return null; - } - - var changed = false; - - var input = target_node.inputs[target_slot]; - var link_info = null; - var output = this.outputs[slot]; - - if (!this.outputs[slot]){ - /*console.debug("Invalid slot passed: "+slot); - console.debug(this.outputs);*/ - return null; - } - - // allow target node to change slot - if (target_node.onBeforeConnectInput) { - // This way node can choose another slot (or make a new one?) - target_slot = target_node.onBeforeConnectInput(target_slot); //callback - } - - //check target_slot and check connection types - if (target_slot===false || target_slot===null || !LiteGraph.isValidConnection(output.type, input.type)) - { - this.setDirtyCanvas(false, true); - if(changed) - this.graph.connectionChange(this, link_info); - return null; - }else{ - //console.debug("valid connection",output.type, input.type); - } - - //allows nodes to block connection, callback - if (target_node.onConnectInput) { - if ( target_node.onConnectInput(target_slot, output.type, output, this, slot) === false ) { - return null; - } - } - if (this.onConnectOutput) { // callback - if ( this.onConnectOutput(slot, input.type, input, target_node, target_slot) === false ) { - return null; - } - } - - //if there is something already plugged there, disconnect - if (target_node.inputs[target_slot] && target_node.inputs[target_slot].link != null) { - this.graph.beforeChange(); - target_node.disconnectInput(target_slot, {doProcessChange: false}); - changed = true; - } - if (output.links !== null && output.links.length){ - switch(output.type){ - case LiteGraph.EVENT: - if (!LiteGraph.allow_multi_output_for_events){ - this.graph.beforeChange(); - this.disconnectOutput(slot, false, {doProcessChange: false}); // Input(target_slot, {doProcessChange: false}); - changed = true; - } - break; - default: - break; - } - } - - var nextId - if (LiteGraph.use_uuids) - nextId = LiteGraph.uuidv4(); - else - nextId = ++this.graph.last_link_id; - - //create link class - link_info = new LLink( - nextId, - input.type || output.type, - this.id, - slot, - target_node.id, - target_slot - ); - - //add to graph links list - this.graph.links[link_info.id] = link_info; - - //connect in output - if (output.links == null) { - output.links = []; - } - output.links.push(link_info.id); - //connect in input - target_node.inputs[target_slot].link = link_info.id; - if (this.graph) { - this.graph._version++; - } - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - true, - link_info, - output - ); - } //link_info has been created now, so its updated - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - target_slot, - true, - link_info, - input - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - target_slot, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot, - target_node, - target_slot - ); - } - - this.setDirtyCanvas(false, true); - this.graph.afterChange(); - this.graph.connectionChange(this, link_info); - - return link_info; - }; - - /** - * disconnect one output to an specific node - * @method disconnectOutput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {LGraphNode} target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] - * @return {boolean} if it was disconnected successfully - */ - LGraphNode.prototype.disconnectOutput = function(slot, target_node) { - if (slot.constructor === String) { - slot = this.findOutputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; - } - } else if (!this.outputs || slot >= this.outputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; - } - - //get output slot - var output = this.outputs[slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } - - //one of the output links in this slot - if (target_node) { - if (target_node.constructor === Number) { - target_node = this.graph.getNodeById(target_node); - } - if (!target_node) { - throw "Target Node not found"; - } - - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; - - //is the link we are searching for... - if (link_info.target_id == target_node.id) { - output.links.splice(i, 1); //remove here - var input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove there - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.graph) { - this.graph._version++; - } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - break; - } - } - } //all the links in this output slot - else { - for (var i = 0, l = output.links.length; i < l; i++) { - var link_id = output.links[i]; - var link_info = this.graph.links[link_id]; - if (!link_info) { - //bug: it happens sometimes - continue; - } - - var target_node = this.graph.getNodeById(link_info.target_id); - var input = null; - if (this.graph) { - this.graph._version++; - } - if (target_node) { - input = target_node.inputs[link_info.target_slot]; - input.link = null; //remove other side link - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.INPUT, - link_info.target_slot, - false, - link_info, - input - ); - } //link_info hasn't been modified so its ok - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - delete this.graph.links[link_id]; //remove the link from the links pool - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.OUTPUT, - slot, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - this, - slot - ); - this.graph.onNodeConnectionChange( - LiteGraph.INPUT, - target_node, - link_info.target_slot - ); - } - } - output.links = null; - } - - this.setDirtyCanvas(false, true); - this.graph.connectionChange(this); - return true; - }; - - /** - * disconnect one input - * @method disconnectInput - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @return {boolean} if it was disconnected successfully - */ - LGraphNode.prototype.disconnectInput = function(slot) { - //seek for the output slot - if (slot.constructor === String) { - slot = this.findInputSlot(slot); - if (slot == -1) { - if (LiteGraph.debug) { - console.log("Connect: Error, no slot of name " + slot); - } - return false; - } - } else if (!this.inputs || slot >= this.inputs.length) { - if (LiteGraph.debug) { - console.log("Connect: Error, slot number not found"); - } - return false; - } - - var input = this.inputs[slot]; - if (!input) { - return false; - } - - var link_id = this.inputs[slot].link; - if(link_id != null) - { - this.inputs[slot].link = null; - - //remove other side - var link_info = this.graph.links[link_id]; - if (link_info) { - var target_node = this.graph.getNodeById(link_info.origin_id); - if (!target_node) { - return false; - } - - var output = target_node.outputs[link_info.origin_slot]; - if (!output || !output.links || output.links.length == 0) { - return false; - } - - //search in the inputs list for this link - for (var i = 0, l = output.links.length; i < l; i++) { - if (output.links[i] == link_id) { - output.links.splice(i, 1); - break; - } - } - - delete this.graph.links[link_id]; //remove from the pool - if (this.graph) { - this.graph._version++; - } - if (this.onConnectionsChange) { - this.onConnectionsChange( - LiteGraph.INPUT, - slot, - false, - link_info, - input - ); - } - if (target_node.onConnectionsChange) { - target_node.onConnectionsChange( - LiteGraph.OUTPUT, - i, - false, - link_info, - output - ); - } - if (this.graph && this.graph.onNodeConnectionChange) { - this.graph.onNodeConnectionChange( - LiteGraph.OUTPUT, - target_node, - i - ); - this.graph.onNodeConnectionChange(LiteGraph.INPUT, this, slot); - } - } - } //link != null - - this.setDirtyCanvas(false, true); - if(this.graph) - this.graph.connectionChange(this); - return true; - }; - - /** - * returns the center of a connection point in canvas coords - * @method getConnectionPos - * @param {boolean} is_input true if if a input slot, false if it is an output - * @param {number_or_string} slot (could be the number of the slot or the string with the name of the slot) - * @param {vec2} out [optional] a place to store the output, to free garbage - * @return {[x,y]} the position - **/ - LGraphNode.prototype.getConnectionPos = function( - is_input, - slot_number, - out - ) { - out = out || new Float32Array(2); - var num_slots = 0; - if (is_input && this.inputs) { - num_slots = this.inputs.length; - } - if (!is_input && this.outputs) { - num_slots = this.outputs.length; - } - - var offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5; - - if (this.flags.collapsed) { - var w = this._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH; - if (this.horizontal) { - out[0] = this.pos[0] + w * 0.5; - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1]; - } - } else { - if (is_input) { - out[0] = this.pos[0]; - } else { - out[0] = this.pos[0] + w; - } - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5; - } - return out; - } - - //weird feature that never got finished - if (is_input && slot_number == -1) { - out[0] = this.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - out[1] = this.pos[1] + LiteGraph.NODE_TITLE_HEIGHT * 0.5; - return out; - } - - //hard-coded pos - if ( - is_input && - num_slots > slot_number && - this.inputs[slot_number].pos - ) { - out[0] = this.pos[0] + this.inputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.inputs[slot_number].pos[1]; - return out; - } else if ( - !is_input && - num_slots > slot_number && - this.outputs[slot_number].pos - ) { - out[0] = this.pos[0] + this.outputs[slot_number].pos[0]; - out[1] = this.pos[1] + this.outputs[slot_number].pos[1]; - return out; - } - - //horizontal distributed slots - if (this.horizontal) { - out[0] = - this.pos[0] + (slot_number + 0.5) * (this.size[0] / num_slots); - if (is_input) { - out[1] = this.pos[1] - LiteGraph.NODE_TITLE_HEIGHT; - } else { - out[1] = this.pos[1] + this.size[1]; - } - return out; - } - - //default vertical slots - if (is_input) { - out[0] = this.pos[0] + offset; - } else { - out[0] = this.pos[0] + this.size[0] + 1 - offset; - } - out[1] = - this.pos[1] + - (slot_number + 0.7) * LiteGraph.NODE_SLOT_HEIGHT + - (this.constructor.slot_start_y || 0); - return out; - }; - - /* Force align to grid */ - LGraphNode.prototype.alignToGrid = function() { - this.pos[0] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[0] / LiteGraph.CANVAS_GRID_SIZE); - this.pos[1] = - LiteGraph.CANVAS_GRID_SIZE * - Math.round(this.pos[1] / LiteGraph.CANVAS_GRID_SIZE); - }; - - /* Console output */ - LGraphNode.prototype.trace = function(msg) { - if (!this.console) { - this.console = []; - } - - this.console.push(msg); - if (this.console.length > LGraphNode.MAX_CONSOLE) { - this.console.shift(); - } - - if(this.graph.onNodeTrace) - this.graph.onNodeTrace(this, msg); - }; - - /* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - LGraphNode.prototype.setDirtyCanvas = function( - dirty_foreground, - dirty_background - ) { - if (!this.graph) { - return; - } - this.graph.sendActionToCanvas("setDirty", [ - dirty_foreground, - dirty_background - ]); - }; - - LGraphNode.prototype.loadImage = function(url) { - var img = new Image(); - img.src = LiteGraph.node_images_path + url; - img.ready = false; - - var that = this; - img.onload = function() { - this.ready = true; - that.setDirtyCanvas(true); - }; - return img; - }; - - //safe LGraphNode action execution (not sure if safe) - /* -LGraphNode.prototype.executeAction = function(action) -{ - if(action == "") return false; - - if( action.indexOf(";") != -1 || action.indexOf("}") != -1) - { - this.trace("Error: Action contains unsafe characters"); - return false; - } - - var tokens = action.split("("); - var func_name = tokens[0]; - if( typeof(this[func_name]) != "function") - { - this.trace("Error: Action not found on node: " + func_name); - return false; - } - - var code = action; - - try - { - var _foo = eval; - eval = null; - (new Function("with(this) { " + code + "}")).call(this); - eval = _foo; - } - catch (err) - { - this.trace("Error executing action {" + action + "} :" + err); - return false; - } - - return true; -} -*/ - - /* Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ - LGraphNode.prototype.captureInput = function(v) { - if (!this.graph || !this.graph.list_of_graphcanvas) { - return; - } - - var list = this.graph.list_of_graphcanvas; - - for (var i = 0; i < list.length; ++i) { - var c = list[i]; - //releasing somebody elses capture?! - if (!v && c.node_capturing_input != this) { - continue; - } - - //change - c.node_capturing_input = v ? this : null; - } - }; - - /** - * Collapse the node to make it smaller on the canvas - * @method collapse - **/ - LGraphNode.prototype.collapse = function(force) { - this.graph._version++; - if (this.constructor.collapsable === false && !force) { - return; - } - if (!this.flags.collapsed) { - this.flags.collapsed = true; - } else { - this.flags.collapsed = false; - } - this.setDirtyCanvas(true, true); - }; - - /** - * Forces the node to do not move or realign on Z - * @method pin - **/ - - LGraphNode.prototype.pin = function(v) { - this.graph._version++; - if (v === undefined) { - this.flags.pinned = !this.flags.pinned; - } else { - this.flags.pinned = v; - } - }; - - LGraphNode.prototype.localToScreen = function(x, y, graphcanvas) { - return [ - (x + this.pos[0]) * graphcanvas.scale + graphcanvas.offset[0], - (y + this.pos[1]) * graphcanvas.scale + graphcanvas.offset[1] - ]; - }; - - function LGraphGroup(title) { - this._ctor(title); - } - - global.LGraphGroup = LiteGraph.LGraphGroup = LGraphGroup; - - LGraphGroup.prototype._ctor = function(title) { - this.title = title || "Group"; - this.font_size = 24; - this.color = LGraphCanvas.node_colors.pale_blue - ? LGraphCanvas.node_colors.pale_blue.groupcolor - : "#AAA"; - this._bounding = new Float32Array([10, 10, 140, 80]); - this._pos = this._bounding.subarray(0, 2); - this._size = this._bounding.subarray(2, 4); - this._nodes = []; - this.graph = null; - - Object.defineProperty(this, "pos", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._pos[0] = v[0]; - this._pos[1] = v[1]; - }, - get: function() { - return this._pos; - }, - enumerable: true - }); - - Object.defineProperty(this, "size", { - set: function(v) { - if (!v || v.length < 2) { - return; - } - this._size[0] = Math.max(140, v[0]); - this._size[1] = Math.max(80, v[1]); - }, - get: function() { - return this._size; - }, - enumerable: true - }); - }; - - LGraphGroup.prototype.configure = function(o) { - this.title = o.title; - this._bounding.set(o.bounding); - this.color = o.color; - if (o.font_size) { - this.font_size = o.font_size; - } - }; - - LGraphGroup.prototype.serialize = function() { - var b = this._bounding; - return { - title: this.title, - bounding: [ - Math.round(b[0]), - Math.round(b[1]), - Math.round(b[2]), - Math.round(b[3]) - ], - color: this.color, - font_size: this.font_size - }; - }; - - LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { - this._pos[0] += deltax; - this._pos[1] += deltay; - if (ignore_nodes) { - return; - } - for (var i = 0; i < this._nodes.length; ++i) { - var node = this._nodes[i]; - node.pos[0] += deltax; - node.pos[1] += deltay; - } - }; - - LGraphGroup.prototype.recomputeInsideNodes = function() { - this._nodes.length = 0; - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); - - for (var i = 0; i < nodes.length; ++i) { - var node = nodes[i]; - node.getBounding(node_bounding); - if (!overlapBounding(this._bounding, node_bounding)) { - continue; - } //out of the visible area - this._nodes.push(node); - } - }; - - LGraphGroup.prototype.isPointInside = LGraphNode.prototype.isPointInside; - LGraphGroup.prototype.setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas; - - //**************************************** - - //Scale and Offset - function DragAndScale(element, skip_events) { - this.offset = new Float32Array([0, 0]); - this.scale = 1; - this.max_scale = 10; - this.min_scale = 0.1; - this.onredraw = null; - this.enabled = true; - this.last_mouse = [0, 0]; - this.element = null; - this.visible_area = new Float32Array(4); - - if (element) { - this.element = element; - if (!skip_events) { - this.bindEvents(element); - } - } - } - - LiteGraph.DragAndScale = DragAndScale; - - DragAndScale.prototype.bindEvents = function(element) { - this.last_mouse = new Float32Array(2); - - this._binded_mouse_callback = this.onMouse.bind(this); - - LiteGraph.pointerListenerAdd(element,"down", this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(element,"move", this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(element,"up", this._binded_mouse_callback); - - element.addEventListener( - "mousewheel", - this._binded_mouse_callback, - false - ); - element.addEventListener("wheel", this._binded_mouse_callback, false); - }; - - DragAndScale.prototype.computeVisibleArea = function( viewport ) { - if (!this.element) { - this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0; - return; - } - var width = this.element.width; - var height = this.element.height; - var startx = -this.offset[0]; - var starty = -this.offset[1]; - if( viewport ) - { - startx += viewport[0] / this.scale; - starty += viewport[1] / this.scale; - width = viewport[2]; - height = viewport[3]; - } - var endx = startx + width / this.scale; - var endy = starty + height / this.scale; - this.visible_area[0] = startx; - this.visible_area[1] = starty; - this.visible_area[2] = endx - startx; - this.visible_area[3] = endy - starty; - }; - - DragAndScale.prototype.onMouse = function(e) { - if (!this.enabled) { - return; - } - - var canvas = this.element; - var rect = canvas.getBoundingClientRect(); - var x = e.clientX - rect.left; - var y = e.clientY - rect.top; - e.canvasx = x; - e.canvasy = y; - e.dragging = this.dragging; - - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - - //console.log("pointerevents: DragAndScale onMouse "+e.type+" "+is_inside); - - var ignore = false; - if (this.onmouse) { - ignore = this.onmouse(e); - } - - if (e.type == LiteGraph.pointerevents_method+"down" && is_inside) { - this.dragging = true; - LiteGraph.pointerListenerRemove(canvas,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(document,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(document,"up",this._binded_mouse_callback); - } else if (e.type == LiteGraph.pointerevents_method+"move") { - if (!ignore) { - var deltax = x - this.last_mouse[0]; - var deltay = y - this.last_mouse[1]; - if (this.dragging) { - this.mouseDrag(deltax, deltay); - } - } - } else if (e.type == LiteGraph.pointerevents_method+"up") { - this.dragging = false; - LiteGraph.pointerListenerRemove(document,"move",this._binded_mouse_callback); - LiteGraph.pointerListenerRemove(document,"up",this._binded_mouse_callback); - LiteGraph.pointerListenerAdd(canvas,"move",this._binded_mouse_callback); - } else if ( is_inside && - (e.type == "mousewheel" || - e.type == "wheel" || - e.type == "DOMMouseScroll") - ) { - e.eventType = "mousewheel"; - if (e.type == "wheel") { - e.wheel = -e.deltaY; - } else { - e.wheel = - e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; - } - - //from stack overflow - e.delta = e.wheelDelta - ? e.wheelDelta / 40 - : e.deltaY - ? -e.deltaY / 3 - : 0; - this.changeDeltaScale(1.0 + e.delta * 0.05); - } - - this.last_mouse[0] = x; - this.last_mouse[1] = y; - - if(is_inside) - { - e.preventDefault(); - e.stopPropagation(); - return false; - } - }; - - DragAndScale.prototype.toCanvasContext = function(ctx) { - ctx.scale(this.scale, this.scale); - ctx.translate(this.offset[0], this.offset[1]); - }; - - DragAndScale.prototype.convertOffsetToCanvas = function(pos) { - //return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]]; - return [ - (pos[0] + this.offset[0]) * this.scale, - (pos[1] + this.offset[1]) * this.scale - ]; - }; - - DragAndScale.prototype.convertCanvasToOffset = function(pos, out) { - out = out || [0, 0]; - out[0] = pos[0] / this.scale - this.offset[0]; - out[1] = pos[1] / this.scale - this.offset[1]; - return out; - }; - - DragAndScale.prototype.mouseDrag = function(x, y) { - this.offset[0] += x / this.scale; - this.offset[1] += y / this.scale; - - if (this.onredraw) { - this.onredraw(this); - } - }; - - DragAndScale.prototype.changeScale = function(value, zooming_center) { - if (value < this.min_scale) { - value = this.min_scale; - } else if (value > this.max_scale) { - value = this.max_scale; - } - - if (value == this.scale) { - return; - } - - if (!this.element) { - return; - } - - var rect = this.element.getBoundingClientRect(); - if (!rect) { - return; - } - - zooming_center = zooming_center || [ - rect.width * 0.5, - rect.height * 0.5 - ]; - var center = this.convertCanvasToOffset(zooming_center); - this.scale = value; - if (Math.abs(this.scale - 1) < 0.01) { - this.scale = 1; - } - - var new_center = this.convertCanvasToOffset(zooming_center); - var delta_offset = [ - new_center[0] - center[0], - new_center[1] - center[1] - ]; - - this.offset[0] += delta_offset[0]; - this.offset[1] += delta_offset[1]; - - if (this.onredraw) { - this.onredraw(this); - } - }; - - DragAndScale.prototype.changeDeltaScale = function(value, zooming_center) { - this.changeScale(this.scale * value, zooming_center); - }; - - DragAndScale.prototype.reset = function() { - this.scale = 1; - this.offset[0] = 0; - this.offset[1] = 0; - }; - - //********************************************************************************* - // LGraphCanvas: LGraph renderer CLASS - //********************************************************************************* - - /** - * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. - * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked - * - * @class LGraphCanvas - * @constructor - * @param {HTMLCanvas} canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) - * @param {LGraph} graph [optional] - * @param {Object} options [optional] { skip_rendering, autoresize, viewport } - */ - function LGraphCanvas(canvas, graph, options) { - this.options = options = options || {}; - - //if(graph === undefined) - // throw ("No graph assigned"); - this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE; - - if (canvas && canvas.constructor === String) { - canvas = document.querySelector(canvas); - } - - this.ds = new DragAndScale(); - this.zoom_modify_alpha = true; //otherwise it generates ugly patterns when scaling down too much - - this.title_text_font = "" + LiteGraph.NODE_TEXT_SIZE + "px Arial"; - this.inner_text_font = - "normal " + LiteGraph.NODE_SUBTEXT_SIZE + "px Arial"; - this.node_title_color = LiteGraph.NODE_TITLE_COLOR; - this.default_link_color = LiteGraph.LINK_COLOR; - this.default_connection_color = { - input_off: "#778", - input_on: "#7F7", //"#BBD" - output_off: "#778", - output_on: "#7F7" //"#BBD" - }; - this.default_connection_color_byType = { - /*number: "#7F7", - string: "#77F", - boolean: "#F77",*/ - } - this.default_connection_color_byTypeOff = { - /*number: "#474", - string: "#447", - boolean: "#744",*/ - }; - - this.highquality_render = true; - this.use_gradients = false; //set to true to render titlebar with gradients - this.editor_alpha = 1; //used for transition - this.pause_rendering = false; - this.clear_background = true; - this.clear_background_color = "#222"; - - this.read_only = false; //if set to true users cannot modify the graph - this.render_only_selected = true; - this.live_mode = false; - this.show_info = true; - this.allow_dragcanvas = true; - this.allow_dragnodes = true; - this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc - this.multi_select = false; //allow selecting multi nodes without pressing extra keys - this.allow_searchbox = true; - this.allow_reconnect_links = true; //allows to change a connection with having to redo it again - this.align_to_grid = false; //snap to grid - - this.drag_mode = false; - this.dragging_rectangle = null; - - this.filter = null; //allows to filter to only accept some type of nodes in a graph - - this.set_canvas_dirty_on_mouse_event = true; //forces to redraw the canvas if the mouse does anything - this.always_render_background = false; - this.render_shadows = true; - this.render_canvas_border = true; - this.render_connections_shadows = false; //too much cpu - this.render_connections_border = true; - this.render_curved_connections = false; - this.render_connection_arrows = false; - this.render_collapsed_slots = true; - this.render_execution_order = false; - this.render_title_colored = true; - this.render_link_tooltip = true; - - this.links_render_mode = LiteGraph.SPLINE_LINK; - - this.mouse = [0, 0]; //mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle - this.graph_mouse = [0, 0]; //mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle - this.canvas_mouse = this.graph_mouse; //LEGACY: REMOVE THIS, USE GRAPH_MOUSE INSTEAD - - //to personalize the search box - this.onSearchBox = null; - this.onSearchBoxSelection = null; - - //callbacks - this.onMouse = null; - this.onDrawBackground = null; //to render background objects (behind nodes and connections) in the canvas affected by transform - this.onDrawForeground = null; //to render foreground objects (above nodes and connections) in the canvas affected by transform - this.onDrawOverlay = null; //to render foreground objects not affected by transform (for GUIs) - this.onDrawLinkTooltip = null; //called when rendering a tooltip - this.onNodeMoved = null; //called after moving a node - this.onSelectionChange = null; //called if the selection changes - this.onConnectingChange = null; //called before any link changes - this.onBeforeChange = null; //called before modifying the graph - this.onAfterChange = null; //called after modifying the graph - - this.connections_width = 3; - this.round_radius = 8; - - this.current_node = null; - this.node_widget = null; //used for widgets - this.over_link_center = null; - this.last_mouse_position = [0, 0]; - this.visible_area = this.ds.visible_area; - this.visible_links = []; - - this.viewport = options.viewport || null; //to constraint render area to a portion of the canvas - - //link canvas and graph - if (graph) { - graph.attachCanvas(this); - } - - this.setCanvas(canvas,options.skip_events); - this.clear(); - - if (!options.skip_render) { - this.startRendering(); - } - - this.autoresize = options.autoresize; - } - - global.LGraphCanvas = LiteGraph.LGraphCanvas = LGraphCanvas; - - LGraphCanvas.DEFAULT_BACKGROUND_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII="; - - LGraphCanvas.link_type_colors = { - "-1": LiteGraph.EVENT_LINK_COLOR, - number: "#AAA", - node: "#DCA" - }; - LGraphCanvas.gradients = {}; //cache of gradients - - /** - * clears all the data inside - * - * @method clear - */ - LGraphCanvas.prototype.clear = function() { - this.frame = 0; - this.last_draw_time = 0; - this.render_time = 0; - this.fps = 0; - - //this.scale = 1; - //this.offset = [0,0]; - - this.dragging_rectangle = null; - - this.selected_nodes = {}; - this.selected_group = null; - - this.visible_nodes = []; - this.node_dragged = null; - this.node_over = null; - this.node_capturing_input = null; - this.connecting_node = null; - this.highlighted_links = {}; - - this.dragging_canvas = false; - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.dirty_area = null; - - this.node_in_panel = null; - this.node_widget = null; - - this.last_mouse = [0, 0]; - this.last_mouseclick = 0; - this.pointer_is_down = false; - this.pointer_is_double = false; - this.visible_area.set([0, 0, 0, 0]); - - if (this.onClear) { - this.onClear(); - } - }; - - /** - * assigns a graph, you can reassign graphs to the same canvas - * - * @method setGraph - * @param {LGraph} graph - */ - LGraphCanvas.prototype.setGraph = function(graph, skip_clear) { - if (this.graph == graph) { - return; - } - - if (!skip_clear) { - this.clear(); - } - - if (!graph && this.graph) { - this.graph.detachCanvas(this); - return; - } - - graph.attachCanvas(this); - - //remove the graph stack in case a subgraph was open - if (this._graph_stack) - this._graph_stack = null; - - this.setDirty(true, true); - }; - - /** - * returns the top level graph (in case there are subgraphs open on the canvas) - * - * @method getTopGraph - * @return {LGraph} graph - */ - LGraphCanvas.prototype.getTopGraph = function() - { - if(this._graph_stack.length) - return this._graph_stack[0]; - return this.graph; - } - - /** - * opens a graph contained inside a node in the current graph - * - * @method openSubgraph - * @param {LGraph} graph - */ - LGraphCanvas.prototype.openSubgraph = function(graph) { - if (!graph) { - throw "graph cannot be null"; - } - - if (this.graph == graph) { - throw "graph cannot be the same"; - } - - this.clear(); - - if (this.graph) { - if (!this._graph_stack) { - this._graph_stack = []; - } - this._graph_stack.push(this.graph); - } - - graph.attachCanvas(this); - this.checkPanels(); - this.setDirty(true, true); - }; - - /** - * closes a subgraph contained inside a node - * - * @method closeSubgraph - * @param {LGraph} assigns a graph - */ - LGraphCanvas.prototype.closeSubgraph = function() { - if (!this._graph_stack || this._graph_stack.length == 0) { - return; - } - var subgraph_node = this.graph._subgraph_node; - var graph = this._graph_stack.pop(); - this.selected_nodes = {}; - this.highlighted_links = {}; - graph.attachCanvas(this); - this.setDirty(true, true); - if (subgraph_node) { - this.centerOnNode(subgraph_node); - this.selectNodes([subgraph_node]); - } - // when close sub graph back to offset [0, 0] scale 1 - this.ds.offset = [0, 0] - this.ds.scale = 1 - }; - - /** - * returns the visually active graph (in case there are more in the stack) - * @method getCurrentGraph - * @return {LGraph} the active graph - */ - LGraphCanvas.prototype.getCurrentGraph = function() { - return this.graph; - }; - - /** - * assigns a canvas - * - * @method setCanvas - * @param {Canvas} assigns a canvas (also accepts the ID of the element (not a selector) - */ - LGraphCanvas.prototype.setCanvas = function(canvas, skip_events) { - var that = this; - - if (canvas) { - if (canvas.constructor === String) { - canvas = document.getElementById(canvas); - if (!canvas) { - throw "Error creating LiteGraph canvas: Canvas not found"; - } - } - } - - if (canvas === this.canvas) { - return; - } - - if (!canvas && this.canvas) { - //maybe detach events from old_canvas - if (!skip_events) { - this.unbindEvents(); - } - } - - this.canvas = canvas; - this.ds.element = canvas; - - if (!canvas) { - return; - } - - //this.canvas.tabindex = "1000"; - canvas.className += " lgraphcanvas"; - canvas.data = this; - canvas.tabindex = "1"; //to allow key events - - //bg canvas: used for non changing stuff - this.bgcanvas = null; - if (!this.bgcanvas) { - this.bgcanvas = document.createElement("canvas"); - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - } - - if (canvas.getContext == null) { - if (canvas.localName != "canvas") { - throw "Element supplied for LGraphCanvas must be a element, you passed a " + - canvas.localName; - } - throw "This browser doesn't support Canvas"; - } - - var ctx = (this.ctx = canvas.getContext("2d")); - if (ctx == null) { - if (!canvas.webgl_enabled) { - console.warn( - "This canvas seems to be WebGL, enabling WebGL renderer" - ); - } - this.enableWebGL(); - } - - //input: (move and up could be unbinded) - // why here? this._mousemove_callback = this.processMouseMove.bind(this); - // why here? this._mouseup_callback = this.processMouseUp.bind(this); - - if (!skip_events) { - this.bindEvents(); - } - }; - - //used in some events to capture them - LGraphCanvas.prototype._doNothing = function doNothing(e) { - //console.log("pointerevents: _doNothing "+e.type); - e.preventDefault(); - return false; - }; - LGraphCanvas.prototype._doReturnTrue = function doNothing(e) { - e.preventDefault(); - return true; - }; - - /** - * binds mouse, keyboard, touch and drag events to the canvas - * @method bindEvents - **/ - LGraphCanvas.prototype.bindEvents = function() { - if (this._events_binded) { - console.warn("LGraphCanvas: events already binded"); - return; - } - - //console.log("pointerevents: bindEvents"); - - var canvas = this.canvas; - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; //hack used when moving canvas between windows - - this._mousedown_callback = this.processMouseDown.bind(this); - this._mousewheel_callback = this.processMouseWheel.bind(this); - // why mousemove and mouseup were not binded here? - this._mousemove_callback = this.processMouseMove.bind(this); - this._mouseup_callback = this.processMouseUp.bind(this); - - //touch events -- TODO IMPLEMENT - //this._touch_callback = this.touchHandler.bind(this); - - LiteGraph.pointerListenerAdd(canvas,"down", this._mousedown_callback, true); //down do not need to store the binded - canvas.addEventListener("mousewheel", this._mousewheel_callback, false); - - LiteGraph.pointerListenerAdd(canvas,"up", this._mouseup_callback, true); // CHECK: ??? binded or not - LiteGraph.pointerListenerAdd(canvas,"move", this._mousemove_callback); - - canvas.addEventListener("contextmenu", this._doNothing); - canvas.addEventListener( - "DOMMouseScroll", - this._mousewheel_callback, - false - ); - - //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents - /*if( 'touchstart' in document.documentElement ) - { - canvas.addEventListener("touchstart", this._touch_callback, true); - canvas.addEventListener("touchmove", this._touch_callback, true); - canvas.addEventListener("touchend", this._touch_callback, true); - canvas.addEventListener("touchcancel", this._touch_callback, true); - }*/ - - //Keyboard ****************** - this._key_callback = this.processKey.bind(this); - - canvas.addEventListener("keydown", this._key_callback, true); - document.addEventListener("keyup", this._key_callback, true); //in document, otherwise it doesn't fire keyup - - //Dropping Stuff over nodes ************************************ - this._ondrop_callback = this.processDrop.bind(this); - - canvas.addEventListener("dragover", this._doNothing, false); - canvas.addEventListener("dragend", this._doNothing, false); - canvas.addEventListener("drop", this._ondrop_callback, false); - canvas.addEventListener("dragenter", this._doReturnTrue, false); - - this._events_binded = true; - }; - - /** - * unbinds mouse events from the canvas - * @method unbindEvents - **/ - LGraphCanvas.prototype.unbindEvents = function() { - if (!this._events_binded) { - console.warn("LGraphCanvas: no events binded"); - return; - } - - //console.log("pointerevents: unbindEvents"); - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; - - LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousedown_callback); - LiteGraph.pointerListenerRemove(this.canvas,"up", this._mousedown_callback); - LiteGraph.pointerListenerRemove(this.canvas,"down", this._mousedown_callback); - this.canvas.removeEventListener( - "mousewheel", - this._mousewheel_callback - ); - this.canvas.removeEventListener( - "DOMMouseScroll", - this._mousewheel_callback - ); - this.canvas.removeEventListener("keydown", this._key_callback); - document.removeEventListener("keyup", this._key_callback); - this.canvas.removeEventListener("contextmenu", this._doNothing); - this.canvas.removeEventListener("drop", this._ondrop_callback); - this.canvas.removeEventListener("dragenter", this._doReturnTrue); - - //touch events -- THIS WAY DOES NOT WORK, finish implementing pointerevents, than clean the touchevents - /*this.canvas.removeEventListener("touchstart", this._touch_callback ); - this.canvas.removeEventListener("touchmove", this._touch_callback ); - this.canvas.removeEventListener("touchend", this._touch_callback ); - this.canvas.removeEventListener("touchcancel", this._touch_callback );*/ - - this._mousedown_callback = null; - this._mousewheel_callback = null; - this._key_callback = null; - this._ondrop_callback = null; - - this._events_binded = false; - }; - - LGraphCanvas.getFileExtension = function(url) { - var question = url.indexOf("?"); - if (question != -1) { - url = url.substr(0, question); - } - var point = url.lastIndexOf("."); - if (point == -1) { - return ""; - } - return url.substr(point + 1).toLowerCase(); - }; - - /** - * this function allows to render the canvas using WebGL instead of Canvas2D - * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL - * @method enableWebGL - **/ - LGraphCanvas.prototype.enableWebGL = function() { - if (typeof GL === "undefined") { - throw "litegl.js must be included to use a WebGL canvas"; - } - if (typeof enableWebGLCanvas === "undefined") { - throw "webglCanvas.js must be included to use this feature"; - } - - this.gl = this.ctx = enableWebGLCanvas(this.canvas); - this.ctx.webgl = true; - this.bgcanvas = this.canvas; - this.bgctx = this.gl; - this.canvas.webgl_enabled = true; - - /* - GL.create({ canvas: this.bgcanvas }); - this.bgctx = enableWebGLCanvas( this.bgcanvas ); - window.gl = this.gl; - */ - }; - - /** - * marks as dirty the canvas, this way it will be rendered again - * - * @class LGraphCanvas - * @method setDirty - * @param {bool} fgcanvas if the foreground canvas is dirty (the one containing the nodes) - * @param {bool} bgcanvas if the background canvas is dirty (the one containing the wires) - */ - LGraphCanvas.prototype.setDirty = function(fgcanvas, bgcanvas) { - if (fgcanvas) { - this.dirty_canvas = true; - } - if (bgcanvas) { - this.dirty_bgcanvas = true; - } - }; - - /** - * Used to attach the canvas in a popup - * - * @method getCanvasWindow - * @return {window} returns the window where the canvas is attached (the DOM root node) - */ - LGraphCanvas.prototype.getCanvasWindow = function() { - if (!this.canvas) { - return window; - } - var doc = this.canvas.ownerDocument; - return doc.defaultView || doc.parentWindow; - }; - - /** - * starts rendering the content of the canvas when needed - * - * @method startRendering - */ - LGraphCanvas.prototype.startRendering = function() { - if (this.is_rendering) { - return; - } //already rendering - - this.is_rendering = true; - renderFrame.call(this); - - function renderFrame() { - if (!this.pause_rendering) { - this.draw(); - } - - var window = this.getCanvasWindow(); - if (this.is_rendering) { - window.requestAnimationFrame(renderFrame.bind(this)); - } - } - }; - - /** - * stops rendering the content of the canvas (to save resources) - * - * @method stopRendering - */ - LGraphCanvas.prototype.stopRendering = function() { - this.is_rendering = false; - /* - if(this.rendering_timer_id) - { - clearInterval(this.rendering_timer_id); - this.rendering_timer_id = null; - } - */ - }; - - /* LiteGraphCanvas input */ - - //used to block future mouse events (because of im gui) - LGraphCanvas.prototype.blockClick = function() - { - this.block_click = true; - this.last_mouseclick = 0; - } - - LGraphCanvas.prototype.processMouseDown = function(e) { - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; - - if (!this.graph) { - return; - } - - this.adjustMouseEvent(e); - - var ref_window = this.getCanvasWindow(); - var document = ref_window.document; - LGraphCanvas.active_canvas = this; - var that = this; - - var x = e.clientX; - var y = e.clientY; - //console.log(y,this.viewport); - //console.log("pointerevents: processMouseDown pointerId:"+e.pointerId+" which:"+e.which+" isPrimary:"+e.isPrimary+" :: x y "+x+" "+y); - - this.ds.viewport = this.viewport; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - - //move mouse move event to the window in case it drags outside of the canvas - if(!this.options.skip_events) - { - LiteGraph.pointerListenerRemove(this.canvas,"move", this._mousemove_callback); - LiteGraph.pointerListenerAdd(ref_window.document,"move", this._mousemove_callback,true); //catch for the entire window - LiteGraph.pointerListenerAdd(ref_window.document,"up", this._mouseup_callback,true); - } - - if(!is_inside){ - return; - } - - var node = this.graph.getNodeOnPos( e.canvasX, e.canvasY, this.visible_nodes, 5 ); - var skip_dragging = false; - var skip_action = false; - var now = LiteGraph.getTime(); - var is_primary = (e.isPrimary === undefined || !e.isPrimary); - var is_double_click = (now - this.last_mouseclick < 300); - this.mouse[0] = e.clientX; - this.mouse[1] = e.clientY; - this.graph_mouse[0] = e.canvasX; - this.graph_mouse[1] = e.canvasY; - this.last_click_position = [this.mouse[0],this.mouse[1]]; - - if (this.pointer_is_down && is_primary ){ - this.pointer_is_double = true; - //console.log("pointerevents: pointer_is_double start"); - }else{ - this.pointer_is_double = false; - } - this.pointer_is_down = true; - - - this.canvas.focus(); - - LiteGraph.closeAllContextMenus(ref_window); - - if (this.onMouse) - { - if (this.onMouse(e) == true) - return; - } - - //left button mouse / single finger - if (e.which == 1 && !this.pointer_is_double) - { - if (e.ctrlKey) - { - this.dragging_rectangle = new Float32Array(4); - this.dragging_rectangle[0] = e.canvasX; - this.dragging_rectangle[1] = e.canvasY; - this.dragging_rectangle[2] = 1; - this.dragging_rectangle[3] = 1; - skip_action = true; - } - - // clone node ALT dragging - if (LiteGraph.alt_drag_do_clone_nodes && e.altKey && node && this.allow_interaction && !skip_action && !this.read_only) - { - if (cloned = node.clone()){ - cloned.pos[0] += 5; - cloned.pos[1] += 5; - this.graph.add(cloned,false,{doCalcSize: false}); - node = cloned; - skip_action = true; - if (!block_drag_node) { - if (this.allow_dragnodes) { - this.graph.beforeChange(); - this.node_dragged = node; - } - if (!this.selected_nodes[node.id]) { - this.processNodeSelected(node, e); - } - } - } - } - - var clicking_canvas_bg = false; - - //when clicked on top of a node - //and it is not interactive - if (node && (this.allow_interaction || node.flags.allow_interaction) && !skip_action && !this.read_only) { - if (!this.live_mode && !node.flags.pinned) { - this.bringToFront(node); - } //if it wasn't selected? - - //not dragging mouse to connect two slots - if ( this.allow_interaction && !this.connecting_node && !node.flags.collapsed && !this.live_mode ) { - //Search for corner for resize - if ( !skip_action && - node.resizable !== false && node.inResizeCorner(e.canvasX, e.canvasY) - ) { - this.graph.beforeChange(); - this.resizing_node = node; - this.canvas.style.cursor = "se-resize"; - skip_action = true; - } else { - //search for outputs - if (node.outputs) { - for ( var i = 0, l = node.outputs.length; i < l; ++i ) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - if ( - isInsideRectangle( - e.canvasX, - e.canvasY, - link_pos[0] - 15, - link_pos[1] - 10, - 30, - 20 - ) - ) { - this.connecting_node = node; - this.connecting_output = output; - this.connecting_output.slot_index = i; - this.connecting_pos = node.getConnectionPos( false, i ); - this.connecting_slot = i; - - if (LiteGraph.shift_click_do_break_link_from){ - if (e.shiftKey) { - node.disconnectOutput(i); - } - } - - if (is_double_click) { - if (node.onOutputDblClick) { - node.onOutputDblClick(i, e); - } - } else { - if (node.onOutputClick) { - node.onOutputClick(i, e); - } - } - - skip_action = true; - break; - } - } - } - - //search for inputs - if (node.inputs) { - for ( var i = 0, l = node.inputs.length; i < l; ++i ) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - if ( - isInsideRectangle( - e.canvasX, - e.canvasY, - link_pos[0] - 15, - link_pos[1] - 10, - 30, - 20 - ) - ) { - if (is_double_click) { - if (node.onInputDblClick) { - node.onInputDblClick(i, e); - } - } else { - if (node.onInputClick) { - node.onInputClick(i, e); - } - } - - if (input.link !== null) { - var link_info = this.graph.links[ - input.link - ]; //before disconnecting - if (LiteGraph.click_do_break_link_to){ - node.disconnectInput(i); - this.dirty_bgcanvas = true; - skip_action = true; - }else{ - // do same action as has not node ? - } - - if ( - this.allow_reconnect_links || - //this.move_destination_link_without_shift || - e.shiftKey - ) { - if (!LiteGraph.click_do_break_link_to){ - node.disconnectInput(i); - } - this.connecting_node = this.graph._nodes_by_id[ - link_info.origin_id - ]; - this.connecting_slot = - link_info.origin_slot; - this.connecting_output = this.connecting_node.outputs[ - this.connecting_slot - ]; - this.connecting_pos = this.connecting_node.getConnectionPos( false, this.connecting_slot ); - - this.dirty_bgcanvas = true; - skip_action = true; - } - - - }else{ - // has not node - } - - if (!skip_action){ - // connect from in to out, from to to from - this.connecting_node = node; - this.connecting_input = input; - this.connecting_input.slot_index = i; - this.connecting_pos = node.getConnectionPos( true, i ); - this.connecting_slot = i; - - this.dirty_bgcanvas = true; - skip_action = true; - } - } - } - } - } //not resizing - } - - //it wasn't clicked on the links boxes - if (!skip_action) { - var block_drag_node = false; - if(node && node.flags && node.flags.pinned) { - block_drag_node = true; - } - var pos = [e.canvasX - node.pos[0], e.canvasY - node.pos[1]]; - - //widgets - var widget = this.processNodeWidgets( node, this.graph_mouse, e ); - if (widget) { - block_drag_node = true; - this.node_widget = [node, widget]; - } - - //double clicking - if (this.allow_interaction && is_double_click && this.selected_nodes[node.id]) { - //double click node - if (node.onDblClick) { - node.onDblClick( e, pos, this ); - } - this.processNodeDblClicked(node); - block_drag_node = true; - } - - //if do not capture mouse - if ( node.onMouseDown && node.onMouseDown( e, pos, this ) ) { - block_drag_node = true; - } else { - //open subgraph button - if(node.subgraph && !node.skip_subgraph_button) - { - if ( !node.flags.collapsed && pos[0] > node.size[0] - LiteGraph.NODE_TITLE_HEIGHT && pos[1] < 0 ) { - var that = this; - setTimeout(function() { - that.openSubgraph(node.subgraph); - }, 10); - } - } - - if (this.live_mode) { - clicking_canvas_bg = true; - block_drag_node = true; - } - } - - if (!block_drag_node) { - if (this.allow_dragnodes) { - this.graph.beforeChange(); - this.node_dragged = node; - } - this.processNodeSelected(node, e); - } else { // double-click - /** - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while its panel is open. - */ - if (!node.is_selected) this.processNodeSelected(node, e); - } - - this.dirty_canvas = true; - } - } //clicked outside of nodes - else { - if (!skip_action){ - //search for link connector - if(!this.read_only) { - for (var i = 0; i < this.visible_links.length; ++i) { - var link = this.visible_links[i]; - var center = link._pos; - if ( - !center || - e.canvasX < center[0] - 4 || - e.canvasX > center[0] + 4 || - e.canvasY < center[1] - 4 || - e.canvasY > center[1] + 4 - ) { - continue; - } - //link clicked - this.showLinkMenu(link, e); - this.over_link_center = null; //clear tooltip - break; - } - } - - this.selected_group = this.graph.getGroupOnPos( e.canvasX, e.canvasY ); - this.selected_group_resizing = false; - if (this.selected_group && !this.read_only ) { - if (e.ctrlKey) { - this.dragging_rectangle = null; - } - - var dist = distance( [e.canvasX, e.canvasY], [ this.selected_group.pos[0] + this.selected_group.size[0], this.selected_group.pos[1] + this.selected_group.size[1] ] ); - if (dist * this.ds.scale < 10) { - this.selected_group_resizing = true; - } else { - this.selected_group.recomputeInsideNodes(); - } - } - - if (is_double_click && !this.read_only && this.allow_searchbox) { - this.showSearchBox(e); - e.preventDefault(); - e.stopPropagation(); - } - - clicking_canvas_bg = true; - } - } - - if (!skip_action && clicking_canvas_bg && this.allow_dragcanvas) { - //console.log("pointerevents: dragging_canvas start"); - this.dragging_canvas = true; - } - - } else if (e.which == 2) { - //middle button - - if (LiteGraph.middle_click_slot_add_default_node){ - if (node && this.allow_interaction && !skip_action && !this.read_only){ - //not dragging mouse to connect two slots - if ( - !this.connecting_node && - !node.flags.collapsed && - !this.live_mode - ) { - var mClikSlot = false; - var mClikSlot_index = false; - var mClikSlot_isOut = false; - //search for outputs - if (node.outputs) { - for ( var i = 0, l = node.outputs.length; i < l; ++i ) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) { - mClikSlot = output; - mClikSlot_index = i; - mClikSlot_isOut = true; - break; - } - } - } - - //search for inputs - if (node.inputs) { - for ( var i = 0, l = node.inputs.length; i < l; ++i ) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - if (isInsideRectangle(e.canvasX,e.canvasY,link_pos[0] - 15,link_pos[1] - 10,30,20)) { - mClikSlot = input; - mClikSlot_index = i; - mClikSlot_isOut = false; - break; - } - } - } - //console.log("middleClickSlots? "+mClikSlot+" & "+(mClikSlot_index!==false)); - if (mClikSlot && mClikSlot_index!==false){ - - var alphaPosY = 0.5-((mClikSlot_index+1)/((mClikSlot_isOut?node.outputs.length:node.inputs.length))); - var node_bounding = node.getBounding(); - // estimate a position: this is a bad semi-bad-working mess .. REFACTOR with a correct autoplacement that knows about the others slots and nodes - var posRef = [ (!mClikSlot_isOut?node_bounding[0]:node_bounding[0]+node_bounding[2])// + node_bounding[0]/this.canvas.width*150 - ,e.canvasY-80// + node_bounding[0]/this.canvas.width*66 // vertical "derive" - ]; - var nodeCreated = this.createDefaultNodeForSlot({ nodeFrom: !mClikSlot_isOut?null:node - ,slotFrom: !mClikSlot_isOut?null:mClikSlot_index - ,nodeTo: !mClikSlot_isOut?node:null - ,slotTo: !mClikSlot_isOut?mClikSlot_index:null - ,position: posRef //,e: e - ,nodeType: "AUTO" //nodeNewType - ,posAdd:[!mClikSlot_isOut?-30:30, -alphaPosY*130] //-alphaPosY*30] - ,posSizeFix:[!mClikSlot_isOut?-1:0, 0] //-alphaPosY*2*/ - }); - skip_action = true; - } - } - } - } - - if (!skip_action && this.allow_dragcanvas) { - //console.log("pointerevents: dragging_canvas start from middle button"); - this.dragging_canvas = true; - } - - - } else if (e.which == 3 || this.pointer_is_double) { - - //right button - if (this.allow_interaction && !skip_action && !this.read_only){ - - // is it hover a node ? - if (node){ - if(Object.keys(this.selected_nodes).length - && (this.selected_nodes[node.id] || e.shiftKey || e.ctrlKey || e.metaKey) - ){ - // is multiselected or using shift to include the now node - if (!this.selected_nodes[node.id]) this.selectNodes([node],true); // add this if not present - }else{ - // update selection - this.selectNodes([node]); - } - } - - // show menu on this node - this.processContextMenu(node, e); - } - - } - - //TODO - //if(this.node_selected != prev_selected) - // this.onNodeSelectionChange(this.node_selected); - - this.last_mouse[0] = e.clientX; - this.last_mouse[1] = e.clientY; - this.last_mouseclick = LiteGraph.getTime(); - this.last_mouse_dragging = true; - - /* - if( (this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) - this.draw(); - */ - - this.graph.change(); - - //this is to ensure to defocus(blur) if a text input element is on focus - if ( - !ref_window.document.activeElement || - (ref_window.document.activeElement.nodeName.toLowerCase() != - "input" && - ref_window.document.activeElement.nodeName.toLowerCase() != - "textarea") - ) { - e.preventDefault(); - } - e.stopPropagation(); - - if (this.onMouseDown) { - this.onMouseDown(e); - } - - return false; - }; - - /** - * Called when a mouse move event has to be processed - * @method processMouseMove - **/ - LGraphCanvas.prototype.processMouseMove = function(e) { - if (this.autoresize) { - this.resize(); - } - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; - - if (!this.graph) { - return; - } - - LGraphCanvas.active_canvas = this; - this.adjustMouseEvent(e); - var mouse = [e.clientX, e.clientY]; - this.mouse[0] = mouse[0]; - this.mouse[1] = mouse[1]; - var delta = [ - mouse[0] - this.last_mouse[0], - mouse[1] - this.last_mouse[1] - ]; - this.last_mouse = mouse; - this.graph_mouse[0] = e.canvasX; - this.graph_mouse[1] = e.canvasY; - - //console.log("pointerevents: processMouseMove "+e.pointerId+" "+e.isPrimary); - - if(this.block_click) - { - //console.log("pointerevents: processMouseMove block_click"); - e.preventDefault(); - return false; - } - - e.dragging = this.last_mouse_dragging; - - if (this.node_widget) { - this.processNodeWidgets( - this.node_widget[0], - this.graph_mouse, - e, - this.node_widget[1] - ); - this.dirty_canvas = true; - } - - //get node over - var node = this.graph.getNodeOnPos(e.canvasX,e.canvasY,this.visible_nodes); - - if (this.dragging_rectangle) - { - this.dragging_rectangle[2] = e.canvasX - this.dragging_rectangle[0]; - this.dragging_rectangle[3] = e.canvasY - this.dragging_rectangle[1]; - this.dirty_canvas = true; - } - else if (this.selected_group && !this.read_only) - { - //moving/resizing a group - if (this.selected_group_resizing) { - this.selected_group.size = [ - e.canvasX - this.selected_group.pos[0], - e.canvasY - this.selected_group.pos[1] - ]; - } else { - var deltax = delta[0] / this.ds.scale; - var deltay = delta[1] / this.ds.scale; - this.selected_group.move(deltax, deltay, e.ctrlKey); - if (this.selected_group._nodes.length) { - this.dirty_canvas = true; - } - } - this.dirty_bgcanvas = true; - } else if (this.dragging_canvas) { - ////console.log("pointerevents: processMouseMove is dragging_canvas"); - this.ds.offset[0] += delta[0] / this.ds.scale; - this.ds.offset[1] += delta[1] / this.ds.scale; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } else if ((this.allow_interaction || (node && node.flags.allow_interaction)) && !this.read_only) { - if (this.connecting_node) { - this.dirty_canvas = true; - } - - //remove mouseover flag - for (var i = 0, l = this.graph._nodes.length; i < l; ++i) { - if (this.graph._nodes[i].mouseOver && node != this.graph._nodes[i] ) { - //mouse leave - this.graph._nodes[i].mouseOver = false; - if (this.node_over && this.node_over.onMouseLeave) { - this.node_over.onMouseLeave(e); - } - this.node_over = null; - this.dirty_canvas = true; - } - } - - //mouse over a node - if (node) { - - if(node.redraw_on_mouse) - this.dirty_canvas = true; - - //this.canvas.style.cursor = "move"; - if (!node.mouseOver) { - //mouse enter - node.mouseOver = true; - this.node_over = node; - this.dirty_canvas = true; - - if (node.onMouseEnter) { - node.onMouseEnter(e); - } - } - - //in case the node wants to do something - if (node.onMouseMove) { - node.onMouseMove( e, [e.canvasX - node.pos[0], e.canvasY - node.pos[1]], this ); - } - - //if dragging a link - if (this.connecting_node) { - - if (this.connecting_output){ - - var pos = this._highlight_input || [0, 0]; //to store the output of isOverNodeInput - - //on top of input - if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { - //mouse on top of the corner box, don't know what to do - } else { - //check if I have a slot below de mouse - var slot = this.isOverNodeInput( node, e.canvasX, e.canvasY, pos ); - if (slot != -1 && node.inputs[slot]) { - var slot_type = node.inputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_output.type, slot_type ) ) { - this._highlight_input = pos; - this._highlight_input_slot = node.inputs[slot]; // XXX CHECK THIS - } - } else { - this._highlight_input = null; - this._highlight_input_slot = null; // XXX CHECK THIS - } - } - - }else if(this.connecting_input){ - - var pos = this._highlight_output || [0, 0]; //to store the output of isOverNodeOutput - - //on top of output - if (this.isOverNodeBox(node, e.canvasX, e.canvasY)) { - //mouse on top of the corner box, don't know what to do - } else { - //check if I have a slot below de mouse - var slot = this.isOverNodeOutput( node, e.canvasX, e.canvasY, pos ); - if (slot != -1 && node.outputs[slot]) { - var slot_type = node.outputs[slot].type; - if ( LiteGraph.isValidConnection( this.connecting_input.type, slot_type ) ) { - this._highlight_output = pos; - } - } else { - this._highlight_output = null; - } - } - } - } - - //Search for corner - if (this.canvas) { - if (node.inResizeCorner(e.canvasX, e.canvasY)) { - this.canvas.style.cursor = "se-resize"; - } else { - this.canvas.style.cursor = "crosshair"; - } - } - } else { //not over a node - - //search for link connector - var over_link = null; - for (var i = 0; i < this.visible_links.length; ++i) { - var link = this.visible_links[i]; - var center = link._pos; - if ( - !center || - e.canvasX < center[0] - 4 || - e.canvasX > center[0] + 4 || - e.canvasY < center[1] - 4 || - e.canvasY > center[1] + 4 - ) { - continue; - } - over_link = link; - break; - } - if( over_link != this.over_link_center ) - { - this.over_link_center = over_link; - this.dirty_canvas = true; - } - - if (this.canvas) { - this.canvas.style.cursor = ""; - } - } //end - - //send event to node if capturing input (used with widgets that allow drag outside of the area of the node) - if ( this.node_capturing_input && this.node_capturing_input != node && this.node_capturing_input.onMouseMove ) { - this.node_capturing_input.onMouseMove(e,[e.canvasX - this.node_capturing_input.pos[0],e.canvasY - this.node_capturing_input.pos[1]], this); - } - - //node being dragged - if (this.node_dragged && !this.live_mode) { - //console.log("draggin!",this.selected_nodes); - for (var i in this.selected_nodes) { - var n = this.selected_nodes[i]; - n.pos[0] += delta[0] / this.ds.scale; - n.pos[1] += delta[1] / this.ds.scale; - if (!n.is_selected) this.processNodeSelected(n, e); /* - * Don't call the function if the block is already selected. - * Otherwise, it could cause the block to be unselected while dragging. - */ - } - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - - if (this.resizing_node && !this.live_mode) { - //convert mouse to node space - var desired_size = [ e.canvasX - this.resizing_node.pos[0], e.canvasY - this.resizing_node.pos[1] ]; - var min_size = this.resizing_node.computeSize(); - desired_size[0] = Math.max( min_size[0], desired_size[0] ); - desired_size[1] = Math.max( min_size[1], desired_size[1] ); - this.resizing_node.setSize( desired_size ); - - this.canvas.style.cursor = "se-resize"; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - } - } - - e.preventDefault(); - return false; - }; - - /** - * Called when a mouse up event has to be processed - * @method processMouseUp - **/ - LGraphCanvas.prototype.processMouseUp = function(e) { - - var is_primary = ( e.isPrimary === undefined || e.isPrimary ); - - //early exit for extra pointer - if(!is_primary){ - /*e.stopPropagation(); - e.preventDefault();*/ - //console.log("pointerevents: processMouseUp pointerN_stop "+e.pointerId+" "+e.isPrimary); - return false; - } - - //console.log("pointerevents: processMouseUp "+e.pointerId+" "+e.isPrimary+" :: "+e.clientX+" "+e.clientY); - - if( this.set_canvas_dirty_on_mouse_event ) - this.dirty_canvas = true; - - if (!this.graph) - return; - - var window = this.getCanvasWindow(); - var document = window.document; - LGraphCanvas.active_canvas = this; - - //restore the mousemove event back to the canvas - if(!this.options.skip_events) - { - //console.log("pointerevents: processMouseUp adjustEventListener"); - LiteGraph.pointerListenerRemove(document,"move", this._mousemove_callback,true); - LiteGraph.pointerListenerAdd(this.canvas,"move", this._mousemove_callback,true); - LiteGraph.pointerListenerRemove(document,"up", this._mouseup_callback,true); - } - - this.adjustMouseEvent(e); - var now = LiteGraph.getTime(); - e.click_time = now - this.last_mouseclick; - this.last_mouse_dragging = false; - this.last_click_position = null; - - if(this.block_click) - { - //console.log("pointerevents: processMouseUp block_clicks"); - this.block_click = false; //used to avoid sending twice a click in a immediate button - } - - //console.log("pointerevents: processMouseUp which: "+e.which); - - if (e.which == 1) { - - if( this.node_widget ) - { - this.processNodeWidgets( this.node_widget[0], this.graph_mouse, e ); - } - - //left button - this.node_widget = null; - - if (this.selected_group) { - var diffx = - this.selected_group.pos[0] - - Math.round(this.selected_group.pos[0]); - var diffy = - this.selected_group.pos[1] - - Math.round(this.selected_group.pos[1]); - this.selected_group.move(diffx, diffy, e.ctrlKey); - this.selected_group.pos[0] = Math.round( - this.selected_group.pos[0] - ); - this.selected_group.pos[1] = Math.round( - this.selected_group.pos[1] - ); - if (this.selected_group._nodes.length) { - this.dirty_canvas = true; - } - this.selected_group = null; - } - this.selected_group_resizing = false; - - var node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ); - - if (this.dragging_rectangle) { - if (this.graph) { - var nodes = this.graph._nodes; - var node_bounding = new Float32Array(4); - - //compute bounding and flip if left to right - var w = Math.abs(this.dragging_rectangle[2]); - var h = Math.abs(this.dragging_rectangle[3]); - var startx = - this.dragging_rectangle[2] < 0 - ? this.dragging_rectangle[0] - w - : this.dragging_rectangle[0]; - var starty = - this.dragging_rectangle[3] < 0 - ? this.dragging_rectangle[1] - h - : this.dragging_rectangle[1]; - this.dragging_rectangle[0] = startx; - this.dragging_rectangle[1] = starty; - this.dragging_rectangle[2] = w; - this.dragging_rectangle[3] = h; - - // test dragging rect size, if minimun simulate a click - if (!node || (w > 10 && h > 10 )){ - //test against all nodes (not visible because the rectangle maybe start outside - var to_select = []; - for (var i = 0; i < nodes.length; ++i) { - var nodeX = nodes[i]; - nodeX.getBounding(node_bounding); - if ( - !overlapBounding( - this.dragging_rectangle, - node_bounding - ) - ) { - continue; - } //out of the visible area - to_select.push(nodeX); - } - if (to_select.length) { - this.selectNodes(to_select,e.shiftKey); // add to selection with shift - } - }else{ - // will select of update selection - this.selectNodes([node],e.shiftKey||e.ctrlKey); // add to selection add to selection with ctrlKey or shiftKey - } - - } - this.dragging_rectangle = null; - } else if (this.connecting_node) { - //dragging a connection - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - - var connInOrOut = this.connecting_output || this.connecting_input; - var connType = connInOrOut.type; - - //node below mouse - if (node) { - - /* no need to condition on event type.. just another type - if ( - connType == LiteGraph.EVENT && - this.isOverNodeBox(node, e.canvasX, e.canvasY) - ) { - - this.connecting_node.connect( - this.connecting_slot, - node, - LiteGraph.EVENT - ); - - } else {*/ - - //slot below mouse? connect - - if (this.connecting_output){ - - var slot = this.isOverNodeInput( - node, - e.canvasX, - e.canvasY - ); - if (slot != -1) { - this.connecting_node.connect(this.connecting_slot, node, slot); - } else { - //not on top of an input - // look for a good slot - this.connecting_node.connectByType(this.connecting_slot,node,connType); - } - - }else if (this.connecting_input){ - - var slot = this.isOverNodeOutput( - node, - e.canvasX, - e.canvasY - ); - - if (slot != -1) { - node.connect(slot, this.connecting_node, this.connecting_slot); // this is inverted has output-input nature like - } else { - //not on top of an input - // look for a good slot - this.connecting_node.connectByTypeOutput(this.connecting_slot,node,connType); - } - - } - - - //} - - }else{ - - // add menu when releasing link in empty space - if (LiteGraph.release_link_on_empty_shows_menu){ - if (e.shiftKey && this.allow_searchbox){ - if(this.connecting_output){ - this.showSearchBox(e,{node_from: this.connecting_node, slot_from: this.connecting_output, type_filter_in: this.connecting_output.type}); - }else if(this.connecting_input){ - this.showSearchBox(e,{node_to: this.connecting_node, slot_from: this.connecting_input, type_filter_out: this.connecting_input.type}); - } - }else{ - if(this.connecting_output){ - this.showConnectionMenu({nodeFrom: this.connecting_node, slotFrom: this.connecting_output, e: e}); - }else if(this.connecting_input){ - this.showConnectionMenu({nodeTo: this.connecting_node, slotTo: this.connecting_input, e: e}); - } - } - } - } - - this.connecting_output = null; - this.connecting_input = null; - this.connecting_pos = null; - this.connecting_node = null; - this.connecting_slot = -1; - } //not dragging connection - else if (this.resizing_node) { - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.graph.afterChange(this.resizing_node); - this.resizing_node = null; - } else if (this.node_dragged) { - //node being dragged? - var node = this.node_dragged; - if ( - node && - e.click_time < 300 && - isInsideRectangle( e.canvasX, e.canvasY, node.pos[0], node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT, LiteGraph.NODE_TITLE_HEIGHT ) - ) { - node.collapse(); - } - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - this.node_dragged.pos[0] = Math.round(this.node_dragged.pos[0]); - this.node_dragged.pos[1] = Math.round(this.node_dragged.pos[1]); - if (this.graph.config.align_to_grid || this.align_to_grid ) { - this.node_dragged.alignToGrid(); - } - if( this.onNodeMoved ) - this.onNodeMoved( this.node_dragged ); - this.graph.afterChange(this.node_dragged); - this.node_dragged = null; - } //no node being dragged - else { - //get node over - var node = this.graph.getNodeOnPos( - e.canvasX, - e.canvasY, - this.visible_nodes - ); - - if (!node && e.click_time < 300) { - this.deselectAllNodes(); - } - - this.dirty_canvas = true; - this.dragging_canvas = false; - - if (this.node_over && this.node_over.onMouseUp) { - this.node_over.onMouseUp( e, [ e.canvasX - this.node_over.pos[0], e.canvasY - this.node_over.pos[1] ], this ); - } - if ( - this.node_capturing_input && - this.node_capturing_input.onMouseUp - ) { - this.node_capturing_input.onMouseUp(e, [ - e.canvasX - this.node_capturing_input.pos[0], - e.canvasY - this.node_capturing_input.pos[1] - ]); - } - } - } else if (e.which == 2) { - //middle button - //trace("middle"); - this.dirty_canvas = true; - this.dragging_canvas = false; - } else if (e.which == 3) { - //right button - //trace("right"); - this.dirty_canvas = true; - this.dragging_canvas = false; - } - - /* - if((this.dirty_canvas || this.dirty_bgcanvas) && this.rendering_timer_id == null) - this.draw(); - */ - - if (is_primary) - { - this.pointer_is_down = false; - this.pointer_is_double = false; - } - - this.graph.change(); - - //console.log("pointerevents: processMouseUp stopPropagation"); - e.stopPropagation(); - e.preventDefault(); - return false; - }; - - /** - * Called when a mouse wheel event has to be processed - * @method processMouseWheel - **/ - LGraphCanvas.prototype.processMouseWheel = function(e) { - if (!this.graph || !this.allow_dragcanvas) { - return; - } - - var delta = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60; - - this.adjustMouseEvent(e); - - var x = e.clientX; - var y = e.clientY; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - if(!is_inside) - return; - - var scale = this.ds.scale; - - if (delta > 0) { - scale *= 1.1; - } else if (delta < 0) { - scale *= 1 / 1.1; - } - - //this.setZoom( scale, [ e.clientX, e.clientY ] ); - this.ds.changeScale(scale, [e.clientX, e.clientY]); - - this.graph.change(); - - e.preventDefault(); - return false; // prevent default - }; - - /** - * returns true if a position (in graph space) is on top of a node little corner box - * @method isOverNodeBox - **/ - LGraphCanvas.prototype.isOverNodeBox = function(node, canvasx, canvasy) { - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - if ( - isInsideRectangle( - canvasx, - canvasy, - node.pos[0] + 2, - node.pos[1] + 2 - title_height, - title_height - 4, - title_height - 4 - ) - ) { - return true; - } - return false; - }; - - /** - * returns the INDEX if a position (in graph space) is on top of a node input slot - * @method isOverNodeInput - **/ - LGraphCanvas.prototype.isOverNodeInput = function( - node, - canvasx, - canvasy, - slot_pos - ) { - if (node.inputs) { - for (var i = 0, l = node.inputs.length; i < l; ++i) { - var input = node.inputs[i]; - var link_pos = node.getConnectionPos(true, i); - var is_inside = false; - if (node.horizontal) { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 5, - link_pos[1] - 10, - 10, - 20 - ); - } else { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 5, - 40, - 10 - ); - } - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0]; - slot_pos[1] = link_pos[1]; - } - return i; - } - } - } - return -1; - }; - - /** - * returns the INDEX if a position (in graph space) is on top of a node output slot - * @method isOverNodeOuput - **/ - LGraphCanvas.prototype.isOverNodeOutput = function( - node, - canvasx, - canvasy, - slot_pos - ) { - if (node.outputs) { - for (var i = 0, l = node.outputs.length; i < l; ++i) { - var output = node.outputs[i]; - var link_pos = node.getConnectionPos(false, i); - var is_inside = false; - if (node.horizontal) { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 5, - link_pos[1] - 10, - 10, - 20 - ); - } else { - is_inside = isInsideRectangle( - canvasx, - canvasy, - link_pos[0] - 10, - link_pos[1] - 5, - 40, - 10 - ); - } - if (is_inside) { - if (slot_pos) { - slot_pos[0] = link_pos[0]; - slot_pos[1] = link_pos[1]; - } - return i; - } - } - } - return -1; - }; - - /** - * process a key event - * @method processKey - **/ - LGraphCanvas.prototype.processKey = function(e) { - if (!this.graph) { - return; - } - - var block_default = false; - //console.log(e); //debug - - if (e.target.localName == "input") { - return; - } - - if (e.type == "keydown") { - if (e.keyCode == 32) { - //space - this.dragging_canvas = true; - block_default = true; - } - - if (e.keyCode == 27) { - //esc - if(this.node_panel) this.node_panel.close(); - if(this.options_panel) this.options_panel.close(); - block_default = true; - } - - //select all Control A - if (e.keyCode == 65 && e.ctrlKey) { - this.selectNodes(); - block_default = true; - } - - if ((e.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - //copy - if (this.selected_nodes) { - this.copyToClipboard(); - block_default = true; - } - } - - if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) { - //paste - this.pasteFromClipboard(e.shiftKey); - } - - //delete or backspace - if (e.keyCode == 46 || e.keyCode == 8) { - if ( - e.target.localName != "input" && - e.target.localName != "textarea" - ) { - this.deleteSelectedNodes(); - block_default = true; - } - } - - //collapse - //... - - //TODO - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].onKeyDown) { - this.selected_nodes[i].onKeyDown(e); - } - } - } - } else if (e.type == "keyup") { - if (e.keyCode == 32) { - // space - this.dragging_canvas = false; - } - - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].onKeyUp) { - this.selected_nodes[i].onKeyUp(e); - } - } - } - } - - this.graph.change(); - - if (block_default) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }; - - LGraphCanvas.prototype.copyToClipboard = function(nodes) { - var clipboard_info = { - nodes: [], - links: [] - }; - var index = 0; - var selected_nodes_array = []; - if (!nodes) nodes = this.selected_nodes; - for (var i in nodes) { - var node = nodes[i]; - if (node.clonable === false) - continue; - node._relative_id = index; - selected_nodes_array.push(node); - index += 1; - } - - for (var i = 0; i < selected_nodes_array.length; ++i) { - var node = selected_nodes_array[i]; - var cloned = node.clone(); - if(!cloned) - { - console.warn("node type not found: " + node.type ); - continue; - } - clipboard_info.nodes.push(cloned.serialize()); - if (node.inputs && node.inputs.length) { - for (var j = 0; j < node.inputs.length; ++j) { - var input = node.inputs[j]; - if (!input || input.link == null) { - continue; - } - var link_info = this.graph.links[input.link]; - if (!link_info) { - continue; - } - var target_node = this.graph.getNodeById( - link_info.origin_id - ); - if (!target_node) { - continue; - } - clipboard_info.links.push([ - target_node._relative_id, - link_info.origin_slot, //j, - node._relative_id, - link_info.target_slot, - target_node.id - ]); - } - } - } - localStorage.setItem( - "litegrapheditor_clipboard", - JSON.stringify(clipboard_info) - ); - }; - - LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) { - // if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior - if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { - return; - } - var data = localStorage.getItem("litegrapheditor_clipboard"); - if (!data) { - return; - } - - this.graph.beforeChange(); - - //create nodes - var clipboard_info = JSON.parse(data); - // calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos - var posMin = false; - var posMinIndexes = false; - for (var i = 0; i < clipboard_info.nodes.length; ++i) { - if (posMin){ - if(posMin[0]>clipboard_info.nodes[i].pos[0]){ - posMin[0] = clipboard_info.nodes[i].pos[0]; - posMinIndexes[0] = i; - } - if(posMin[1]>clipboard_info.nodes[i].pos[1]){ - posMin[1] = clipboard_info.nodes[i].pos[1]; - posMinIndexes[1] = i; - } - } - else{ - posMin = [clipboard_info.nodes[i].pos[0], clipboard_info.nodes[i].pos[1]]; - posMinIndexes = [i, i]; - } - } - var nodes = []; - for (var i = 0; i < clipboard_info.nodes.length; ++i) { - var node_data = clipboard_info.nodes[i]; - var node = LiteGraph.createNode(node_data.type); - if (node) { - node.configure(node_data); - - //paste in last known mouse position - node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5; - node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5; - - this.graph.add(node,{doProcessChange:false}); - - nodes.push(node); - } - } - - //create links - for (var i = 0; i < clipboard_info.links.length; ++i) { - var link_info = clipboard_info.links[i]; - var origin_node = undefined; - var origin_node_relative_id = link_info[0]; - if (origin_node_relative_id != null) { - origin_node = nodes[origin_node_relative_id]; - } else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { - var origin_node_id = link_info[4]; - if (origin_node_id) { - origin_node = this.graph.getNodeById(origin_node_id); - } - } - var target_node = nodes[link_info[2]]; - if( origin_node && target_node ) - origin_node.connect(link_info[1], target_node, link_info[3]); - else - console.warn("Warning, nodes missing on pasting"); - } - - this.selectNodes(nodes); - - this.graph.afterChange(); - }; - - /** - * process a item drop event on top the canvas - * @method processDrop - **/ - LGraphCanvas.prototype.processDrop = function(e) { - e.preventDefault(); - this.adjustMouseEvent(e); - var x = e.clientX; - var y = e.clientY; - var is_inside = !this.viewport || ( this.viewport && x >= this.viewport[0] && x < (this.viewport[0] + this.viewport[2]) && y >= this.viewport[1] && y < (this.viewport[1] + this.viewport[3]) ); - if(!is_inside){ - return; - // --- BREAK --- - } - - var pos = [e.canvasX, e.canvasY]; - - - var node = this.graph ? this.graph.getNodeOnPos(pos[0], pos[1]) : null; - - if (!node) { - var r = null; - if (this.onDropItem) { - r = this.onDropItem(event); - } - if (!r) { - this.checkDropItem(e); - } - return; - } - - if (node.onDropFile || node.onDropData) { - var files = e.dataTransfer.files; - if (files && files.length) { - for (var i = 0; i < files.length; i++) { - var file = e.dataTransfer.files[0]; - var filename = file.name; - var ext = LGraphCanvas.getFileExtension(filename); - //console.log(file); - - if (node.onDropFile) { - node.onDropFile(file); - } - - if (node.onDropData) { - //prepare reader - var reader = new FileReader(); - reader.onload = function(event) { - //console.log(event.target); - var data = event.target.result; - node.onDropData(data, filename, file); - }; - - //read data - var type = file.type.split("/")[0]; - if (type == "text" || type == "") { - reader.readAsText(file); - } else if (type == "image") { - reader.readAsDataURL(file); - } else { - reader.readAsArrayBuffer(file); - } - } - } - } - } - - if (node.onDropItem) { - if (node.onDropItem(event)) { - return true; - } - } - - if (this.onDropItem) { - return this.onDropItem(event); - } - - return false; - }; - - //called if the graph doesn't have a default drop item behaviour - LGraphCanvas.prototype.checkDropItem = function(e) { - if (e.dataTransfer.files.length) { - var file = e.dataTransfer.files[0]; - var ext = LGraphCanvas.getFileExtension(file.name).toLowerCase(); - var nodetype = LiteGraph.node_types_by_file_extension[ext]; - if (nodetype) { - this.graph.beforeChange(); - var node = LiteGraph.createNode(nodetype.type); - node.pos = [e.canvasX, e.canvasY]; - this.graph.add(node); - if (node.onDropFile) { - node.onDropFile(file); - } - this.graph.afterChange(); - } - } - }; - - LGraphCanvas.prototype.processNodeDblClicked = function(n) { - if (this.onShowNodePanel) { - this.onShowNodePanel(n); - } - - if (this.onNodeDblClicked) { - this.onNodeDblClicked(n); - } - - this.setDirty(true); - }; - - LGraphCanvas.prototype.processNodeSelected = function(node, e) { - this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select)); - if (this.onNodeSelected) { - this.onNodeSelected(node); - } - }; - - /** - * selects a given node (or adds it to the current selection) - * @method selectNode - **/ - LGraphCanvas.prototype.selectNode = function( - node, - add_to_current_selection - ) { - if (node == null) { - this.deselectAllNodes(); - } else { - this.selectNodes([node], add_to_current_selection); - } - }; - - /** - * selects several nodes (or adds them to the current selection) - * @method selectNodes - **/ - LGraphCanvas.prototype.selectNodes = function( nodes, add_to_current_selection ) - { - if (!add_to_current_selection) { - this.deselectAllNodes(); - } - - nodes = nodes || this.graph._nodes; - if (typeof nodes == "string") nodes = [nodes]; - for (var i in nodes) { - var node = nodes[i]; - if (node.is_selected) { - this.deselectNode(node); - continue; - } - - if (!node.is_selected && node.onSelected) { - node.onSelected(); - } - node.is_selected = true; - this.selected_nodes[node.id] = node; - - if (node.inputs) { - for (var j = 0; j < node.inputs.length; ++j) { - this.highlighted_links[node.inputs[j].link] = true; - } - } - if (node.outputs) { - for (var j = 0; j < node.outputs.length; ++j) { - var out = node.outputs[j]; - if (out.links) { - for (var k = 0; k < out.links.length; ++k) { - this.highlighted_links[out.links[k]] = true; - } - } - } - } - } - - if( this.onSelectionChange ) - this.onSelectionChange( this.selected_nodes ); - - this.setDirty(true); - }; - - /** - * removes a node from the current selection - * @method deselectNode - **/ - LGraphCanvas.prototype.deselectNode = function(node) { - if (!node.is_selected) { - return; - } - if (node.onDeselected) { - node.onDeselected(); - } - node.is_selected = false; - - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } - - //remove highlighted - if (node.inputs) { - for (var i = 0; i < node.inputs.length; ++i) { - delete this.highlighted_links[node.inputs[i].link]; - } - } - if (node.outputs) { - for (var i = 0; i < node.outputs.length; ++i) { - var out = node.outputs[i]; - if (out.links) { - for (var j = 0; j < out.links.length; ++j) { - delete this.highlighted_links[out.links[j]]; - } - } - } - } - }; - - /** - * removes all nodes from the current selection - * @method deselectAllNodes - **/ - LGraphCanvas.prototype.deselectAllNodes = function() { - if (!this.graph) { - return; - } - var nodes = this.graph._nodes; - for (var i = 0, l = nodes.length; i < l; ++i) { - var node = nodes[i]; - if (!node.is_selected) { - continue; - } - if (node.onDeselected) { - node.onDeselected(); - } - node.is_selected = false; - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } - } - this.selected_nodes = {}; - this.current_node = null; - this.highlighted_links = {}; - if( this.onSelectionChange ) - this.onSelectionChange( this.selected_nodes ); - this.setDirty(true); - }; - - /** - * deletes all nodes in the current selection from the graph - * @method deleteSelectedNodes - **/ - LGraphCanvas.prototype.deleteSelectedNodes = function() { - - this.graph.beforeChange(); - - for (var i in this.selected_nodes) { - var node = this.selected_nodes[i]; - - if(node.block_delete) - continue; - - //autoconnect when possible (very basic, only takes into account first input-output) - if(node.inputs && node.inputs.length && node.outputs && node.outputs.length && LiteGraph.isValidConnection( node.inputs[0].type, node.outputs[0].type ) && node.inputs[0].link && node.outputs[0].links && node.outputs[0].links.length ) - { - var input_link = node.graph.links[ node.inputs[0].link ]; - var output_link = node.graph.links[ node.outputs[0].links[0] ]; - var input_node = node.getInputNode(0); - var output_node = node.getOutputNodes(0)[0]; - if(input_node && output_node) - input_node.connect( input_link.origin_slot, output_node, output_link.target_slot ); - } - this.graph.remove(node); - if (this.onNodeDeselected) { - this.onNodeDeselected(node); - } - } - this.selected_nodes = {}; - this.current_node = null; - this.highlighted_links = {}; - this.setDirty(true); - this.graph.afterChange(); - }; - - /** - * centers the camera on a given node - * @method centerOnNode - **/ - LGraphCanvas.prototype.centerOnNode = function(node) { - this.ds.offset[0] = - -node.pos[0] - - node.size[0] * 0.5 + - (this.canvas.width * 0.5) / this.ds.scale; - this.ds.offset[1] = - -node.pos[1] - - node.size[1] * 0.5 + - (this.canvas.height * 0.5) / this.ds.scale; - this.setDirty(true, true); - }; - - /** - * adds some useful properties to a mouse event, like the position in graph coordinates - * @method adjustMouseEvent - **/ - LGraphCanvas.prototype.adjustMouseEvent = function(e) { - var clientX_rel = 0; - var clientY_rel = 0; - - if (this.canvas) { - var b = this.canvas.getBoundingClientRect(); - clientX_rel = e.clientX - b.left; - clientY_rel = e.clientY - b.top; - } else { - clientX_rel = e.clientX; - clientY_rel = e.clientY; - } - - e.deltaX = clientX_rel - this.last_mouse_position[0]; - e.deltaY = clientY_rel- this.last_mouse_position[1]; - - this.last_mouse_position[0] = clientX_rel; - this.last_mouse_position[1] = clientY_rel; - - e.canvasX = clientX_rel / this.ds.scale - this.ds.offset[0]; - e.canvasY = clientY_rel / this.ds.scale - this.ds.offset[1]; - - //console.log("pointerevents: adjustMouseEvent "+e.clientX+":"+e.clientY+" "+clientX_rel+":"+clientY_rel+" "+e.canvasX+":"+e.canvasY); - }; - - /** - * changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom - * @method setZoom - **/ - LGraphCanvas.prototype.setZoom = function(value, zooming_center) { - this.ds.changeScale(value, zooming_center); - /* - if(!zooming_center && this.canvas) - zooming_center = [this.canvas.width * 0.5,this.canvas.height * 0.5]; - - var center = this.convertOffsetToCanvas( zooming_center ); - - this.ds.scale = value; - - if(this.scale > this.max_zoom) - this.scale = this.max_zoom; - else if(this.scale < this.min_zoom) - this.scale = this.min_zoom; - - var new_center = this.convertOffsetToCanvas( zooming_center ); - var delta_offset = [new_center[0] - center[0], new_center[1] - center[1]]; - - this.offset[0] += delta_offset[0]; - this.offset[1] += delta_offset[1]; - */ - - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - }; - - /** - * converts a coordinate from graph coordinates to canvas2D coordinates - * @method convertOffsetToCanvas - **/ - LGraphCanvas.prototype.convertOffsetToCanvas = function(pos, out) { - return this.ds.convertOffsetToCanvas(pos, out); - }; - - /** - * converts a coordinate from Canvas2D coordinates to graph space - * @method convertCanvasToOffset - **/ - LGraphCanvas.prototype.convertCanvasToOffset = function(pos, out) { - return this.ds.convertCanvasToOffset(pos, out); - }; - - //converts event coordinates from canvas2D to graph coordinates - LGraphCanvas.prototype.convertEventToCanvasOffset = function(e) { - var rect = this.canvas.getBoundingClientRect(); - return this.convertCanvasToOffset([ - e.clientX - rect.left, - e.clientY - rect.top - ]); - }; - - /** - * brings a node to front (above all other nodes) - * @method bringToFront - **/ - LGraphCanvas.prototype.bringToFront = function(node) { - var i = this.graph._nodes.indexOf(node); - if (i == -1) { - return; - } - - this.graph._nodes.splice(i, 1); - this.graph._nodes.push(node); - }; - - /** - * sends a node to the back (below all other nodes) - * @method sendToBack - **/ - LGraphCanvas.prototype.sendToBack = function(node) { - var i = this.graph._nodes.indexOf(node); - if (i == -1) { - return; - } - - this.graph._nodes.splice(i, 1); - this.graph._nodes.unshift(node); - }; - - /* Interaction */ - - /* LGraphCanvas render */ - var temp = new Float32Array(4); - - /** - * checks which nodes are visible (inside the camera area) - * @method computeVisibleNodes - **/ - LGraphCanvas.prototype.computeVisibleNodes = function(nodes, out) { - var visible_nodes = out || []; - visible_nodes.length = 0; - nodes = nodes || this.graph._nodes; - for (var i = 0, l = nodes.length; i < l; ++i) { - var n = nodes[i]; - - //skip rendering nodes in live mode - if (this.live_mode && !n.onDrawBackground && !n.onDrawForeground) { - continue; - } - - if (!overlapBounding(this.visible_area, n.getBounding(temp, true))) { - continue; - } //out of the visible area - - visible_nodes.push(n); - } - return visible_nodes; - }; - - /** - * renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) - * @method draw - **/ - LGraphCanvas.prototype.draw = function(force_canvas, force_bgcanvas) { - if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0) { - return; - } - - //fps counting - var now = LiteGraph.getTime(); - this.render_time = (now - this.last_draw_time) * 0.001; - this.last_draw_time = now; - - if (this.graph) { - this.ds.computeVisibleArea(this.viewport); - } - - if ( - this.dirty_bgcanvas || - force_bgcanvas || - this.always_render_background || - (this.graph && - this.graph._last_trigger_time && - now - this.graph._last_trigger_time < 1000) - ) { - this.drawBackCanvas(); - } - - if (this.dirty_canvas || force_canvas) { - this.drawFrontCanvas(); - } - - this.fps = this.render_time ? 1.0 / this.render_time : 0; - this.frame += 1; - }; - - /** - * draws the front canvas (the one containing all the nodes) - * @method drawFrontCanvas - **/ - LGraphCanvas.prototype.drawFrontCanvas = function() { - this.dirty_canvas = false; - - if (!this.ctx) { - this.ctx = this.bgcanvas.getContext("2d"); - } - var ctx = this.ctx; - if (!ctx) { - //maybe is using webgl... - return; - } - - var canvas = this.canvas; - if ( ctx.start2D && !this.viewport ) { - ctx.start2D(); - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - - //clip dirty area if there is one, otherwise work in full canvas - var area = this.viewport || this.dirty_area; - if (area) { - ctx.save(); - ctx.beginPath(); - ctx.rect( area[0],area[1],area[2],area[3] ); - ctx.clip(); - } - - //clear - //canvas.width = canvas.width; - if (this.clear_background) { - if(area) - ctx.clearRect( area[0],area[1],area[2],area[3] ); - else - ctx.clearRect(0, 0, canvas.width, canvas.height); - } - - //draw bg canvas - if (this.bgcanvas == this.canvas) { - this.drawBackCanvas(); - } else { - ctx.drawImage( this.bgcanvas, 0, 0 ); - } - - //rendering - if (this.onRender) { - this.onRender(canvas, ctx); - } - - //info widget - if (this.show_info) { - this.renderInfo(ctx, area ? area[0] : 0, area ? area[1] : 0 ); - } - - if (this.graph) { - //apply transformations - ctx.save(); - this.ds.toCanvasContext(ctx); - - //draw nodes - var drawn_nodes = 0; - var visible_nodes = this.computeVisibleNodes( - null, - this.visible_nodes - ); - - for (var i = 0; i < visible_nodes.length; ++i) { - var node = visible_nodes[i]; - - //transform coords system - ctx.save(); - ctx.translate(node.pos[0], node.pos[1]); - - //Draw - this.drawNode(node, ctx); - drawn_nodes += 1; - - //Restore - ctx.restore(); - } - - //on top (debug) - if (this.render_execution_order) { - this.drawExecutionOrder(ctx); - } - - //connections ontop? - if (this.graph.config.links_ontop) { - if (!this.live_mode) { - this.drawConnections(ctx); - } - } - - //current connection (the one being dragged by the mouse) - if (this.connecting_pos != null) { - ctx.lineWidth = this.connections_width; - var link_color = null; - - var connInOrOut = this.connecting_output || this.connecting_input; - - var connType = connInOrOut.type; - var connDir = connInOrOut.dir; - if(connDir == null) - { - if (this.connecting_output) - connDir = this.connecting_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT; - else - connDir = this.connecting_node.horizontal ? LiteGraph.UP : LiteGraph.LEFT; - } - var connShape = connInOrOut.shape; - - switch (connType) { - case LiteGraph.EVENT: - link_color = LiteGraph.EVENT_LINK_COLOR; - break; - default: - link_color = LiteGraph.CONNECTING_LINK_COLOR; - } - - //the connection being dragged by the mouse - this.renderLink( - ctx, - this.connecting_pos, - [this.graph_mouse[0], this.graph_mouse[1]], - null, - false, - null, - link_color, - connDir, - LiteGraph.CENTER - ); - - ctx.beginPath(); - if ( - connType === LiteGraph.EVENT || - connShape === LiteGraph.BOX_SHAPE - ) { - ctx.rect( - this.connecting_pos[0] - 6 + 0.5, - this.connecting_pos[1] - 5 + 0.5, - 14, - 10 - ); - ctx.fill(); - ctx.beginPath(); - ctx.rect( - this.graph_mouse[0] - 6 + 0.5, - this.graph_mouse[1] - 5 + 0.5, - 14, - 10 - ); - } else if (connShape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this.connecting_pos[0] + 8, this.connecting_pos[1] + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] + 6 + 0.5); - ctx.lineTo(this.connecting_pos[0] - 4, this.connecting_pos[1] - 6 + 0.5); - ctx.closePath(); - } - else { - ctx.arc( - this.connecting_pos[0], - this.connecting_pos[1], - 4, - 0, - Math.PI * 2 - ); - ctx.fill(); - ctx.beginPath(); - ctx.arc( - this.graph_mouse[0], - this.graph_mouse[1], - 4, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - - ctx.fillStyle = "#ffcc00"; - if (this._highlight_input) { - ctx.beginPath(); - var shape = this._highlight_input_slot.shape; - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_input[0] + 8, this._highlight_input[1] + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] + 6 + 0.5); - ctx.lineTo(this._highlight_input[0] - 4, this._highlight_input[1] - 6 + 0.5); - ctx.closePath(); - } else { - ctx.arc( - this._highlight_input[0], - this._highlight_input[1], - 6, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - } - if (this._highlight_output) { - ctx.beginPath(); - if (shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(this._highlight_output[0] + 8, this._highlight_output[1] + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] + 6 + 0.5); - ctx.lineTo(this._highlight_output[0] - 4, this._highlight_output[1] - 6 + 0.5); - ctx.closePath(); - } else { - ctx.arc( - this._highlight_output[0], - this._highlight_output[1], - 6, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - } - } - - //the selection rectangle - if (this.dragging_rectangle) { - ctx.strokeStyle = "#FFF"; - ctx.strokeRect( - this.dragging_rectangle[0], - this.dragging_rectangle[1], - this.dragging_rectangle[2], - this.dragging_rectangle[3] - ); - } - - //on top of link center - if(this.over_link_center && this.render_link_tooltip) - this.drawLinkTooltip( ctx, this.over_link_center ); - else - if(this.onDrawLinkTooltip) //to remove - this.onDrawLinkTooltip(ctx,null); - - //custom info - if (this.onDrawForeground) { - this.onDrawForeground(ctx, this.visible_rect); - } - - ctx.restore(); - } - - //draws panel in the corner - if (this._graph_stack && this._graph_stack.length) { - this.drawSubgraphPanel( ctx ); - } - - - if (this.onDrawOverlay) { - this.onDrawOverlay(ctx); - } - - if (area){ - ctx.restore(); - } - - if (ctx.finish2D) { - //this is a function I use in webgl renderer - ctx.finish2D(); - } - }; - - /** - * draws the panel in the corner that shows subgraph properties - * @method drawSubgraphPanel - **/ - LGraphCanvas.prototype.drawSubgraphPanel = function (ctx) { - var subgraph = this.graph; - var subnode = subgraph._subgraph_node; - if (!subnode) { - console.warn("subgraph without subnode"); - return; - } - this.drawSubgraphPanelLeft(subgraph, subnode, ctx) - this.drawSubgraphPanelRight(subgraph, subnode, ctx) - } - - LGraphCanvas.prototype.drawSubgraphPanelLeft = function (subgraph, subnode, ctx) { - var num = subnode.inputs ? subnode.inputs.length : 0; - var w = 200; - var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); - - ctx.fillStyle = "#111"; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - ctx.roundRect(10, 10, w, (num + 1) * h + 50, [8]); - ctx.fill(); - ctx.globalAlpha = 1; - - ctx.fillStyle = "#888"; - ctx.font = "14px Arial"; - ctx.textAlign = "left"; - ctx.fillText("Graph Inputs", 20, 34); - // var pos = this.mouse; - - if (this.drawButton(w - 20, 20, 20, 20, "X", "#151515")) { - this.closeSubgraph(); - return; - } - - var y = 50; - ctx.font = "14px Arial"; - if (subnode.inputs) - for (var i = 0; i < subnode.inputs.length; ++i) { - var input = subnode.inputs[i]; - if (input.not_subgraph_input) - continue; - - //input button clicked - if (this.drawButton(20, y + 2, w - 20, h - 2)) { - var type = subnode.constructor.input_node_type || "graph/input"; - this.graph.beforeChange(); - var newnode = LiteGraph.createNode(type); - if (newnode) { - subgraph.add(newnode); - this.block_click = false; - this.last_click_position = null; - this.selectNodes([newnode]); - this.node_dragged = newnode; - this.dragging_canvas = false; - newnode.setProperty("name", input.name); - newnode.setProperty("type", input.type); - this.node_dragged.pos[0] = this.graph_mouse[0] - 5; - this.node_dragged.pos[1] = this.graph_mouse[1] - 5; - this.graph.afterChange(); - } - else - console.error("graph input node not found:", type); - } - ctx.fillStyle = "#9C9"; - ctx.beginPath(); - ctx.arc(w - 16, y + h * 0.5, 5, 0, 2 * Math.PI); - ctx.fill(); - ctx.fillStyle = "#AAA"; - ctx.fillText(input.name, 30, y + h * 0.75); - // var tw = ctx.measureText(input.name); - ctx.fillStyle = "#777"; - ctx.fillText(input.type, 130, y + h * 0.75); - y += h; - } - //add + button - if (this.drawButton(20, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { - this.showSubgraphPropertiesDialog(subnode); - } - } - LGraphCanvas.prototype.drawSubgraphPanelRight = function (subgraph, subnode, ctx) { - var num = subnode.outputs ? subnode.outputs.length : 0; - var canvas_w = this.bgcanvas.width - var w = 200; - var h = Math.floor(LiteGraph.NODE_SLOT_HEIGHT * 1.6); - - ctx.fillStyle = "#111"; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - ctx.roundRect(canvas_w - w - 10, 10, w, (num + 1) * h + 50, [8]); - ctx.fill(); - ctx.globalAlpha = 1; - - ctx.fillStyle = "#888"; - ctx.font = "14px Arial"; - ctx.textAlign = "left"; - var title_text = "Graph Outputs" - var tw = ctx.measureText(title_text).width - ctx.fillText(title_text, (canvas_w - tw) - 20, 34); - // var pos = this.mouse; - if (this.drawButton(canvas_w - w, 20, 20, 20, "X", "#151515")) { - this.closeSubgraph(); - return; - } - - var y = 50; - ctx.font = "14px Arial"; - if (subnode.outputs) - for (var i = 0; i < subnode.outputs.length; ++i) { - var output = subnode.outputs[i]; - if (output.not_subgraph_input) - continue; - - //output button clicked - if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2)) { - var type = subnode.constructor.output_node_type || "graph/output"; - this.graph.beforeChange(); - var newnode = LiteGraph.createNode(type); - if (newnode) { - subgraph.add(newnode); - this.block_click = false; - this.last_click_position = null; - this.selectNodes([newnode]); - this.node_dragged = newnode; - this.dragging_canvas = false; - newnode.setProperty("name", output.name); - newnode.setProperty("type", output.type); - this.node_dragged.pos[0] = this.graph_mouse[0] - 5; - this.node_dragged.pos[1] = this.graph_mouse[1] - 5; - this.graph.afterChange(); - } - else - console.error("graph input node not found:", type); - } - ctx.fillStyle = "#9C9"; - ctx.beginPath(); - ctx.arc(canvas_w - w + 16, y + h * 0.5, 5, 0, 2 * Math.PI); - ctx.fill(); - ctx.fillStyle = "#AAA"; - ctx.fillText(output.name, canvas_w - w + 30, y + h * 0.75); - // var tw = ctx.measureText(input.name); - ctx.fillStyle = "#777"; - ctx.fillText(output.type, canvas_w - w + 130, y + h * 0.75); - y += h; - } - //add + button - if (this.drawButton(canvas_w - w, y + 2, w - 20, h - 2, "+", "#151515", "#222")) { - this.showSubgraphPropertiesDialogRight(subnode); - } - } - //Draws a button into the canvas overlay and computes if it was clicked using the immediate gui paradigm - LGraphCanvas.prototype.drawButton = function( x,y,w,h, text, bgcolor, hovercolor, textcolor ) - { - var ctx = this.ctx; - bgcolor = bgcolor || LiteGraph.NODE_DEFAULT_COLOR; - hovercolor = hovercolor || "#555"; - textcolor = textcolor || LiteGraph.NODE_TEXT_COLOR; - var pos = this.ds.convertOffsetToCanvas(this.graph_mouse); - var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - pos = this.last_click_position ? [this.last_click_position[0], this.last_click_position[1]] : null; - if(pos) { - var rect = this.canvas.getBoundingClientRect(); - pos[0] -= rect.left; - pos[1] -= rect.top; - } - var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - - ctx.fillStyle = hover ? hovercolor : bgcolor; - if(clicked) - ctx.fillStyle = "#AAA"; - ctx.beginPath(); - ctx.roundRect(x,y,w,h,[4] ); - ctx.fill(); - - if(text != null) - { - if(text.constructor == String) - { - ctx.fillStyle = textcolor; - ctx.textAlign = "center"; - ctx.font = ((h * 0.65)|0) + "px Arial"; - ctx.fillText( text, x + w * 0.5,y + h * 0.75 ); - ctx.textAlign = "left"; - } - } - - var was_clicked = clicked && !this.block_click; - if(clicked) - this.blockClick(); - return was_clicked; - } - - LGraphCanvas.prototype.isAreaClicked = function( x,y,w,h, hold_click ) - { - var pos = this.mouse; - var hover = LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - pos = this.last_click_position; - var clicked = pos && LiteGraph.isInsideRectangle( pos[0], pos[1], x,y,w,h ); - var was_clicked = clicked && !this.block_click; - if(clicked && hold_click) - this.blockClick(); - return was_clicked; - } - - /** - * draws some useful stats in the corner of the canvas - * @method renderInfo - **/ - LGraphCanvas.prototype.renderInfo = function(ctx, x, y) { - x = x || 10; - y = y || this.canvas.offsetHeight - 80; - - ctx.save(); - ctx.translate(x, y); - - ctx.font = "10px Arial"; - ctx.fillStyle = "#888"; - ctx.textAlign = "left"; - if (this.graph) { - ctx.fillText( "T: " + this.graph.globaltime.toFixed(2) + "s", 5, 13 * 1 ); - ctx.fillText("I: " + this.graph.iteration, 5, 13 * 2 ); - ctx.fillText("N: " + this.graph._nodes.length + " [" + this.visible_nodes.length + "]", 5, 13 * 3 ); - ctx.fillText("V: " + this.graph._version, 5, 13 * 4); - ctx.fillText("FPS:" + this.fps.toFixed(2), 5, 13 * 5); - } else { - ctx.fillText("No graph selected", 5, 13 * 1); - } - ctx.restore(); - }; - - /** - * draws the back canvas (the one containing the background and the connections) - * @method drawBackCanvas - **/ - LGraphCanvas.prototype.drawBackCanvas = function() { - var canvas = this.bgcanvas; - if ( - canvas.width != this.canvas.width || - canvas.height != this.canvas.height - ) { - canvas.width = this.canvas.width; - canvas.height = this.canvas.height; - } - - if (!this.bgctx) { - this.bgctx = this.bgcanvas.getContext("2d"); - } - var ctx = this.bgctx; - if (ctx.start) { - ctx.start(); - } - - var viewport = this.viewport || [0,0,ctx.canvas.width,ctx.canvas.height]; - - //clear - if (this.clear_background) { - ctx.clearRect( viewport[0], viewport[1], viewport[2], viewport[3] ); - } - - //show subgraph stack header - if (this._graph_stack && this._graph_stack.length) { - ctx.save(); - var parent_graph = this._graph_stack[this._graph_stack.length - 1]; - var subgraph_node = this.graph._subgraph_node; - ctx.strokeStyle = subgraph_node.bgcolor; - ctx.lineWidth = 10; - ctx.strokeRect(1, 1, canvas.width - 2, canvas.height - 2); - ctx.lineWidth = 1; - ctx.font = "40px Arial"; - ctx.textAlign = "center"; - ctx.fillStyle = subgraph_node.bgcolor || "#AAA"; - var title = ""; - for (var i = 1; i < this._graph_stack.length; ++i) { - title += - this._graph_stack[i]._subgraph_node.getTitle() + " >> "; - } - ctx.fillText( - title + subgraph_node.getTitle(), - canvas.width * 0.5, - 40 - ); - ctx.restore(); - } - - var bg_already_painted = false; - if (this.onRenderBackground) { - bg_already_painted = this.onRenderBackground(canvas, ctx); - } - - //reset in case of error - if ( !this.viewport ) - { - ctx.restore(); - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - this.visible_links.length = 0; - - if (this.graph) { - //apply transformations - ctx.save(); - this.ds.toCanvasContext(ctx); - - //render BG - if ( this.ds.scale < 1.5 && !bg_already_painted && this.clear_background_color ) - { - ctx.fillStyle = this.clear_background_color; - ctx.fillRect( - this.visible_area[0], - this.visible_area[1], - this.visible_area[2], - this.visible_area[3] - ); - } - - if ( - this.background_image && - this.ds.scale > 0.5 && - !bg_already_painted - ) { - if (this.zoom_modify_alpha) { - ctx.globalAlpha = - (1.0 - 0.5 / this.ds.scale) * this.editor_alpha; - } else { - ctx.globalAlpha = this.editor_alpha; - } - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = false; // ctx.mozImageSmoothingEnabled = - if ( - !this._bg_img || - this._bg_img.name != this.background_image - ) { - this._bg_img = new Image(); - this._bg_img.name = this.background_image; - this._bg_img.src = this.background_image; - var that = this; - this._bg_img.onload = function() { - that.draw(true, true); - }; - } - - var pattern = null; - if (this._pattern == null && this._bg_img.width > 0) { - pattern = ctx.createPattern(this._bg_img, "repeat"); - this._pattern_img = this._bg_img; - this._pattern = pattern; - } else { - pattern = this._pattern; - } - if (pattern) { - ctx.fillStyle = pattern; - ctx.fillRect( - this.visible_area[0], - this.visible_area[1], - this.visible_area[2], - this.visible_area[3] - ); - ctx.fillStyle = "transparent"; - } - - ctx.globalAlpha = 1.0; - ctx.imageSmoothingEnabled = ctx.imageSmoothingEnabled = true; //= ctx.mozImageSmoothingEnabled - } - - //groups - if (this.graph._groups.length && !this.live_mode) { - this.drawGroups(canvas, ctx); - } - - if (this.onDrawBackground) { - this.onDrawBackground(ctx, this.visible_area); - } - if (this.onBackgroundRender) { - //LEGACY - console.error( - "WARNING! onBackgroundRender deprecated, now is named onDrawBackground " - ); - this.onBackgroundRender = null; - } - - //DEBUG: show clipping area - //ctx.fillStyle = "red"; - //ctx.fillRect( this.visible_area[0] + 10, this.visible_area[1] + 10, this.visible_area[2] - 20, this.visible_area[3] - 20); - - //bg - if (this.render_canvas_border) { - ctx.strokeStyle = "#235"; - ctx.strokeRect(0, 0, canvas.width, canvas.height); - } - - if (this.render_connections_shadows) { - ctx.shadowColor = "#000"; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.shadowBlur = 6; - } else { - ctx.shadowColor = "rgba(0,0,0,0)"; - } - - //draw connections - if (!this.live_mode) { - this.drawConnections(ctx); - } - - ctx.shadowColor = "rgba(0,0,0,0)"; - - //restore state - ctx.restore(); - } - - if (ctx.finish) { - ctx.finish(); - } - - this.dirty_bgcanvas = false; - this.dirty_canvas = true; //to force to repaint the front canvas with the bgcanvas - }; - - var temp_vec2 = new Float32Array(2); - - /** - * draws the given node inside the canvas - * @method drawNode - **/ - LGraphCanvas.prototype.drawNode = function(node, ctx) { - var glow = false; - this.current_node = node; - - var color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR; - var bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR; - - //shadow and glow - if (node.mouseOver) { - glow = true; - } - - var low_quality = this.ds.scale < 0.6; //zoomed out - - //only render if it forces it to do it - if (this.live_mode) { - if (!node.flags.collapsed) { - ctx.shadowColor = "transparent"; - if (node.onDrawForeground) { - node.onDrawForeground(ctx, this, this.canvas); - } - } - return; - } - - var editor_alpha = this.editor_alpha; - ctx.globalAlpha = editor_alpha; - - if (this.render_shadows && !low_quality) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; - ctx.shadowOffsetX = 2 * this.ds.scale; - ctx.shadowOffsetY = 2 * this.ds.scale; - ctx.shadowBlur = 3 * this.ds.scale; - } else { - ctx.shadowColor = "transparent"; - } - - //custom draw collapsed method (draw after shadows because they are affected) - if ( - node.flags.collapsed && - node.onDrawCollapsed && - node.onDrawCollapsed(ctx, this) == true - ) { - return; - } - - //clip if required (mask) - var shape = node._shape || LiteGraph.BOX_SHAPE; - var size = temp_vec2; - temp_vec2.set(node.size); - var horizontal = node.horizontal; // || node.flags.horizontal; - - if (node.flags.collapsed) { - ctx.font = this.inner_text_font; - var title = node.getTitle ? node.getTitle() : node.title; - if (title != null) { - node._collapsed_width = Math.min( - node.size[0], - ctx.measureText(title).width + - LiteGraph.NODE_TITLE_HEIGHT * 2 - ); //LiteGraph.NODE_COLLAPSED_WIDTH; - size[0] = node._collapsed_width; - size[1] = 0; - } - } - - if (node.clip_area) { - //Start clipping - ctx.save(); - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) { - ctx.rect(0, 0, size[0], size[1]); - } else if (shape == LiteGraph.ROUND_SHAPE) { - ctx.roundRect(0, 0, size[0], size[1], [10]); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ); - } - ctx.clip(); - } - - //draw shape - if (node.has_errors) { - bgcolor = "red"; - } - this.drawNodeShape( - node, - ctx, - size, - color, - bgcolor, - node.is_selected, - node.mouseOver - ); - ctx.shadowColor = "transparent"; - - //draw foreground - if (node.onDrawForeground) { - node.onDrawForeground(ctx, this, this.canvas); - } - - //connection slots - ctx.textAlign = horizontal ? "center" : "left"; - ctx.font = this.inner_text_font; - - var render_text = !low_quality; - - var out_slot = this.connecting_output; - var in_slot = this.connecting_input; - ctx.lineWidth = 1; - - var max_y = 0; - var slot_pos = new Float32Array(2); //to reuse - - //render inputs and outputs - if (!node.flags.collapsed) { - //input connection slots - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - - var slot_type = slot.type; - var slot_shape = slot.shape; - - ctx.globalAlpha = editor_alpha; - //change opacity of incompatible slots when dragging a connection - if ( this.connecting_output && !LiteGraph.isValidConnection( slot.type , out_slot.type) ) { - ctx.globalAlpha = 0.4 * editor_alpha; - } - - ctx.fillStyle = - slot.link != null - ? slot.color_on || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.input_on - : slot.color_off || - this.default_connection_color_byTypeOff[slot_type] || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.input_off; - - var pos = node.getConnectionPos(true, i, slot_pos); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; - } - - ctx.beginPath(); - - if (slot_type == "array"){ - slot_shape = LiteGraph.GRID_SHAPE; // place in addInput? addOutput instead? - } - - var doStroke = true; - - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - if (horizontal) { - ctx.rect( - pos[0] - 5 + 0.5, - pos[1] - 8 + 0.5, - 10, - 14 - ); - } else { - ctx.rect( - pos[0] - 6 + 0.5, - pos[1] - 5 + 0.5, - 14, - 10 - ); - } - } else if (slot_shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(pos[0] + 8, pos[1] + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); - ctx.closePath(); - } else if (slot_shape === LiteGraph.GRID_SHAPE) { - ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); - ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); - ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); - doStroke = false; - } else { - if(low_quality) - ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); //faster - else - ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); - } - ctx.fill(); - - //render name - if (render_text) { - var text = slot.label != null ? slot.label : slot.name; - if (text) { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - if (horizontal || slot.dir == LiteGraph.UP) { - ctx.fillText(text, pos[0], pos[1] - 10); - } else { - ctx.fillText(text, pos[0] + 10, pos[1] + 5); - } - } - } - } - } - - //output connection slots - - ctx.textAlign = horizontal ? "center" : "right"; - ctx.strokeStyle = "black"; - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - - var slot_type = slot.type; - var slot_shape = slot.shape; - - //change opacity of incompatible slots when dragging a connection - if (this.connecting_input && !LiteGraph.isValidConnection( slot_type , in_slot.type) ) { - ctx.globalAlpha = 0.4 * editor_alpha; - } - - var pos = node.getConnectionPos(false, i, slot_pos); - pos[0] -= node.pos[0]; - pos[1] -= node.pos[1]; - if (max_y < pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5) { - max_y = pos[1] + LiteGraph.NODE_SLOT_HEIGHT * 0.5; - } - - ctx.fillStyle = - slot.links && slot.links.length - ? slot.color_on || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.output_on - : slot.color_off || - this.default_connection_color_byTypeOff[slot_type] || - this.default_connection_color_byType[slot_type] || - this.default_connection_color.output_off; - ctx.beginPath(); - //ctx.rect( node.size[0] - 14,i*14,10,10); - - if (slot_type == "array"){ - slot_shape = LiteGraph.GRID_SHAPE; - } - - var doStroke = true; - - if ( - slot_type === LiteGraph.EVENT || - slot_shape === LiteGraph.BOX_SHAPE - ) { - if (horizontal) { - ctx.rect( - pos[0] - 5 + 0.5, - pos[1] - 8 + 0.5, - 10, - 14 - ); - } else { - ctx.rect( - pos[0] - 6 + 0.5, - pos[1] - 5 + 0.5, - 14, - 10 - ); - } - } else if (slot_shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(pos[0] + 8, pos[1] + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5); - ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5); - ctx.closePath(); - } else if (slot_shape === LiteGraph.GRID_SHAPE) { - ctx.rect(pos[0] - 4, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 4, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 4, 2, 2); - ctx.rect(pos[0] - 4, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 1, pos[1] - 1, 2, 2); - ctx.rect(pos[0] + 2, pos[1] - 1, 2, 2); - ctx.rect(pos[0] - 4, pos[1] + 2, 2, 2); - ctx.rect(pos[0] - 1, pos[1] + 2, 2, 2); - ctx.rect(pos[0] + 2, pos[1] + 2, 2, 2); - doStroke = false; - } else { - if(low_quality) - ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8 ); - else - ctx.arc(pos[0], pos[1], 4, 0, Math.PI * 2); - } - - //trigger - //if(slot.node_id != null && slot.slot == -1) - // ctx.fillStyle = "#F85"; - - //if(slot.links != null && slot.links.length) - ctx.fill(); - if(!low_quality && doStroke) - ctx.stroke(); - - //render output name - if (render_text) { - var text = slot.label != null ? slot.label : slot.name; - if (text) { - ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR; - if (horizontal || slot.dir == LiteGraph.DOWN) { - ctx.fillText(text, pos[0], pos[1] - 8); - } else { - ctx.fillText(text, pos[0] - 10, pos[1] + 5); - } - } - } - } - } - - ctx.textAlign = "left"; - ctx.globalAlpha = 1; - - if (node.widgets) { - var widgets_y = max_y; - if (horizontal || node.widgets_up) { - widgets_y = 2; - } - if( node.widgets_start_y != null ) - widgets_y = node.widgets_start_y; - this.drawNodeWidgets( - node, - widgets_y, - ctx, - this.node_widget && this.node_widget[0] == node - ? this.node_widget[1] - : null - ); - } - } else if (this.render_collapsed_slots) { - //if collapsed - var input_slot = null; - var output_slot = null; - - //get first connected slot to render - if (node.inputs) { - for (var i = 0; i < node.inputs.length; i++) { - var slot = node.inputs[i]; - if (slot.link == null) { - continue; - } - input_slot = slot; - break; - } - } - if (node.outputs) { - for (var i = 0; i < node.outputs.length; i++) { - var slot = node.outputs[i]; - if (!slot.links || !slot.links.length) { - continue; - } - output_slot = slot; - } - } - - if (input_slot) { - var x = 0; - var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center - if (horizontal) { - x = node._collapsed_width * 0.5; - y = -LiteGraph.NODE_TITLE_HEIGHT; - } - ctx.fillStyle = "#686"; - ctx.beginPath(); - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8); - } else if (slot.shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(x + 8, y); - ctx.lineTo(x + -4, y - 4); - ctx.lineTo(x + -4, y + 4); - ctx.closePath(); - } else { - ctx.arc(x, y, 4, 0, Math.PI * 2); - } - ctx.fill(); - } - - if (output_slot) { - var x = node._collapsed_width; - var y = LiteGraph.NODE_TITLE_HEIGHT * -0.5; //center - if (horizontal) { - x = node._collapsed_width * 0.5; - y = 0; - } - ctx.fillStyle = "#686"; - ctx.strokeStyle = "black"; - ctx.beginPath(); - if ( - slot.type === LiteGraph.EVENT || - slot.shape === LiteGraph.BOX_SHAPE - ) { - ctx.rect(x - 7 + 0.5, y - 4, 14, 8); - } else if (slot.shape === LiteGraph.ARROW_SHAPE) { - ctx.moveTo(x + 6, y); - ctx.lineTo(x - 6, y - 4); - ctx.lineTo(x - 6, y + 4); - ctx.closePath(); - } else { - ctx.arc(x, y, 4, 0, Math.PI * 2); - } - ctx.fill(); - //ctx.stroke(); - } - } - - if (node.clip_area) { - ctx.restore(); - } - - ctx.globalAlpha = 1.0; - }; - - //used by this.over_link_center - LGraphCanvas.prototype.drawLinkTooltip = function( ctx, link ) - { - var pos = link._pos; - ctx.fillStyle = "black"; - ctx.beginPath(); - ctx.arc( pos[0], pos[1], 3, 0, Math.PI * 2 ); - ctx.fill(); - - if(link.data == null) - return; - - if(this.onDrawLinkTooltip) - if( this.onDrawLinkTooltip(ctx,link,this) == true ) - return; - - var data = link.data; - var text = null; - - if( data.constructor === Number ) - text = data.toFixed(2); - else if( data.constructor === String ) - text = "\"" + data + "\""; - else if( data.constructor === Boolean ) - text = String(data); - else if (data.toToolTip) - text = data.toToolTip(); - else - text = "[" + data.constructor.name + "]"; - - if(text == null) - return; - text = text.substr(0,30); //avoid weird - - ctx.font = "14px Courier New"; - var info = ctx.measureText(text); - var w = info.width + 20; - var h = 24; - ctx.shadowColor = "black"; - ctx.shadowOffsetX = 2; - ctx.shadowOffsetY = 2; - ctx.shadowBlur = 3; - ctx.fillStyle = "#454"; - ctx.beginPath(); - ctx.roundRect( pos[0] - w*0.5, pos[1] - 15 - h, w, h, [3]); - ctx.moveTo( pos[0] - 10, pos[1] - 15 ); - ctx.lineTo( pos[0] + 10, pos[1] - 15 ); - ctx.lineTo( pos[0], pos[1] - 5 ); - ctx.fill(); - ctx.shadowColor = "transparent"; - ctx.textAlign = "center"; - ctx.fillStyle = "#CEC"; - ctx.fillText(text, pos[0], pos[1] - 15 - h * 0.3); - } - - /** - * draws the shape of the given node in the canvas - * @method drawNodeShape - **/ - var tmp_area = new Float32Array(4); - - LGraphCanvas.prototype.drawNodeShape = function( - node, - ctx, - size, - fgcolor, - bgcolor, - selected, - mouse_over - ) { - //bg rect - ctx.strokeStyle = fgcolor; - ctx.fillStyle = bgcolor; - - var title_height = LiteGraph.NODE_TITLE_HEIGHT; - var low_quality = this.ds.scale < 0.5; - - //render node area depending on shape - var shape = - node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; - - var title_mode = node.constructor.title_mode; - - var render_title = true; - if (title_mode == LiteGraph.TRANSPARENT_TITLE || title_mode == LiteGraph.NO_TITLE) { - render_title = false; - } else if (title_mode == LiteGraph.AUTOHIDE_TITLE && mouse_over) { - render_title = true; - } - - var area = tmp_area; - area[0] = 0; //x - area[1] = render_title ? -title_height : 0; //y - area[2] = size[0] + 1; //w - area[3] = render_title ? size[1] + title_height : size[1]; //h - - var old_alpha = ctx.globalAlpha; - - //full node shape - //if(node.flags.collapsed) - { - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE || low_quality) { - ctx.fillRect(area[0], area[1], area[2], area[3]); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - shape == LiteGraph.CARD_SHAPE - ) { - ctx.roundRect( - area[0], - area[1], - area[2], - area[3], - shape == LiteGraph.CARD_SHAPE ? [this.round_radius,this.round_radius,0,0] : [this.round_radius] - ); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5, - 0, - Math.PI * 2 - ); - } - ctx.fill(); - - //separator - if(!node.flags.collapsed && render_title) - { - ctx.shadowColor = "transparent"; - ctx.fillStyle = "rgba(0,0,0,0.2)"; - ctx.fillRect(0, -1, area[2], 2); - } - } - ctx.shadowColor = "transparent"; - - if (node.onDrawBackground) { - node.onDrawBackground(ctx, this, this.canvas, this.graph_mouse ); - } - - //title bg (remember, it is rendered ABOVE the node) - if (render_title || title_mode == LiteGraph.TRANSPARENT_TITLE) { - //title bar - if (node.onDrawTitleBar) { - node.onDrawTitleBar( ctx, title_height, size, this.ds.scale, fgcolor ); - } else if ( - title_mode != LiteGraph.TRANSPARENT_TITLE && - (node.constructor.title_color || this.render_title_colored) - ) { - var title_color = node.constructor.title_color || fgcolor; - - if (node.flags.collapsed) { - ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR; - } - - //* gradient test - if (this.use_gradients) { - var grad = LGraphCanvas.gradients[title_color]; - if (!grad) { - grad = LGraphCanvas.gradients[ title_color ] = ctx.createLinearGradient(0, 0, 400, 0); - grad.addColorStop(0, title_color); // TODO refactor: validate color !! prevent DOMException - grad.addColorStop(1, "#000"); - } - ctx.fillStyle = grad; - } else { - ctx.fillStyle = title_color; - } - - //ctx.globalAlpha = 0.5 * old_alpha; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE || low_quality) { - ctx.rect(0, -title_height, size[0] + 1, title_height); - } else if ( shape == LiteGraph.ROUND_SHAPE || shape == LiteGraph.CARD_SHAPE ) { - ctx.roundRect( - 0, - -title_height, - size[0] + 1, - title_height, - node.flags.collapsed ? [this.round_radius] : [this.round_radius,this.round_radius,0,0] - ); - } - ctx.fill(); - ctx.shadowColor = "transparent"; - } - - var colState = false; - if (LiteGraph.node_box_coloured_by_mode){ - if(LiteGraph.NODE_MODES_COLORS[node.mode]){ - colState = LiteGraph.NODE_MODES_COLORS[node.mode]; - } - } - if (LiteGraph.node_box_coloured_when_on){ - colState = node.action_triggered ? "#FFF" : (node.execute_triggered ? "#AAA" : colState); - } - - //title box - var box_size = 10; - if (node.onDrawTitleBox) { - node.onDrawTitleBox(ctx, title_height, size, this.ds.scale); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - shape == LiteGraph.CIRCLE_SHAPE || - shape == LiteGraph.CARD_SHAPE - ) { - if (low_quality) { - ctx.fillStyle = "black"; - ctx.beginPath(); - ctx.arc( - title_height * 0.5, - title_height * -0.5, - box_size * 0.5 + 1, - 0, - Math.PI * 2 - ); - ctx.fill(); - } - - ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; - if(low_quality) - ctx.fillRect( title_height * 0.5 - box_size *0.5, title_height * -0.5 - box_size *0.5, box_size , box_size ); - else - { - ctx.beginPath(); - ctx.arc( - title_height * 0.5, - title_height * -0.5, - box_size * 0.5, - 0, - Math.PI * 2 - ); - ctx.fill(); - } - } else { - if (low_quality) { - ctx.fillStyle = "black"; - ctx.fillRect( - (title_height - box_size) * 0.5 - 1, - (title_height + box_size) * -0.5 - 1, - box_size + 2, - box_size + 2 - ); - } - ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR; - ctx.fillRect( - (title_height - box_size) * 0.5, - (title_height + box_size) * -0.5, - box_size, - box_size - ); - } - ctx.globalAlpha = old_alpha; - - //title text - if (node.onDrawTitleText) { - node.onDrawTitleText( - ctx, - title_height, - size, - this.ds.scale, - this.title_text_font, - selected - ); - } - if (!low_quality) { - ctx.font = this.title_text_font; - var title = String(node.getTitle()); - if (title) { - if (selected) { - ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR; - } else { - ctx.fillStyle = - node.constructor.title_text_color || - this.node_title_color; - } - if (node.flags.collapsed) { - ctx.textAlign = "left"; - var measure = ctx.measureText(title); - ctx.fillText( - title.substr(0,20), //avoid urls too long - title_height,// + measure.width * 0.5, - LiteGraph.NODE_TITLE_TEXT_Y - title_height - ); - ctx.textAlign = "left"; - } else { - ctx.textAlign = "left"; - ctx.fillText( - title, - title_height, - LiteGraph.NODE_TITLE_TEXT_Y - title_height - ); - } - } - } - - //subgraph box - if (!node.flags.collapsed && node.subgraph && !node.skip_subgraph_button) { - var w = LiteGraph.NODE_TITLE_HEIGHT; - var x = node.size[0] - w; - var over = LiteGraph.isInsideRectangle( this.graph_mouse[0] - node.pos[0], this.graph_mouse[1] - node.pos[1], x+2, -w+2, w-4, w-4 ); - ctx.fillStyle = over ? "#888" : "#555"; - if( shape == LiteGraph.BOX_SHAPE || low_quality) - ctx.fillRect(x+2, -w+2, w-4, w-4); - else - { - ctx.beginPath(); - ctx.roundRect(x+2, -w+2, w-4, w-4,[4]); - ctx.fill(); - } - ctx.fillStyle = "#333"; - ctx.beginPath(); - ctx.moveTo(x + w * 0.2, -w * 0.6); - ctx.lineTo(x + w * 0.8, -w * 0.6); - ctx.lineTo(x + w * 0.5, -w * 0.3); - ctx.fill(); - } - - //custom title render - if (node.onDrawTitle) { - node.onDrawTitle(ctx); - } - } - - //render selection marker - if (selected) { - if (node.onBounding) { - node.onBounding(area); - } - - if (title_mode == LiteGraph.TRANSPARENT_TITLE) { - area[1] -= title_height; - area[3] += title_height; - } - ctx.lineWidth = 1; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) { - ctx.rect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3] - ); - } else if ( - shape == LiteGraph.ROUND_SHAPE || - (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed) - ) { - ctx.roundRect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3], - [this.round_radius * 2] - ); - } else if (shape == LiteGraph.CARD_SHAPE) { - ctx.roundRect( - -6 + area[0], - -6 + area[1], - 12 + area[2], - 12 + area[3], - [this.round_radius * 2,2,this.round_radius * 2,2] - ); - } else if (shape == LiteGraph.CIRCLE_SHAPE) { - ctx.arc( - size[0] * 0.5, - size[1] * 0.5, - size[0] * 0.5 + 6, - 0, - Math.PI * 2 - ); - } - ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - ctx.globalAlpha = 1; - } - - // these counter helps in conditioning drawing based on if the node has been executed or an action occurred - if (node.execute_triggered>0) node.execute_triggered--; - if (node.action_triggered>0) node.action_triggered--; - }; - - var margin_area = new Float32Array(4); - var link_bounding = new Float32Array(4); - var tempA = new Float32Array(2); - var tempB = new Float32Array(2); - - /** - * draws every connection visible in the canvas - * OPTIMIZE THIS: pre-catch connections position instead of recomputing them every time - * @method drawConnections - **/ - LGraphCanvas.prototype.drawConnections = function(ctx) { - var now = LiteGraph.getTime(); - var visible_area = this.visible_area; - margin_area[0] = visible_area[0] - 20; - margin_area[1] = visible_area[1] - 20; - margin_area[2] = visible_area[2] + 40; - margin_area[3] = visible_area[3] + 40; - - //draw connections - ctx.lineWidth = this.connections_width; - - ctx.fillStyle = "#AAA"; - ctx.strokeStyle = "#AAA"; - ctx.globalAlpha = this.editor_alpha; - //for every node - var nodes = this.graph._nodes; - for (var n = 0, l = nodes.length; n < l; ++n) { - var node = nodes[n]; - //for every input (we render just inputs because it is easier as every slot can only have one input) - if (!node.inputs || !node.inputs.length) { - continue; - } - - for (var i = 0; i < node.inputs.length; ++i) { - var input = node.inputs[i]; - if (!input || input.link == null) { - continue; - } - var link_id = input.link; - var link = this.graph.links[link_id]; - if (!link) { - continue; - } - - //find link info - var start_node = this.graph.getNodeById(link.origin_id); - if (start_node == null) { - continue; - } - var start_node_slot = link.origin_slot; - var start_node_slotpos = null; - if (start_node_slot == -1) { - start_node_slotpos = [ - start_node.pos[0] + 10, - start_node.pos[1] + 10 - ]; - } else { - start_node_slotpos = start_node.getConnectionPos( - false, - start_node_slot, - tempA - ); - } - var end_node_slotpos = node.getConnectionPos(true, i, tempB); - - //compute link bounding - link_bounding[0] = start_node_slotpos[0]; - link_bounding[1] = start_node_slotpos[1]; - link_bounding[2] = end_node_slotpos[0] - start_node_slotpos[0]; - link_bounding[3] = end_node_slotpos[1] - start_node_slotpos[1]; - if (link_bounding[2] < 0) { - link_bounding[0] += link_bounding[2]; - link_bounding[2] = Math.abs(link_bounding[2]); - } - if (link_bounding[3] < 0) { - link_bounding[1] += link_bounding[3]; - link_bounding[3] = Math.abs(link_bounding[3]); - } - - //skip links outside of the visible area of the canvas - if (!overlapBounding(link_bounding, margin_area)) { - continue; - } - - var start_slot = start_node.outputs[start_node_slot]; - var end_slot = node.inputs[i]; - if (!start_slot || !end_slot) { - continue; - } - var start_dir = - start_slot.dir || - (start_node.horizontal ? LiteGraph.DOWN : LiteGraph.RIGHT); - var end_dir = - end_slot.dir || - (node.horizontal ? LiteGraph.UP : LiteGraph.LEFT); - - this.renderLink( - ctx, - start_node_slotpos, - end_node_slotpos, - link, - false, - 0, - null, - start_dir, - end_dir - ); - - //event triggered rendered on top - if (link && link._last_time && now - link._last_time < 1000) { - var f = 2.0 - (now - link._last_time) * 0.002; - var tmp = ctx.globalAlpha; - ctx.globalAlpha = tmp * f; - this.renderLink( - ctx, - start_node_slotpos, - end_node_slotpos, - link, - true, - f, - "white", - start_dir, - end_dir - ); - ctx.globalAlpha = tmp; - } - } - } - ctx.globalAlpha = 1; - }; - - /** - * draws a link between two points - * @method renderLink - * @param {vec2} a start pos - * @param {vec2} b end pos - * @param {Object} link the link object with all the link info - * @param {boolean} skip_border ignore the shadow of the link - * @param {boolean} flow show flow animation (for events) - * @param {string} color the color for the link - * @param {number} start_dir the direction enum - * @param {number} end_dir the direction enum - * @param {number} num_sublines number of sublines (useful to represent vec3 or rgb) - **/ - LGraphCanvas.prototype.renderLink = function( - ctx, - a, - b, - link, - skip_border, - flow, - color, - start_dir, - end_dir, - num_sublines - ) { - if (link) { - this.visible_links.push(link); - } - - //choose color - if (!color && link) { - color = link.color || LGraphCanvas.link_type_colors[link.type]; - } - if (!color) { - color = this.default_link_color; - } - if (link != null && this.highlighted_links[link.id]) { - color = "#FFF"; - } - - start_dir = start_dir || LiteGraph.RIGHT; - end_dir = end_dir || LiteGraph.LEFT; - - var dist = distance(a, b); - - if (this.render_connections_border && this.ds.scale > 0.6) { - ctx.lineWidth = this.connections_width + 4; - } - ctx.lineJoin = "round"; - num_sublines = num_sublines || 1; - if (num_sublines > 1) { - ctx.lineWidth = 0.5; - } - - //begin line shape - ctx.beginPath(); - for (var i = 0; i < num_sublines; i += 1) { - var offsety = (i - (num_sublines - 1) * 0.5) * 5; - - if (this.links_render_mode == LiteGraph.SPLINE_LINK) { - ctx.moveTo(a[0], a[1] + offsety); - var start_offset_x = 0; - var start_offset_y = 0; - var end_offset_x = 0; - var end_offset_y = 0; - switch (start_dir) { - case LiteGraph.LEFT: - start_offset_x = dist * -0.25; - break; - case LiteGraph.RIGHT: - start_offset_x = dist * 0.25; - break; - case LiteGraph.UP: - start_offset_y = dist * -0.25; - break; - case LiteGraph.DOWN: - start_offset_y = dist * 0.25; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - end_offset_x = dist * -0.25; - break; - case LiteGraph.RIGHT: - end_offset_x = dist * 0.25; - break; - case LiteGraph.UP: - end_offset_y = dist * -0.25; - break; - case LiteGraph.DOWN: - end_offset_y = dist * 0.25; - break; - } - ctx.bezierCurveTo( - a[0] + start_offset_x, - a[1] + start_offset_y + offsety, - b[0] + end_offset_x, - b[1] + end_offset_y + offsety, - b[0], - b[1] + offsety - ); - } else if (this.links_render_mode == LiteGraph.LINEAR_LINK) { - ctx.moveTo(a[0], a[1] + offsety); - var start_offset_x = 0; - var start_offset_y = 0; - var end_offset_x = 0; - var end_offset_y = 0; - switch (start_dir) { - case LiteGraph.LEFT: - start_offset_x = -1; - break; - case LiteGraph.RIGHT: - start_offset_x = 1; - break; - case LiteGraph.UP: - start_offset_y = -1; - break; - case LiteGraph.DOWN: - start_offset_y = 1; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - end_offset_x = -1; - break; - case LiteGraph.RIGHT: - end_offset_x = 1; - break; - case LiteGraph.UP: - end_offset_y = -1; - break; - case LiteGraph.DOWN: - end_offset_y = 1; - break; - } - var l = 15; - ctx.lineTo( - a[0] + start_offset_x * l, - a[1] + start_offset_y * l + offsety - ); - ctx.lineTo( - b[0] + end_offset_x * l, - b[1] + end_offset_y * l + offsety - ); - ctx.lineTo(b[0], b[1] + offsety); - } else if (this.links_render_mode == LiteGraph.STRAIGHT_LINK) { - ctx.moveTo(a[0], a[1]); - var start_x = a[0]; - var start_y = a[1]; - var end_x = b[0]; - var end_y = b[1]; - if (start_dir == LiteGraph.RIGHT) { - start_x += 10; - } else { - start_y += 10; - } - if (end_dir == LiteGraph.LEFT) { - end_x -= 10; - } else { - end_y -= 10; - } - ctx.lineTo(start_x, start_y); - ctx.lineTo((start_x + end_x) * 0.5, start_y); - ctx.lineTo((start_x + end_x) * 0.5, end_y); - ctx.lineTo(end_x, end_y); - ctx.lineTo(b[0], b[1]); - } else { - return; - } //unknown - } - - //rendering the outline of the connection can be a little bit slow - if ( - this.render_connections_border && - this.ds.scale > 0.6 && - !skip_border - ) { - ctx.strokeStyle = "rgba(0,0,0,0.5)"; - ctx.stroke(); - } - - ctx.lineWidth = this.connections_width; - ctx.fillStyle = ctx.strokeStyle = color; - ctx.stroke(); - //end line shape - - var pos = this.computeConnectionPoint(a, b, 0.5, start_dir, end_dir); - if (link && link._pos) { - link._pos[0] = pos[0]; - link._pos[1] = pos[1]; - } - - //render arrow in the middle - if ( - this.ds.scale >= 0.6 && - this.highquality_render && - end_dir != LiteGraph.CENTER - ) { - //render arrow - if (this.render_connection_arrows) { - //compute two points in the connection - var posA = this.computeConnectionPoint( - a, - b, - 0.25, - start_dir, - end_dir - ); - var posB = this.computeConnectionPoint( - a, - b, - 0.26, - start_dir, - end_dir - ); - var posC = this.computeConnectionPoint( - a, - b, - 0.75, - start_dir, - end_dir - ); - var posD = this.computeConnectionPoint( - a, - b, - 0.76, - start_dir, - end_dir - ); - - //compute the angle between them so the arrow points in the right direction - var angleA = 0; - var angleB = 0; - if (this.render_curved_connections) { - angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1]); - angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1]); - } else { - angleB = angleA = b[1] > a[1] ? 0 : Math.PI; - } - - //render arrow - ctx.save(); - ctx.translate(posA[0], posA[1]); - ctx.rotate(angleA); - ctx.beginPath(); - ctx.moveTo(-5, -3); - ctx.lineTo(0, +7); - ctx.lineTo(+5, -3); - ctx.fill(); - ctx.restore(); - ctx.save(); - ctx.translate(posC[0], posC[1]); - ctx.rotate(angleB); - ctx.beginPath(); - ctx.moveTo(-5, -3); - ctx.lineTo(0, +7); - ctx.lineTo(+5, -3); - ctx.fill(); - ctx.restore(); - } - - //circle - ctx.beginPath(); - ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2); - ctx.fill(); - } - - //render flowing points - if (flow) { - ctx.fillStyle = color; - for (var i = 0; i < 5; ++i) { - var f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1; - var pos = this.computeConnectionPoint( - a, - b, - f, - start_dir, - end_dir - ); - ctx.beginPath(); - ctx.arc(pos[0], pos[1], 5, 0, 2 * Math.PI); - ctx.fill(); - } - } - }; - - //returns the link center point based on curvature - LGraphCanvas.prototype.computeConnectionPoint = function( - a, - b, - t, - start_dir, - end_dir - ) { - start_dir = start_dir || LiteGraph.RIGHT; - end_dir = end_dir || LiteGraph.LEFT; - - var dist = distance(a, b); - var p0 = a; - var p1 = [a[0], a[1]]; - var p2 = [b[0], b[1]]; - var p3 = b; - - switch (start_dir) { - case LiteGraph.LEFT: - p1[0] += dist * -0.25; - break; - case LiteGraph.RIGHT: - p1[0] += dist * 0.25; - break; - case LiteGraph.UP: - p1[1] += dist * -0.25; - break; - case LiteGraph.DOWN: - p1[1] += dist * 0.25; - break; - } - switch (end_dir) { - case LiteGraph.LEFT: - p2[0] += dist * -0.25; - break; - case LiteGraph.RIGHT: - p2[0] += dist * 0.25; - break; - case LiteGraph.UP: - p2[1] += dist * -0.25; - break; - case LiteGraph.DOWN: - p2[1] += dist * 0.25; - break; - } - - var c1 = (1 - t) * (1 - t) * (1 - t); - var c2 = 3 * ((1 - t) * (1 - t)) * t; - var c3 = 3 * (1 - t) * (t * t); - var c4 = t * t * t; - - var x = c1 * p0[0] + c2 * p1[0] + c3 * p2[0] + c4 * p3[0]; - var y = c1 * p0[1] + c2 * p1[1] + c3 * p2[1] + c4 * p3[1]; - return [x, y]; - }; - - LGraphCanvas.prototype.drawExecutionOrder = function(ctx) { - ctx.shadowColor = "transparent"; - ctx.globalAlpha = 0.25; - - ctx.textAlign = "center"; - ctx.strokeStyle = "white"; - ctx.globalAlpha = 0.75; - - var visible_nodes = this.visible_nodes; - for (var i = 0; i < visible_nodes.length; ++i) { - var node = visible_nodes[i]; - ctx.fillStyle = "black"; - ctx.fillRect( - node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT, - node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT - ); - if (node.order == 0) { - ctx.strokeRect( - node.pos[0] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, - node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5, - LiteGraph.NODE_TITLE_HEIGHT, - LiteGraph.NODE_TITLE_HEIGHT - ); - } - ctx.fillStyle = "#FFF"; - ctx.fillText( - node.order, - node.pos[0] + LiteGraph.NODE_TITLE_HEIGHT * -0.5, - node.pos[1] - 6 - ); - } - ctx.globalAlpha = 1; - }; - - /** - * draws the widgets stored inside a node - * @method drawNodeWidgets - **/ - LGraphCanvas.prototype.drawNodeWidgets = function( - node, - posY, - ctx, - active_widget - ) { - if (!node.widgets || !node.widgets.length) { - return 0; - } - var width = node.size[0]; - var widgets = node.widgets; - posY += 2; - var H = LiteGraph.NODE_WIDGET_HEIGHT; - var show_text = this.ds.scale > 0.5; - ctx.save(); - ctx.globalAlpha = this.editor_alpha; - var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; - var background_color = LiteGraph.WIDGET_BGCOLOR; - var text_color = LiteGraph.WIDGET_TEXT_COLOR; - var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; - var margin = 15; - - for (var i = 0; i < widgets.length; ++i) { - var w = widgets[i]; - var y = posY; - if (w.y) { - y = w.y; - } - w.last_y = y; - ctx.strokeStyle = outline_color; - ctx.fillStyle = "#222"; - ctx.textAlign = "left"; - //ctx.lineWidth = 2; - if(w.disabled) - ctx.globalAlpha *= 0.5; - var widget_width = w.width || width; - - switch (w.type) { - case "button": - ctx.fillStyle = background_color; - if (w.clicked) { - ctx.fillStyle = "#AAA"; - w.clicked = false; - this.dirty_canvas = true; - } - ctx.fillRect(margin, y, widget_width - margin * 2, H); - if(show_text && !w.disabled) - ctx.strokeRect( margin, y, widget_width - margin * 2, H ); - if (show_text) { - ctx.textAlign = "center"; - ctx.fillStyle = text_color; - ctx.fillText(w.label || w.name, widget_width * 0.5, y + H * 0.7); - } - break; - case "toggle": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect(margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if(show_text && !w.disabled) - ctx.stroke(); - ctx.fillStyle = w.value ? "#89A" : "#333"; - ctx.beginPath(); - ctx.arc( widget_width - margin * 2, y + H * 0.5, H * 0.36, 0, Math.PI * 2 ); - ctx.fill(); - if (show_text) { - ctx.fillStyle = secondary_text_color; - const label = w.label || w.name; - if (label != null) { - ctx.fillText(label, margin * 2, y + H * 0.7); - } - ctx.fillStyle = w.value ? text_color : secondary_text_color; - ctx.textAlign = "right"; - ctx.fillText( - w.value - ? w.options.on || "true" - : w.options.off || "false", - widget_width - 40, - y + H * 0.7 - ); - } - break; - case "slider": - ctx.fillStyle = background_color; - ctx.fillRect(margin, y, widget_width - margin * 2, H); - var range = w.options.max - w.options.min; - var nvalue = (w.value - w.options.min) / range; - if(nvalue < 0.0) nvalue = 0.0; - if(nvalue > 1.0) nvalue = 1.0; - ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#89A" : "#678"); - ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H); - if(show_text && !w.disabled) - ctx.strokeRect(margin, y, widget_width - margin * 2, H); - if (w.marker) { - var marker_nvalue = (w.marker - w.options.min) / range; - if(marker_nvalue < 0.0) marker_nvalue = 0.0; - if(marker_nvalue > 1.0) marker_nvalue = 1.0; - ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9"; - ctx.fillRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H ); - } - if (show_text) { - ctx.textAlign = "center"; - ctx.fillStyle = text_color; - ctx.fillText( - w.label || w.name + " " + Number(w.value).toFixed( - w.options.precision != null - ? w.options.precision - : 3 - ), - widget_width * 0.5, - y + H * 0.7 - ); - } - break; - case "number": - case "combo": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if(show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5] ); - else - ctx.rect(margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if (show_text) { - if(!w.disabled) - ctx.stroke(); - ctx.fillStyle = text_color; - if(!w.disabled) - { - ctx.beginPath(); - ctx.moveTo(margin + 16, y + 5); - ctx.lineTo(margin + 6, y + H * 0.5); - ctx.lineTo(margin + 16, y + H - 5); - ctx.fill(); - ctx.beginPath(); - ctx.moveTo(widget_width - margin - 16, y + 5); - ctx.lineTo(widget_width - margin - 6, y + H * 0.5); - ctx.lineTo(widget_width - margin - 16, y + H - 5); - ctx.fill(); - } - ctx.fillStyle = secondary_text_color; - ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7); - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - if (w.type == "number") { - ctx.fillText( - Number(w.value).toFixed( - w.options.precision !== undefined - ? w.options.precision - : 3 - ), - widget_width - margin * 2 - 20, - y + H * 0.7 - ); - } else { - var v = w.value; - if( w.options.values ) - { - var values = w.options.values; - if( values.constructor === Function ) - values = values(); - if(values && values.constructor !== Array) - v = values[ w.value ]; - } - ctx.fillText( - v, - widget_width - margin * 2 - 20, - y + H * 0.7 - ); - } - } - break; - case "string": - case "text": - ctx.textAlign = "left"; - ctx.strokeStyle = outline_color; - ctx.fillStyle = background_color; - ctx.beginPath(); - if (show_text) - ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); - else - ctx.rect( margin, y, widget_width - margin * 2, H ); - ctx.fill(); - if (show_text) { - if(!w.disabled) - ctx.stroke(); - ctx.save(); - ctx.beginPath(); - ctx.rect(margin, y, widget_width - margin * 2, H); - ctx.clip(); - - //ctx.stroke(); - ctx.fillStyle = secondary_text_color; - const label = w.label || w.name; - if (label != null) { - ctx.fillText(label, margin * 2, y + H * 0.7); - } - ctx.fillStyle = text_color; - ctx.textAlign = "right"; - ctx.fillText(String(w.value).substr(0,30), widget_width - margin * 2, y + H * 0.7); //30 chars max - ctx.restore(); - } - break; - default: - if (w.draw) { - w.draw(ctx, node, widget_width, y, H); - } - break; - } - posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4; - ctx.globalAlpha = this.editor_alpha; - - } - ctx.restore(); - ctx.textAlign = "left"; - }; - - /** - * process an event on widgets - * @method processNodeWidgets - **/ - LGraphCanvas.prototype.processNodeWidgets = function( - node, - pos, - event, - active_widget - ) { - if (!node.widgets || !node.widgets.length || (!this.allow_interaction && !node.flags.allow_interaction)) { - return null; - } - - var x = pos[0] - node.pos[0]; - var y = pos[1] - node.pos[1]; - var width = node.size[0]; - var that = this; - var ref_window = this.getCanvasWindow(); - - for (var i = 0; i < node.widgets.length; ++i) { - var w = node.widgets[i]; - if(!w || w.disabled) - continue; - var widget_height = w.computeSize ? w.computeSize(width)[1] : LiteGraph.NODE_WIDGET_HEIGHT; - var widget_width = w.width || width; - //outside - if ( w != active_widget && - (x < 6 || x > widget_width - 12 || y < w.last_y || y > w.last_y + widget_height || w.last_y === undefined) ) - continue; - - var old_value = w.value; - - //if ( w == active_widget || (x > 6 && x < widget_width - 12 && y > w.last_y && y < w.last_y + widget_height) ) { - //inside widget - switch (w.type) { - case "button": - if (event.type === LiteGraph.pointerevents_method+"down") { - if (w.callback) { - setTimeout(function() { - w.callback(w, that, node, pos, event); - }, 20); - } - w.clicked = true; - this.dirty_canvas = true; - } - break; - case "slider": - var old_value = w.value; - var nvalue = clamp((x - 15) / (widget_width - 30), 0, 1); - if(w.options.read_only) break; - w.value = w.options.min + (w.options.max - w.options.min) * nvalue; - if (old_value != w.value) { - setTimeout(function() { - inner_value_change(w, w.value); - }, 20); - } - this.dirty_canvas = true; - break; - case "number": - case "combo": - var old_value = w.value; - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - var allow_scroll = true; - if (delta) { - if (x > -3 && x < widget_width + 3) { - allow_scroll = false; - } - } - if (allow_scroll && event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") { - if(event.deltaX) - w.value += event.deltaX * 0.1 * (w.options.step || 1); - if ( w.options.min != null && w.value < w.options.min ) { - w.value = w.options.min; - } - if ( w.options.max != null && w.value > w.options.max ) { - w.value = w.options.max; - } - } else if (event.type == LiteGraph.pointerevents_method+"down") { - var values = w.options.values; - if (values && values.constructor === Function) { - values = w.options.values(w, node); - } - var values_list = null; - - if( w.type != "number") - values_list = values.constructor === Array ? values : Object.keys(values); - - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (w.type == "number") { - w.value += delta * 0.1 * (w.options.step || 1); - if ( w.options.min != null && w.value < w.options.min ) { - w.value = w.options.min; - } - if ( w.options.max != null && w.value > w.options.max ) { - w.value = w.options.max; - } - } else if (delta) { //clicked in arrow, used for combos - var index = -1; - this.last_mouseclick = 0; //avoids dobl click event - if(values.constructor === Object) - index = values_list.indexOf( String( w.value ) ) + delta; - else - index = values_list.indexOf( w.value ) + delta; - if (index >= values_list.length) { - index = values_list.length - 1; - } - if (index < 0) { - index = 0; - } - if( values.constructor === Array ) - w.value = values[index]; - else - w.value = index; - } else { //combo clicked - var text_values = values != values_list ? Object.values(values) : values; - var menu = new LiteGraph.ContextMenu(text_values, { - scale: Math.max(1, this.ds.scale), - event: event, - className: "dark", - callback: inner_clicked.bind(w) - }, - ref_window); - function inner_clicked(v, option, event) { - if(values != values_list) - v = text_values.indexOf(v); - this.value = v; - inner_value_change(this, v); - that.dirty_canvas = true; - return false; - } - } - } //end mousedown - else if(event.type == LiteGraph.pointerevents_method+"up" && w.type == "number") - { - var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; - if (event.click_time < 200 && delta == 0) { - this.prompt("Value",w.value,function(v) { - // check if v is a valid equation or a number - if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { - try {//solve the equation if possible - v = eval(v); - } catch (e) { } - } - this.value = Number(v); - inner_value_change(this, this.value); - }.bind(w), - event); - } - } - - if( old_value != w.value ) - setTimeout( - function() { - inner_value_change(this, this.value); - }.bind(w), - 20 - ); - this.dirty_canvas = true; - break; - case "toggle": - if (event.type == LiteGraph.pointerevents_method+"down") { - w.value = !w.value; - setTimeout(function() { - inner_value_change(w, w.value); - }, 20); - } - break; - case "string": - case "text": - if (event.type == LiteGraph.pointerevents_method+"down") { - this.prompt("Value",w.value,function(v) { - inner_value_change(this, v); - }.bind(w), - event,w.options ? w.options.multiline : false ); - } - break; - default: - if (w.mouse) { - this.dirty_canvas = w.mouse(event, [x, y], node); - } - break; - } //end switch - - //value changed - if( old_value != w.value ) - { - if(node.onWidgetChanged) - node.onWidgetChanged( w.name,w.value,old_value,w ); - node.graph._version++; - } - - return w; - }//end for - - function inner_value_change(widget, value) { - if(widget.type == "number"){ - value = Number(value); - } - widget.value = value; - if ( widget.options && widget.options.property && node.properties[widget.options.property] !== undefined ) { - node.setProperty( widget.options.property, value ); - } - if (widget.callback) { - widget.callback(widget.value, that, node, pos, event); - } - } - - return null; - }; - - /** - * draws every group area in the background - * @method drawGroups - **/ - LGraphCanvas.prototype.drawGroups = function(canvas, ctx) { - if (!this.graph) { - return; - } - - var groups = this.graph._groups; - - ctx.save(); - ctx.globalAlpha = 0.5 * this.editor_alpha; - - for (var i = 0; i < groups.length; ++i) { - var group = groups[i]; - - if (!overlapBounding(this.visible_area, group._bounding)) { - continue; - } //out of the visible area - - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25 * this.editor_alpha; - ctx.beginPath(); - ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], size[1]); - ctx.fill(); - ctx.globalAlpha = this.editor_alpha; - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(pos[0] + size[0], pos[1] + size[1]); - ctx.lineTo(pos[0] + size[0] - 10, pos[1] + size[1]); - ctx.lineTo(pos[0] + size[0], pos[1] + size[1] - 10); - ctx.fill(); - - var font_size = - group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - ctx.font = font_size + "px Arial"; - ctx.textAlign = "left"; - ctx.fillText(group.title, pos[0] + 4, pos[1] + font_size); - } - - ctx.restore(); - }; - - LGraphCanvas.prototype.adjustNodesSize = function() { - var nodes = this.graph._nodes; - for (var i = 0; i < nodes.length; ++i) { - nodes[i].size = nodes[i].computeSize(); - } - this.setDirty(true, true); - }; - - /** - * resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode - * @method resize - **/ - LGraphCanvas.prototype.resize = function(width, height) { - if (!width && !height) { - var parent = this.canvas.parentNode; - width = parent.offsetWidth; - height = parent.offsetHeight; - } - - if (this.canvas.width == width && this.canvas.height == height) { - return; - } - - this.canvas.width = width; - this.canvas.height = height; - this.bgcanvas.width = this.canvas.width; - this.bgcanvas.height = this.canvas.height; - this.setDirty(true, true); - }; - - /** - * switches to live mode (node shapes are not rendered, only the content) - * this feature was designed when graphs where meant to create user interfaces - * @method switchLiveMode - **/ - LGraphCanvas.prototype.switchLiveMode = function(transition) { - if (!transition) { - this.live_mode = !this.live_mode; - this.dirty_canvas = true; - this.dirty_bgcanvas = true; - return; - } - - var self = this; - var delta = this.live_mode ? 1.1 : 0.9; - if (this.live_mode) { - this.live_mode = false; - this.editor_alpha = 0.1; - } - - var t = setInterval(function() { - self.editor_alpha *= delta; - self.dirty_canvas = true; - self.dirty_bgcanvas = true; - - if (delta < 1 && self.editor_alpha < 0.01) { - clearInterval(t); - if (delta < 1) { - self.live_mode = true; - } - } - if (delta > 1 && self.editor_alpha > 0.99) { - clearInterval(t); - self.editor_alpha = 1; - } - }, 1); - }; - - LGraphCanvas.prototype.onNodeSelectionChange = function(node) { - return; //disabled - }; - - /* this is an implementation for touch not in production and not ready - */ - /*LGraphCanvas.prototype.touchHandler = function(event) { - //alert("foo"); - var touches = event.changedTouches, - first = touches[0], - type = ""; - - switch (event.type) { - case "touchstart": - type = "mousedown"; - break; - case "touchmove": - type = "mousemove"; - break; - case "touchend": - type = "mouseup"; - break; - default: - return; - } - - //initMouseEvent(type, canBubble, cancelable, view, clickCount, - // screenX, screenY, clientX, clientY, ctrlKey, - // altKey, shiftKey, metaKey, button, relatedTarget); - - // this is eventually a Dom object, get the LGraphCanvas back - if(typeof this.getCanvasWindow == "undefined"){ - var window = this.lgraphcanvas.getCanvasWindow(); - }else{ - var window = this.getCanvasWindow(); - } - - var document = window.document; - - var simulatedEvent = document.createEvent("MouseEvent"); - simulatedEvent.initMouseEvent( - type, - true, - true, - window, - 1, - first.screenX, - first.screenY, - first.clientX, - first.clientY, - false, - false, - false, - false, - 0, //left - null - ); - first.target.dispatchEvent(simulatedEvent); - event.preventDefault(); - };*/ - - /* CONTEXT MENU ********************/ - - LGraphCanvas.onGroupAdd = function(info, entry, mouse_event) { - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var group = new LiteGraph.LGraphGroup(); - group.pos = canvas.convertEventToCanvasOffset(mouse_event); - canvas.graph.add(group); - }; - - /** - * Determines the furthest nodes in each direction - * @param nodes {LGraphNode[]} the nodes to from which boundary nodes will be extracted - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - LGraphCanvas.getBoundaryNodes = function(nodes) { - let top = null; - let right = null; - let bottom = null; - let left = null; - for (const nID in nodes) { - const node = nodes[nID]; - const [x, y] = node.pos; - const [width, height] = node.size; - - if (top === null || y < top.pos[1]) { - top = node; - } - if (right === null || x + width > right.pos[0] + right.size[0]) { - right = node; - } - if (bottom === null || y + height > bottom.pos[1] + bottom.size[1]) { - bottom = node; - } - if (left === null || x < left.pos[0]) { - left = node; - } - } - - return { - "top": top, - "right": right, - "bottom": bottom, - "left": left - }; - } - /** - * Determines the furthest nodes in each direction for the currently selected nodes - * @return {{left: LGraphNode, top: LGraphNode, right: LGraphNode, bottom: LGraphNode}} - */ - LGraphCanvas.prototype.boundaryNodesForSelection = function() { - return LGraphCanvas.getBoundaryNodes(Object.values(this.selected_nodes)); - } - - /** - * - * @param {LGraphNode[]} nodes a list of nodes - * @param {"top"|"bottom"|"left"|"right"} direction Direction to align the nodes - * @param {LGraphNode?} align_to Node to align to (if null, align to the furthest node in the given direction) - */ - LGraphCanvas.alignNodes = function (nodes, direction, align_to) { - if (!nodes) { - return; - } - - const canvas = LGraphCanvas.active_canvas; - let boundaryNodes = [] - if (align_to === undefined) { - boundaryNodes = LGraphCanvas.getBoundaryNodes(nodes) - } else { - boundaryNodes = { - "top": align_to, - "right": align_to, - "bottom": align_to, - "left": align_to - } - } - - for (const [_, node] of Object.entries(canvas.selected_nodes)) { - switch (direction) { - case "right": - node.pos[0] = boundaryNodes["right"].pos[0] + boundaryNodes["right"].size[0] - node.size[0]; - break; - case "left": - node.pos[0] = boundaryNodes["left"].pos[0]; - break; - case "top": - node.pos[1] = boundaryNodes["top"].pos[1]; - break; - case "bottom": - node.pos[1] = boundaryNodes["bottom"].pos[1] + boundaryNodes["bottom"].size[1] - node.size[1]; - break; - } - } - - canvas.dirty_canvas = true; - canvas.dirty_bgcanvas = true; - }; - - LGraphCanvas.onNodeAlign = function(value, options, event, prev_menu, node) { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, - }); - - function inner_clicked(value) { - LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase(), node); - } - } - - LGraphCanvas.onGroupAlign = function(value, options, event, prev_menu) { - new LiteGraph.ContextMenu(["Top", "Bottom", "Left", "Right"], { - event: event, - callback: inner_clicked, - parentMenu: prev_menu, - }); - - function inner_clicked(value) { - LGraphCanvas.alignNodes(LGraphCanvas.active_canvas.selected_nodes, value.toLowerCase()); - } - } - - LGraphCanvas.onMenuAdd = function (node, options, e, prev_menu, callback) { - - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - var graph = canvas.graph; - if (!graph) - return; - - function inner_onMenuAdded(base_category ,prev_menu){ - - var categories = LiteGraph.getNodeTypesCategories(canvas.filter || graph.filter).filter(function(category){return category.startsWith(base_category)}); - var entries = []; - - categories.map(function(category){ - - if (!category) - return; - - var base_category_regex = new RegExp('^(' + base_category + ')'); - var category_name = category.replace(base_category_regex,"").split('/')[0]; - var category_path = base_category === '' ? category_name + '/' : base_category + category_name + '/'; - - var name = category_name; - if(name.indexOf("::") != -1) //in case it has a namespace like "shader::math/rand" it hides the namespace - name = name.split("::")[1]; - - var index = entries.findIndex(function(entry){return entry.value === category_path}); - if (index === -1) { - entries.push({ value: category_path, content: name, has_submenu: true, callback : function(value, event, mouseEvent, contextMenu){ - inner_onMenuAdded(value.value, contextMenu) - }}); - } - - }); - - var nodes = LiteGraph.getNodeTypesInCategory(base_category.slice(0, -1), canvas.filter || graph.filter ); - nodes.map(function(node){ - - if (node.skip_list) - return; - - var entry = { value: node.type, content: node.title, has_submenu: false , callback : function(value, event, mouseEvent, contextMenu){ - - var first_event = contextMenu.getFirstEvent(); - canvas.graph.beforeChange(); - var node = LiteGraph.createNode(value.value); - if (node) { - node.pos = canvas.convertEventToCanvasOffset(first_event); - canvas.graph.add(node); - } - if(callback) - callback(node); - canvas.graph.afterChange(); - - } - } - - entries.push(entry); - - }); - - new LiteGraph.ContextMenu( entries, { event: e, parentMenu: prev_menu }, ref_window ); - - } - - inner_onMenuAdded('',prev_menu); - return false; - - }; - - LGraphCanvas.onMenuCollapseAll = function() {}; - - LGraphCanvas.onMenuNodeEdit = function() {}; - - LGraphCanvas.showMenuNodeOptionalInputs = function( - v, - options, - e, - prev_menu, - node - ) { - if (!node) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_inputs; - if (node.onGetInputs) { - options = node.onGetInputs(); - } - - var entries = []; - if (options) { - for (var i=0; i < options.length; i++) { - var entry = options[i]; - if (!entry) { - entries.push(null); - continue; - } - var label = entry[0]; - if(!entry[2]) - entry[2] = {}; - - if (entry[2].label) { - label = entry[2].label; - } - - entry[2].removable = true; - var data = { content: label, value: entry }; - if (entry[1] == LiteGraph.ACTION) { - data.className = "event"; - } - entries.push(data); - } - } - - if (node.onMenuNodeInputs) { - var retEntries = node.onMenuNodeInputs(entries); - if(retEntries) entries = retEntries; - } - - if (!entries.length) { - console.log("no input entries"); - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }, - ref_window - ); - - function inner_clicked(v, e, prev) { - if (!node) { - return; - } - - if (v.callback) { - v.callback.call(that, node, v, e, prev); - } - - if (v.value) { - node.graph.beforeChange(); - node.addInput(v.value[0], v.value[1], v.value[2]); - - if (node.onNodeInputAdd) { // callback to the node when adding a slot - node.onNodeInputAdd(v.value); - } - node.setDirtyCanvas(true, true); - node.graph.afterChange(); - } - } - - return false; - }; - - LGraphCanvas.showMenuNodeOptionalOutputs = function( - v, - options, - e, - prev_menu, - node - ) { - if (!node) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var options = node.optional_outputs; - if (node.onGetOutputs) { - options = node.onGetOutputs(); - } - - var entries = []; - if (options) { - for (var i=0; i < options.length; i++) { - var entry = options[i]; - if (!entry) { - //separator? - entries.push(null); - continue; - } - - if ( - node.flags && - node.flags.skip_repeated_outputs && - node.findOutputSlot(entry[0]) != -1 - ) { - continue; - } //skip the ones already on - var label = entry[0]; - if(!entry[2]) - entry[2] = {}; - if (entry[2].label) { - label = entry[2].label; - } - entry[2].removable = true; - var data = { content: label, value: entry }; - if (entry[1] == LiteGraph.EVENT) { - data.className = "event"; - } - entries.push(data); - } - } - - if (this.onMenuNodeOutputs) { - entries = this.onMenuNodeOutputs(entries); - } - if (LiteGraph.do_add_triggers_slots){ //canvas.allow_addOutSlot_onExecuted - if (node.findOutputSlot("onExecuted") == -1){ - entries.push({content: "On Executed", value: ["onExecuted", LiteGraph.EVENT, {nameLocked: true}], className: "event"}); //, opts: {} - } - } - // add callback for modifing the menu elements onMenuNodeOutputs - if (node.onMenuNodeOutputs) { - var retEntries = node.onMenuNodeOutputs(entries); - if(retEntries) entries = retEntries; - } - - if (!entries.length) { - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }, - ref_window - ); - - function inner_clicked(v, e, prev) { - if (!node) { - return; - } - - if (v.callback) { - v.callback.call(that, node, v, e, prev); - } - - if (!v.value) { - return; - } - - var value = v.value[1]; - - if ( - value && - (value.constructor === Object || value.constructor === Array) - ) { - //submenu why? - var entries = []; - for (var i in value) { - entries.push({ content: i, value: value[i] }); - } - new LiteGraph.ContextMenu(entries, { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - node: node - }); - return false; - } else { - node.graph.beforeChange(); - node.addOutput(v.value[0], v.value[1], v.value[2]); - - if (node.onNodeOutputAdd) { // a callback to the node when adding a slot - node.onNodeOutputAdd(v.value); - } - node.setDirtyCanvas(true, true); - node.graph.afterChange(); - } - } - - return false; - }; - - LGraphCanvas.onShowMenuNodeProperties = function( - value, - options, - e, - prev_menu, - node - ) { - if (!node || !node.properties) { - return; - } - - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var entries = []; - for (var i in node.properties) { - var value = node.properties[i] !== undefined ? node.properties[i] : " "; - if( typeof value == "object" ) - value = JSON.stringify(value); - var info = node.getPropertyInfo(i); - if(info.type == "enum" || info.type == "combo") - value = LGraphCanvas.getPropertyPrintableValue( value, info.values ); - - //value could contain invalid html characters, clean that - value = LGraphCanvas.decodeHTML(value); - entries.push({ - content: - "" + - (info.label ? info.label : i) + - "" + - "" + - value + - "", - value: i - }); - } - if (!entries.length) { - return; - } - - var menu = new LiteGraph.ContextMenu( - entries, - { - event: e, - callback: inner_clicked, - parentMenu: prev_menu, - allow_html: true, - node: node - }, - ref_window - ); - - function inner_clicked(v, options, e, prev) { - if (!node) { - return; - } - var rect = this.getBoundingClientRect(); - canvas.showEditPropertyValue(node, v.value, { - position: [rect.left, rect.top] - }); - } - - return false; - }; - - LGraphCanvas.decodeHTML = function(str) { - var e = document.createElement("div"); - e.innerText = str; - return e.innerHTML; - }; - - LGraphCanvas.onMenuResizeNode = function(value, options, e, menu, node) { - if (!node) { - return; - } - - var fApplyMultiNode = function(node){ - node.size = node.computeSize(); - if (node.onResize) - node.onResize(node.size); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.prototype.showLinkMenu = function(link, e) { - var that = this; - // console.log(link); - var node_left = that.graph.getNodeById( link.origin_id ); - var node_right = that.graph.getNodeById( link.target_id ); - var fromType = false; - if (node_left && node_left.outputs && node_left.outputs[link.origin_slot]) fromType = node_left.outputs[link.origin_slot].type; - var destType = false; - if (node_right && node_right.outputs && node_right.outputs[link.target_slot]) destType = node_right.inputs[link.target_slot].type; - - var options = ["Add Node",null,"Delete",null]; - - - var menu = new LiteGraph.ContextMenu(options, { - event: e, - title: link.data != null ? link.data.constructor.name : null, - callback: inner_clicked - }); - - function inner_clicked(v,options,e) { - switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ - // console.debug("node autoconnect"); - if(!node.inputs || !node.inputs.length || !node.outputs || !node.outputs.length){ - return; - } - // leave the connection type checking inside connectByType - if (node_left.connectByType( link.origin_slot, node, fromType )){ - node.connectByType( link.target_slot, node_right, destType ); - node.pos[0] -= node.size[0] * 0.5; - } - }); - break; - - case "Delete": - that.graph.removeLink(link.id); - break; - default: - /*var nodeCreated = createDefaultNodeForSlot({ nodeFrom: node_left - ,slotFrom: link.origin_slot - ,nodeTo: node - ,slotTo: link.target_slot - ,e: e - ,nodeType: "AUTO" - }); - if(nodeCreated) console.log("new node in beetween "+v+" created");*/ - } - } - - return false; - }; - - LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection - var optPass = optPass || {}; - var opts = Object.assign({ nodeFrom: null // input - ,slotFrom: null // input - ,nodeTo: null // output - ,slotTo: null // output - ,position: [] // pass the event coords - ,nodeType: null // choose a nodetype to add, AUTO to set at first good - ,posAdd:[0,0] // adjust x,y - ,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h - } - ,optPass - ); - var that = this; - - var isFrom = opts.nodeFrom && opts.slotFrom!==null; - var isTo = !isFrom && opts.nodeTo && opts.slotTo!==null; - - if (!isFrom && !isTo){ - console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo); - return false; - } - if (!opts.nodeType){ - console.warn("No type to createDefaultNodeForSlot"); - return false; - } - - var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; - var slotX = isFrom ? opts.slotFrom : opts.slotTo; - - var iSlotConn = false; - switch (typeof slotX){ - case "string": - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "object": - // ok slotX - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); - break; - case "number": - iSlotConn = slotX; - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "undefined": - default: - // bad ? - //iSlotConn = 0; - console.warn("Cant get slot information "+slotX); - return false; - } - - if (slotX===false || iSlotConn===false){ - console.warn("createDefaultNodeForSlot bad slotX "+slotX+" "+iSlotConn); - } - - // check for defaults nodes for this slottype - var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; - var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; - if(slotTypesDefault && slotTypesDefault[fromSlotType]){ - if (slotX.link !== null) { - // is connected - }else{ - // is not not connected - } - nodeNewType = false; - if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ - for(var typeX in slotTypesDefault[fromSlotType]){ - if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){ - nodeNewType = slotTypesDefault[fromSlotType][typeX]; - // console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType); - break; // -------- - } - } - }else{ - if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") nodeNewType = slotTypesDefault[fromSlotType]; - } - if (nodeNewType) { - var nodeNewOpts = false; - if (typeof nodeNewType == "object" && nodeNewType.node){ - nodeNewOpts = nodeNewType; - nodeNewType = nodeNewType.node; - } - - //that.graph.beforeChange(); - - var newNode = LiteGraph.createNode(nodeNewType); - if(newNode){ - // if is object pass options - if (nodeNewOpts){ - if (nodeNewOpts.properties) { - for (var i in nodeNewOpts.properties) { - newNode.addProperty( i, nodeNewOpts.properties[i] ); - } - } - if (nodeNewOpts.inputs) { - newNode.inputs = []; - for (var i in nodeNewOpts.inputs) { - newNode.addOutput( - nodeNewOpts.inputs[i][0], - nodeNewOpts.inputs[i][1] - ); - } - } - if (nodeNewOpts.outputs) { - newNode.outputs = []; - for (var i in nodeNewOpts.outputs) { - newNode.addOutput( - nodeNewOpts.outputs[i][0], - nodeNewOpts.outputs[i][1] - ); - } - } - if (nodeNewOpts.title) { - newNode.title = nodeNewOpts.title; - } - if (nodeNewOpts.json) { - newNode.configure(nodeNewOpts.json); - } - - } - - // add the node - that.graph.add(newNode); - newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0) - ,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/ - - //that.graph.afterChange(); - - // connect the two! - if (isFrom){ - opts.nodeFrom.connectByType( iSlotConn, newNode, fromSlotType ); - }else{ - opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType ); - } - - // if connecting in between - if (isFrom && isTo){ - // TODO - } - - return true; - - }else{ - console.log("failed creating "+nodeNewType); - } - } - } - return false; - } - - LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection - var optPass = optPass || {}; - var opts = Object.assign({ nodeFrom: null // input - ,slotFrom: null // input - ,nodeTo: null // output - ,slotTo: null // output - ,e: null - } - ,optPass - ); - var that = this; - - var isFrom = opts.nodeFrom && opts.slotFrom; - var isTo = !isFrom && opts.nodeTo && opts.slotTo; - - if (!isFrom && !isTo){ - console.warn("No data passed to showConnectionMenu"); - return false; - } - - var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; - var slotX = isFrom ? opts.slotFrom : opts.slotTo; - - var iSlotConn = false; - switch (typeof slotX){ - case "string": - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - case "object": - // ok slotX - iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); - break; - case "number": - iSlotConn = slotX; - slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; - break; - default: - // bad ? - //iSlotConn = 0; - console.warn("Cant get slot information "+slotX); - return false; - } - - var options = ["Add Node",null]; - - if (that.allow_searchbox){ - options.push("Search"); - options.push(null); - } - - // get defaults nodes for this slottype - var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; - var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; - if(slotTypesDefault && slotTypesDefault[fromSlotType]){ - if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ - for(var typeX in slotTypesDefault[fromSlotType]){ - options.push(slotTypesDefault[fromSlotType][typeX]); - } - }else{ - options.push(slotTypesDefault[fromSlotType]); - } - } - - // build menu - var menu = new LiteGraph.ContextMenu(options, { - event: opts.e, - title: (slotX && slotX.name!="" ? (slotX.name + (fromSlotType?" | ":"")) : "")+(slotX && fromSlotType ? fromSlotType : ""), - callback: inner_clicked - }); - - // callback - function inner_clicked(v,options,e) { - //console.log("Process showConnectionMenu selection"); - switch (v) { - case "Add Node": - LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){ - if (isFrom){ - opts.nodeFrom.connectByType( iSlotConn, node, fromSlotType ); - }else{ - opts.nodeTo.connectByTypeOutput( iSlotConn, node, fromSlotType ); - } - }); - break; - case "Search": - if(isFrom){ - that.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType}); - }else{ - that.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType}); - } - break; - default: - // check for defaults nodes for this slottype - var nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts,{ position: [opts.e.canvasX, opts.e.canvasY] - ,nodeType: v - })); - if (nodeCreated){ - // new node created - //console.log("node "+v+" created") - }else{ - // failed or v is not in defaults - } - break; - } - } - - return false; - }; - - // TODO refactor :: this is used fot title but not for properties! - LGraphCanvas.onShowPropertyEditor = function(item, options, e, menu, node) { - var input_html = ""; - var property = item.property || "title"; - var value = node[property]; - - // TODO refactor :: use createDialog ? - - var dialog = document.createElement("div"); - dialog.is_modified = false; - dialog.className = "graphdialog"; - dialog.innerHTML = - ""; - dialog.close = function() { - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - var title = dialog.querySelector(".name"); - title.innerText = property; - var input = dialog.querySelector(".value"); - if (input) { - input.value = value; - input.addEventListener("blur", function(e) { - this.focus(); - }); - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - inner(); // save - } else if (e.keyCode != 13 && e.target.localName != "textarea") { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - } - - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; - } - - var button = dialog.querySelector("button"); - button.addEventListener("click", inner); - canvas.parentNode.appendChild(dialog); - - if(input) input.focus(); - - var dialogCloseTimer = null; - dialog.addEventListener("mouseleave", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - dialog.addEventListener("mouseenter", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - - function inner() { - if(input) setValue(input.value); - } - - function setValue(value) { - if (item.type == "Number") { - value = Number(value); - } else if (item.type == "Boolean") { - value = Boolean(value); - } - node[property] = value; - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - node.setDirtyCanvas(true, true); - } - }; - - // refactor: there are different dialogs, some uses createDialog some dont - LGraphCanvas.prototype.prompt = function(title, value, callback, event, multiline) { - var that = this; - var input_html = ""; - title = title || ""; - - var dialog = document.createElement("div"); - dialog.is_modified = false; - dialog.className = "graphdialog rounded"; - if(multiline) - dialog.innerHTML = " "; - else - dialog.innerHTML = " "; - dialog.close = function() { - that.prompt_box = null; - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - canvas.parentNode.appendChild(dialog); - - if (this.ds.scale > 1) { - dialog.style.transform = "scale(" + this.ds.scale + ")"; - } - - var dialogCloseTimer = null; - var prevent_timeout = false; - LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { - if (prevent_timeout) - return; - if(LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { - if(LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - var selInDia = dialog.querySelectorAll("select"); - if (selInDia){ - // if filtering, check focus changed to comboboxes and prevent closing - selInDia.forEach(function(selIn) { - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - }); - } - - if (that.prompt_box) { - that.prompt_box.close(); - } - that.prompt_box = dialog; - - var first = null; - var timeout = null; - var selected = null; - - var name_element = dialog.querySelector(".name"); - name_element.innerText = title; - var value_element = dialog.querySelector(".value"); - value_element.value = value; - value_element.select(); - - var input = value_element; - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13 && e.target.localName != "textarea") { - if (callback) { - callback(this.value); - } - dialog.close(); - } else { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - - var button = dialog.querySelector("button"); - button.addEventListener("click", function(e) { - if (callback) { - callback(input.value); - } - that.setDirty(true); - dialog.close(); - }); - - var rect = canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; - } - - setTimeout(function() { - input.focus(); - }, 10); - - return dialog; - }; - - LGraphCanvas.search_limit = -1; - LGraphCanvas.prototype.showSearchBox = function(event, options) { - // proposed defaults - var def_options = { slot_from: null - ,node_from: null - ,node_to: null - ,do_type_filter: LiteGraph.search_filter_enabled // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out - ,type_filter_in: false // these are default: pass to set initially set values - ,type_filter_out: false - ,show_general_if_none_on_typefilter: true - ,show_general_after_typefiltered: true - ,hide_on_mouse_leave: LiteGraph.search_hide_on_mouse_leave - ,show_all_if_empty: true - ,show_all_on_open: LiteGraph.search_show_all_on_open - }; - options = Object.assign(def_options, options || {}); - - //console.log(options); - - var that = this; - var input_html = ""; - var graphcanvas = LGraphCanvas.active_canvas; - var canvas = graphcanvas.canvas; - var root_document = canvas.ownerDocument || document; - - var dialog = document.createElement("div"); - dialog.className = "litegraph litesearchbox graphdialog rounded"; - dialog.innerHTML = "Search "; - if (options.do_type_filter){ - dialog.innerHTML += ""; - dialog.innerHTML += ""; - } - dialog.innerHTML += "
"; - - if( root_document.fullscreenElement ) - root_document.fullscreenElement.appendChild(dialog); - else - { - root_document.body.appendChild(dialog); - root_document.body.style.overflow = "hidden"; - } - // dialog element has been appended - - if (options.do_type_filter){ - var selIn = dialog.querySelector(".slot_in_type_filter"); - var selOut = dialog.querySelector(".slot_out_type_filter"); - } - - dialog.close = function() { - that.search_box = null; - this.blur(); - canvas.focus(); - root_document.body.style.overflow = ""; - - setTimeout(function() { - that.canvas.focus(); - }, 20); //important, if canvas loses focus keys wont be captured - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - if (this.ds.scale > 1) { - dialog.style.transform = "scale(" + this.ds.scale + ")"; - } - - // hide on mouse leave - if(options.hide_on_mouse_leave){ - var prevent_timeout = false; - var timeout_close = null; - LiteGraph.pointerListenerAdd(dialog,"enter", function(e) { - if (timeout_close) { - clearTimeout(timeout_close); - timeout_close = null; - } - }); - LiteGraph.pointerListenerAdd(dialog,"leave", function(e) { - if (prevent_timeout){ - return; - } - timeout_close = setTimeout(function() { - dialog.close(); - }, typeof options.hide_on_mouse_leave === "number" ? options.hide_on_mouse_leave : 500); - }); - // if filtering, check focus changed to comboboxes and prevent closing - if (options.do_type_filter){ - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - selOut.addEventListener("click", function(e) { - prevent_timeout++; - }); - selOut.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selOut.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - } - } - - if (that.search_box) { - that.search_box.close(); - } - that.search_box = dialog; - - var helper = dialog.querySelector(".helper"); - - var first = null; - var timeout = null; - var selected = null; - - var input = dialog.querySelector("input"); - if (input) { - input.addEventListener("blur", function(e) { - this.focus(); - }); - input.addEventListener("keydown", function(e) { - if (e.keyCode == 38) { - //UP - changeSelection(false); - } else if (e.keyCode == 40) { - //DOWN - changeSelection(true); - } else if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - if (selected) { - select(unescape(selected.dataset["type"])); - } else if (first) { - select(first); - } else { - dialog.close(); - } - } else { - if (timeout) { - clearInterval(timeout); - } - timeout = setTimeout(refreshHelper, 10); - return; - } - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - return true; - }); - } - - // if should filter on type, load and fill selected and choose elements if passed - if (options.do_type_filter){ - if (selIn){ - var aSlots = LiteGraph.slot_types_in; - var nSlots = aSlots.length; // this for object :: Object.keys(aSlots).length; - - if (options.type_filter_in == LiteGraph.EVENT || options.type_filter_in == LiteGraph.ACTION) - options.type_filter_in = "_event_"; - /* this will filter on * .. but better do it manually in case - else if(options.type_filter_in === "" || options.type_filter_in === 0) - options.type_filter_in = "*";*/ - - for (var iK=0; iK (rect.height - 200)) - helper.style.maxHeight = (rect.height - event.layerY - 20) + "px"; - - /* - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (event) { - dialog.style.left = event.clientX + offsetx + "px"; - dialog.style.top = event.clientY + offsety + "px"; - } else { - dialog.style.left = canvas.width * 0.5 + offsetx + "px"; - dialog.style.top = canvas.height * 0.5 + offsety + "px"; - } - canvas.parentNode.appendChild(dialog); - */ - - input.focus(); - if (options.show_all_on_open) refreshHelper(); - - function select(name) { - if (name) { - if (that.onSearchBoxSelection) { - that.onSearchBoxSelection(name, event, graphcanvas); - } else { - var extra = LiteGraph.searchbox_extras[name.toLowerCase()]; - if (extra) { - name = extra.type; - } - - graphcanvas.graph.beforeChange(); - var node = LiteGraph.createNode(name); - if (node) { - node.pos = graphcanvas.convertEventToCanvasOffset( - event - ); - graphcanvas.graph.add(node, false); - } - - if (extra && extra.data) { - if (extra.data.properties) { - for (var i in extra.data.properties) { - node.addProperty( i, extra.data.properties[i] ); - } - } - if (extra.data.inputs) { - node.inputs = []; - for (var i in extra.data.inputs) { - node.addOutput( - extra.data.inputs[i][0], - extra.data.inputs[i][1] - ); - } - } - if (extra.data.outputs) { - node.outputs = []; - for (var i in extra.data.outputs) { - node.addOutput( - extra.data.outputs[i][0], - extra.data.outputs[i][1] - ); - } - } - if (extra.data.title) { - node.title = extra.data.title; - } - if (extra.data.json) { - node.configure(extra.data.json); - } - - } - - // join node after inserting - if (options.node_from){ - var iS = false; - switch (typeof options.slot_from){ - case "string": - iS = options.node_from.findOutputSlot(options.slot_from); - break; - case "object": - if (options.slot_from.name){ - iS = options.node_from.findOutputSlot(options.slot_from.name); - }else{ - iS = -1; - } - if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; - break; - case "number": - iS = options.slot_from; - break; - default: - iS = 0; // try with first if no name set - } - if (typeof options.node_from.outputs[iS] !== "undefined"){ - if (iS!==false && iS>-1){ - options.node_from.connectByType( iS, node, options.node_from.outputs[iS].type ); - } - }else{ - // console.warn("cant find slot " + options.slot_from); - } - } - if (options.node_to){ - var iS = false; - switch (typeof options.slot_from){ - case "string": - iS = options.node_to.findInputSlot(options.slot_from); - break; - case "object": - if (options.slot_from.name){ - iS = options.node_to.findInputSlot(options.slot_from.name); - }else{ - iS = -1; - } - if (iS==-1 && typeof options.slot_from.slot_index !== "undefined") iS = options.slot_from.slot_index; - break; - case "number": - iS = options.slot_from; - break; - default: - iS = 0; // try with first if no name set - } - if (typeof options.node_to.inputs[iS] !== "undefined"){ - if (iS!==false && iS>-1){ - // try connection - options.node_to.connectByTypeOutput(iS,node,options.node_to.inputs[iS].type); - } - }else{ - // console.warn("cant find slot_nodeTO " + options.slot_from); - } - } - - graphcanvas.graph.afterChange(); - } - } - - dialog.close(); - } - - function changeSelection(forward) { - var prev = selected; - if (selected) { - selected.classList.remove("selected"); - } - if (!selected) { - selected = forward - ? helper.childNodes[0] - : helper.childNodes[helper.childNodes.length]; - } else { - selected = forward - ? selected.nextSibling - : selected.previousSibling; - if (!selected) { - selected = prev; - } - } - if (!selected) { - return; - } - selected.classList.add("selected"); - selected.scrollIntoView({block: "end", behavior: "smooth"}); - } - - function refreshHelper() { - timeout = null; - var str = input.value; - first = null; - helper.innerHTML = ""; - if (!str && !options.show_all_if_empty) { - return; - } - - if (that.onSearchBox) { - var list = that.onSearchBox(helper, str, graphcanvas); - if (list) { - for (var i = 0; i < list.length; ++i) { - addResult(list[i]); - } - } - } else { - var c = 0; - str = str.toLowerCase(); - var filter = graphcanvas.filter || graphcanvas.graph.filter; - - // filter by type preprocess - if(options.do_type_filter && that.search_box){ - var sIn = that.search_box.querySelector(".slot_in_type_filter"); - var sOut = that.search_box.querySelector(".slot_out_type_filter"); - }else{ - var sIn = false; - var sOut = false; - } - - //extras - for (var i in LiteGraph.searchbox_extras) { - var extra = LiteGraph.searchbox_extras[i]; - if ((!options.show_all_if_empty || str) && extra.desc.toLowerCase().indexOf(str) === -1) { - continue; - } - var ctor = LiteGraph.registered_node_types[ extra.type ]; - if( ctor && ctor.filter != filter ) - continue; - if( ! inner_test_filter(extra.type) ) - continue; - addResult( extra.desc, "searchbox_extra" ); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - - var filtered = null; - if (Array.prototype.filter) { //filter supported - var keys = Object.keys( LiteGraph.registered_node_types ); //types - var filtered = keys.filter( inner_test_filter ); - } else { - filtered = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i) ) - filtered.push(i); - } - } - - for (var i = 0; i < filtered.length; i++) { - addResult(filtered[i]); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - - // add general type if filtering - if (options.show_general_after_typefiltered - && (sIn.value || sOut.value) - ){ - filtered_extra = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i, {inTypeOverride: sIn&&sIn.value?"*":false, outTypeOverride: sOut&&sOut.value?"*":false}) ) - filtered_extra.push(i); - } - for (var i = 0; i < filtered_extra.length; i++) { - addResult(filtered_extra[i], "generic_type"); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - } - - // check il filtering gave no results - if ((sIn.value || sOut.value) && - ( (helper.childNodes.length == 0 && options.show_general_if_none_on_typefilter) ) - ){ - filtered_extra = []; - for (var i in LiteGraph.registered_node_types) { - if( inner_test_filter(i, {skipFilter: true}) ) - filtered_extra.push(i); - } - for (var i = 0; i < filtered_extra.length; i++) { - addResult(filtered_extra[i], "not_in_filter"); - if ( LGraphCanvas.search_limit !== -1 && c++ > LGraphCanvas.search_limit ) { - break; - } - } - } - - function inner_test_filter( type, optsIn ) - { - var optsIn = optsIn || {}; - var optsDef = { skipFilter: false - ,inTypeOverride: false - ,outTypeOverride: false - }; - var opts = Object.assign(optsDef,optsIn); - var ctor = LiteGraph.registered_node_types[ type ]; - if(filter && ctor.filter != filter ) - return false; - if ((!options.show_all_if_empty || str) && type.toLowerCase().indexOf(str) === -1 && (!ctor.title || ctor.title.toLowerCase().indexOf(str) === -1)) - return false; - - // filter by slot IN, OUT types - if(options.do_type_filter && !opts.skipFilter){ - var sType = type; - - var sV = sIn.value; - if (opts.inTypeOverride!==false) sV = opts.inTypeOverride; - //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 - - if(sIn && sV){ - //console.log("will check filter against "+sV); - if (LiteGraph.registered_slot_in_types[sV] && LiteGraph.registered_slot_in_types[sV].nodes){ // type is stored - //console.debug("check "+sType+" in "+LiteGraph.registered_slot_in_types[sV].nodes); - var doesInc = LiteGraph.registered_slot_in_types[sV].nodes.includes(sType); - if (doesInc!==false){ - //console.log(sType+" HAS "+sV); - }else{ - /*console.debug(LiteGraph.registered_slot_in_types[sV]); - console.log(+" DONT includes "+type);*/ - return false; - } - } - } - - var sV = sOut.value; - if (opts.outTypeOverride!==false) sV = opts.outTypeOverride; - //if (sV.toLowerCase() == "_event_") sV = LiteGraph.EVENT; // -1 - - if(sOut && sV){ - //console.log("search will check filter against "+sV); - if (LiteGraph.registered_slot_out_types[sV] && LiteGraph.registered_slot_out_types[sV].nodes){ // type is stored - //console.debug("check "+sType+" in "+LiteGraph.registered_slot_out_types[sV].nodes); - var doesInc = LiteGraph.registered_slot_out_types[sV].nodes.includes(sType); - if (doesInc!==false){ - //console.log(sType+" HAS "+sV); - }else{ - /*console.debug(LiteGraph.registered_slot_out_types[sV]); - console.log(+" DONT includes "+type);*/ - return false; - } - } - } - } - return true; - } - } - - function addResult(type, className) { - var help = document.createElement("div"); - if (!first) { - first = type; - } - - const nodeType = LiteGraph.registered_node_types[type]; - if (nodeType?.title) { - help.innerText = nodeType?.title; - const typeEl = document.createElement("span"); - typeEl.className = "litegraph lite-search-item-type"; - typeEl.textContent = type; - help.append(typeEl); - } else { - help.innerText = type; - } - - help.dataset["type"] = escape(type); - help.className = "litegraph lite-search-item"; - if (className) { - help.className += " " + className; - } - help.addEventListener("click", function(e) { - select(unescape(this.dataset["type"])); - }); - helper.appendChild(help); - } - } - - return dialog; - }; - - LGraphCanvas.prototype.showEditPropertyValue = function( node, property, options ) { - if (!node || node.properties[property] === undefined) { - return; - } - - options = options || {}; - var that = this; - - var info = node.getPropertyInfo(property); - var type = info.type; - - var input_html = ""; - - if (type == "string" || type == "number" || type == "array" || type == "object") { - input_html = ""; - } else if ( (type == "enum" || type == "combo") && info.values) { - input_html = ""; - } else if (type == "boolean" || type == "toggle") { - input_html = - ""; - } else { - console.warn("unknown type: " + type); - return; - } - - var dialog = this.createDialog( - "" + - (info.label ? info.label : property) + - "" + - input_html + - "", - options - ); - - var input = false; - if ((type == "enum" || type == "combo") && info.values) { - input = dialog.querySelector("select"); - input.addEventListener("change", function(e) { - dialog.modified(); - setValue(e.target.value); - //var index = e.target.value; - //setValue( e.options[e.selectedIndex].value ); - }); - } else if (type == "boolean" || type == "toggle") { - input = dialog.querySelector("input"); - if (input) { - input.addEventListener("click", function(e) { - dialog.modified(); - setValue(!!input.checked); - }); - } - } else { - input = dialog.querySelector("input"); - if (input) { - input.addEventListener("blur", function(e) { - this.focus(); - }); - - var v = node.properties[property] !== undefined ? node.properties[property] : ""; - if (type !== 'string') { - v = JSON.stringify(v); - } - - input.value = v; - input.addEventListener("keydown", function(e) { - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - // ENTER - inner(); // save - } else if (e.keyCode != 13) { - dialog.modified(); - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - } - } - if (input) input.focus(); - - var button = dialog.querySelector("button"); - button.addEventListener("click", inner); - - function inner() { - setValue(input.value); - } - - function setValue(value) { - - if(info && info.values && info.values.constructor === Object && info.values[value] != undefined ) - value = info.values[value]; - - if (typeof node.properties[property] == "number") { - value = Number(value); - } - if (type == "array" || type == "object") { - value = JSON.parse(value); - } - node.properties[property] = value; - if (node.graph) { - node.graph._version++; - } - if (node.onPropertyChanged) { - node.onPropertyChanged(property, value); - } - if(options.onclose) - options.onclose(); - dialog.close(); - node.setDirtyCanvas(true, true); - } - - return dialog; - }; - - // TODO refactor, theer are different dialog, some uses createDialog, some dont - LGraphCanvas.prototype.createDialog = function(html, options) { - var def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true }; - options = Object.assign(def_options, options || {}); - - var dialog = document.createElement("div"); - dialog.className = "graphdialog"; - dialog.innerHTML = html; - dialog.is_modified = false; - - var rect = this.canvas.getBoundingClientRect(); - var offsetx = -20; - var offsety = -20; - if (rect) { - offsetx -= rect.left; - offsety -= rect.top; - } - - if (options.position) { - offsetx += options.position[0]; - offsety += options.position[1]; - } else if (options.event) { - offsetx += options.event.clientX; - offsety += options.event.clientY; - } //centered - else { - offsetx += this.canvas.width * 0.5; - offsety += this.canvas.height * 0.5; - } - - dialog.style.left = offsetx + "px"; - dialog.style.top = offsety + "px"; - - this.canvas.parentNode.appendChild(dialog); - - // acheck for input and use default behaviour: save on enter, close on esc - if (options.checkForInput){ - var aI = []; - var focused = false; - if (aI = dialog.querySelectorAll("input")){ - aI.forEach(function(iX) { - iX.addEventListener("keydown",function(e){ - dialog.modified(); - if (e.keyCode == 27) { - dialog.close(); - } else if (e.keyCode != 13) { - return; - } - // set value ? - e.preventDefault(); - e.stopPropagation(); - }); - if (!focused) iX.focus(); - }); - } - } - - dialog.modified = function(){ - dialog.is_modified = true; - } - dialog.close = function() { - if (dialog.parentNode) { - dialog.parentNode.removeChild(dialog); - } - }; - - var dialogCloseTimer = null; - var prevent_timeout = false; - dialog.addEventListener("mouseleave", function(e) { - if (prevent_timeout) - return; - if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) - if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) - dialogCloseTimer = setTimeout(dialog.close, LiteGraph.dialog_close_on_mouse_leave_delay); //dialog.close(); - }); - dialog.addEventListener("mouseenter", function(e) { - if(options.closeOnLeave || LiteGraph.dialog_close_on_mouse_leave) - if(dialogCloseTimer) clearTimeout(dialogCloseTimer); - }); - var selInDia = dialog.querySelectorAll("select"); - if (selInDia){ - // if filtering, check focus changed to comboboxes and prevent closing - selInDia.forEach(function(selIn) { - selIn.addEventListener("click", function(e) { - prevent_timeout++; - }); - selIn.addEventListener("blur", function(e) { - prevent_timeout = 0; - }); - selIn.addEventListener("change", function(e) { - prevent_timeout = -1; - }); - }); - } - - return dialog; - }; - - LGraphCanvas.prototype.createPanel = function(title, options) { - options = options || {}; - - var ref_window = options.window || window; - var root = document.createElement("div"); - root.className = "litegraph dialog"; - root.innerHTML = "
"; - root.header = root.querySelector(".dialog-header"); - - if(options.width) - root.style.width = options.width + (options.width.constructor === Number ? "px" : ""); - if(options.height) - root.style.height = options.height + (options.height.constructor === Number ? "px" : ""); - if(options.closable) - { - var close = document.createElement("span"); - close.innerHTML = "✕"; - close.classList.add("close"); - close.addEventListener("click",function(){ - root.close(); - }); - root.header.appendChild(close); - } - root.title_element = root.querySelector(".dialog-title"); - root.title_element.innerText = title; - root.content = root.querySelector(".dialog-content"); - root.alt_content = root.querySelector(".dialog-alt-content"); - root.footer = root.querySelector(".dialog-footer"); - - root.close = function() - { - if (root.onClose && typeof root.onClose == "function"){ - root.onClose(); - } - if(root.parentNode) - root.parentNode.removeChild(root); - /* XXX CHECK THIS */ - if(this.parentNode){ - this.parentNode.removeChild(this); - } - /* XXX this was not working, was fixed with an IF, check this */ - } - - // function to swap panel content - root.toggleAltContent = function(force){ - if (typeof force != "undefined"){ - var vTo = force ? "block" : "none"; - var vAlt = force ? "none" : "block"; - }else{ - var vTo = root.alt_content.style.display != "block" ? "block" : "none"; - var vAlt = root.alt_content.style.display != "block" ? "none" : "block"; - } - root.alt_content.style.display = vTo; - root.content.style.display = vAlt; - } - - root.toggleFooterVisibility = function(force){ - if (typeof force != "undefined"){ - var vTo = force ? "block" : "none"; - }else{ - var vTo = root.footer.style.display != "block" ? "block" : "none"; - } - root.footer.style.display = vTo; - } - - root.clear = function() - { - this.content.innerHTML = ""; - } - - root.addHTML = function(code, classname, on_footer) - { - var elem = document.createElement("div"); - if(classname) - elem.className = classname; - elem.innerHTML = code; - if(on_footer) - root.footer.appendChild(elem); - else - root.content.appendChild(elem); - return elem; - } - - root.addButton = function( name, callback, options ) - { - var elem = document.createElement("button"); - elem.innerText = name; - elem.options = options; - elem.classList.add("btn"); - elem.addEventListener("click",callback); - root.footer.appendChild(elem); - return elem; - } - - root.addSeparator = function() - { - var elem = document.createElement("div"); - elem.className = "separator"; - root.content.appendChild(elem); - } - - root.addWidget = function( type, name, value, options, callback ) - { - options = options || {}; - var str_value = String(value); - type = type.toLowerCase(); - if(type == "number") - str_value = value.toFixed(3); - - var elem = document.createElement("div"); - elem.className = "property"; - elem.innerHTML = ""; - elem.querySelector(".property_name").innerText = options.label || name; - var value_element = elem.querySelector(".property_value"); - value_element.innerText = str_value; - elem.dataset["property"] = name; - elem.dataset["type"] = options.type || type; - elem.options = options; - elem.value = value; - - if( type == "code" ) - elem.addEventListener("click", function(e){ root.inner_showCodePad( this.dataset["property"] ); }); - else if (type == "boolean") - { - elem.classList.add("boolean"); - if(value) - elem.classList.add("bool-on"); - elem.addEventListener("click", function(){ - //var v = node.properties[this.dataset["property"]]; - //node.setProperty(this.dataset["property"],!v); this.innerText = v ? "true" : "false"; - var propname = this.dataset["property"]; - this.value = !this.value; - this.classList.toggle("bool-on"); - this.querySelector(".property_value").innerText = this.value ? "true" : "false"; - innerChange(propname, this.value ); - }); - } - else if (type == "string" || type == "number") - { - value_element.setAttribute("contenteditable",true); - value_element.addEventListener("keydown", function(e){ - if(e.code == "Enter" && (type != "string" || !e.shiftKey)) // allow for multiline - { - e.preventDefault(); - this.blur(); - } - }); - value_element.addEventListener("blur", function(){ - var v = this.innerText; - var propname = this.parentNode.dataset["property"]; - var proptype = this.parentNode.dataset["type"]; - if( proptype == "number") - v = Number(v); - innerChange(propname, v); - }); - } - else if (type == "enum" || type == "combo") { - var str_value = LGraphCanvas.getPropertyPrintableValue( value, options.values ); - value_element.innerText = str_value; - - value_element.addEventListener("click", function(event){ - var values = options.values || []; - var propname = this.parentNode.dataset["property"]; - var elem_that = this; - var menu = new LiteGraph.ContextMenu(values,{ - event: event, - className: "dark", - callback: inner_clicked - }, - ref_window); - function inner_clicked(v, option, event) { - //node.setProperty(propname,v); - //graphcanvas.dirty_canvas = true; - elem_that.innerText = v; - innerChange(propname,v); - return false; - } - }); - } - - root.content.appendChild(elem); - - function innerChange(name, value) - { - //console.log("change",name,value); - //that.dirty_canvas = true; - if(options.callback) - options.callback(name,value,options); - if(callback) - callback(name,value,options); - } - - return elem; - } - - if (root.onOpen && typeof root.onOpen == "function") root.onOpen(); - - return root; - }; - - LGraphCanvas.getPropertyPrintableValue = function(value, values) - { - if(!values) - return String(value); - - if(values.constructor === Array) - { - return String(value); - } - - if(values.constructor === Object) - { - var desc_value = ""; - for(var k in values) - { - if(values[k] != value) - continue; - desc_value = k; - break; - } - return String(value) + " ("+desc_value+")"; - } - } - - LGraphCanvas.prototype.closePanels = function(){ - var panel = document.querySelector("#node-panel"); - if(panel) - panel.close(); - var panel = document.querySelector("#option-panel"); - if(panel) - panel.close(); - } - - LGraphCanvas.prototype.showShowGraphOptionsPanel = function(refOpts, obEv, refMenu, refMenu2){ - if(this.constructor && this.constructor.name == "HTMLDivElement"){ - // assume coming from the menu event click - if (!obEv || !obEv.event || !obEv.event.target || !obEv.event.target.lgraphcanvas){ - console.warn("Canvas not found"); // need a ref to canvas obj - /*console.debug(event); - console.debug(event.target);*/ - return; - } - var graphcanvas = obEv.event.target.lgraphcanvas; - }else{ - // assume called internally - var graphcanvas = this; - } - graphcanvas.closePanels(); - var ref_window = graphcanvas.getCanvasWindow(); - panel = graphcanvas.createPanel("Options",{ - closable: true - ,window: ref_window - ,onOpen: function(){ - graphcanvas.OPTIONPANEL_IS_OPEN = true; - } - ,onClose: function(){ - graphcanvas.OPTIONPANEL_IS_OPEN = false; - graphcanvas.options_panel = null; - } - }); - graphcanvas.options_panel = panel; - panel.id = "option-panel"; - panel.classList.add("settings"); - - function inner_refresh(){ - - panel.content.innerHTML = ""; //clear - - var fUpdate = function(name, value, options){ - switch(name){ - /*case "Render mode": - // Case "".. - if (options.values && options.key){ - var kV = Object.values(options.values).indexOf(value); - if (kV>=0 && options.values[kV]){ - console.debug("update graph options: "+options.key+": "+kV); - graphcanvas[options.key] = kV; - //console.debug(graphcanvas); - break; - } - } - console.warn("unexpected options"); - console.debug(options); - break;*/ - default: - //console.debug("want to update graph options: "+name+": "+value); - if (options && options.key){ - name = options.key; - } - if (options.values){ - value = Object.values(options.values).indexOf(value); - } - //console.debug("update graph option: "+name+": "+value); - graphcanvas[name] = value; - break; - } - }; - - // panel.addWidget( "string", "Graph name", "", {}, fUpdate); // implement - - var aProps = LiteGraph.availableCanvasOptions; - aProps.sort(); - for(var pI in aProps){ - var pX = aProps[pI]; - panel.addWidget( "boolean", pX, graphcanvas[pX], {key: pX, on: "True", off: "False"}, fUpdate); - } - - var aLinks = [ graphcanvas.links_render_mode ]; - panel.addWidget( "combo", "Render mode", LiteGraph.LINK_RENDER_MODES[graphcanvas.links_render_mode], {key: "links_render_mode", values: LiteGraph.LINK_RENDER_MODES}, fUpdate); - - panel.addSeparator(); - - panel.footer.innerHTML = ""; // clear - - } - inner_refresh(); - - graphcanvas.canvas.parentNode.appendChild( panel ); - } - - LGraphCanvas.prototype.showShowNodePanel = function( node ) - { - this.SELECTED_NODE = node; - this.closePanels(); - var ref_window = this.getCanvasWindow(); - var that = this; - var graphcanvas = this; - var panel = this.createPanel(node.title || "",{ - closable: true - ,window: ref_window - ,onOpen: function(){ - graphcanvas.NODEPANEL_IS_OPEN = true; - } - ,onClose: function(){ - graphcanvas.NODEPANEL_IS_OPEN = false; - graphcanvas.node_panel = null; - } - }); - graphcanvas.node_panel = panel; - panel.id = "node-panel"; - panel.node = node; - panel.classList.add("settings"); - - function inner_refresh() - { - panel.content.innerHTML = ""; //clear - panel.addHTML(""+node.type+""+(node.constructor.desc || "")+""); - - panel.addHTML("

Properties

"); - - var fUpdate = function(name,value){ - graphcanvas.graph.beforeChange(node); - switch(name){ - case "Title": - node.title = value; - break; - case "Mode": - var kV = Object.values(LiteGraph.NODE_MODES).indexOf(value); - if (kV>=0 && LiteGraph.NODE_MODES[kV]){ - node.changeMode(kV); - }else{ - console.warn("unexpected mode: "+value); - } - break; - case "Color": - if (LGraphCanvas.node_colors[value]){ - node.color = LGraphCanvas.node_colors[value].color; - node.bgcolor = LGraphCanvas.node_colors[value].bgcolor; - }else{ - console.warn("unexpected color: "+value); - } - break; - default: - node.setProperty(name,value); - break; - } - graphcanvas.graph.afterChange(); - graphcanvas.dirty_canvas = true; - }; - - panel.addWidget( "string", "Title", node.title, {}, fUpdate); - - panel.addWidget( "combo", "Mode", LiteGraph.NODE_MODES[node.mode], {values: LiteGraph.NODE_MODES}, fUpdate); - - var nodeCol = ""; - if (node.color !== undefined){ - nodeCol = Object.keys(LGraphCanvas.node_colors).filter(function(nK){ return LGraphCanvas.node_colors[nK].color == node.color; }); - } - - panel.addWidget( "combo", "Color", nodeCol, {values: Object.keys(LGraphCanvas.node_colors)}, fUpdate); - - for(var pName in node.properties) - { - var value = node.properties[pName]; - var info = node.getPropertyInfo(pName); - var type = info.type || "string"; - - //in case the user wants control over the side panel widget - if( node.onAddPropertyToPanel && node.onAddPropertyToPanel(pName,panel) ) - continue; - - panel.addWidget( info.widget || info.type, pName, value, info, fUpdate); - } - - panel.addSeparator(); - - if(node.onShowCustomPanelInfo) - node.onShowCustomPanelInfo(panel); - - panel.footer.innerHTML = ""; // clear - panel.addButton("Delete",function(){ - if(node.block_delete) - return; - node.graph.remove(node); - panel.close(); - }).classList.add("delete"); - } - - panel.inner_showCodePad = function( propname ) - { - panel.classList.remove("settings"); - panel.classList.add("centered"); - - - /*if(window.CodeFlask) //disabled for now - { - panel.content.innerHTML = "
"; - var flask = new CodeFlask( "div.code", { language: 'js' }); - flask.updateCode(node.properties[propname]); - flask.onUpdate( function(code) { - node.setProperty(propname, code); - }); - } - else - {*/ - panel.alt_content.innerHTML = ""; - var textarea = panel.alt_content.querySelector("textarea"); - var fDoneWith = function(){ - panel.toggleAltContent(false); //if(node_prop_div) node_prop_div.style.display = "block"; // panel.close(); - panel.toggleFooterVisibility(true); - textarea.parentNode.removeChild(textarea); - panel.classList.add("settings"); - panel.classList.remove("centered"); - inner_refresh(); - } - textarea.value = node.properties[propname]; - textarea.addEventListener("keydown", function(e){ - if(e.code == "Enter" && e.ctrlKey ) - { - node.setProperty(propname, textarea.value); - fDoneWith(); - } - }); - panel.toggleAltContent(true); - panel.toggleFooterVisibility(false); - textarea.style.height = "calc(100% - 40px)"; - /*}*/ - var assign = panel.addButton( "Assign", function(){ - node.setProperty(propname, textarea.value); - fDoneWith(); - }); - panel.alt_content.appendChild(assign); //panel.content.appendChild(assign); - var button = panel.addButton( "Close", fDoneWith); - button.style.float = "right"; - panel.alt_content.appendChild(button); // panel.content.appendChild(button); - } - - inner_refresh(); - - this.canvas.parentNode.appendChild( panel ); - } - - LGraphCanvas.prototype.showSubgraphPropertiesDialog = function(node) - { - console.log("showing subgraph properties dialog"); - - var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); - if(old_panel) - old_panel.close(); - - var panel = this.createPanel("Subgraph Inputs",{closable:true, width: 500}); - panel.node = node; - panel.classList.add("subgraph_dialog"); - - function inner_refresh() - { - panel.clear(); - - //show currents - if(node.inputs) - for(var i = 0; i < node.inputs.length; ++i) - { - var input = node.inputs[i]; - if(input.not_subgraph_input) - continue; - var html = " "; - var elem = panel.addHTML(html,"subgraph_property"); - elem.dataset["name"] = input.name; - elem.dataset["slot"] = i; - elem.querySelector(".name").innerText = input.name; - elem.querySelector(".type").innerText = input.type; - elem.querySelector("button").addEventListener("click",function(e){ - node.removeInput( Number( this.parentNode.dataset["slot"] ) ); - inner_refresh(); - }); - } - } - - //add extra - var html = " + NameType"; - var elem = panel.addHTML(html,"subgraph_property extra", true); - elem.querySelector("button").addEventListener("click", function(e){ - var elem = this.parentNode; - var name = elem.querySelector(".name").value; - var type = elem.querySelector(".type").value; - if(!name || node.findInputSlot(name) != -1) - return; - node.addInput(name,type); - elem.querySelector(".name").value = ""; - elem.querySelector(".type").value = ""; - inner_refresh(); - }); - - inner_refresh(); - this.canvas.parentNode.appendChild(panel); - return panel; - } - LGraphCanvas.prototype.showSubgraphPropertiesDialogRight = function (node) { - - // console.log("showing subgraph properties dialog"); - var that = this; - // old_panel if old_panel is exist close it - var old_panel = this.canvas.parentNode.querySelector(".subgraph_dialog"); - if (old_panel) - old_panel.close(); - // new panel - var panel = this.createPanel("Subgraph Outputs", { closable: true, width: 500 }); - panel.node = node; - panel.classList.add("subgraph_dialog"); - - function inner_refresh() { - panel.clear(); - //show currents - if (node.outputs) - for (var i = 0; i < node.outputs.length; ++i) { - var input = node.outputs[i]; - if (input.not_subgraph_output) - continue; - var html = " "; - var elem = panel.addHTML(html, "subgraph_property"); - elem.dataset["name"] = input.name; - elem.dataset["slot"] = i; - elem.querySelector(".name").innerText = input.name; - elem.querySelector(".type").innerText = input.type; - elem.querySelector("button").addEventListener("click", function (e) { - node.removeOutput(Number(this.parentNode.dataset["slot"])); - inner_refresh(); - }); - } - } - - //add extra - var html = " + NameType"; - var elem = panel.addHTML(html, "subgraph_property extra", true); - elem.querySelector(".name").addEventListener("keydown", function (e) { - if (e.keyCode == 13) { - addOutput.apply(this) - } - }) - elem.querySelector("button").addEventListener("click", function (e) { - addOutput.apply(this) - }); - function addOutput() { - var elem = this.parentNode; - var name = elem.querySelector(".name").value; - var type = elem.querySelector(".type").value; - if (!name || node.findOutputSlot(name) != -1) - return; - node.addOutput(name, type); - elem.querySelector(".name").value = ""; - elem.querySelector(".type").value = ""; - inner_refresh(); - } - - inner_refresh(); - this.canvas.parentNode.appendChild(panel); - return panel; - } - LGraphCanvas.prototype.checkPanels = function() - { - if(!this.canvas) - return; - var panels = this.canvas.parentNode.querySelectorAll(".litegraph.dialog"); - for(var i = 0; i < panels.length; ++i) - { - var panel = panels[i]; - if( !panel.node ) - continue; - if( !panel.node.graph || panel.graph != this.graph ) - panel.close(); - } - } - - LGraphCanvas.onMenuNodeCollapse = function(value, options, e, menu, node) { - node.graph.beforeChange(/*?*/); - - var fApplyMultiNode = function(node){ - node.collapse(); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.graph.afterChange(/*?*/); - }; - - LGraphCanvas.onMenuNodePin = function(value, options, e, menu, node) { - node.pin(); - }; - - LGraphCanvas.onMenuNodeMode = function(value, options, e, menu, node) { - new LiteGraph.ContextMenu( - LiteGraph.NODE_MODES, - { event: e, callback: inner_clicked, parentMenu: menu, node: node } - ); - - function inner_clicked(v) { - if (!node) { - return; - } - var kV = Object.values(LiteGraph.NODE_MODES).indexOf(v); - var fApplyMultiNode = function(node){ - if (kV>=0 && LiteGraph.NODE_MODES[kV]) - node.changeMode(kV); - else{ - console.warn("unexpected mode: "+v); - node.changeMode(LiteGraph.ALWAYS); - } - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - } - - return false; - }; - - LGraphCanvas.onMenuNodeColors = function(value, options, e, menu, node) { - if (!node) { - throw "no node for color"; - } - - var values = []; - values.push({ - value: null, - content: - "No color" - }); - - for (var i in LGraphCanvas.node_colors) { - var color = LGraphCanvas.node_colors[i]; - var value = { - value: i, - content: - "" + - i + - "" - }; - values.push(value); - } - new LiteGraph.ContextMenu(values, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node - }); - - function inner_clicked(v) { - if (!node) { - return; - } - - var color = v.value ? LGraphCanvas.node_colors[v.value] : null; - - var fApplyColor = function(node){ - if (color) { - if (node.constructor === LiteGraph.LGraphGroup) { - node.color = color.groupcolor; - } else { - node.color = color.color; - node.bgcolor = color.bgcolor; - } - } else { - delete node.color; - delete node.bgcolor; - } - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyColor(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyColor(graphcanvas.selected_nodes[i]); - } - } - node.setDirtyCanvas(true, true); - } - - return false; - }; - - LGraphCanvas.onMenuNodeShapes = function(value, options, e, menu, node) { - if (!node) { - throw "no node passed"; - } - - new LiteGraph.ContextMenu(LiteGraph.VALID_SHAPES, { - event: e, - callback: inner_clicked, - parentMenu: menu, - node: node - }); - - function inner_clicked(v) { - if (!node) { - return; - } - node.graph.beforeChange(/*?*/); //node - - var fApplyMultiNode = function(node){ - node.shape = v; - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - node.graph.afterChange(/*?*/); //node - node.setDirtyCanvas(true); - } - - return false; - }; - - LGraphCanvas.onMenuNodeRemove = function(value, options, e, menu, node) { - if (!node) { - throw "no node passed"; - } - - var graph = node.graph; - graph.beforeChange(); - - - var fApplyMultiNode = function(node){ - if (node.removable === false) { - return; - } - graph.remove(node); - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - graph.afterChange(); - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.onMenuNodeToSubgraph = function(value, options, e, menu, node) { - var graph = node.graph; - var graphcanvas = LGraphCanvas.active_canvas; - if(!graphcanvas) //?? - return; - - var nodes_list = Object.values( graphcanvas.selected_nodes || {} ); - if( !nodes_list.length ) - nodes_list = [ node ]; - - var subgraph_node = LiteGraph.createNode("graph/subgraph"); - subgraph_node.pos = node.pos.concat(); - graph.add(subgraph_node); - - subgraph_node.buildFromNodes( nodes_list ); - - graphcanvas.deselectAllNodes(); - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.onMenuNodeClone = function(value, options, e, menu, node) { - - node.graph.beforeChange(); - - var newSelected = {}; - - var fApplyMultiNode = function(node){ - if (node.clonable === false) { - return; - } - var newnode = node.clone(); - if (!newnode) { - return; - } - newnode.pos = [node.pos[0] + 5, node.pos[1] + 5]; - node.graph.add(newnode); - newSelected[newnode.id] = newnode; - } - - var graphcanvas = LGraphCanvas.active_canvas; - if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1){ - fApplyMultiNode(node); - }else{ - for (var i in graphcanvas.selected_nodes) { - fApplyMultiNode(graphcanvas.selected_nodes[i]); - } - } - - if(Object.keys(newSelected).length){ - graphcanvas.selectNodes(newSelected); - } - - node.graph.afterChange(); - - node.setDirtyCanvas(true, true); - }; - - LGraphCanvas.node_colors = { - red: { color: "#322", bgcolor: "#533", groupcolor: "#A88" }, - brown: { color: "#332922", bgcolor: "#593930", groupcolor: "#b06634" }, - green: { color: "#232", bgcolor: "#353", groupcolor: "#8A8" }, - blue: { color: "#223", bgcolor: "#335", groupcolor: "#88A" }, - pale_blue: { - color: "#2a363b", - bgcolor: "#3f5159", - groupcolor: "#3f789e" - }, - cyan: { color: "#233", bgcolor: "#355", groupcolor: "#8AA" }, - purple: { color: "#323", bgcolor: "#535", groupcolor: "#a1309b" }, - yellow: { color: "#432", bgcolor: "#653", groupcolor: "#b58b2a" }, - black: { color: "#222", bgcolor: "#000", groupcolor: "#444" } - }; - - LGraphCanvas.prototype.getCanvasMenuOptions = function() { - var options = null; - var that = this; - if (this.getMenuOptions) { - options = this.getMenuOptions(); - } else { - options = [ - { - content: "Add Node", - has_submenu: true, - callback: LGraphCanvas.onMenuAdd - }, - { content: "Add Group", callback: LGraphCanvas.onGroupAdd }, - //{ content: "Arrange", callback: that.graph.arrange }, - //{content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll } - ]; - /*if (LiteGraph.showCanvasOptions){ - options.push({ content: "Options", callback: that.showShowGraphOptionsPanel }); - }*/ - - if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Align", - has_submenu: true, - callback: LGraphCanvas.onGroupAlign, - }) - } - - if (this._graph_stack && this._graph_stack.length > 0) { - options.push(null, { - content: "Close subgraph", - callback: this.closeSubgraph.bind(this) - }); - } - } - - if (this.getExtraMenuOptions) { - var extra = this.getExtraMenuOptions(this, options); - if (extra) { - options = options.concat(extra); - } - } - - return options; - }; - - //called by processContextMenu to extract the menu list - LGraphCanvas.prototype.getNodeMenuOptions = function(node) { - var options = null; - - if (node.getMenuOptions) { - options = node.getMenuOptions(this); - } else { - options = [ - { - content: "Inputs", - has_submenu: true, - disabled: true, - callback: LGraphCanvas.showMenuNodeOptionalInputs - }, - { - content: "Outputs", - has_submenu: true, - disabled: true, - callback: LGraphCanvas.showMenuNodeOptionalOutputs - }, - null, - { - content: "Properties", - has_submenu: true, - callback: LGraphCanvas.onShowMenuNodeProperties - }, - { - content: "Properties Panel", - callback: function(item, options, e, menu, node) { LGraphCanvas.active_canvas.showShowNodePanel(node) } - }, - null, - { - content: "Title", - callback: LGraphCanvas.onShowPropertyEditor - }, - { - content: "Mode", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeMode - }]; - if(node.resizable !== false){ - options.push({ - content: "Resize", callback: LGraphCanvas.onMenuResizeNode - }); - } - options.push( - { - content: "Collapse", - callback: LGraphCanvas.onMenuNodeCollapse - }, - { content: "Pin", callback: LGraphCanvas.onMenuNodePin }, - { - content: "Colors", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors - }, - { - content: "Shapes", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeShapes - }, - null - ); - } - - if (node.onGetInputs) { - var inputs = node.onGetInputs(); - if (inputs && inputs.length) { - options[0].disabled = false; - } - } - - if (node.onGetOutputs) { - var outputs = node.onGetOutputs(); - if (outputs && outputs.length) { - options[1].disabled = false; - } - } - - if (node.getExtraMenuOptions) { - var extra = node.getExtraMenuOptions(this, options); - if (extra) { - extra.push(null); - options = extra.concat(options); - } - } - - if (node.clonable !== false) { - options.push({ - content: "Clone", - callback: LGraphCanvas.onMenuNodeClone - }); - } - - if(0) //TODO - options.push({ - content: "To Subgraph", - callback: LGraphCanvas.onMenuNodeToSubgraph - }); - - if (Object.keys(this.selected_nodes).length > 1) { - options.push({ - content: "Align Selected To", - has_submenu: true, - callback: LGraphCanvas.onNodeAlign, - }) - } - - options.push(null, { - content: "Remove", - disabled: !(node.removable !== false && !node.block_delete ), - callback: LGraphCanvas.onMenuNodeRemove - }); - - if (node.graph && node.graph.onGetNodeMenuOptions) { - node.graph.onGetNodeMenuOptions(options, node); - } - - return options; - }; - - LGraphCanvas.prototype.getGroupMenuOptions = function(node) { - var o = [ - { content: "Title", callback: LGraphCanvas.onShowPropertyEditor }, - { - content: "Color", - has_submenu: true, - callback: LGraphCanvas.onMenuNodeColors - }, - { - content: "Font size", - property: "font_size", - type: "Number", - callback: LGraphCanvas.onShowPropertyEditor - }, - null, - { content: "Remove", callback: LGraphCanvas.onMenuNodeRemove } - ]; - - return o; - }; - - LGraphCanvas.prototype.processContextMenu = function(node, event) { - var that = this; - var canvas = LGraphCanvas.active_canvas; - var ref_window = canvas.getCanvasWindow(); - - var menu_info = null; - var options = { - event: event, - callback: inner_option_clicked, - extra: node - }; - - if(node) - options.title = node.type; - - //check if mouse is in input - var slot = null; - if (node) { - slot = node.getSlotInPosition(event.canvasX, event.canvasY); - LGraphCanvas.active_node = node; - } - - if (slot) { - //on slot - menu_info = []; - if (node.getSlotMenuOptions) { - menu_info = node.getSlotMenuOptions(slot); - } else { - if ( - slot && - slot.output && - slot.output.links && - slot.output.links.length - ) { - menu_info.push({ content: "Disconnect Links", slot: slot }); - } - var _slot = slot.input || slot.output; - if (_slot.removable){ - menu_info.push( - _slot.locked - ? "Cannot remove" - : { content: "Remove Slot", slot: slot } - ); - } - if (!_slot.nameLocked){ - menu_info.push({ content: "Rename Slot", slot: slot }); - } - - } - options.title = - (slot.input ? slot.input.type : slot.output.type) || "*"; - if (slot.input && slot.input.type == LiteGraph.ACTION) { - options.title = "Action"; - } - if (slot.output && slot.output.type == LiteGraph.EVENT) { - options.title = "Event"; - } - } else { - if (node) { - //on node - menu_info = this.getNodeMenuOptions(node); - } else { - menu_info = this.getCanvasMenuOptions(); - var group = this.graph.getGroupOnPos( - event.canvasX, - event.canvasY - ); - if (group) { - //on group - menu_info.push(null, { - content: "Edit Group", - has_submenu: true, - submenu: { - title: "Group", - extra: group, - options: this.getGroupMenuOptions(group) - } - }); - } - } - } - - //show menu - if (!menu_info) { - return; - } - - var menu = new LiteGraph.ContextMenu(menu_info, options, ref_window); - - function inner_option_clicked(v, options, e) { - if (!v) { - return; - } - - if (v.content == "Remove Slot") { - var info = v.slot; - node.graph.beforeChange(); - if (info.input) { - node.removeInput(info.slot); - } else if (info.output) { - node.removeOutput(info.slot); - } - node.graph.afterChange(); - return; - } else if (v.content == "Disconnect Links") { - var info = v.slot; - node.graph.beforeChange(); - if (info.output) { - node.disconnectOutput(info.slot); - } else if (info.input) { - node.disconnectInput(info.slot); - } - node.graph.afterChange(); - return; - } else if (v.content == "Rename Slot") { - var info = v.slot; - var slot_info = info.input - ? node.getInputInfo(info.slot) - : node.getOutputInfo(info.slot); - var dialog = that.createDialog( - "Name", - options - ); - var input = dialog.querySelector("input"); - if (input && slot_info) { - input.value = slot_info.label || ""; - } - var inner = function(){ - node.graph.beforeChange(); - if (input.value) { - if (slot_info) { - slot_info.label = input.value; - } - that.setDirty(true); - } - dialog.close(); - node.graph.afterChange(); - } - dialog.querySelector("button").addEventListener("click", inner); - input.addEventListener("keydown", function(e) { - dialog.is_modified = true; - if (e.keyCode == 27) { - //ESC - dialog.close(); - } else if (e.keyCode == 13) { - inner(); // save - } else if (e.keyCode != 13 && e.target.localName != "textarea") { - return; - } - e.preventDefault(); - e.stopPropagation(); - }); - input.focus(); - } - - //if(v.callback) - // return v.callback.call(that, node, options, e, menu, that, event ); - } - }; - - //API ************************************************* - //like rect but rounded corners - if (typeof(window) != "undefined" && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) { - window.CanvasRenderingContext2D.prototype.roundRect = function( - x, - y, - w, - h, - radius, - radius_low - ) { - var top_left_radius = 0; - var top_right_radius = 0; - var bottom_left_radius = 0; - var bottom_right_radius = 0; - - if ( radius === 0 ) - { - this.rect(x,y,w,h); - return; - } - - if(radius_low === undefined) - radius_low = radius; - - //make it compatible with official one - if(radius != null && radius.constructor === Array) - { - if(radius.length == 1) - top_left_radius = top_right_radius = bottom_left_radius = bottom_right_radius = radius[0]; - else if(radius.length == 2) - { - top_left_radius = bottom_right_radius = radius[0]; - top_right_radius = bottom_left_radius = radius[1]; - } - else if(radius.length == 4) - { - top_left_radius = radius[0]; - top_right_radius = radius[1]; - bottom_left_radius = radius[2]; - bottom_right_radius = radius[3]; - } - else - return; - } - else //old using numbers - { - top_left_radius = radius || 0; - top_right_radius = radius || 0; - bottom_left_radius = radius_low || 0; - bottom_right_radius = radius_low || 0; - } - - //top right - this.moveTo(x + top_left_radius, y); - this.lineTo(x + w - top_right_radius, y); - this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius); - - //bottom right - this.lineTo(x + w, y + h - bottom_right_radius); - this.quadraticCurveTo( - x + w, - y + h, - x + w - bottom_right_radius, - y + h - ); - - //bottom left - this.lineTo(x + bottom_right_radius, y + h); - this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius); - - //top left - this.lineTo(x, y + bottom_left_radius); - this.quadraticCurveTo(x, y, x + top_left_radius, y); - }; - }//if - - function compareObjects(a, b) { - for (var i in a) { - if (a[i] != b[i]) { - return false; - } - } - return true; - } - LiteGraph.compareObjects = compareObjects; - - function distance(a, b) { - return Math.sqrt( - (b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]) - ); - } - LiteGraph.distance = distance; - - function colorToString(c) { - return ( - "rgba(" + - Math.round(c[0] * 255).toFixed() + - "," + - Math.round(c[1] * 255).toFixed() + - "," + - Math.round(c[2] * 255).toFixed() + - "," + - (c.length == 4 ? c[3].toFixed(2) : "1.0") + - ")" - ); - } - LiteGraph.colorToString = colorToString; - - function isInsideRectangle(x, y, left, top, width, height) { - if (left < x && left + width > x && top < y && top + height > y) { - return true; - } - return false; - } - LiteGraph.isInsideRectangle = isInsideRectangle; - - //[minx,miny,maxx,maxy] - function growBounding(bounding, x, y) { - if (x < bounding[0]) { - bounding[0] = x; - } else if (x > bounding[2]) { - bounding[2] = x; - } - - if (y < bounding[1]) { - bounding[1] = y; - } else if (y > bounding[3]) { - bounding[3] = y; - } - } - LiteGraph.growBounding = growBounding; - - //point inside bounding box - function isInsideBounding(p, bb) { - if ( - p[0] < bb[0][0] || - p[1] < bb[0][1] || - p[0] > bb[1][0] || - p[1] > bb[1][1] - ) { - return false; - } - return true; - } - LiteGraph.isInsideBounding = isInsideBounding; - - //bounding overlap, format: [ startx, starty, width, height ] - function overlapBounding(a, b) { - var A_end_x = a[0] + a[2]; - var A_end_y = a[1] + a[3]; - var B_end_x = b[0] + b[2]; - var B_end_y = b[1] + b[3]; - - if ( - a[0] > B_end_x || - a[1] > B_end_y || - A_end_x < b[0] || - A_end_y < b[1] - ) { - return false; - } - return true; - } - LiteGraph.overlapBounding = overlapBounding; - - //Convert a hex value to its decimal value - the inputted hex must be in the - // format of a hex triplet - the kind we use for HTML colours. The function - // will return an array with three values. - function hex2num(hex) { - if (hex.charAt(0) == "#") { - hex = hex.slice(1); - } //Remove the '#' char - if there is one. - hex = hex.toUpperCase(); - var hex_alphabets = "0123456789ABCDEF"; - var value = new Array(3); - var k = 0; - var int1, int2; - for (var i = 0; i < 6; i += 2) { - int1 = hex_alphabets.indexOf(hex.charAt(i)); - int2 = hex_alphabets.indexOf(hex.charAt(i + 1)); - value[k] = int1 * 16 + int2; - k++; - } - return value; - } - - LiteGraph.hex2num = hex2num; - - //Give a array with three values as the argument and the function will return - // the corresponding hex triplet. - function num2hex(triplet) { - var hex_alphabets = "0123456789ABCDEF"; - var hex = "#"; - var int1, int2; - for (var i = 0; i < 3; i++) { - int1 = triplet[i] / 16; - int2 = triplet[i] % 16; - - hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2); - } - return hex; - } - - LiteGraph.num2hex = num2hex; - - /* LiteGraph GUI elements used for canvas editing *************************************/ - - /** - * ContextMenu from LiteGUI - * - * @class ContextMenu - * @constructor - * @param {Array} values (allows object { title: "Nice text", callback: function ... }) - * @param {Object} options [optional] Some options:\ - * - title: title to show on top of the menu - * - callback: function to call when an option is clicked, it receives the item information - * - ignore_item_callbacks: ignores the callback inside the item, it just calls the options.callback - * - event: you can pass a MouseEvent, this way the ContextMenu appears in that position - */ - function ContextMenu(values, options) { - options = options || {}; - this.options = options; - var that = this; - - //to link a menu with its parent - if (options.parentMenu) { - if (options.parentMenu.constructor !== this.constructor) { - console.error( - "parentMenu must be of class ContextMenu, ignoring it" - ); - options.parentMenu = null; - } else { - this.parentMenu = options.parentMenu; - this.parentMenu.lock = true; - this.parentMenu.current_submenu = this; - } - } - - var eventClass = null; - if(options.event) //use strings because comparing classes between windows doesnt work - eventClass = options.event.constructor.name; - if ( eventClass !== "MouseEvent" && - eventClass !== "CustomEvent" && - eventClass !== "PointerEvent" - ) { - console.error( - "Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. ("+eventClass+")" - ); - options.event = null; - } - - var root = document.createElement("div"); - root.className = "litegraph litecontextmenu litemenubar-panel"; - if (options.className) { - root.className += " " + options.className; - } - root.style.minWidth = 100; - root.style.minHeight = 100; - root.style.pointerEvents = "none"; - setTimeout(function() { - root.style.pointerEvents = "auto"; - }, 100); //delay so the mouse up event is not caught by this element - - //this prevents the default context browser menu to open in case this menu was created when pressing right button - LiteGraph.pointerListenerAdd(root,"up", - function(e) { - //console.log("pointerevents: ContextMenu up root prevent"); - e.preventDefault(); - return true; - }, - true - ); - root.addEventListener( - "contextmenu", - function(e) { - if (e.button != 2) { - //right button - return false; - } - e.preventDefault(); - return false; - }, - true - ); - - LiteGraph.pointerListenerAdd(root,"down", - function(e) { - //console.log("pointerevents: ContextMenu down"); - if (e.button == 2) { - that.close(); - e.preventDefault(); - return true; - } - }, - true - ); - - function on_mouse_wheel(e) { - var pos = parseInt(root.style.top); - root.style.top = - (pos + e.deltaY * options.scroll_speed).toFixed() + "px"; - e.preventDefault(); - return true; - } - - if (!options.scroll_speed) { - options.scroll_speed = 0.1; - } - - root.addEventListener("wheel", on_mouse_wheel, true); - root.addEventListener("mousewheel", on_mouse_wheel, true); - - this.root = root; - - //title - if (options.title) { - var element = document.createElement("div"); - element.className = "litemenu-title"; - element.innerHTML = options.title; - root.appendChild(element); - } - - //entries - var num = 0; - for (var i=0; i < values.length; i++) { - var name = values.constructor == Array ? values[i] : i; - if (name != null && name.constructor !== String) { - name = name.content === undefined ? String(name) : name.content; - } - var value = values[i]; - this.addItem(name, value, options); - num++; - } - - //close on leave? touch enabled devices won't work TODO use a global device detector and condition on that - /*LiteGraph.pointerListenerAdd(root,"leave", function(e) { - console.log("pointerevents: ContextMenu leave"); - if (that.lock) { - return; - } - if (root.closing_timer) { - clearTimeout(root.closing_timer); - } - root.closing_timer = setTimeout(that.close.bind(that, e), 500); - //that.close(e); - });*/ - - LiteGraph.pointerListenerAdd(root,"enter", function(e) { - //console.log("pointerevents: ContextMenu enter"); - if (root.closing_timer) { - clearTimeout(root.closing_timer); - } - }); - - //insert before checking position - var root_document = document; - if (options.event) { - root_document = options.event.target.ownerDocument; - } - - if (!root_document) { - root_document = document; - } - - if( root_document.fullscreenElement ) - root_document.fullscreenElement.appendChild(root); - else - root_document.body.appendChild(root); - - //compute best position - var left = options.left || 0; - var top = options.top || 0; - if (options.event) { - left = options.event.clientX - 10; - top = options.event.clientY - 10; - if (options.title) { - top -= 20; - } - - if (options.parentMenu) { - var rect = options.parentMenu.root.getBoundingClientRect(); - left = rect.left + rect.width; - } - - var body_rect = document.body.getBoundingClientRect(); - var root_rect = root.getBoundingClientRect(); - if(body_rect.height == 0) - console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }"); - - if (body_rect.width && left > body_rect.width - root_rect.width - 10) { - left = body_rect.width - root_rect.width - 10; - } - if (body_rect.height && top > body_rect.height - root_rect.height - 10) { - top = body_rect.height - root_rect.height - 10; - } - } - - root.style.left = left + "px"; - root.style.top = top + "px"; - - if (options.scale) { - root.style.transform = "scale(" + options.scale + ")"; - } - } - - ContextMenu.prototype.addItem = function(name, value, options) { - var that = this; - options = options || {}; - - var element = document.createElement("div"); - element.className = "litemenu-entry submenu"; - - var disabled = false; - - if (value === null) { - element.classList.add("separator"); - //element.innerHTML = "
" - //continue; - } else { - element.innerHTML = value && value.title ? value.title : name; - element.value = value; - - if (value) { - if (value.disabled) { - disabled = true; - element.classList.add("disabled"); - } - if (value.submenu || value.has_submenu) { - element.classList.add("has_submenu"); - } - } - - if (typeof value == "function") { - element.dataset["value"] = name; - element.onclick_callback = value; - } else { - element.dataset["value"] = value; - } - - if (value.className) { - element.className += " " + value.className; - } - } - - this.root.appendChild(element); - if (!disabled) { - element.addEventListener("click", inner_onclick); - } - if (!disabled && options.autoopen) { - LiteGraph.pointerListenerAdd(element,"enter",inner_over); - } - - function inner_over(e) { - var value = this.value; - if (!value || !value.has_submenu) { - return; - } - //if it is a submenu, autoopen like the item was clicked - inner_onclick.call(this, e); - } - - //menu option clicked - function inner_onclick(e) { - var value = this.value; - var close_parent = true; - - if (that.current_submenu) { - that.current_submenu.close(e); - } - - //global callback - if (options.callback) { - var r = options.callback.call( - this, - value, - options, - e, - that, - options.node - ); - if (r === true) { - close_parent = false; - } - } - - //special cases - if (value) { - if ( - value.callback && - !options.ignore_item_callbacks && - value.disabled !== true - ) { - //item callback - var r = value.callback.call( - this, - value, - options, - e, - that, - options.extra - ); - if (r === true) { - close_parent = false; - } - } - if (value.submenu) { - if (!value.submenu.options) { - throw "ContextMenu submenu needs options"; - } - var submenu = new that.constructor(value.submenu.options, { - callback: value.submenu.callback, - event: e, - parentMenu: that, - ignore_item_callbacks: - value.submenu.ignore_item_callbacks, - title: value.submenu.title, - extra: value.submenu.extra, - autoopen: options.autoopen - }); - close_parent = false; - } - } - - if (close_parent && !that.lock) { - that.close(); - } - } - - return element; - }; - - ContextMenu.prototype.close = function(e, ignore_parent_menu) { - if (this.root.parentNode) { - this.root.parentNode.removeChild(this.root); - } - if (this.parentMenu && !ignore_parent_menu) { - this.parentMenu.lock = false; - this.parentMenu.current_submenu = null; - if (e === undefined) { - this.parentMenu.close(); - } else if ( - e && - !ContextMenu.isCursorOverElement(e, this.parentMenu.root) - ) { - ContextMenu.trigger(this.parentMenu.root, LiteGraph.pointerevents_method+"leave", e); - } - } - if (this.current_submenu) { - this.current_submenu.close(e, true); - } - - if (this.root.closing_timer) { - clearTimeout(this.root.closing_timer); - } - - // TODO implement : LiteGraph.contextMenuClosed(); :: keep track of opened / closed / current ContextMenu - // on key press, allow filtering/selecting the context menu elements - }; - - //this code is used to trigger events easily (used in the context menu mouseleave - ContextMenu.trigger = function(element, event_name, params, origin) { - var evt = document.createEvent("CustomEvent"); - evt.initCustomEvent(event_name, true, true, params); //canBubble, cancelable, detail - evt.srcElement = origin; - if (element.dispatchEvent) { - element.dispatchEvent(evt); - } else if (element.__events) { - element.__events.dispatchEvent(evt); - } - //else nothing seems binded here so nothing to do - return evt; - }; - - //returns the top most menu - ContextMenu.prototype.getTopMenu = function() { - if (this.options.parentMenu) { - return this.options.parentMenu.getTopMenu(); - } - return this; - }; - - ContextMenu.prototype.getFirstEvent = function() { - if (this.options.parentMenu) { - return this.options.parentMenu.getFirstEvent(); - } - return this.options.event; - }; - - ContextMenu.isCursorOverElement = function(event, element) { - var left = event.clientX; - var top = event.clientY; - var rect = element.getBoundingClientRect(); - if (!rect) { - return false; - } - if ( - top > rect.top && - top < rect.top + rect.height && - left > rect.left && - left < rect.left + rect.width - ) { - return true; - } - return false; - }; - - LiteGraph.ContextMenu = ContextMenu; - - LiteGraph.closeAllContextMenus = function(ref_window) { - ref_window = ref_window || window; - - var elements = ref_window.document.querySelectorAll(".litecontextmenu"); - if (!elements.length) { - return; - } - - var result = []; - for (var i = 0; i < elements.length; i++) { - result.push(elements[i]); - } - - for (var i=0; i < result.length; i++) { - if (result[i].close) { - result[i].close(); - } else if (result[i].parentNode) { - result[i].parentNode.removeChild(result[i]); - } - } - }; - - LiteGraph.extendClass = function(target, origin) { - for (var i in origin) { - //copy class properties - if (target.hasOwnProperty(i)) { - continue; - } - target[i] = origin[i]; - } - - if (origin.prototype) { - //copy prototype properties - for (var i in origin.prototype) { - //only enumerable - if (!origin.prototype.hasOwnProperty(i)) { - continue; - } - - if (target.prototype.hasOwnProperty(i)) { - //avoid overwriting existing ones - continue; - } - - //copy getters - if (origin.prototype.__lookupGetter__(i)) { - target.prototype.__defineGetter__( - i, - origin.prototype.__lookupGetter__(i) - ); - } else { - target.prototype[i] = origin.prototype[i]; - } - - //and setters - if (origin.prototype.__lookupSetter__(i)) { - target.prototype.__defineSetter__( - i, - origin.prototype.__lookupSetter__(i) - ); - } - } - } - }; - - //used by some widgets to render a curve editor - function CurveEditor( points ) - { - this.points = points; - this.selected = -1; - this.nearest = -1; - this.size = null; //stores last size used - this.must_update = true; - this.margin = 5; - } - - CurveEditor.sampleCurve = function(f,points) - { - if(!points) - return; - for(var i = 0; i < points.length - 1; ++i) - { - var p = points[i]; - var pn = points[i+1]; - if(pn[0] < f) - continue; - var r = (pn[0] - p[0]); - if( Math.abs(r) < 0.00001 ) - return p[1]; - var local_f = (f - p[0]) / r; - return p[1] * (1.0 - local_f) + pn[1] * local_f; - } - return 0; - } - - CurveEditor.prototype.draw = function( ctx, size, graphcanvas, background_color, line_color, inactive ) - { - var points = this.points; - if(!points) - return; - this.size = size; - var w = size[0] - this.margin * 2; - var h = size[1] - this.margin * 2; - - line_color = line_color || "#666"; - - ctx.save(); - ctx.translate(this.margin,this.margin); - - if(background_color) - { - ctx.fillStyle = "#111"; - ctx.fillRect(0,0,w,h); - ctx.fillStyle = "#222"; - ctx.fillRect(w*0.5,0,1,h); - ctx.strokeStyle = "#333"; - ctx.strokeRect(0,0,w,h); - } - ctx.strokeStyle = line_color; - if(inactive) - ctx.globalAlpha = 0.5; - ctx.beginPath(); - for(var i = 0; i < points.length; ++i) - { - var p = points[i]; - ctx.lineTo( p[0] * w, (1.0 - p[1]) * h ); - } - ctx.stroke(); - ctx.globalAlpha = 1; - if(!inactive) - for(var i = 0; i < points.length; ++i) - { - var p = points[i]; - ctx.fillStyle = this.selected == i ? "#FFF" : (this.nearest == i ? "#DDD" : "#AAA"); - ctx.beginPath(); - ctx.arc( p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2 ); - ctx.fill(); - } - ctx.restore(); - } - - //localpos is mouse in curve editor space - CurveEditor.prototype.onMouseDown = function( localpos, graphcanvas ) - { - var points = this.points; - if(!points) - return; - if( localpos[1] < 0 ) - return; - - //this.captureInput(true); - var w = this.size[0] - this.margin * 2; - var h = this.size[1] - this.margin * 2; - var x = localpos[0] - this.margin; - var y = localpos[1] - this.margin; - var pos = [x,y]; - var max_dist = 30 / graphcanvas.ds.scale; - //search closer one - this.selected = this.getCloserPoint(pos, max_dist); - //create one - if(this.selected == -1) - { - var point = [x / w, 1 - y / h]; - points.push(point); - points.sort(function(a,b){ return a[0] - b[0]; }); - this.selected = points.indexOf(point); - this.must_update = true; - } - if(this.selected != -1) - return true; - } - - CurveEditor.prototype.onMouseMove = function( localpos, graphcanvas ) - { - var points = this.points; - if(!points) - return; - var s = this.selected; - if(s < 0) - return; - var x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2 ); - var y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2 ); - var curvepos = [(localpos[0] - this.margin),(localpos[1] - this.margin)]; - var max_dist = 30 / graphcanvas.ds.scale; - this._nearest = this.getCloserPoint(curvepos, max_dist); - var point = points[s]; - if(point) - { - var is_edge_point = s == 0 || s == points.length - 1; - if( !is_edge_point && (localpos[0] < -10 || localpos[0] > this.size[0] + 10 || localpos[1] < -10 || localpos[1] > this.size[1] + 10) ) - { - points.splice(s,1); - this.selected = -1; - return; - } - if( !is_edge_point ) //not edges - point[0] = clamp(x, 0, 1); - else - point[0] = s == 0 ? 0 : 1; - point[1] = 1.0 - clamp(y, 0, 1); - points.sort(function(a,b){ return a[0] - b[0]; }); - this.selected = points.indexOf(point); - this.must_update = true; - } - } - - CurveEditor.prototype.onMouseUp = function( localpos, graphcanvas ) - { - this.selected = -1; - return false; - } - - CurveEditor.prototype.getCloserPoint = function(pos, max_dist) - { - var points = this.points; - if(!points) - return -1; - max_dist = max_dist || 30; - var w = (this.size[0] - this.margin * 2); - var h = (this.size[1] - this.margin * 2); - var num = points.length; - var p2 = [0,0]; - var min_dist = 1000000; - var closest = -1; - var last_valid = -1; - for(var i = 0; i < num; ++i) - { - var p = points[i]; - p2[0] = p[0] * w; - p2[1] = (1.0 - p[1]) * h; - if(p2[0] < pos[0]) - last_valid = i; - var dist = vec2.distance(pos,p2); - if(dist > min_dist || dist > max_dist) - continue; - closest = i; - min_dist = dist; - } - return closest; - } - - LiteGraph.CurveEditor = CurveEditor; - - //used to create nodes from wrapping functions - LiteGraph.getParameterNames = function(func) { - return (func + "") - .replace(/[/][/].*$/gm, "") // strip single-line comments - .replace(/\s+/g, "") // strip white space - .replace(/[/][*][^/*]*[*][/]/g, "") // strip multi-line comments /**/ - .split("){", 1)[0] - .replace(/^[^(]*[(]/, "") // extract the parameters - .replace(/=[^,]+/g, "") // strip any ES6 defaults - .split(",") - .filter(Boolean); // split & filter [""] - }; - - /* helper for interaction: pointer, touch, mouse Listeners - used by LGraphCanvas DragAndScale ContextMenu*/ - LiteGraph.pointerListenerAdd = function(oDOM, sEvIn, fCall, capture=false) { - if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall!=="function"){ - //console.log("cant pointerListenerAdd "+oDOM+", "+sEvent+", "+fCall); - return; // -- break -- - } - - var sMethod = LiteGraph.pointerevents_method; - var sEvent = sEvIn; - - // UNDER CONSTRUCTION - // convert pointerevents to touch event when not available - if (sMethod=="pointer" && !window.PointerEvent){ - console.warn("sMethod=='pointer' && !window.PointerEvent"); - console.log("Converting pointer["+sEvent+"] : down move up cancel enter TO touchstart touchmove touchend, etc .."); - switch(sEvent){ - case "down":{ - sMethod = "touch"; - sEvent = "start"; - break; - } - case "move":{ - sMethod = "touch"; - //sEvent = "move"; - break; - } - case "up":{ - sMethod = "touch"; - sEvent = "end"; - break; - } - case "cancel":{ - sMethod = "touch"; - //sEvent = "cancel"; - break; - } - case "enter":{ - console.log("debug: Should I send a move event?"); // ??? - break; - } - // case "over": case "out": not used at now - default:{ - console.warn("PointerEvent not available in this browser ? The event "+sEvent+" would not be called"); - } - } - } - - switch(sEvent){ - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - oDOM.addEventListener(sMethod+sEvent, fCall, capture); - } - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (sMethod!="mouse"){ - return oDOM.addEventListener(sMethod+sEvent, fCall, capture); - } - } - // not "pointer" || "mouse" - default: - return oDOM.addEventListener(sEvent, fCall, capture); - } - } - LiteGraph.pointerListenerRemove = function(oDOM, sEvent, fCall, capture=false) { - if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall!=="function"){ - //console.log("cant pointerListenerRemove "+oDOM+", "+sEvent+", "+fCall); - return; // -- break -- - } - switch(sEvent){ - //both pointer and move events - case "down": case "up": case "move": case "over": case "out": case "enter": - { - if (LiteGraph.pointerevents_method=="pointer" || LiteGraph.pointerevents_method=="mouse"){ - oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); - } - } - // only pointerevents - case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture": - { - if (LiteGraph.pointerevents_method=="pointer"){ - return oDOM.removeEventListener(LiteGraph.pointerevents_method+sEvent, fCall, capture); - } - } - // not "pointer" || "mouse" - default: - return oDOM.removeEventListener(sEvent, fCall, capture); - } - } - - function clamp(v, a, b) { - return a > v ? a : b < v ? b : v; - }; - global.clamp = clamp; - - if (typeof window != "undefined" && !window["requestAnimationFrame"]) { - window.requestAnimationFrame = - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - }; - } -})(this); - -if (typeof exports != "undefined") { - exports.LiteGraph = this.LiteGraph; - exports.LGraph = this.LGraph; - exports.LLink = this.LLink; - exports.LGraphNode = this.LGraphNode; - exports.LGraphGroup = this.LGraphGroup; - exports.DragAndScale = this.DragAndScale; - exports.LGraphCanvas = this.LGraphCanvas; - exports.ContextMenu = this.ContextMenu; -} - - diff --git a/web/lib/litegraph.css b/web/lib/litegraph.css deleted file mode 100644 index 5524e24ba..000000000 --- a/web/lib/litegraph.css +++ /dev/null @@ -1,693 +0,0 @@ -/* this CSS contains only the basic CSS needed to run the app and use it */ - -.lgraphcanvas { - /*cursor: crosshair;*/ - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - outline: none; - font-family: Tahoma, sans-serif; -} - -.lgraphcanvas * { - box-sizing: border-box; -} - -.litegraph.litecontextmenu { - font-family: Tahoma, sans-serif; - position: fixed; - top: 100px; - left: 100px; - min-width: 100px; - color: #aaf; - padding: 0; - box-shadow: 0 0 10px black !important; - background-color: #2e2e2e !important; - z-index: 10; -} - -.litegraph.litecontextmenu.dark { - background-color: #000 !important; -} - -.litegraph.litecontextmenu .litemenu-title img { - margin-top: 2px; - margin-left: 2px; - margin-right: 4px; -} - -.litegraph.litecontextmenu .litemenu-entry { - margin: 2px; - padding: 2px; -} - -.litegraph.litecontextmenu .litemenu-entry.submenu { - background-color: #2e2e2e !important; -} - -.litegraph.litecontextmenu.dark .litemenu-entry.submenu { - background-color: #000 !important; -} - -.litegraph .litemenubar ul { - font-family: Tahoma, sans-serif; - margin: 0; - padding: 0; -} - -.litegraph .litemenubar li { - font-size: 14px; - color: #999; - display: inline-block; - min-width: 50px; - padding-left: 10px; - padding-right: 10px; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - cursor: pointer; -} - -.litegraph .litemenubar li:hover { - background-color: #777; - color: #eee; -} - -.litegraph .litegraph .litemenubar-panel { - position: absolute; - top: 5px; - left: 5px; - min-width: 100px; - background-color: #444; - box-shadow: 0 0 3px black; - padding: 4px; - border-bottom: 2px solid #aaf; - z-index: 10; -} - -.litegraph .litemenu-entry, -.litemenu-title { - font-size: 12px; - color: #aaa; - padding: 0 0 0 4px; - margin: 2px; - padding-left: 2px; - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - cursor: pointer; -} - -.litegraph .litemenu-entry .icon { - display: inline-block; - width: 12px; - height: 12px; - margin: 2px; - vertical-align: top; -} - -.litegraph .litemenu-entry.checked .icon { - background-color: #aaf; -} - -.litegraph .litemenu-entry .more { - float: right; - padding-right: 5px; -} - -.litegraph .litemenu-entry.disabled { - opacity: 0.5; - cursor: default; -} - -.litegraph .litemenu-entry.separator { - display: block; - border-top: 1px solid #333; - border-bottom: 1px solid #666; - width: 100%; - height: 0px; - margin: 3px 0 2px 0; - background-color: transparent; - padding: 0 !important; - cursor: default !important; -} - -.litegraph .litemenu-entry.has_submenu { - border-right: 2px solid cyan; -} - -.litegraph .litemenu-title { - color: #dde; - background-color: #111; - margin: 0; - padding: 2px; - cursor: default; -} - -.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) { - background-color: #444 !important; - color: #eee; - transition: all 0.2s; -} - -.litegraph .litemenu-entry .property_name { - display: inline-block; - text-align: left; - min-width: 80px; - min-height: 1.2em; -} - -.litegraph .litemenu-entry .property_value { - display: inline-block; - background-color: rgba(0, 0, 0, 0.5); - text-align: right; - min-width: 80px; - min-height: 1.2em; - vertical-align: middle; - padding-right: 10px; -} - -.litegraph.litesearchbox { - font-family: Tahoma, sans-serif; - position: absolute; - background-color: rgba(0, 0, 0, 0.5); - padding-top: 4px; -} - -.litegraph.litesearchbox input, -.litegraph.litesearchbox select { - margin-top: 3px; - min-width: 60px; - min-height: 1.5em; - background-color: black; - border: 0; - color: white; - padding-left: 10px; - margin-right: 5px; - max-width: 300px; -} - -.litegraph.litesearchbox .name { - display: inline-block; - min-width: 60px; - min-height: 1.5em; - padding-left: 10px; -} - -.litegraph.litesearchbox .helper { - overflow: auto; - max-height: 200px; - margin-top: 2px; -} - -.litegraph.lite-search-item { - font-family: Tahoma, sans-serif; - background-color: rgba(0, 0, 0, 0.5); - color: white; - padding-top: 2px; -} - -.litegraph.lite-search-item.not_in_filter{ - /*background-color: rgba(50, 50, 50, 0.5);*/ - /*color: #999;*/ - color: #B99; - font-style: italic; -} - -.litegraph.lite-search-item.generic_type{ - /*background-color: rgba(50, 50, 50, 0.5);*/ - /*color: #DD9;*/ - color: #999; - font-style: italic; -} - -.litegraph.lite-search-item:hover, -.litegraph.lite-search-item.selected { - cursor: pointer; - background-color: white; - color: black; -} - -.litegraph.lite-search-item-type { - display: inline-block; - background: rgba(0,0,0,0.2); - margin-left: 5px; - font-size: 14px; - padding: 2px 5px; - position: relative; - top: -2px; - opacity: 0.8; - border-radius: 4px; - } - -/* DIALOGs ******/ - -.litegraph .dialog { - position: absolute; - top: 50%; - left: 50%; - margin-top: -150px; - margin-left: -200px; - - background-color: #2A2A2A; - - min-width: 400px; - min-height: 200px; - box-shadow: 0 0 4px #111; - border-radius: 6px; -} - -.litegraph .dialog.settings { - left: 10px; - top: 10px; - height: calc( 100% - 20px ); - margin: auto; - max-width: 50%; -} - -.litegraph .dialog.centered { - top: 50px; - left: 50%; - position: absolute; - transform: translateX(-50%); - min-width: 600px; - min-height: 300px; - height: calc( 100% - 100px ); - margin: auto; -} - -.litegraph .dialog .close { - float: right; - margin: 4px; - margin-right: 10px; - cursor: pointer; - font-size: 1.4em; -} - -.litegraph .dialog .close:hover { - color: white; -} - -.litegraph .dialog .dialog-header { - color: #AAA; - border-bottom: 1px solid #161616; -} - -.litegraph .dialog .dialog-header { height: 40px; } -.litegraph .dialog .dialog-footer { height: 50px; padding: 10px; border-top: 1px solid #1a1a1a;} - -.litegraph .dialog .dialog-header .dialog-title { - font: 20px "Arial"; - margin: 4px; - padding: 4px 10px; - display: inline-block; -} - -.litegraph .dialog .dialog-content, .litegraph .dialog .dialog-alt-content { - height: calc(100% - 90px); - width: 100%; - min-height: 100px; - display: inline-block; - color: #AAA; - /*background-color: black;*/ - overflow: auto; -} - -.litegraph .dialog .dialog-content h3 { - margin: 10px; -} - -.litegraph .dialog .dialog-content .connections { - flex-direction: row; -} - -.litegraph .dialog .dialog-content .connections .connections_side { - width: calc(50% - 5px); - min-height: 100px; - background-color: black; - display: flex; -} - -.litegraph .dialog .node_type { - font-size: 1.2em; - display: block; - margin: 10px; -} - -.litegraph .dialog .node_desc { - opacity: 0.5; - display: block; - margin: 10px; -} - -.litegraph .dialog .separator { - display: block; - width: calc( 100% - 4px ); - height: 1px; - border-top: 1px solid #000; - border-bottom: 1px solid #333; - margin: 10px 2px; - padding: 0; -} - -.litegraph .dialog .property { - margin-bottom: 2px; - padding: 4px; -} - -.litegraph .dialog .property:hover { - background: #545454; -} - -.litegraph .dialog .property_name { - color: #737373; - display: inline-block; - text-align: left; - vertical-align: top; - width: 160px; - padding-left: 4px; - overflow: hidden; - margin-right: 6px; -} - -.litegraph .dialog .property:hover .property_name { - color: white; -} - -.litegraph .dialog .property_value { - display: inline-block; - text-align: right; - color: #AAA; - background-color: #1A1A1A; - /*width: calc( 100% - 122px );*/ - max-width: calc( 100% - 162px ); - min-width: 200px; - max-height: 300px; - min-height: 20px; - padding: 4px; - padding-right: 12px; - overflow: hidden; - cursor: pointer; - border-radius: 3px; -} - -.litegraph .dialog .property_value:hover { - color: white; -} - -.litegraph .dialog .property.boolean .property_value { - padding-right: 30px; - color: #A88; - /*width: auto; - float: right;*/ -} - -.litegraph .dialog .property.boolean.bool-on .property_name{ - color: #8A8; -} -.litegraph .dialog .property.boolean.bool-on .property_value{ - color: #8A8; -} - -.litegraph .dialog .btn { - border: 0; - border-radius: 4px; - padding: 4px 20px; - margin-left: 0px; - background-color: #060606; - color: #8e8e8e; -} - -.litegraph .dialog .btn:hover { - background-color: #111; - color: #FFF; -} - -.litegraph .dialog .btn.delete:hover { - background-color: #F33; - color: black; -} - -.litegraph .subgraph_property { - padding: 4px; -} - -.litegraph .subgraph_property:hover { - background-color: #333; -} - -.litegraph .subgraph_property.extra { - margin-top: 8px; -} - -.litegraph .subgraph_property span.name { - font-size: 1.3em; - padding-left: 4px; -} - -.litegraph .subgraph_property span.type { - opacity: 0.5; - margin-right: 20px; - padding-left: 4px; -} - -.litegraph .subgraph_property span.label { - display: inline-block; - width: 60px; - padding: 0px 10px; -} - -.litegraph .subgraph_property input { - width: 140px; - color: #999; - background-color: #1A1A1A; - border-radius: 4px; - border: 0; - margin-right: 10px; - padding: 4px; - padding-left: 10px; -} - -.litegraph .subgraph_property button { - background-color: #1c1c1c; - color: #aaa; - border: 0; - border-radius: 2px; - padding: 4px 10px; - cursor: pointer; -} - -.litegraph .subgraph_property.extra { - color: #ccc; -} - -.litegraph .subgraph_property.extra input { - background-color: #111; -} - -.litegraph .bullet_icon { - margin-left: 10px; - border-radius: 10px; - width: 12px; - height: 12px; - background-color: #666; - display: inline-block; - margin-top: 2px; - margin-right: 4px; - transition: background-color 0.1s ease 0s; - -moz-transition: background-color 0.1s ease 0s; -} - -.litegraph .bullet_icon:hover { - background-color: #698; - cursor: pointer; -} - -/* OLD */ - -.graphcontextmenu { - padding: 4px; - min-width: 100px; -} - -.graphcontextmenu-title { - color: #dde; - background-color: #222; - margin: 0; - padding: 2px; - cursor: default; -} - -.graphmenu-entry { - box-sizing: border-box; - margin: 2px; - padding-left: 20px; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - transition: all linear 0.3s; -} - -.graphmenu-entry.event, -.litemenu-entry.event { - border-left: 8px solid orange; - padding-left: 12px; -} - -.graphmenu-entry.disabled { - opacity: 0.3; -} - -.graphmenu-entry.submenu { - border-right: 2px solid #eee; -} - -.graphmenu-entry:hover { - background-color: #555; -} - -.graphmenu-entry.separator { - background-color: #111; - border-bottom: 1px solid #666; - height: 1px; - width: calc(100% - 20px); - -moz-width: calc(100% - 20px); - -webkit-width: calc(100% - 20px); -} - -.graphmenu-entry .property_name { - display: inline-block; - text-align: left; - min-width: 80px; - min-height: 1.2em; -} - -.graphmenu-entry .property_value, -.litemenu-entry .property_value { - display: inline-block; - background-color: rgba(0, 0, 0, 0.5); - text-align: right; - min-width: 80px; - min-height: 1.2em; - vertical-align: middle; - padding-right: 10px; -} - -.graphdialog { - position: absolute; - top: 10px; - left: 10px; - min-height: 2em; - background-color: #333; - font-size: 1.2em; - box-shadow: 0 0 10px black !important; - z-index: 10; -} - -.graphdialog.rounded { - border-radius: 12px; - padding-right: 2px; -} - -.graphdialog .name { - display: inline-block; - min-width: 60px; - min-height: 1.5em; - padding-left: 10px; -} - -.graphdialog input, -.graphdialog textarea, -.graphdialog select { - margin: 3px; - min-width: 60px; - min-height: 1.5em; - background-color: black; - border: 0; - color: white; - padding-left: 10px; - outline: none; -} - -.graphdialog textarea { - min-height: 150px; -} - -.graphdialog button { - margin-top: 3px; - vertical-align: top; - background-color: #999; - border: 0; -} - -.graphdialog button.rounded, -.graphdialog input.rounded { - border-radius: 0 12px 12px 0; -} - -.graphdialog .helper { - overflow: auto; - max-height: 200px; -} - -.graphdialog .help-item { - padding-left: 10px; -} - -.graphdialog .help-item:hover, -.graphdialog .help-item.selected { - cursor: pointer; - background-color: white; - color: black; -} - -.litegraph .dialog { - min-height: 0; -} -.litegraph .dialog .dialog-content { -display: block; -} -.litegraph .dialog .dialog-content .subgraph_property { -padding: 5px; -} -.litegraph .dialog .dialog-footer { -margin: 0; -} -.litegraph .dialog .dialog-footer .subgraph_property { -margin-top: 0; -display: flex; -align-items: center; -padding: 5px; -} -.litegraph .dialog .dialog-footer .subgraph_property .name { -flex: 1; -} -.litegraph .graphdialog { -display: flex; -align-items: center; -border-radius: 20px; -padding: 4px 10px; -position: fixed; -} -.litegraph .graphdialog .name { -padding: 0; -min-height: 0; -font-size: 16px; -vertical-align: middle; -} -.litegraph .graphdialog .value { -font-size: 16px; -min-height: 0; -margin: 0 10px; -padding: 2px 5px; -} -.litegraph .graphdialog input[type="checkbox"] { -width: 16px; -height: 16px; -} -.litegraph .graphdialog button { -padding: 4px 18px; -border-radius: 20px; -cursor: pointer; -} - diff --git a/web/lib/litegraph.extensions.js b/web/lib/litegraph.extensions.js deleted file mode 100644 index 32853fe49..000000000 --- a/web/lib/litegraph.extensions.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Changes the background color of the canvas. - * - * @method updateBackground - * @param {image} String - * @param {clearBackgroundColor} String - * @ - */ -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 -} diff --git a/web/lib/materialdesignicons.min.css b/web/lib/materialdesignicons.min.css deleted file mode 100644 index 459ce9ea8..000000000 --- a/web/lib/materialdesignicons.min.css +++ /dev/null @@ -1,3 +0,0 @@ -@font-face{font-family:"Material Design Icons";src:url("../fonts/materialdesignicons-webfont.eot?v=7.4.47");src:url("../fonts/materialdesignicons-webfont.eot?#iefix&v=7.4.47") format("embedded-opentype"),url("../fonts/materialdesignicons-webfont.woff2?v=7.4.47") format("woff2"),url("../fonts/materialdesignicons-webfont.woff?v=7.4.47") format("woff"),url("../fonts/materialdesignicons-webfont.ttf?v=7.4.47") format("truetype");font-weight:normal;font-style:normal}.mdi:before,.mdi-set{display:inline-block;font:normal normal normal 24px/1 "Material Design Icons";font-size:inherit;text-rendering:auto;line-height:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.mdi-ab-testing::before{content:"\F01C9"}.mdi-abacus::before{content:"\F16E0"}.mdi-abjad-arabic::before{content:"\F1328"}.mdi-abjad-hebrew::before{content:"\F1329"}.mdi-abugida-devanagari::before{content:"\F132A"}.mdi-abugida-thai::before{content:"\F132B"}.mdi-access-point::before{content:"\F0003"}.mdi-access-point-check::before{content:"\F1538"}.mdi-access-point-minus::before{content:"\F1539"}.mdi-access-point-network::before{content:"\F0002"}.mdi-access-point-network-off::before{content:"\F0BE1"}.mdi-access-point-off::before{content:"\F1511"}.mdi-access-point-plus::before{content:"\F153A"}.mdi-access-point-remove::before{content:"\F153B"}.mdi-account::before{content:"\F0004"}.mdi-account-alert::before{content:"\F0005"}.mdi-account-alert-outline::before{content:"\F0B50"}.mdi-account-arrow-down::before{content:"\F1868"}.mdi-account-arrow-down-outline::before{content:"\F1869"}.mdi-account-arrow-left::before{content:"\F0B51"}.mdi-account-arrow-left-outline::before{content:"\F0B52"}.mdi-account-arrow-right::before{content:"\F0B53"}.mdi-account-arrow-right-outline::before{content:"\F0B54"}.mdi-account-arrow-up::before{content:"\F1867"}.mdi-account-arrow-up-outline::before{content:"\F186A"}.mdi-account-badge::before{content:"\F1B0A"}.mdi-account-badge-outline::before{content:"\F1B0B"}.mdi-account-box::before{content:"\F0006"}.mdi-account-box-edit-outline::before{content:"\F1CC8"}.mdi-account-box-minus-outline::before{content:"\F1CC9"}.mdi-account-box-multiple::before{content:"\F0934"}.mdi-account-box-multiple-outline::before{content:"\F100A"}.mdi-account-box-outline::before{content:"\F0007"}.mdi-account-box-plus-outline::before{content:"\F1CCA"}.mdi-account-cancel::before{content:"\F12DF"}.mdi-account-cancel-outline::before{content:"\F12E0"}.mdi-account-card::before{content:"\F1BA4"}.mdi-account-card-outline::before{content:"\F1BA5"}.mdi-account-cash::before{content:"\F1097"}.mdi-account-cash-outline::before{content:"\F1098"}.mdi-account-check::before{content:"\F0008"}.mdi-account-check-outline::before{content:"\F0BE2"}.mdi-account-child::before{content:"\F0A89"}.mdi-account-child-circle::before{content:"\F0A8A"}.mdi-account-child-outline::before{content:"\F10C8"}.mdi-account-circle::before{content:"\F0009"}.mdi-account-circle-outline::before{content:"\F0B55"}.mdi-account-clock::before{content:"\F0B56"}.mdi-account-clock-outline::before{content:"\F0B57"}.mdi-account-cog::before{content:"\F1370"}.mdi-account-cog-outline::before{content:"\F1371"}.mdi-account-convert::before{content:"\F000A"}.mdi-account-convert-outline::before{content:"\F1301"}.mdi-account-cowboy-hat::before{content:"\F0E9B"}.mdi-account-cowboy-hat-outline::before{content:"\F17F3"}.mdi-account-credit-card::before{content:"\F1BA6"}.mdi-account-credit-card-outline::before{content:"\F1BA7"}.mdi-account-details::before{content:"\F0631"}.mdi-account-details-outline::before{content:"\F1372"}.mdi-account-edit::before{content:"\F06BC"}.mdi-account-edit-outline::before{content:"\F0FFB"}.mdi-account-eye::before{content:"\F0420"}.mdi-account-eye-outline::before{content:"\F127B"}.mdi-account-file::before{content:"\F1CA7"}.mdi-account-file-outline::before{content:"\F1CA8"}.mdi-account-file-text::before{content:"\F1CA9"}.mdi-account-file-text-outline::before{content:"\F1CAA"}.mdi-account-filter::before{content:"\F0936"}.mdi-account-filter-outline::before{content:"\F0F9D"}.mdi-account-group::before{content:"\F0849"}.mdi-account-group-outline::before{content:"\F0B58"}.mdi-account-hard-hat::before{content:"\F05B5"}.mdi-account-hard-hat-outline::before{content:"\F1A1F"}.mdi-account-heart::before{content:"\F0899"}.mdi-account-heart-outline::before{content:"\F0BE3"}.mdi-account-injury::before{content:"\F1815"}.mdi-account-injury-outline::before{content:"\F1816"}.mdi-account-key::before{content:"\F000B"}.mdi-account-key-outline::before{content:"\F0BE4"}.mdi-account-lock::before{content:"\F115E"}.mdi-account-lock-open::before{content:"\F1960"}.mdi-account-lock-open-outline::before{content:"\F1961"}.mdi-account-lock-outline::before{content:"\F115F"}.mdi-account-minus::before{content:"\F000D"}.mdi-account-minus-outline::before{content:"\F0AEC"}.mdi-account-multiple::before{content:"\F000E"}.mdi-account-multiple-check::before{content:"\F08C5"}.mdi-account-multiple-check-outline::before{content:"\F11FE"}.mdi-account-multiple-minus::before{content:"\F05D3"}.mdi-account-multiple-minus-outline::before{content:"\F0BE5"}.mdi-account-multiple-outline::before{content:"\F000F"}.mdi-account-multiple-plus::before{content:"\F0010"}.mdi-account-multiple-plus-outline::before{content:"\F0800"}.mdi-account-multiple-remove::before{content:"\F120A"}.mdi-account-multiple-remove-outline::before{content:"\F120B"}.mdi-account-music::before{content:"\F0803"}.mdi-account-music-outline::before{content:"\F0CE9"}.mdi-account-network::before{content:"\F0011"}.mdi-account-network-off::before{content:"\F1AF1"}.mdi-account-network-off-outline::before{content:"\F1AF2"}.mdi-account-network-outline::before{content:"\F0BE6"}.mdi-account-off::before{content:"\F0012"}.mdi-account-off-outline::before{content:"\F0BE7"}.mdi-account-outline::before{content:"\F0013"}.mdi-account-plus::before{content:"\F0014"}.mdi-account-plus-outline::before{content:"\F0801"}.mdi-account-question::before{content:"\F0B59"}.mdi-account-question-outline::before{content:"\F0B5A"}.mdi-account-reactivate::before{content:"\F152B"}.mdi-account-reactivate-outline::before{content:"\F152C"}.mdi-account-remove::before{content:"\F0015"}.mdi-account-remove-outline::before{content:"\F0AED"}.mdi-account-school::before{content:"\F1A20"}.mdi-account-school-outline::before{content:"\F1A21"}.mdi-account-search::before{content:"\F0016"}.mdi-account-search-outline::before{content:"\F0935"}.mdi-account-settings::before{content:"\F0630"}.mdi-account-settings-outline::before{content:"\F10C9"}.mdi-account-star::before{content:"\F0017"}.mdi-account-star-outline::before{content:"\F0BE8"}.mdi-account-supervisor::before{content:"\F0A8B"}.mdi-account-supervisor-circle::before{content:"\F0A8C"}.mdi-account-supervisor-circle-outline::before{content:"\F14EC"}.mdi-account-supervisor-outline::before{content:"\F112D"}.mdi-account-switch::before{content:"\F0019"}.mdi-account-switch-outline::before{content:"\F04CB"}.mdi-account-sync::before{content:"\F191B"}.mdi-account-sync-outline::before{content:"\F191C"}.mdi-account-tag::before{content:"\F1C1B"}.mdi-account-tag-outline::before{content:"\F1C1C"}.mdi-account-tie::before{content:"\F0CE3"}.mdi-account-tie-hat::before{content:"\F1898"}.mdi-account-tie-hat-outline::before{content:"\F1899"}.mdi-account-tie-outline::before{content:"\F10CA"}.mdi-account-tie-voice::before{content:"\F1308"}.mdi-account-tie-voice-off::before{content:"\F130A"}.mdi-account-tie-voice-off-outline::before{content:"\F130B"}.mdi-account-tie-voice-outline::before{content:"\F1309"}.mdi-account-tie-woman::before{content:"\F1A8C"}.mdi-account-voice::before{content:"\F05CB"}.mdi-account-voice-off::before{content:"\F0ED4"}.mdi-account-wrench::before{content:"\F189A"}.mdi-account-wrench-outline::before{content:"\F189B"}.mdi-adjust::before{content:"\F001A"}.mdi-advertisements::before{content:"\F192A"}.mdi-advertisements-off::before{content:"\F192B"}.mdi-air-conditioner::before{content:"\F001B"}.mdi-air-filter::before{content:"\F0D43"}.mdi-air-horn::before{content:"\F0DAC"}.mdi-air-humidifier::before{content:"\F1099"}.mdi-air-humidifier-off::before{content:"\F1466"}.mdi-air-purifier::before{content:"\F0D44"}.mdi-air-purifier-off::before{content:"\F1B57"}.mdi-airbag::before{content:"\F0BE9"}.mdi-airballoon::before{content:"\F001C"}.mdi-airballoon-outline::before{content:"\F100B"}.mdi-airplane::before{content:"\F001D"}.mdi-airplane-alert::before{content:"\F187A"}.mdi-airplane-check::before{content:"\F187B"}.mdi-airplane-clock::before{content:"\F187C"}.mdi-airplane-cog::before{content:"\F187D"}.mdi-airplane-edit::before{content:"\F187E"}.mdi-airplane-landing::before{content:"\F05D4"}.mdi-airplane-marker::before{content:"\F187F"}.mdi-airplane-minus::before{content:"\F1880"}.mdi-airplane-off::before{content:"\F001E"}.mdi-airplane-plus::before{content:"\F1881"}.mdi-airplane-remove::before{content:"\F1882"}.mdi-airplane-search::before{content:"\F1883"}.mdi-airplane-settings::before{content:"\F1884"}.mdi-airplane-takeoff::before{content:"\F05D5"}.mdi-airport::before{content:"\F084B"}.mdi-alarm::before{content:"\F0020"}.mdi-alarm-bell::before{content:"\F078E"}.mdi-alarm-check::before{content:"\F0021"}.mdi-alarm-light::before{content:"\F078F"}.mdi-alarm-light-off::before{content:"\F171E"}.mdi-alarm-light-off-outline::before{content:"\F171F"}.mdi-alarm-light-outline::before{content:"\F0BEA"}.mdi-alarm-multiple::before{content:"\F0022"}.mdi-alarm-note::before{content:"\F0E71"}.mdi-alarm-note-off::before{content:"\F0E72"}.mdi-alarm-off::before{content:"\F0023"}.mdi-alarm-panel::before{content:"\F15C4"}.mdi-alarm-panel-outline::before{content:"\F15C5"}.mdi-alarm-plus::before{content:"\F0024"}.mdi-alarm-snooze::before{content:"\F068E"}.mdi-album::before{content:"\F0025"}.mdi-alert::before{content:"\F0026"}.mdi-alert-box::before{content:"\F0027"}.mdi-alert-box-outline::before{content:"\F0CE4"}.mdi-alert-circle::before{content:"\F0028"}.mdi-alert-circle-check::before{content:"\F11ED"}.mdi-alert-circle-check-outline::before{content:"\F11EE"}.mdi-alert-circle-outline::before{content:"\F05D6"}.mdi-alert-decagram::before{content:"\F06BD"}.mdi-alert-decagram-outline::before{content:"\F0CE5"}.mdi-alert-minus::before{content:"\F14BB"}.mdi-alert-minus-outline::before{content:"\F14BE"}.mdi-alert-octagon::before{content:"\F0029"}.mdi-alert-octagon-outline::before{content:"\F0CE6"}.mdi-alert-octagram::before{content:"\F0767"}.mdi-alert-octagram-outline::before{content:"\F0CE7"}.mdi-alert-outline::before{content:"\F002A"}.mdi-alert-plus::before{content:"\F14BA"}.mdi-alert-plus-outline::before{content:"\F14BD"}.mdi-alert-remove::before{content:"\F14BC"}.mdi-alert-remove-outline::before{content:"\F14BF"}.mdi-alert-rhombus::before{content:"\F11CE"}.mdi-alert-rhombus-outline::before{content:"\F11CF"}.mdi-alien::before{content:"\F089A"}.mdi-alien-outline::before{content:"\F10CB"}.mdi-align-horizontal-center::before{content:"\F11C3"}.mdi-align-horizontal-distribute::before{content:"\F1962"}.mdi-align-horizontal-left::before{content:"\F11C2"}.mdi-align-horizontal-right::before{content:"\F11C4"}.mdi-align-vertical-bottom::before{content:"\F11C5"}.mdi-align-vertical-center::before{content:"\F11C6"}.mdi-align-vertical-distribute::before{content:"\F1963"}.mdi-align-vertical-top::before{content:"\F11C7"}.mdi-all-inclusive::before{content:"\F06BE"}.mdi-all-inclusive-box::before{content:"\F188D"}.mdi-all-inclusive-box-outline::before{content:"\F188E"}.mdi-allergy::before{content:"\F1258"}.mdi-alpha::before{content:"\F002B"}.mdi-alpha-a::before{content:"\F0AEE"}.mdi-alpha-a-box::before{content:"\F0B08"}.mdi-alpha-a-box-outline::before{content:"\F0BEB"}.mdi-alpha-a-circle::before{content:"\F0BEC"}.mdi-alpha-a-circle-outline::before{content:"\F0BED"}.mdi-alpha-b::before{content:"\F0AEF"}.mdi-alpha-b-box::before{content:"\F0B09"}.mdi-alpha-b-box-outline::before{content:"\F0BEE"}.mdi-alpha-b-circle::before{content:"\F0BEF"}.mdi-alpha-b-circle-outline::before{content:"\F0BF0"}.mdi-alpha-c::before{content:"\F0AF0"}.mdi-alpha-c-box::before{content:"\F0B0A"}.mdi-alpha-c-box-outline::before{content:"\F0BF1"}.mdi-alpha-c-circle::before{content:"\F0BF2"}.mdi-alpha-c-circle-outline::before{content:"\F0BF3"}.mdi-alpha-d::before{content:"\F0AF1"}.mdi-alpha-d-box::before{content:"\F0B0B"}.mdi-alpha-d-box-outline::before{content:"\F0BF4"}.mdi-alpha-d-circle::before{content:"\F0BF5"}.mdi-alpha-d-circle-outline::before{content:"\F0BF6"}.mdi-alpha-e::before{content:"\F0AF2"}.mdi-alpha-e-box::before{content:"\F0B0C"}.mdi-alpha-e-box-outline::before{content:"\F0BF7"}.mdi-alpha-e-circle::before{content:"\F0BF8"}.mdi-alpha-e-circle-outline::before{content:"\F0BF9"}.mdi-alpha-f::before{content:"\F0AF3"}.mdi-alpha-f-box::before{content:"\F0B0D"}.mdi-alpha-f-box-outline::before{content:"\F0BFA"}.mdi-alpha-f-circle::before{content:"\F0BFB"}.mdi-alpha-f-circle-outline::before{content:"\F0BFC"}.mdi-alpha-g::before{content:"\F0AF4"}.mdi-alpha-g-box::before{content:"\F0B0E"}.mdi-alpha-g-box-outline::before{content:"\F0BFD"}.mdi-alpha-g-circle::before{content:"\F0BFE"}.mdi-alpha-g-circle-outline::before{content:"\F0BFF"}.mdi-alpha-h::before{content:"\F0AF5"}.mdi-alpha-h-box::before{content:"\F0B0F"}.mdi-alpha-h-box-outline::before{content:"\F0C00"}.mdi-alpha-h-circle::before{content:"\F0C01"}.mdi-alpha-h-circle-outline::before{content:"\F0C02"}.mdi-alpha-i::before{content:"\F0AF6"}.mdi-alpha-i-box::before{content:"\F0B10"}.mdi-alpha-i-box-outline::before{content:"\F0C03"}.mdi-alpha-i-circle::before{content:"\F0C04"}.mdi-alpha-i-circle-outline::before{content:"\F0C05"}.mdi-alpha-j::before{content:"\F0AF7"}.mdi-alpha-j-box::before{content:"\F0B11"}.mdi-alpha-j-box-outline::before{content:"\F0C06"}.mdi-alpha-j-circle::before{content:"\F0C07"}.mdi-alpha-j-circle-outline::before{content:"\F0C08"}.mdi-alpha-k::before{content:"\F0AF8"}.mdi-alpha-k-box::before{content:"\F0B12"}.mdi-alpha-k-box-outline::before{content:"\F0C09"}.mdi-alpha-k-circle::before{content:"\F0C0A"}.mdi-alpha-k-circle-outline::before{content:"\F0C0B"}.mdi-alpha-l::before{content:"\F0AF9"}.mdi-alpha-l-box::before{content:"\F0B13"}.mdi-alpha-l-box-outline::before{content:"\F0C0C"}.mdi-alpha-l-circle::before{content:"\F0C0D"}.mdi-alpha-l-circle-outline::before{content:"\F0C0E"}.mdi-alpha-m::before{content:"\F0AFA"}.mdi-alpha-m-box::before{content:"\F0B14"}.mdi-alpha-m-box-outline::before{content:"\F0C0F"}.mdi-alpha-m-circle::before{content:"\F0C10"}.mdi-alpha-m-circle-outline::before{content:"\F0C11"}.mdi-alpha-n::before{content:"\F0AFB"}.mdi-alpha-n-box::before{content:"\F0B15"}.mdi-alpha-n-box-outline::before{content:"\F0C12"}.mdi-alpha-n-circle::before{content:"\F0C13"}.mdi-alpha-n-circle-outline::before{content:"\F0C14"}.mdi-alpha-o::before{content:"\F0AFC"}.mdi-alpha-o-box::before{content:"\F0B16"}.mdi-alpha-o-box-outline::before{content:"\F0C15"}.mdi-alpha-o-circle::before{content:"\F0C16"}.mdi-alpha-o-circle-outline::before{content:"\F0C17"}.mdi-alpha-p::before{content:"\F0AFD"}.mdi-alpha-p-box::before{content:"\F0B17"}.mdi-alpha-p-box-outline::before{content:"\F0C18"}.mdi-alpha-p-circle::before{content:"\F0C19"}.mdi-alpha-p-circle-outline::before{content:"\F0C1A"}.mdi-alpha-q::before{content:"\F0AFE"}.mdi-alpha-q-box::before{content:"\F0B18"}.mdi-alpha-q-box-outline::before{content:"\F0C1B"}.mdi-alpha-q-circle::before{content:"\F0C1C"}.mdi-alpha-q-circle-outline::before{content:"\F0C1D"}.mdi-alpha-r::before{content:"\F0AFF"}.mdi-alpha-r-box::before{content:"\F0B19"}.mdi-alpha-r-box-outline::before{content:"\F0C1E"}.mdi-alpha-r-circle::before{content:"\F0C1F"}.mdi-alpha-r-circle-outline::before{content:"\F0C20"}.mdi-alpha-s::before{content:"\F0B00"}.mdi-alpha-s-box::before{content:"\F0B1A"}.mdi-alpha-s-box-outline::before{content:"\F0C21"}.mdi-alpha-s-circle::before{content:"\F0C22"}.mdi-alpha-s-circle-outline::before{content:"\F0C23"}.mdi-alpha-t::before{content:"\F0B01"}.mdi-alpha-t-box::before{content:"\F0B1B"}.mdi-alpha-t-box-outline::before{content:"\F0C24"}.mdi-alpha-t-circle::before{content:"\F0C25"}.mdi-alpha-t-circle-outline::before{content:"\F0C26"}.mdi-alpha-u::before{content:"\F0B02"}.mdi-alpha-u-box::before{content:"\F0B1C"}.mdi-alpha-u-box-outline::before{content:"\F0C27"}.mdi-alpha-u-circle::before{content:"\F0C28"}.mdi-alpha-u-circle-outline::before{content:"\F0C29"}.mdi-alpha-v::before{content:"\F0B03"}.mdi-alpha-v-box::before{content:"\F0B1D"}.mdi-alpha-v-box-outline::before{content:"\F0C2A"}.mdi-alpha-v-circle::before{content:"\F0C2B"}.mdi-alpha-v-circle-outline::before{content:"\F0C2C"}.mdi-alpha-w::before{content:"\F0B04"}.mdi-alpha-w-box::before{content:"\F0B1E"}.mdi-alpha-w-box-outline::before{content:"\F0C2D"}.mdi-alpha-w-circle::before{content:"\F0C2E"}.mdi-alpha-w-circle-outline::before{content:"\F0C2F"}.mdi-alpha-x::before{content:"\F0B05"}.mdi-alpha-x-box::before{content:"\F0B1F"}.mdi-alpha-x-box-outline::before{content:"\F0C30"}.mdi-alpha-x-circle::before{content:"\F0C31"}.mdi-alpha-x-circle-outline::before{content:"\F0C32"}.mdi-alpha-y::before{content:"\F0B06"}.mdi-alpha-y-box::before{content:"\F0B20"}.mdi-alpha-y-box-outline::before{content:"\F0C33"}.mdi-alpha-y-circle::before{content:"\F0C34"}.mdi-alpha-y-circle-outline::before{content:"\F0C35"}.mdi-alpha-z::before{content:"\F0B07"}.mdi-alpha-z-box::before{content:"\F0B21"}.mdi-alpha-z-box-outline::before{content:"\F0C36"}.mdi-alpha-z-circle::before{content:"\F0C37"}.mdi-alpha-z-circle-outline::before{content:"\F0C38"}.mdi-alphabet-aurebesh::before{content:"\F132C"}.mdi-alphabet-cyrillic::before{content:"\F132D"}.mdi-alphabet-greek::before{content:"\F132E"}.mdi-alphabet-latin::before{content:"\F132F"}.mdi-alphabet-piqad::before{content:"\F1330"}.mdi-alphabet-tengwar::before{content:"\F1337"}.mdi-alphabetical::before{content:"\F002C"}.mdi-alphabetical-off::before{content:"\F100C"}.mdi-alphabetical-variant::before{content:"\F100D"}.mdi-alphabetical-variant-off::before{content:"\F100E"}.mdi-altimeter::before{content:"\F05D7"}.mdi-ambulance::before{content:"\F002F"}.mdi-ammunition::before{content:"\F0CE8"}.mdi-ampersand::before{content:"\F0A8D"}.mdi-amplifier::before{content:"\F0030"}.mdi-amplifier-off::before{content:"\F11B5"}.mdi-anchor::before{content:"\F0031"}.mdi-android::before{content:"\F0032"}.mdi-android-studio::before{content:"\F0034"}.mdi-angle-acute::before{content:"\F0937"}.mdi-angle-obtuse::before{content:"\F0938"}.mdi-angle-right::before{content:"\F0939"}.mdi-angular::before{content:"\F06B2"}.mdi-angularjs::before{content:"\F06BF"}.mdi-animation::before{content:"\F05D8"}.mdi-animation-outline::before{content:"\F0A8F"}.mdi-animation-play::before{content:"\F093A"}.mdi-animation-play-outline::before{content:"\F0A90"}.mdi-ansible::before{content:"\F109A"}.mdi-antenna::before{content:"\F1119"}.mdi-anvil::before{content:"\F089B"}.mdi-apache-kafka::before{content:"\F100F"}.mdi-api::before{content:"\F109B"}.mdi-api-off::before{content:"\F1257"}.mdi-apple::before{content:"\F0035"}.mdi-apple-finder::before{content:"\F0036"}.mdi-apple-icloud::before{content:"\F0038"}.mdi-apple-ios::before{content:"\F0037"}.mdi-apple-keyboard-caps::before{content:"\F0632"}.mdi-apple-keyboard-command::before{content:"\F0633"}.mdi-apple-keyboard-control::before{content:"\F0634"}.mdi-apple-keyboard-option::before{content:"\F0635"}.mdi-apple-keyboard-shift::before{content:"\F0636"}.mdi-apple-safari::before{content:"\F0039"}.mdi-application::before{content:"\F08C6"}.mdi-application-array::before{content:"\F10F5"}.mdi-application-array-outline::before{content:"\F10F6"}.mdi-application-braces::before{content:"\F10F7"}.mdi-application-braces-outline::before{content:"\F10F8"}.mdi-application-brackets::before{content:"\F0C8B"}.mdi-application-brackets-outline::before{content:"\F0C8C"}.mdi-application-cog::before{content:"\F0675"}.mdi-application-cog-outline::before{content:"\F1577"}.mdi-application-edit::before{content:"\F00AE"}.mdi-application-edit-outline::before{content:"\F0619"}.mdi-application-export::before{content:"\F0DAD"}.mdi-application-import::before{content:"\F0DAE"}.mdi-application-outline::before{content:"\F0614"}.mdi-application-parentheses::before{content:"\F10F9"}.mdi-application-parentheses-outline::before{content:"\F10FA"}.mdi-application-settings::before{content:"\F0B60"}.mdi-application-settings-outline::before{content:"\F1555"}.mdi-application-variable::before{content:"\F10FB"}.mdi-application-variable-outline::before{content:"\F10FC"}.mdi-approximately-equal::before{content:"\F0F9E"}.mdi-approximately-equal-box::before{content:"\F0F9F"}.mdi-apps::before{content:"\F003B"}.mdi-apps-box::before{content:"\F0D46"}.mdi-arch::before{content:"\F08C7"}.mdi-archive::before{content:"\F003C"}.mdi-archive-alert::before{content:"\F14FD"}.mdi-archive-alert-outline::before{content:"\F14FE"}.mdi-archive-arrow-down::before{content:"\F1259"}.mdi-archive-arrow-down-outline::before{content:"\F125A"}.mdi-archive-arrow-up::before{content:"\F125B"}.mdi-archive-arrow-up-outline::before{content:"\F125C"}.mdi-archive-cancel::before{content:"\F174B"}.mdi-archive-cancel-outline::before{content:"\F174C"}.mdi-archive-check::before{content:"\F174D"}.mdi-archive-check-outline::before{content:"\F174E"}.mdi-archive-clock::before{content:"\F174F"}.mdi-archive-clock-outline::before{content:"\F1750"}.mdi-archive-cog::before{content:"\F1751"}.mdi-archive-cog-outline::before{content:"\F1752"}.mdi-archive-edit::before{content:"\F1753"}.mdi-archive-edit-outline::before{content:"\F1754"}.mdi-archive-eye::before{content:"\F1755"}.mdi-archive-eye-outline::before{content:"\F1756"}.mdi-archive-lock::before{content:"\F1757"}.mdi-archive-lock-open::before{content:"\F1758"}.mdi-archive-lock-open-outline::before{content:"\F1759"}.mdi-archive-lock-outline::before{content:"\F175A"}.mdi-archive-marker::before{content:"\F175B"}.mdi-archive-marker-outline::before{content:"\F175C"}.mdi-archive-minus::before{content:"\F175D"}.mdi-archive-minus-outline::before{content:"\F175E"}.mdi-archive-music::before{content:"\F175F"}.mdi-archive-music-outline::before{content:"\F1760"}.mdi-archive-off::before{content:"\F1761"}.mdi-archive-off-outline::before{content:"\F1762"}.mdi-archive-outline::before{content:"\F120E"}.mdi-archive-plus::before{content:"\F1763"}.mdi-archive-plus-outline::before{content:"\F1764"}.mdi-archive-refresh::before{content:"\F1765"}.mdi-archive-refresh-outline::before{content:"\F1766"}.mdi-archive-remove::before{content:"\F1767"}.mdi-archive-remove-outline::before{content:"\F1768"}.mdi-archive-search::before{content:"\F1769"}.mdi-archive-search-outline::before{content:"\F176A"}.mdi-archive-settings::before{content:"\F176B"}.mdi-archive-settings-outline::before{content:"\F176C"}.mdi-archive-star::before{content:"\F176D"}.mdi-archive-star-outline::before{content:"\F176E"}.mdi-archive-sync::before{content:"\F176F"}.mdi-archive-sync-outline::before{content:"\F1770"}.mdi-arm-flex::before{content:"\F0FD7"}.mdi-arm-flex-outline::before{content:"\F0FD6"}.mdi-arrange-bring-forward::before{content:"\F003D"}.mdi-arrange-bring-to-front::before{content:"\F003E"}.mdi-arrange-send-backward::before{content:"\F003F"}.mdi-arrange-send-to-back::before{content:"\F0040"}.mdi-arrow-all::before{content:"\F0041"}.mdi-arrow-bottom-left::before{content:"\F0042"}.mdi-arrow-bottom-left-bold-box::before{content:"\F1964"}.mdi-arrow-bottom-left-bold-box-outline::before{content:"\F1965"}.mdi-arrow-bottom-left-bold-outline::before{content:"\F09B7"}.mdi-arrow-bottom-left-thick::before{content:"\F09B8"}.mdi-arrow-bottom-left-thin::before{content:"\F19B6"}.mdi-arrow-bottom-left-thin-circle-outline::before{content:"\F1596"}.mdi-arrow-bottom-right::before{content:"\F0043"}.mdi-arrow-bottom-right-bold-box::before{content:"\F1966"}.mdi-arrow-bottom-right-bold-box-outline::before{content:"\F1967"}.mdi-arrow-bottom-right-bold-outline::before{content:"\F09B9"}.mdi-arrow-bottom-right-thick::before{content:"\F09BA"}.mdi-arrow-bottom-right-thin::before{content:"\F19B7"}.mdi-arrow-bottom-right-thin-circle-outline::before{content:"\F1595"}.mdi-arrow-collapse::before{content:"\F0615"}.mdi-arrow-collapse-all::before{content:"\F0044"}.mdi-arrow-collapse-down::before{content:"\F0792"}.mdi-arrow-collapse-horizontal::before{content:"\F084C"}.mdi-arrow-collapse-left::before{content:"\F0793"}.mdi-arrow-collapse-right::before{content:"\F0794"}.mdi-arrow-collapse-up::before{content:"\F0795"}.mdi-arrow-collapse-vertical::before{content:"\F084D"}.mdi-arrow-decision::before{content:"\F09BB"}.mdi-arrow-decision-auto::before{content:"\F09BC"}.mdi-arrow-decision-auto-outline::before{content:"\F09BD"}.mdi-arrow-decision-outline::before{content:"\F09BE"}.mdi-arrow-down::before{content:"\F0045"}.mdi-arrow-down-bold::before{content:"\F072E"}.mdi-arrow-down-bold-box::before{content:"\F072F"}.mdi-arrow-down-bold-box-outline::before{content:"\F0730"}.mdi-arrow-down-bold-circle::before{content:"\F0047"}.mdi-arrow-down-bold-circle-outline::before{content:"\F0048"}.mdi-arrow-down-bold-hexagon-outline::before{content:"\F0049"}.mdi-arrow-down-bold-outline::before{content:"\F09BF"}.mdi-arrow-down-box::before{content:"\F06C0"}.mdi-arrow-down-circle::before{content:"\F0CDB"}.mdi-arrow-down-circle-outline::before{content:"\F0CDC"}.mdi-arrow-down-drop-circle::before{content:"\F004A"}.mdi-arrow-down-drop-circle-outline::before{content:"\F004B"}.mdi-arrow-down-left::before{content:"\F17A1"}.mdi-arrow-down-left-bold::before{content:"\F17A2"}.mdi-arrow-down-right::before{content:"\F17A3"}.mdi-arrow-down-right-bold::before{content:"\F17A4"}.mdi-arrow-down-thick::before{content:"\F0046"}.mdi-arrow-down-thin::before{content:"\F19B3"}.mdi-arrow-down-thin-circle-outline::before{content:"\F1599"}.mdi-arrow-expand::before{content:"\F0616"}.mdi-arrow-expand-all::before{content:"\F004C"}.mdi-arrow-expand-down::before{content:"\F0796"}.mdi-arrow-expand-horizontal::before{content:"\F084E"}.mdi-arrow-expand-left::before{content:"\F0797"}.mdi-arrow-expand-right::before{content:"\F0798"}.mdi-arrow-expand-up::before{content:"\F0799"}.mdi-arrow-expand-vertical::before{content:"\F084F"}.mdi-arrow-horizontal-lock::before{content:"\F115B"}.mdi-arrow-left::before{content:"\F004D"}.mdi-arrow-left-bold::before{content:"\F0731"}.mdi-arrow-left-bold-box::before{content:"\F0732"}.mdi-arrow-left-bold-box-outline::before{content:"\F0733"}.mdi-arrow-left-bold-circle::before{content:"\F004F"}.mdi-arrow-left-bold-circle-outline::before{content:"\F0050"}.mdi-arrow-left-bold-hexagon-outline::before{content:"\F0051"}.mdi-arrow-left-bold-outline::before{content:"\F09C0"}.mdi-arrow-left-bottom::before{content:"\F17A5"}.mdi-arrow-left-bottom-bold::before{content:"\F17A6"}.mdi-arrow-left-box::before{content:"\F06C1"}.mdi-arrow-left-circle::before{content:"\F0CDD"}.mdi-arrow-left-circle-outline::before{content:"\F0CDE"}.mdi-arrow-left-drop-circle::before{content:"\F0052"}.mdi-arrow-left-drop-circle-outline::before{content:"\F0053"}.mdi-arrow-left-right::before{content:"\F0E73"}.mdi-arrow-left-right-bold::before{content:"\F0E74"}.mdi-arrow-left-right-bold-outline::before{content:"\F09C1"}.mdi-arrow-left-thick::before{content:"\F004E"}.mdi-arrow-left-thin::before{content:"\F19B1"}.mdi-arrow-left-thin-circle-outline::before{content:"\F159A"}.mdi-arrow-left-top::before{content:"\F17A7"}.mdi-arrow-left-top-bold::before{content:"\F17A8"}.mdi-arrow-oscillating::before{content:"\F1C91"}.mdi-arrow-oscillating-off::before{content:"\F1C92"}.mdi-arrow-projectile::before{content:"\F1840"}.mdi-arrow-projectile-multiple::before{content:"\F183F"}.mdi-arrow-right::before{content:"\F0054"}.mdi-arrow-right-bold::before{content:"\F0734"}.mdi-arrow-right-bold-box::before{content:"\F0735"}.mdi-arrow-right-bold-box-outline::before{content:"\F0736"}.mdi-arrow-right-bold-circle::before{content:"\F0056"}.mdi-arrow-right-bold-circle-outline::before{content:"\F0057"}.mdi-arrow-right-bold-hexagon-outline::before{content:"\F0058"}.mdi-arrow-right-bold-outline::before{content:"\F09C2"}.mdi-arrow-right-bottom::before{content:"\F17A9"}.mdi-arrow-right-bottom-bold::before{content:"\F17AA"}.mdi-arrow-right-box::before{content:"\F06C2"}.mdi-arrow-right-circle::before{content:"\F0CDF"}.mdi-arrow-right-circle-outline::before{content:"\F0CE0"}.mdi-arrow-right-drop-circle::before{content:"\F0059"}.mdi-arrow-right-drop-circle-outline::before{content:"\F005A"}.mdi-arrow-right-thick::before{content:"\F0055"}.mdi-arrow-right-thin::before{content:"\F19B0"}.mdi-arrow-right-thin-circle-outline::before{content:"\F1598"}.mdi-arrow-right-top::before{content:"\F17AB"}.mdi-arrow-right-top-bold::before{content:"\F17AC"}.mdi-arrow-split-horizontal::before{content:"\F093B"}.mdi-arrow-split-vertical::before{content:"\F093C"}.mdi-arrow-top-left::before{content:"\F005B"}.mdi-arrow-top-left-bold-box::before{content:"\F1968"}.mdi-arrow-top-left-bold-box-outline::before{content:"\F1969"}.mdi-arrow-top-left-bold-outline::before{content:"\F09C3"}.mdi-arrow-top-left-bottom-right::before{content:"\F0E75"}.mdi-arrow-top-left-bottom-right-bold::before{content:"\F0E76"}.mdi-arrow-top-left-thick::before{content:"\F09C4"}.mdi-arrow-top-left-thin::before{content:"\F19B5"}.mdi-arrow-top-left-thin-circle-outline::before{content:"\F1593"}.mdi-arrow-top-right::before{content:"\F005C"}.mdi-arrow-top-right-bold-box::before{content:"\F196A"}.mdi-arrow-top-right-bold-box-outline::before{content:"\F196B"}.mdi-arrow-top-right-bold-outline::before{content:"\F09C5"}.mdi-arrow-top-right-bottom-left::before{content:"\F0E77"}.mdi-arrow-top-right-bottom-left-bold::before{content:"\F0E78"}.mdi-arrow-top-right-thick::before{content:"\F09C6"}.mdi-arrow-top-right-thin::before{content:"\F19B4"}.mdi-arrow-top-right-thin-circle-outline::before{content:"\F1594"}.mdi-arrow-u-down-left::before{content:"\F17AD"}.mdi-arrow-u-down-left-bold::before{content:"\F17AE"}.mdi-arrow-u-down-right::before{content:"\F17AF"}.mdi-arrow-u-down-right-bold::before{content:"\F17B0"}.mdi-arrow-u-left-bottom::before{content:"\F17B1"}.mdi-arrow-u-left-bottom-bold::before{content:"\F17B2"}.mdi-arrow-u-left-top::before{content:"\F17B3"}.mdi-arrow-u-left-top-bold::before{content:"\F17B4"}.mdi-arrow-u-right-bottom::before{content:"\F17B5"}.mdi-arrow-u-right-bottom-bold::before{content:"\F17B6"}.mdi-arrow-u-right-top::before{content:"\F17B7"}.mdi-arrow-u-right-top-bold::before{content:"\F17B8"}.mdi-arrow-u-up-left::before{content:"\F17B9"}.mdi-arrow-u-up-left-bold::before{content:"\F17BA"}.mdi-arrow-u-up-right::before{content:"\F17BB"}.mdi-arrow-u-up-right-bold::before{content:"\F17BC"}.mdi-arrow-up::before{content:"\F005D"}.mdi-arrow-up-bold::before{content:"\F0737"}.mdi-arrow-up-bold-box::before{content:"\F0738"}.mdi-arrow-up-bold-box-outline::before{content:"\F0739"}.mdi-arrow-up-bold-circle::before{content:"\F005F"}.mdi-arrow-up-bold-circle-outline::before{content:"\F0060"}.mdi-arrow-up-bold-hexagon-outline::before{content:"\F0061"}.mdi-arrow-up-bold-outline::before{content:"\F09C7"}.mdi-arrow-up-box::before{content:"\F06C3"}.mdi-arrow-up-circle::before{content:"\F0CE1"}.mdi-arrow-up-circle-outline::before{content:"\F0CE2"}.mdi-arrow-up-down::before{content:"\F0E79"}.mdi-arrow-up-down-bold::before{content:"\F0E7A"}.mdi-arrow-up-down-bold-outline::before{content:"\F09C8"}.mdi-arrow-up-drop-circle::before{content:"\F0062"}.mdi-arrow-up-drop-circle-outline::before{content:"\F0063"}.mdi-arrow-up-left::before{content:"\F17BD"}.mdi-arrow-up-left-bold::before{content:"\F17BE"}.mdi-arrow-up-right::before{content:"\F17BF"}.mdi-arrow-up-right-bold::before{content:"\F17C0"}.mdi-arrow-up-thick::before{content:"\F005E"}.mdi-arrow-up-thin::before{content:"\F19B2"}.mdi-arrow-up-thin-circle-outline::before{content:"\F1597"}.mdi-arrow-vertical-lock::before{content:"\F115C"}.mdi-artboard::before{content:"\F1B9A"}.mdi-artstation::before{content:"\F0B5B"}.mdi-aspect-ratio::before{content:"\F0A24"}.mdi-assistant::before{content:"\F0064"}.mdi-asterisk::before{content:"\F06C4"}.mdi-asterisk-circle-outline::before{content:"\F1A27"}.mdi-at::before{content:"\F0065"}.mdi-atlassian::before{content:"\F0804"}.mdi-atm::before{content:"\F0D47"}.mdi-atom::before{content:"\F0768"}.mdi-atom-variant::before{content:"\F0E7B"}.mdi-attachment::before{content:"\F0066"}.mdi-attachment-check::before{content:"\F1AC1"}.mdi-attachment-lock::before{content:"\F19C4"}.mdi-attachment-minus::before{content:"\F1AC2"}.mdi-attachment-off::before{content:"\F1AC3"}.mdi-attachment-plus::before{content:"\F1AC4"}.mdi-attachment-remove::before{content:"\F1AC5"}.mdi-atv::before{content:"\F1B70"}.mdi-audio-input-rca::before{content:"\F186B"}.mdi-audio-input-stereo-minijack::before{content:"\F186C"}.mdi-audio-input-xlr::before{content:"\F186D"}.mdi-audio-video::before{content:"\F093D"}.mdi-audio-video-off::before{content:"\F11B6"}.mdi-augmented-reality::before{content:"\F0850"}.mdi-aurora::before{content:"\F1BB9"}.mdi-auto-download::before{content:"\F137E"}.mdi-auto-fix::before{content:"\F0068"}.mdi-auto-mode::before{content:"\F1C20"}.mdi-auto-upload::before{content:"\F0069"}.mdi-autorenew::before{content:"\F006A"}.mdi-autorenew-off::before{content:"\F19E7"}.mdi-av-timer::before{content:"\F006B"}.mdi-awning::before{content:"\F1B87"}.mdi-awning-outline::before{content:"\F1B88"}.mdi-aws::before{content:"\F0E0F"}.mdi-axe::before{content:"\F08C8"}.mdi-axe-battle::before{content:"\F1842"}.mdi-axis::before{content:"\F0D48"}.mdi-axis-arrow::before{content:"\F0D49"}.mdi-axis-arrow-info::before{content:"\F140E"}.mdi-axis-arrow-lock::before{content:"\F0D4A"}.mdi-axis-lock::before{content:"\F0D4B"}.mdi-axis-x-arrow::before{content:"\F0D4C"}.mdi-axis-x-arrow-lock::before{content:"\F0D4D"}.mdi-axis-x-rotate-clockwise::before{content:"\F0D4E"}.mdi-axis-x-rotate-counterclockwise::before{content:"\F0D4F"}.mdi-axis-x-y-arrow-lock::before{content:"\F0D50"}.mdi-axis-y-arrow::before{content:"\F0D51"}.mdi-axis-y-arrow-lock::before{content:"\F0D52"}.mdi-axis-y-rotate-clockwise::before{content:"\F0D53"}.mdi-axis-y-rotate-counterclockwise::before{content:"\F0D54"}.mdi-axis-z-arrow::before{content:"\F0D55"}.mdi-axis-z-arrow-lock::before{content:"\F0D56"}.mdi-axis-z-rotate-clockwise::before{content:"\F0D57"}.mdi-axis-z-rotate-counterclockwise::before{content:"\F0D58"}.mdi-babel::before{content:"\F0A25"}.mdi-baby::before{content:"\F006C"}.mdi-baby-bottle::before{content:"\F0F39"}.mdi-baby-bottle-outline::before{content:"\F0F3A"}.mdi-baby-buggy::before{content:"\F13E0"}.mdi-baby-buggy-off::before{content:"\F1AF3"}.mdi-baby-carriage::before{content:"\F068F"}.mdi-baby-carriage-off::before{content:"\F0FA0"}.mdi-baby-face::before{content:"\F0E7C"}.mdi-baby-face-outline::before{content:"\F0E7D"}.mdi-backburger::before{content:"\F006D"}.mdi-backspace::before{content:"\F006E"}.mdi-backspace-outline::before{content:"\F0B5C"}.mdi-backspace-reverse::before{content:"\F0E7E"}.mdi-backspace-reverse-outline::before{content:"\F0E7F"}.mdi-backup-restore::before{content:"\F006F"}.mdi-bacteria::before{content:"\F0ED5"}.mdi-bacteria-outline::before{content:"\F0ED6"}.mdi-badge-account::before{content:"\F0DA7"}.mdi-badge-account-alert::before{content:"\F0DA8"}.mdi-badge-account-alert-outline::before{content:"\F0DA9"}.mdi-badge-account-horizontal::before{content:"\F0E0D"}.mdi-badge-account-horizontal-outline::before{content:"\F0E0E"}.mdi-badge-account-outline::before{content:"\F0DAA"}.mdi-badminton::before{content:"\F0851"}.mdi-bag-carry-on::before{content:"\F0F3B"}.mdi-bag-carry-on-check::before{content:"\F0D65"}.mdi-bag-carry-on-off::before{content:"\F0F3C"}.mdi-bag-checked::before{content:"\F0F3D"}.mdi-bag-personal::before{content:"\F0E10"}.mdi-bag-personal-off::before{content:"\F0E11"}.mdi-bag-personal-off-outline::before{content:"\F0E12"}.mdi-bag-personal-outline::before{content:"\F0E13"}.mdi-bag-personal-plus::before{content:"\F1CA4"}.mdi-bag-personal-plus-outline::before{content:"\F1CA5"}.mdi-bag-personal-tag::before{content:"\F1B0C"}.mdi-bag-personal-tag-outline::before{content:"\F1B0D"}.mdi-bag-suitcase::before{content:"\F158B"}.mdi-bag-suitcase-off::before{content:"\F158D"}.mdi-bag-suitcase-off-outline::before{content:"\F158E"}.mdi-bag-suitcase-outline::before{content:"\F158C"}.mdi-baguette::before{content:"\F0F3E"}.mdi-balcony::before{content:"\F1817"}.mdi-balloon::before{content:"\F0A26"}.mdi-ballot::before{content:"\F09C9"}.mdi-ballot-outline::before{content:"\F09CA"}.mdi-ballot-recount::before{content:"\F0C39"}.mdi-ballot-recount-outline::before{content:"\F0C3A"}.mdi-bandage::before{content:"\F0DAF"}.mdi-bank::before{content:"\F0070"}.mdi-bank-check::before{content:"\F1655"}.mdi-bank-circle::before{content:"\F1C03"}.mdi-bank-circle-outline::before{content:"\F1C04"}.mdi-bank-minus::before{content:"\F0DB0"}.mdi-bank-off::before{content:"\F1656"}.mdi-bank-off-outline::before{content:"\F1657"}.mdi-bank-outline::before{content:"\F0E80"}.mdi-bank-plus::before{content:"\F0DB1"}.mdi-bank-remove::before{content:"\F0DB2"}.mdi-bank-transfer::before{content:"\F0A27"}.mdi-bank-transfer-in::before{content:"\F0A28"}.mdi-bank-transfer-out::before{content:"\F0A29"}.mdi-barcode::before{content:"\F0071"}.mdi-barcode-off::before{content:"\F1236"}.mdi-barcode-scan::before{content:"\F0072"}.mdi-barley::before{content:"\F0073"}.mdi-barley-off::before{content:"\F0B5D"}.mdi-barn::before{content:"\F0B5E"}.mdi-barrel::before{content:"\F0074"}.mdi-barrel-outline::before{content:"\F1A28"}.mdi-baseball::before{content:"\F0852"}.mdi-baseball-bat::before{content:"\F0853"}.mdi-baseball-diamond::before{content:"\F15EC"}.mdi-baseball-diamond-outline::before{content:"\F15ED"}.mdi-baseball-outline::before{content:"\F1C5A"}.mdi-bash::before{content:"\F1183"}.mdi-basket::before{content:"\F0076"}.mdi-basket-check::before{content:"\F18E5"}.mdi-basket-check-outline::before{content:"\F18E6"}.mdi-basket-fill::before{content:"\F0077"}.mdi-basket-minus::before{content:"\F1523"}.mdi-basket-minus-outline::before{content:"\F1524"}.mdi-basket-off::before{content:"\F1525"}.mdi-basket-off-outline::before{content:"\F1526"}.mdi-basket-outline::before{content:"\F1181"}.mdi-basket-plus::before{content:"\F1527"}.mdi-basket-plus-outline::before{content:"\F1528"}.mdi-basket-remove::before{content:"\F1529"}.mdi-basket-remove-outline::before{content:"\F152A"}.mdi-basket-unfill::before{content:"\F0078"}.mdi-basketball::before{content:"\F0806"}.mdi-basketball-hoop::before{content:"\F0C3B"}.mdi-basketball-hoop-outline::before{content:"\F0C3C"}.mdi-bat::before{content:"\F0B5F"}.mdi-bathtub::before{content:"\F1818"}.mdi-bathtub-outline::before{content:"\F1819"}.mdi-battery::before{content:"\F0079"}.mdi-battery-10::before{content:"\F007A"}.mdi-battery-10-bluetooth::before{content:"\F093E"}.mdi-battery-20::before{content:"\F007B"}.mdi-battery-20-bluetooth::before{content:"\F093F"}.mdi-battery-30::before{content:"\F007C"}.mdi-battery-30-bluetooth::before{content:"\F0940"}.mdi-battery-40::before{content:"\F007D"}.mdi-battery-40-bluetooth::before{content:"\F0941"}.mdi-battery-50::before{content:"\F007E"}.mdi-battery-50-bluetooth::before{content:"\F0942"}.mdi-battery-60::before{content:"\F007F"}.mdi-battery-60-bluetooth::before{content:"\F0943"}.mdi-battery-70::before{content:"\F0080"}.mdi-battery-70-bluetooth::before{content:"\F0944"}.mdi-battery-80::before{content:"\F0081"}.mdi-battery-80-bluetooth::before{content:"\F0945"}.mdi-battery-90::before{content:"\F0082"}.mdi-battery-90-bluetooth::before{content:"\F0946"}.mdi-battery-alert::before{content:"\F0083"}.mdi-battery-alert-bluetooth::before{content:"\F0947"}.mdi-battery-alert-variant::before{content:"\F10CC"}.mdi-battery-alert-variant-outline::before{content:"\F10CD"}.mdi-battery-arrow-down::before{content:"\F17DE"}.mdi-battery-arrow-down-outline::before{content:"\F17DF"}.mdi-battery-arrow-up::before{content:"\F17E0"}.mdi-battery-arrow-up-outline::before{content:"\F17E1"}.mdi-battery-bluetooth::before{content:"\F0948"}.mdi-battery-bluetooth-variant::before{content:"\F0949"}.mdi-battery-charging::before{content:"\F0084"}.mdi-battery-charging-10::before{content:"\F089C"}.mdi-battery-charging-100::before{content:"\F0085"}.mdi-battery-charging-20::before{content:"\F0086"}.mdi-battery-charging-30::before{content:"\F0087"}.mdi-battery-charging-40::before{content:"\F0088"}.mdi-battery-charging-50::before{content:"\F089D"}.mdi-battery-charging-60::before{content:"\F0089"}.mdi-battery-charging-70::before{content:"\F089E"}.mdi-battery-charging-80::before{content:"\F008A"}.mdi-battery-charging-90::before{content:"\F008B"}.mdi-battery-charging-high::before{content:"\F12A6"}.mdi-battery-charging-low::before{content:"\F12A4"}.mdi-battery-charging-medium::before{content:"\F12A5"}.mdi-battery-charging-outline::before{content:"\F089F"}.mdi-battery-charging-wireless::before{content:"\F0807"}.mdi-battery-charging-wireless-10::before{content:"\F0808"}.mdi-battery-charging-wireless-20::before{content:"\F0809"}.mdi-battery-charging-wireless-30::before{content:"\F080A"}.mdi-battery-charging-wireless-40::before{content:"\F080B"}.mdi-battery-charging-wireless-50::before{content:"\F080C"}.mdi-battery-charging-wireless-60::before{content:"\F080D"}.mdi-battery-charging-wireless-70::before{content:"\F080E"}.mdi-battery-charging-wireless-80::before{content:"\F080F"}.mdi-battery-charging-wireless-90::before{content:"\F0810"}.mdi-battery-charging-wireless-alert::before{content:"\F0811"}.mdi-battery-charging-wireless-outline::before{content:"\F0812"}.mdi-battery-check::before{content:"\F17E2"}.mdi-battery-check-outline::before{content:"\F17E3"}.mdi-battery-clock::before{content:"\F19E5"}.mdi-battery-clock-outline::before{content:"\F19E6"}.mdi-battery-heart::before{content:"\F120F"}.mdi-battery-heart-outline::before{content:"\F1210"}.mdi-battery-heart-variant::before{content:"\F1211"}.mdi-battery-high::before{content:"\F12A3"}.mdi-battery-lock::before{content:"\F179C"}.mdi-battery-lock-open::before{content:"\F179D"}.mdi-battery-low::before{content:"\F12A1"}.mdi-battery-medium::before{content:"\F12A2"}.mdi-battery-minus::before{content:"\F17E4"}.mdi-battery-minus-outline::before{content:"\F17E5"}.mdi-battery-minus-variant::before{content:"\F008C"}.mdi-battery-negative::before{content:"\F008D"}.mdi-battery-off::before{content:"\F125D"}.mdi-battery-off-outline::before{content:"\F125E"}.mdi-battery-outline::before{content:"\F008E"}.mdi-battery-plus::before{content:"\F17E6"}.mdi-battery-plus-outline::before{content:"\F17E7"}.mdi-battery-plus-variant::before{content:"\F008F"}.mdi-battery-positive::before{content:"\F0090"}.mdi-battery-remove::before{content:"\F17E8"}.mdi-battery-remove-outline::before{content:"\F17E9"}.mdi-battery-sync::before{content:"\F1834"}.mdi-battery-sync-outline::before{content:"\F1835"}.mdi-battery-unknown::before{content:"\F0091"}.mdi-battery-unknown-bluetooth::before{content:"\F094A"}.mdi-beach::before{content:"\F0092"}.mdi-beaker::before{content:"\F0CEA"}.mdi-beaker-alert::before{content:"\F1229"}.mdi-beaker-alert-outline::before{content:"\F122A"}.mdi-beaker-check::before{content:"\F122B"}.mdi-beaker-check-outline::before{content:"\F122C"}.mdi-beaker-minus::before{content:"\F122D"}.mdi-beaker-minus-outline::before{content:"\F122E"}.mdi-beaker-outline::before{content:"\F0690"}.mdi-beaker-plus::before{content:"\F122F"}.mdi-beaker-plus-outline::before{content:"\F1230"}.mdi-beaker-question::before{content:"\F1231"}.mdi-beaker-question-outline::before{content:"\F1232"}.mdi-beaker-remove::before{content:"\F1233"}.mdi-beaker-remove-outline::before{content:"\F1234"}.mdi-bed::before{content:"\F02E3"}.mdi-bed-clock::before{content:"\F1B94"}.mdi-bed-double::before{content:"\F0FD4"}.mdi-bed-double-outline::before{content:"\F0FD3"}.mdi-bed-empty::before{content:"\F08A0"}.mdi-bed-king::before{content:"\F0FD2"}.mdi-bed-king-outline::before{content:"\F0FD1"}.mdi-bed-outline::before{content:"\F0099"}.mdi-bed-queen::before{content:"\F0FD0"}.mdi-bed-queen-outline::before{content:"\F0FDB"}.mdi-bed-single::before{content:"\F106D"}.mdi-bed-single-outline::before{content:"\F106E"}.mdi-bee::before{content:"\F0FA1"}.mdi-bee-flower::before{content:"\F0FA2"}.mdi-beehive-off-outline::before{content:"\F13ED"}.mdi-beehive-outline::before{content:"\F10CE"}.mdi-beekeeper::before{content:"\F14E2"}.mdi-beer::before{content:"\F0098"}.mdi-beer-outline::before{content:"\F130C"}.mdi-bell::before{content:"\F009A"}.mdi-bell-alert::before{content:"\F0D59"}.mdi-bell-alert-outline::before{content:"\F0E81"}.mdi-bell-badge::before{content:"\F116B"}.mdi-bell-badge-outline::before{content:"\F0178"}.mdi-bell-cancel::before{content:"\F13E7"}.mdi-bell-cancel-outline::before{content:"\F13E8"}.mdi-bell-check::before{content:"\F11E5"}.mdi-bell-check-outline::before{content:"\F11E6"}.mdi-bell-circle::before{content:"\F0D5A"}.mdi-bell-circle-outline::before{content:"\F0D5B"}.mdi-bell-cog::before{content:"\F1A29"}.mdi-bell-cog-outline::before{content:"\F1A2A"}.mdi-bell-minus::before{content:"\F13E9"}.mdi-bell-minus-outline::before{content:"\F13EA"}.mdi-bell-off::before{content:"\F009B"}.mdi-bell-off-outline::before{content:"\F0A91"}.mdi-bell-outline::before{content:"\F009C"}.mdi-bell-plus::before{content:"\F009D"}.mdi-bell-plus-outline::before{content:"\F0A92"}.mdi-bell-remove::before{content:"\F13EB"}.mdi-bell-remove-outline::before{content:"\F13EC"}.mdi-bell-ring::before{content:"\F009E"}.mdi-bell-ring-outline::before{content:"\F009F"}.mdi-bell-sleep::before{content:"\F00A0"}.mdi-bell-sleep-outline::before{content:"\F0A93"}.mdi-bench::before{content:"\F1C21"}.mdi-bench-back::before{content:"\F1C22"}.mdi-beta::before{content:"\F00A1"}.mdi-betamax::before{content:"\F09CB"}.mdi-biathlon::before{content:"\F0E14"}.mdi-bicycle::before{content:"\F109C"}.mdi-bicycle-basket::before{content:"\F1235"}.mdi-bicycle-cargo::before{content:"\F189C"}.mdi-bicycle-electric::before{content:"\F15B4"}.mdi-bicycle-penny-farthing::before{content:"\F15E9"}.mdi-bike::before{content:"\F00A3"}.mdi-bike-fast::before{content:"\F111F"}.mdi-bike-pedal::before{content:"\F1C23"}.mdi-bike-pedal-clipless::before{content:"\F1C24"}.mdi-bike-pedal-mountain::before{content:"\F1C25"}.mdi-billboard::before{content:"\F1010"}.mdi-billiards::before{content:"\F0B61"}.mdi-billiards-rack::before{content:"\F0B62"}.mdi-binoculars::before{content:"\F00A5"}.mdi-bio::before{content:"\F00A6"}.mdi-biohazard::before{content:"\F00A7"}.mdi-bird::before{content:"\F15C6"}.mdi-bitbucket::before{content:"\F00A8"}.mdi-bitcoin::before{content:"\F0813"}.mdi-black-mesa::before{content:"\F00A9"}.mdi-blender::before{content:"\F0CEB"}.mdi-blender-outline::before{content:"\F181A"}.mdi-blender-software::before{content:"\F00AB"}.mdi-blinds::before{content:"\F00AC"}.mdi-blinds-horizontal::before{content:"\F1A2B"}.mdi-blinds-horizontal-closed::before{content:"\F1A2C"}.mdi-blinds-open::before{content:"\F1011"}.mdi-blinds-vertical::before{content:"\F1A2D"}.mdi-blinds-vertical-closed::before{content:"\F1A2E"}.mdi-block-helper::before{content:"\F00AD"}.mdi-blood-bag::before{content:"\F0CEC"}.mdi-bluetooth::before{content:"\F00AF"}.mdi-bluetooth-audio::before{content:"\F00B0"}.mdi-bluetooth-connect::before{content:"\F00B1"}.mdi-bluetooth-off::before{content:"\F00B2"}.mdi-bluetooth-settings::before{content:"\F00B3"}.mdi-bluetooth-transfer::before{content:"\F00B4"}.mdi-blur::before{content:"\F00B5"}.mdi-blur-linear::before{content:"\F00B6"}.mdi-blur-off::before{content:"\F00B7"}.mdi-blur-radial::before{content:"\F00B8"}.mdi-bolt::before{content:"\F0DB3"}.mdi-bomb::before{content:"\F0691"}.mdi-bomb-off::before{content:"\F06C5"}.mdi-bone::before{content:"\F00B9"}.mdi-bone-off::before{content:"\F19E0"}.mdi-book::before{content:"\F00BA"}.mdi-book-account::before{content:"\F13AD"}.mdi-book-account-outline::before{content:"\F13AE"}.mdi-book-alert::before{content:"\F167C"}.mdi-book-alert-outline::before{content:"\F167D"}.mdi-book-alphabet::before{content:"\F061D"}.mdi-book-arrow-down::before{content:"\F167E"}.mdi-book-arrow-down-outline::before{content:"\F167F"}.mdi-book-arrow-left::before{content:"\F1680"}.mdi-book-arrow-left-outline::before{content:"\F1681"}.mdi-book-arrow-right::before{content:"\F1682"}.mdi-book-arrow-right-outline::before{content:"\F1683"}.mdi-book-arrow-up::before{content:"\F1684"}.mdi-book-arrow-up-outline::before{content:"\F1685"}.mdi-book-cancel::before{content:"\F1686"}.mdi-book-cancel-outline::before{content:"\F1687"}.mdi-book-check::before{content:"\F14F3"}.mdi-book-check-outline::before{content:"\F14F4"}.mdi-book-clock::before{content:"\F1688"}.mdi-book-clock-outline::before{content:"\F1689"}.mdi-book-cog::before{content:"\F168A"}.mdi-book-cog-outline::before{content:"\F168B"}.mdi-book-cross::before{content:"\F00A2"}.mdi-book-edit::before{content:"\F168C"}.mdi-book-edit-outline::before{content:"\F168D"}.mdi-book-education::before{content:"\F16C9"}.mdi-book-education-outline::before{content:"\F16CA"}.mdi-book-heart::before{content:"\F1A1D"}.mdi-book-heart-outline::before{content:"\F1A1E"}.mdi-book-information-variant::before{content:"\F106F"}.mdi-book-lock::before{content:"\F079A"}.mdi-book-lock-open::before{content:"\F079B"}.mdi-book-lock-open-outline::before{content:"\F168E"}.mdi-book-lock-outline::before{content:"\F168F"}.mdi-book-marker::before{content:"\F1690"}.mdi-book-marker-outline::before{content:"\F1691"}.mdi-book-minus::before{content:"\F05D9"}.mdi-book-minus-multiple::before{content:"\F0A94"}.mdi-book-minus-multiple-outline::before{content:"\F090B"}.mdi-book-minus-outline::before{content:"\F1692"}.mdi-book-multiple::before{content:"\F00BB"}.mdi-book-multiple-outline::before{content:"\F0436"}.mdi-book-music::before{content:"\F0067"}.mdi-book-music-outline::before{content:"\F1693"}.mdi-book-off::before{content:"\F1694"}.mdi-book-off-outline::before{content:"\F1695"}.mdi-book-open::before{content:"\F00BD"}.mdi-book-open-blank-variant::before{content:"\F00BE"}.mdi-book-open-blank-variant-outline::before{content:"\F1CCB"}.mdi-book-open-outline::before{content:"\F0B63"}.mdi-book-open-page-variant::before{content:"\F05DA"}.mdi-book-open-page-variant-outline::before{content:"\F15D6"}.mdi-book-open-variant::before{content:"\F14F7"}.mdi-book-open-variant-outline::before{content:"\F1CCC"}.mdi-book-outline::before{content:"\F0B64"}.mdi-book-play::before{content:"\F0E82"}.mdi-book-play-outline::before{content:"\F0E83"}.mdi-book-plus::before{content:"\F05DB"}.mdi-book-plus-multiple::before{content:"\F0A95"}.mdi-book-plus-multiple-outline::before{content:"\F0ADE"}.mdi-book-plus-outline::before{content:"\F1696"}.mdi-book-refresh::before{content:"\F1697"}.mdi-book-refresh-outline::before{content:"\F1698"}.mdi-book-remove::before{content:"\F0A97"}.mdi-book-remove-multiple::before{content:"\F0A96"}.mdi-book-remove-multiple-outline::before{content:"\F04CA"}.mdi-book-remove-outline::before{content:"\F1699"}.mdi-book-search::before{content:"\F0E84"}.mdi-book-search-outline::before{content:"\F0E85"}.mdi-book-settings::before{content:"\F169A"}.mdi-book-settings-outline::before{content:"\F169B"}.mdi-book-sync::before{content:"\F169C"}.mdi-book-sync-outline::before{content:"\F16C8"}.mdi-book-variant::before{content:"\F00BF"}.mdi-bookmark::before{content:"\F00C0"}.mdi-bookmark-box::before{content:"\F1B75"}.mdi-bookmark-box-multiple::before{content:"\F196C"}.mdi-bookmark-box-multiple-outline::before{content:"\F196D"}.mdi-bookmark-box-outline::before{content:"\F1B76"}.mdi-bookmark-check::before{content:"\F00C1"}.mdi-bookmark-check-outline::before{content:"\F137B"}.mdi-bookmark-minus::before{content:"\F09CC"}.mdi-bookmark-minus-outline::before{content:"\F09CD"}.mdi-bookmark-multiple::before{content:"\F0E15"}.mdi-bookmark-multiple-outline::before{content:"\F0E16"}.mdi-bookmark-music::before{content:"\F00C2"}.mdi-bookmark-music-outline::before{content:"\F1379"}.mdi-bookmark-off::before{content:"\F09CE"}.mdi-bookmark-off-outline::before{content:"\F09CF"}.mdi-bookmark-outline::before{content:"\F00C3"}.mdi-bookmark-plus::before{content:"\F00C5"}.mdi-bookmark-plus-outline::before{content:"\F00C4"}.mdi-bookmark-remove::before{content:"\F00C6"}.mdi-bookmark-remove-outline::before{content:"\F137A"}.mdi-bookshelf::before{content:"\F125F"}.mdi-boom-gate::before{content:"\F0E86"}.mdi-boom-gate-alert::before{content:"\F0E87"}.mdi-boom-gate-alert-outline::before{content:"\F0E88"}.mdi-boom-gate-arrow-down::before{content:"\F0E89"}.mdi-boom-gate-arrow-down-outline::before{content:"\F0E8A"}.mdi-boom-gate-arrow-up::before{content:"\F0E8C"}.mdi-boom-gate-arrow-up-outline::before{content:"\F0E8D"}.mdi-boom-gate-outline::before{content:"\F0E8B"}.mdi-boom-gate-up::before{content:"\F17F9"}.mdi-boom-gate-up-outline::before{content:"\F17FA"}.mdi-boombox::before{content:"\F05DC"}.mdi-boomerang::before{content:"\F10CF"}.mdi-bootstrap::before{content:"\F06C6"}.mdi-border-all::before{content:"\F00C7"}.mdi-border-all-variant::before{content:"\F08A1"}.mdi-border-bottom::before{content:"\F00C8"}.mdi-border-bottom-variant::before{content:"\F08A2"}.mdi-border-color::before{content:"\F00C9"}.mdi-border-horizontal::before{content:"\F00CA"}.mdi-border-inside::before{content:"\F00CB"}.mdi-border-left::before{content:"\F00CC"}.mdi-border-left-variant::before{content:"\F08A3"}.mdi-border-none::before{content:"\F00CD"}.mdi-border-none-variant::before{content:"\F08A4"}.mdi-border-outside::before{content:"\F00CE"}.mdi-border-radius::before{content:"\F1AF4"}.mdi-border-right::before{content:"\F00CF"}.mdi-border-right-variant::before{content:"\F08A5"}.mdi-border-style::before{content:"\F00D0"}.mdi-border-top::before{content:"\F00D1"}.mdi-border-top-variant::before{content:"\F08A6"}.mdi-border-vertical::before{content:"\F00D2"}.mdi-bottle-soda::before{content:"\F1070"}.mdi-bottle-soda-classic::before{content:"\F1071"}.mdi-bottle-soda-classic-outline::before{content:"\F1363"}.mdi-bottle-soda-outline::before{content:"\F1072"}.mdi-bottle-tonic::before{content:"\F112E"}.mdi-bottle-tonic-outline::before{content:"\F112F"}.mdi-bottle-tonic-plus::before{content:"\F1130"}.mdi-bottle-tonic-plus-outline::before{content:"\F1131"}.mdi-bottle-tonic-skull::before{content:"\F1132"}.mdi-bottle-tonic-skull-outline::before{content:"\F1133"}.mdi-bottle-wine::before{content:"\F0854"}.mdi-bottle-wine-outline::before{content:"\F1310"}.mdi-bow-arrow::before{content:"\F1841"}.mdi-bow-tie::before{content:"\F0678"}.mdi-bowl::before{content:"\F028E"}.mdi-bowl-mix::before{content:"\F0617"}.mdi-bowl-mix-outline::before{content:"\F02E4"}.mdi-bowl-outline::before{content:"\F02A9"}.mdi-bowling::before{content:"\F00D3"}.mdi-box::before{content:"\F00D4"}.mdi-box-cutter::before{content:"\F00D5"}.mdi-box-cutter-off::before{content:"\F0B4A"}.mdi-box-shadow::before{content:"\F0637"}.mdi-boxing-glove::before{content:"\F0B65"}.mdi-braille::before{content:"\F09D0"}.mdi-brain::before{content:"\F09D1"}.mdi-bread-slice::before{content:"\F0CEE"}.mdi-bread-slice-outline::before{content:"\F0CEF"}.mdi-bridge::before{content:"\F0618"}.mdi-briefcase::before{content:"\F00D6"}.mdi-briefcase-account::before{content:"\F0CF0"}.mdi-briefcase-account-outline::before{content:"\F0CF1"}.mdi-briefcase-arrow-left-right::before{content:"\F1A8D"}.mdi-briefcase-arrow-left-right-outline::before{content:"\F1A8E"}.mdi-briefcase-arrow-up-down::before{content:"\F1A8F"}.mdi-briefcase-arrow-up-down-outline::before{content:"\F1A90"}.mdi-briefcase-check::before{content:"\F00D7"}.mdi-briefcase-check-outline::before{content:"\F131E"}.mdi-briefcase-clock::before{content:"\F10D0"}.mdi-briefcase-clock-outline::before{content:"\F10D1"}.mdi-briefcase-download::before{content:"\F00D8"}.mdi-briefcase-download-outline::before{content:"\F0C3D"}.mdi-briefcase-edit::before{content:"\F0A98"}.mdi-briefcase-edit-outline::before{content:"\F0C3E"}.mdi-briefcase-eye::before{content:"\F17D9"}.mdi-briefcase-eye-outline::before{content:"\F17DA"}.mdi-briefcase-minus::before{content:"\F0A2A"}.mdi-briefcase-minus-outline::before{content:"\F0C3F"}.mdi-briefcase-off::before{content:"\F1658"}.mdi-briefcase-off-outline::before{content:"\F1659"}.mdi-briefcase-outline::before{content:"\F0814"}.mdi-briefcase-plus::before{content:"\F0A2B"}.mdi-briefcase-plus-outline::before{content:"\F0C40"}.mdi-briefcase-remove::before{content:"\F0A2C"}.mdi-briefcase-remove-outline::before{content:"\F0C41"}.mdi-briefcase-search::before{content:"\F0A2D"}.mdi-briefcase-search-outline::before{content:"\F0C42"}.mdi-briefcase-upload::before{content:"\F00D9"}.mdi-briefcase-upload-outline::before{content:"\F0C43"}.mdi-briefcase-variant::before{content:"\F1494"}.mdi-briefcase-variant-off::before{content:"\F165A"}.mdi-briefcase-variant-off-outline::before{content:"\F165B"}.mdi-briefcase-variant-outline::before{content:"\F1495"}.mdi-brightness-1::before{content:"\F00DA"}.mdi-brightness-2::before{content:"\F00DB"}.mdi-brightness-3::before{content:"\F00DC"}.mdi-brightness-4::before{content:"\F00DD"}.mdi-brightness-5::before{content:"\F00DE"}.mdi-brightness-6::before{content:"\F00DF"}.mdi-brightness-7::before{content:"\F00E0"}.mdi-brightness-auto::before{content:"\F00E1"}.mdi-brightness-percent::before{content:"\F0CF2"}.mdi-broadcast::before{content:"\F1720"}.mdi-broadcast-off::before{content:"\F1721"}.mdi-broom::before{content:"\F00E2"}.mdi-brush::before{content:"\F00E3"}.mdi-brush-off::before{content:"\F1771"}.mdi-brush-outline::before{content:"\F1A0D"}.mdi-brush-variant::before{content:"\F1813"}.mdi-bucket::before{content:"\F1415"}.mdi-bucket-outline::before{content:"\F1416"}.mdi-buffet::before{content:"\F0578"}.mdi-bug::before{content:"\F00E4"}.mdi-bug-check::before{content:"\F0A2E"}.mdi-bug-check-outline::before{content:"\F0A2F"}.mdi-bug-outline::before{content:"\F0A30"}.mdi-bug-pause::before{content:"\F1AF5"}.mdi-bug-pause-outline::before{content:"\F1AF6"}.mdi-bug-play::before{content:"\F1AF7"}.mdi-bug-play-outline::before{content:"\F1AF8"}.mdi-bug-stop::before{content:"\F1AF9"}.mdi-bug-stop-outline::before{content:"\F1AFA"}.mdi-bugle::before{content:"\F0DB4"}.mdi-bulkhead-light::before{content:"\F1A2F"}.mdi-bulldozer::before{content:"\F0B22"}.mdi-bullet::before{content:"\F0CF3"}.mdi-bulletin-board::before{content:"\F00E5"}.mdi-bullhorn::before{content:"\F00E6"}.mdi-bullhorn-outline::before{content:"\F0B23"}.mdi-bullhorn-variant::before{content:"\F196E"}.mdi-bullhorn-variant-outline::before{content:"\F196F"}.mdi-bullseye::before{content:"\F05DD"}.mdi-bullseye-arrow::before{content:"\F08C9"}.mdi-bulma::before{content:"\F12E7"}.mdi-bunk-bed::before{content:"\F1302"}.mdi-bunk-bed-outline::before{content:"\F0097"}.mdi-bus::before{content:"\F00E7"}.mdi-bus-alert::before{content:"\F0A99"}.mdi-bus-articulated-end::before{content:"\F079C"}.mdi-bus-articulated-front::before{content:"\F079D"}.mdi-bus-clock::before{content:"\F08CA"}.mdi-bus-double-decker::before{content:"\F079E"}.mdi-bus-electric::before{content:"\F191D"}.mdi-bus-marker::before{content:"\F1212"}.mdi-bus-multiple::before{content:"\F0F3F"}.mdi-bus-school::before{content:"\F079F"}.mdi-bus-side::before{content:"\F07A0"}.mdi-bus-sign::before{content:"\F1CC1"}.mdi-bus-stop::before{content:"\F1012"}.mdi-bus-stop-covered::before{content:"\F1013"}.mdi-bus-stop-uncovered::before{content:"\F1014"}.mdi-bus-wrench::before{content:"\F1CC2"}.mdi-butterfly::before{content:"\F1589"}.mdi-butterfly-outline::before{content:"\F158A"}.mdi-button-cursor::before{content:"\F1B4F"}.mdi-button-pointer::before{content:"\F1B50"}.mdi-cabin-a-frame::before{content:"\F188C"}.mdi-cable-data::before{content:"\F1394"}.mdi-cached::before{content:"\F00E8"}.mdi-cactus::before{content:"\F0DB5"}.mdi-cake::before{content:"\F00E9"}.mdi-cake-layered::before{content:"\F00EA"}.mdi-cake-variant::before{content:"\F00EB"}.mdi-cake-variant-outline::before{content:"\F17F0"}.mdi-calculator::before{content:"\F00EC"}.mdi-calculator-variant::before{content:"\F0A9A"}.mdi-calculator-variant-outline::before{content:"\F15A6"}.mdi-calendar::before{content:"\F00ED"}.mdi-calendar-account::before{content:"\F0ED7"}.mdi-calendar-account-outline::before{content:"\F0ED8"}.mdi-calendar-alert::before{content:"\F0A31"}.mdi-calendar-alert-outline::before{content:"\F1B62"}.mdi-calendar-arrow-left::before{content:"\F1134"}.mdi-calendar-arrow-right::before{content:"\F1135"}.mdi-calendar-badge::before{content:"\F1B9D"}.mdi-calendar-badge-outline::before{content:"\F1B9E"}.mdi-calendar-blank::before{content:"\F00EE"}.mdi-calendar-blank-multiple::before{content:"\F1073"}.mdi-calendar-blank-outline::before{content:"\F0B66"}.mdi-calendar-check::before{content:"\F00EF"}.mdi-calendar-check-outline::before{content:"\F0C44"}.mdi-calendar-clock::before{content:"\F00F0"}.mdi-calendar-clock-outline::before{content:"\F16E1"}.mdi-calendar-collapse-horizontal::before{content:"\F189D"}.mdi-calendar-collapse-horizontal-outline::before{content:"\F1B63"}.mdi-calendar-cursor::before{content:"\F157B"}.mdi-calendar-cursor-outline::before{content:"\F1B64"}.mdi-calendar-edit::before{content:"\F08A7"}.mdi-calendar-edit-outline::before{content:"\F1B65"}.mdi-calendar-end::before{content:"\F166C"}.mdi-calendar-end-outline::before{content:"\F1B66"}.mdi-calendar-expand-horizontal::before{content:"\F189E"}.mdi-calendar-expand-horizontal-outline::before{content:"\F1B67"}.mdi-calendar-export::before{content:"\F0B24"}.mdi-calendar-export-outline::before{content:"\F1B68"}.mdi-calendar-filter::before{content:"\F1A32"}.mdi-calendar-filter-outline::before{content:"\F1A33"}.mdi-calendar-heart::before{content:"\F09D2"}.mdi-calendar-heart-outline::before{content:"\F1B69"}.mdi-calendar-import::before{content:"\F0B25"}.mdi-calendar-import-outline::before{content:"\F1B6A"}.mdi-calendar-lock::before{content:"\F1641"}.mdi-calendar-lock-open::before{content:"\F1B5B"}.mdi-calendar-lock-open-outline::before{content:"\F1B5C"}.mdi-calendar-lock-outline::before{content:"\F1642"}.mdi-calendar-minus::before{content:"\F0D5C"}.mdi-calendar-minus-outline::before{content:"\F1B6B"}.mdi-calendar-month::before{content:"\F0E17"}.mdi-calendar-month-outline::before{content:"\F0E18"}.mdi-calendar-multiple::before{content:"\F00F1"}.mdi-calendar-multiple-check::before{content:"\F00F2"}.mdi-calendar-multiselect::before{content:"\F0A32"}.mdi-calendar-multiselect-outline::before{content:"\F1B55"}.mdi-calendar-outline::before{content:"\F0B67"}.mdi-calendar-plus::before{content:"\F00F3"}.mdi-calendar-plus-outline::before{content:"\F1B6C"}.mdi-calendar-question::before{content:"\F0692"}.mdi-calendar-question-outline::before{content:"\F1B6D"}.mdi-calendar-range::before{content:"\F0679"}.mdi-calendar-range-outline::before{content:"\F0B68"}.mdi-calendar-refresh::before{content:"\F01E1"}.mdi-calendar-refresh-outline::before{content:"\F0203"}.mdi-calendar-remove::before{content:"\F00F4"}.mdi-calendar-remove-outline::before{content:"\F0C45"}.mdi-calendar-search::before{content:"\F094C"}.mdi-calendar-search-outline::before{content:"\F1B6E"}.mdi-calendar-star::before{content:"\F09D3"}.mdi-calendar-star-four-points::before{content:"\F1C1F"}.mdi-calendar-star-outline::before{content:"\F1B53"}.mdi-calendar-start::before{content:"\F166D"}.mdi-calendar-start-outline::before{content:"\F1B6F"}.mdi-calendar-sync::before{content:"\F0E8E"}.mdi-calendar-sync-outline::before{content:"\F0E8F"}.mdi-calendar-text::before{content:"\F00F5"}.mdi-calendar-text-outline::before{content:"\F0C46"}.mdi-calendar-today::before{content:"\F00F6"}.mdi-calendar-today-outline::before{content:"\F1A30"}.mdi-calendar-week::before{content:"\F0A33"}.mdi-calendar-week-begin::before{content:"\F0A34"}.mdi-calendar-week-begin-outline::before{content:"\F1A31"}.mdi-calendar-week-outline::before{content:"\F1A34"}.mdi-calendar-weekend::before{content:"\F0ED9"}.mdi-calendar-weekend-outline::before{content:"\F0EDA"}.mdi-call-made::before{content:"\F00F7"}.mdi-call-merge::before{content:"\F00F8"}.mdi-call-missed::before{content:"\F00F9"}.mdi-call-received::before{content:"\F00FA"}.mdi-call-split::before{content:"\F00FB"}.mdi-camcorder::before{content:"\F00FC"}.mdi-camcorder-off::before{content:"\F00FF"}.mdi-camera::before{content:"\F0100"}.mdi-camera-account::before{content:"\F08CB"}.mdi-camera-burst::before{content:"\F0693"}.mdi-camera-control::before{content:"\F0B69"}.mdi-camera-document::before{content:"\F1871"}.mdi-camera-document-off::before{content:"\F1872"}.mdi-camera-enhance::before{content:"\F0101"}.mdi-camera-enhance-outline::before{content:"\F0B6A"}.mdi-camera-flip::before{content:"\F15D9"}.mdi-camera-flip-outline::before{content:"\F15DA"}.mdi-camera-front::before{content:"\F0102"}.mdi-camera-front-variant::before{content:"\F0103"}.mdi-camera-gopro::before{content:"\F07A1"}.mdi-camera-image::before{content:"\F08CC"}.mdi-camera-iris::before{content:"\F0104"}.mdi-camera-lock::before{content:"\F1A14"}.mdi-camera-lock-open::before{content:"\F1C0D"}.mdi-camera-lock-open-outline::before{content:"\F1C0E"}.mdi-camera-lock-outline::before{content:"\F1A15"}.mdi-camera-marker::before{content:"\F19A7"}.mdi-camera-marker-outline::before{content:"\F19A8"}.mdi-camera-metering-center::before{content:"\F07A2"}.mdi-camera-metering-matrix::before{content:"\F07A3"}.mdi-camera-metering-partial::before{content:"\F07A4"}.mdi-camera-metering-spot::before{content:"\F07A5"}.mdi-camera-off::before{content:"\F05DF"}.mdi-camera-off-outline::before{content:"\F19BF"}.mdi-camera-outline::before{content:"\F0D5D"}.mdi-camera-party-mode::before{content:"\F0105"}.mdi-camera-plus::before{content:"\F0EDB"}.mdi-camera-plus-outline::before{content:"\F0EDC"}.mdi-camera-rear::before{content:"\F0106"}.mdi-camera-rear-variant::before{content:"\F0107"}.mdi-camera-retake::before{content:"\F0E19"}.mdi-camera-retake-outline::before{content:"\F0E1A"}.mdi-camera-switch::before{content:"\F0108"}.mdi-camera-switch-outline::before{content:"\F084A"}.mdi-camera-timer::before{content:"\F0109"}.mdi-camera-wireless::before{content:"\F0DB6"}.mdi-camera-wireless-outline::before{content:"\F0DB7"}.mdi-campfire::before{content:"\F0EDD"}.mdi-cancel::before{content:"\F073A"}.mdi-candelabra::before{content:"\F17D2"}.mdi-candelabra-fire::before{content:"\F17D3"}.mdi-candle::before{content:"\F05E2"}.mdi-candy::before{content:"\F1970"}.mdi-candy-off::before{content:"\F1971"}.mdi-candy-off-outline::before{content:"\F1972"}.mdi-candy-outline::before{content:"\F1973"}.mdi-candycane::before{content:"\F010A"}.mdi-cannabis::before{content:"\F07A6"}.mdi-cannabis-off::before{content:"\F166E"}.mdi-caps-lock::before{content:"\F0A9B"}.mdi-car::before{content:"\F010B"}.mdi-car-2-plus::before{content:"\F1015"}.mdi-car-3-plus::before{content:"\F1016"}.mdi-car-arrow-left::before{content:"\F13B2"}.mdi-car-arrow-right::before{content:"\F13B3"}.mdi-car-back::before{content:"\F0E1B"}.mdi-car-battery::before{content:"\F010C"}.mdi-car-brake-abs::before{content:"\F0C47"}.mdi-car-brake-alert::before{content:"\F0C48"}.mdi-car-brake-fluid-level::before{content:"\F1909"}.mdi-car-brake-hold::before{content:"\F0D5E"}.mdi-car-brake-low-pressure::before{content:"\F190A"}.mdi-car-brake-parking::before{content:"\F0D5F"}.mdi-car-brake-retarder::before{content:"\F1017"}.mdi-car-brake-temperature::before{content:"\F190B"}.mdi-car-brake-worn-linings::before{content:"\F190C"}.mdi-car-child-seat::before{content:"\F0FA3"}.mdi-car-clock::before{content:"\F1974"}.mdi-car-clutch::before{content:"\F1018"}.mdi-car-cog::before{content:"\F13CC"}.mdi-car-connected::before{content:"\F010D"}.mdi-car-convertible::before{content:"\F07A7"}.mdi-car-coolant-level::before{content:"\F1019"}.mdi-car-cruise-control::before{content:"\F0D60"}.mdi-car-defrost-front::before{content:"\F0D61"}.mdi-car-defrost-rear::before{content:"\F0D62"}.mdi-car-door::before{content:"\F0B6B"}.mdi-car-door-lock::before{content:"\F109D"}.mdi-car-door-lock-open::before{content:"\F1C81"}.mdi-car-electric::before{content:"\F0B6C"}.mdi-car-electric-outline::before{content:"\F15B5"}.mdi-car-emergency::before{content:"\F160F"}.mdi-car-esp::before{content:"\F0C49"}.mdi-car-estate::before{content:"\F07A8"}.mdi-car-hatchback::before{content:"\F07A9"}.mdi-car-info::before{content:"\F11BE"}.mdi-car-key::before{content:"\F0B6D"}.mdi-car-lifted-pickup::before{content:"\F152D"}.mdi-car-light-alert::before{content:"\F190D"}.mdi-car-light-dimmed::before{content:"\F0C4A"}.mdi-car-light-fog::before{content:"\F0C4B"}.mdi-car-light-high::before{content:"\F0C4C"}.mdi-car-limousine::before{content:"\F08CD"}.mdi-car-multiple::before{content:"\F0B6E"}.mdi-car-off::before{content:"\F0E1C"}.mdi-car-outline::before{content:"\F14ED"}.mdi-car-parking-lights::before{content:"\F0D63"}.mdi-car-pickup::before{content:"\F07AA"}.mdi-car-search::before{content:"\F1B8D"}.mdi-car-search-outline::before{content:"\F1B8E"}.mdi-car-seat::before{content:"\F0FA4"}.mdi-car-seat-cooler::before{content:"\F0FA5"}.mdi-car-seat-heater::before{content:"\F0FA6"}.mdi-car-select::before{content:"\F1879"}.mdi-car-settings::before{content:"\F13CD"}.mdi-car-shift-pattern::before{content:"\F0F40"}.mdi-car-side::before{content:"\F07AB"}.mdi-car-speed-limiter::before{content:"\F190E"}.mdi-car-sports::before{content:"\F07AC"}.mdi-car-tire-alert::before{content:"\F0C4D"}.mdi-car-traction-control::before{content:"\F0D64"}.mdi-car-turbocharger::before{content:"\F101A"}.mdi-car-wash::before{content:"\F010E"}.mdi-car-windshield::before{content:"\F101B"}.mdi-car-windshield-outline::before{content:"\F101C"}.mdi-car-wireless::before{content:"\F1878"}.mdi-car-wrench::before{content:"\F1814"}.mdi-carabiner::before{content:"\F14C0"}.mdi-caravan::before{content:"\F07AD"}.mdi-card::before{content:"\F0B6F"}.mdi-card-account-details::before{content:"\F05D2"}.mdi-card-account-details-outline::before{content:"\F0DAB"}.mdi-card-account-details-star::before{content:"\F02A3"}.mdi-card-account-details-star-outline::before{content:"\F06DB"}.mdi-card-account-mail::before{content:"\F018E"}.mdi-card-account-mail-outline::before{content:"\F0E98"}.mdi-card-account-phone::before{content:"\F0E99"}.mdi-card-account-phone-outline::before{content:"\F0E9A"}.mdi-card-bulleted::before{content:"\F0B70"}.mdi-card-bulleted-off::before{content:"\F0B71"}.mdi-card-bulleted-off-outline::before{content:"\F0B72"}.mdi-card-bulleted-outline::before{content:"\F0B73"}.mdi-card-bulleted-settings::before{content:"\F0B74"}.mdi-card-bulleted-settings-outline::before{content:"\F0B75"}.mdi-card-minus::before{content:"\F1600"}.mdi-card-minus-outline::before{content:"\F1601"}.mdi-card-multiple::before{content:"\F17F1"}.mdi-card-multiple-outline::before{content:"\F17F2"}.mdi-card-off::before{content:"\F1602"}.mdi-card-off-outline::before{content:"\F1603"}.mdi-card-outline::before{content:"\F0B76"}.mdi-card-plus::before{content:"\F11FF"}.mdi-card-plus-outline::before{content:"\F1200"}.mdi-card-remove::before{content:"\F1604"}.mdi-card-remove-outline::before{content:"\F1605"}.mdi-card-search::before{content:"\F1074"}.mdi-card-search-outline::before{content:"\F1075"}.mdi-card-text::before{content:"\F0B77"}.mdi-card-text-outline::before{content:"\F0B78"}.mdi-cards::before{content:"\F0638"}.mdi-cards-club::before{content:"\F08CE"}.mdi-cards-club-outline::before{content:"\F189F"}.mdi-cards-diamond::before{content:"\F08CF"}.mdi-cards-diamond-outline::before{content:"\F101D"}.mdi-cards-heart::before{content:"\F08D0"}.mdi-cards-heart-outline::before{content:"\F18A0"}.mdi-cards-outline::before{content:"\F0639"}.mdi-cards-playing::before{content:"\F18A1"}.mdi-cards-playing-club::before{content:"\F18A2"}.mdi-cards-playing-club-multiple::before{content:"\F18A3"}.mdi-cards-playing-club-multiple-outline::before{content:"\F18A4"}.mdi-cards-playing-club-outline::before{content:"\F18A5"}.mdi-cards-playing-diamond::before{content:"\F18A6"}.mdi-cards-playing-diamond-multiple::before{content:"\F18A7"}.mdi-cards-playing-diamond-multiple-outline::before{content:"\F18A8"}.mdi-cards-playing-diamond-outline::before{content:"\F18A9"}.mdi-cards-playing-heart::before{content:"\F18AA"}.mdi-cards-playing-heart-multiple::before{content:"\F18AB"}.mdi-cards-playing-heart-multiple-outline::before{content:"\F18AC"}.mdi-cards-playing-heart-outline::before{content:"\F18AD"}.mdi-cards-playing-outline::before{content:"\F063A"}.mdi-cards-playing-spade::before{content:"\F18AE"}.mdi-cards-playing-spade-multiple::before{content:"\F18AF"}.mdi-cards-playing-spade-multiple-outline::before{content:"\F18B0"}.mdi-cards-playing-spade-outline::before{content:"\F18B1"}.mdi-cards-spade::before{content:"\F08D1"}.mdi-cards-spade-outline::before{content:"\F18B2"}.mdi-cards-variant::before{content:"\F06C7"}.mdi-carrot::before{content:"\F010F"}.mdi-cart::before{content:"\F0110"}.mdi-cart-arrow-down::before{content:"\F0D66"}.mdi-cart-arrow-right::before{content:"\F0C4E"}.mdi-cart-arrow-up::before{content:"\F0D67"}.mdi-cart-check::before{content:"\F15EA"}.mdi-cart-heart::before{content:"\F18E0"}.mdi-cart-minus::before{content:"\F0D68"}.mdi-cart-off::before{content:"\F066B"}.mdi-cart-outline::before{content:"\F0111"}.mdi-cart-percent::before{content:"\F1BAE"}.mdi-cart-plus::before{content:"\F0112"}.mdi-cart-remove::before{content:"\F0D69"}.mdi-cart-variant::before{content:"\F15EB"}.mdi-case-sensitive-alt::before{content:"\F0113"}.mdi-cash::before{content:"\F0114"}.mdi-cash-100::before{content:"\F0115"}.mdi-cash-check::before{content:"\F14EE"}.mdi-cash-clock::before{content:"\F1A91"}.mdi-cash-edit::before{content:"\F1CAB"}.mdi-cash-fast::before{content:"\F185C"}.mdi-cash-lock::before{content:"\F14EA"}.mdi-cash-lock-open::before{content:"\F14EB"}.mdi-cash-marker::before{content:"\F0DB8"}.mdi-cash-minus::before{content:"\F1260"}.mdi-cash-multiple::before{content:"\F0116"}.mdi-cash-off::before{content:"\F1C79"}.mdi-cash-plus::before{content:"\F1261"}.mdi-cash-refund::before{content:"\F0A9C"}.mdi-cash-register::before{content:"\F0CF4"}.mdi-cash-remove::before{content:"\F1262"}.mdi-cash-sync::before{content:"\F1A92"}.mdi-cassette::before{content:"\F09D4"}.mdi-cast::before{content:"\F0118"}.mdi-cast-audio::before{content:"\F101E"}.mdi-cast-audio-variant::before{content:"\F1749"}.mdi-cast-connected::before{content:"\F0119"}.mdi-cast-education::before{content:"\F0E1D"}.mdi-cast-off::before{content:"\F078A"}.mdi-cast-variant::before{content:"\F001F"}.mdi-castle::before{content:"\F011A"}.mdi-cat::before{content:"\F011B"}.mdi-cctv::before{content:"\F07AE"}.mdi-cctv-off::before{content:"\F185F"}.mdi-ceiling-fan::before{content:"\F1797"}.mdi-ceiling-fan-light::before{content:"\F1798"}.mdi-ceiling-light::before{content:"\F0769"}.mdi-ceiling-light-multiple::before{content:"\F18DD"}.mdi-ceiling-light-multiple-outline::before{content:"\F18DE"}.mdi-ceiling-light-outline::before{content:"\F17C7"}.mdi-cellphone::before{content:"\F011C"}.mdi-cellphone-arrow-down::before{content:"\F09D5"}.mdi-cellphone-arrow-down-variant::before{content:"\F19C5"}.mdi-cellphone-basic::before{content:"\F011E"}.mdi-cellphone-charging::before{content:"\F1397"}.mdi-cellphone-check::before{content:"\F17FD"}.mdi-cellphone-cog::before{content:"\F0951"}.mdi-cellphone-dock::before{content:"\F011F"}.mdi-cellphone-information::before{content:"\F0F41"}.mdi-cellphone-key::before{content:"\F094E"}.mdi-cellphone-link::before{content:"\F0121"}.mdi-cellphone-link-off::before{content:"\F0122"}.mdi-cellphone-lock::before{content:"\F094F"}.mdi-cellphone-marker::before{content:"\F183A"}.mdi-cellphone-message::before{content:"\F08D3"}.mdi-cellphone-message-off::before{content:"\F10D2"}.mdi-cellphone-nfc::before{content:"\F0E90"}.mdi-cellphone-nfc-off::before{content:"\F12D8"}.mdi-cellphone-off::before{content:"\F0950"}.mdi-cellphone-play::before{content:"\F101F"}.mdi-cellphone-remove::before{content:"\F094D"}.mdi-cellphone-screenshot::before{content:"\F0A35"}.mdi-cellphone-settings::before{content:"\F0123"}.mdi-cellphone-sound::before{content:"\F0952"}.mdi-cellphone-text::before{content:"\F08D2"}.mdi-cellphone-wireless::before{content:"\F0815"}.mdi-centos::before{content:"\F111A"}.mdi-certificate::before{content:"\F0124"}.mdi-certificate-outline::before{content:"\F1188"}.mdi-chair-rolling::before{content:"\F0F48"}.mdi-chair-school::before{content:"\F0125"}.mdi-chandelier::before{content:"\F1793"}.mdi-charity::before{content:"\F0C4F"}.mdi-charity-search::before{content:"\F1C82"}.mdi-chart-arc::before{content:"\F0126"}.mdi-chart-areaspline::before{content:"\F0127"}.mdi-chart-areaspline-variant::before{content:"\F0E91"}.mdi-chart-bar::before{content:"\F0128"}.mdi-chart-bar-stacked::before{content:"\F076A"}.mdi-chart-bell-curve::before{content:"\F0C50"}.mdi-chart-bell-curve-cumulative::before{content:"\F0FA7"}.mdi-chart-box::before{content:"\F154D"}.mdi-chart-box-multiple::before{content:"\F1CCD"}.mdi-chart-box-multiple-outline::before{content:"\F1CCE"}.mdi-chart-box-outline::before{content:"\F154E"}.mdi-chart-box-plus-outline::before{content:"\F154F"}.mdi-chart-bubble::before{content:"\F05E3"}.mdi-chart-donut::before{content:"\F07AF"}.mdi-chart-donut-variant::before{content:"\F07B0"}.mdi-chart-gantt::before{content:"\F066C"}.mdi-chart-histogram::before{content:"\F0129"}.mdi-chart-line::before{content:"\F012A"}.mdi-chart-line-stacked::before{content:"\F076B"}.mdi-chart-line-variant::before{content:"\F07B1"}.mdi-chart-multiline::before{content:"\F08D4"}.mdi-chart-multiple::before{content:"\F1213"}.mdi-chart-pie::before{content:"\F012B"}.mdi-chart-pie-outline::before{content:"\F1BDF"}.mdi-chart-ppf::before{content:"\F1380"}.mdi-chart-sankey::before{content:"\F11DF"}.mdi-chart-sankey-variant::before{content:"\F11E0"}.mdi-chart-scatter-plot::before{content:"\F0E92"}.mdi-chart-scatter-plot-hexbin::before{content:"\F066D"}.mdi-chart-timeline::before{content:"\F066E"}.mdi-chart-timeline-variant::before{content:"\F0E93"}.mdi-chart-timeline-variant-shimmer::before{content:"\F15B6"}.mdi-chart-tree::before{content:"\F0E94"}.mdi-chart-waterfall::before{content:"\F1918"}.mdi-chat::before{content:"\F0B79"}.mdi-chat-alert::before{content:"\F0B7A"}.mdi-chat-alert-outline::before{content:"\F12C9"}.mdi-chat-minus::before{content:"\F1410"}.mdi-chat-minus-outline::before{content:"\F1413"}.mdi-chat-outline::before{content:"\F0EDE"}.mdi-chat-plus::before{content:"\F140F"}.mdi-chat-plus-outline::before{content:"\F1412"}.mdi-chat-processing::before{content:"\F0B7B"}.mdi-chat-processing-outline::before{content:"\F12CA"}.mdi-chat-question::before{content:"\F1738"}.mdi-chat-question-outline::before{content:"\F1739"}.mdi-chat-remove::before{content:"\F1411"}.mdi-chat-remove-outline::before{content:"\F1414"}.mdi-chat-sleep::before{content:"\F12D1"}.mdi-chat-sleep-outline::before{content:"\F12D2"}.mdi-check::before{content:"\F012C"}.mdi-check-all::before{content:"\F012D"}.mdi-check-bold::before{content:"\F0E1E"}.mdi-check-circle::before{content:"\F05E0"}.mdi-check-circle-outline::before{content:"\F05E1"}.mdi-check-decagram::before{content:"\F0791"}.mdi-check-decagram-outline::before{content:"\F1740"}.mdi-check-network::before{content:"\F0C53"}.mdi-check-network-outline::before{content:"\F0C54"}.mdi-check-outline::before{content:"\F0855"}.mdi-check-underline::before{content:"\F0E1F"}.mdi-check-underline-circle::before{content:"\F0E20"}.mdi-check-underline-circle-outline::before{content:"\F0E21"}.mdi-checkbook::before{content:"\F0A9D"}.mdi-checkbook-arrow-left::before{content:"\F1C1D"}.mdi-checkbook-arrow-right::before{content:"\F1C1E"}.mdi-checkbox-blank::before{content:"\F012E"}.mdi-checkbox-blank-badge::before{content:"\F1176"}.mdi-checkbox-blank-badge-outline::before{content:"\F0117"}.mdi-checkbox-blank-circle::before{content:"\F012F"}.mdi-checkbox-blank-circle-outline::before{content:"\F0130"}.mdi-checkbox-blank-off::before{content:"\F12EC"}.mdi-checkbox-blank-off-outline::before{content:"\F12ED"}.mdi-checkbox-blank-outline::before{content:"\F0131"}.mdi-checkbox-intermediate::before{content:"\F0856"}.mdi-checkbox-intermediate-variant::before{content:"\F1B54"}.mdi-checkbox-marked::before{content:"\F0132"}.mdi-checkbox-marked-circle::before{content:"\F0133"}.mdi-checkbox-marked-circle-auto-outline::before{content:"\F1C26"}.mdi-checkbox-marked-circle-minus-outline::before{content:"\F1C27"}.mdi-checkbox-marked-circle-outline::before{content:"\F0134"}.mdi-checkbox-marked-circle-plus-outline::before{content:"\F1927"}.mdi-checkbox-marked-outline::before{content:"\F0135"}.mdi-checkbox-multiple-blank::before{content:"\F0136"}.mdi-checkbox-multiple-blank-circle::before{content:"\F063B"}.mdi-checkbox-multiple-blank-circle-outline::before{content:"\F063C"}.mdi-checkbox-multiple-blank-outline::before{content:"\F0137"}.mdi-checkbox-multiple-marked::before{content:"\F0138"}.mdi-checkbox-multiple-marked-circle::before{content:"\F063D"}.mdi-checkbox-multiple-marked-circle-outline::before{content:"\F063E"}.mdi-checkbox-multiple-marked-outline::before{content:"\F0139"}.mdi-checkbox-multiple-outline::before{content:"\F0C51"}.mdi-checkbox-outline::before{content:"\F0C52"}.mdi-checkerboard::before{content:"\F013A"}.mdi-checkerboard-minus::before{content:"\F1202"}.mdi-checkerboard-plus::before{content:"\F1201"}.mdi-checkerboard-remove::before{content:"\F1203"}.mdi-cheese::before{content:"\F12B9"}.mdi-cheese-off::before{content:"\F13EE"}.mdi-chef-hat::before{content:"\F0B7C"}.mdi-chemical-weapon::before{content:"\F013B"}.mdi-chess-bishop::before{content:"\F085C"}.mdi-chess-king::before{content:"\F0857"}.mdi-chess-knight::before{content:"\F0858"}.mdi-chess-pawn::before{content:"\F0859"}.mdi-chess-queen::before{content:"\F085A"}.mdi-chess-rook::before{content:"\F085B"}.mdi-chevron-double-down::before{content:"\F013C"}.mdi-chevron-double-left::before{content:"\F013D"}.mdi-chevron-double-right::before{content:"\F013E"}.mdi-chevron-double-up::before{content:"\F013F"}.mdi-chevron-down::before{content:"\F0140"}.mdi-chevron-down-box::before{content:"\F09D6"}.mdi-chevron-down-box-outline::before{content:"\F09D7"}.mdi-chevron-down-circle::before{content:"\F0B26"}.mdi-chevron-down-circle-outline::before{content:"\F0B27"}.mdi-chevron-left::before{content:"\F0141"}.mdi-chevron-left-box::before{content:"\F09D8"}.mdi-chevron-left-box-outline::before{content:"\F09D9"}.mdi-chevron-left-circle::before{content:"\F0B28"}.mdi-chevron-left-circle-outline::before{content:"\F0B29"}.mdi-chevron-right::before{content:"\F0142"}.mdi-chevron-right-box::before{content:"\F09DA"}.mdi-chevron-right-box-outline::before{content:"\F09DB"}.mdi-chevron-right-circle::before{content:"\F0B2A"}.mdi-chevron-right-circle-outline::before{content:"\F0B2B"}.mdi-chevron-triple-down::before{content:"\F0DB9"}.mdi-chevron-triple-left::before{content:"\F0DBA"}.mdi-chevron-triple-right::before{content:"\F0DBB"}.mdi-chevron-triple-up::before{content:"\F0DBC"}.mdi-chevron-up::before{content:"\F0143"}.mdi-chevron-up-box::before{content:"\F09DC"}.mdi-chevron-up-box-outline::before{content:"\F09DD"}.mdi-chevron-up-circle::before{content:"\F0B2C"}.mdi-chevron-up-circle-outline::before{content:"\F0B2D"}.mdi-chili-alert::before{content:"\F17EA"}.mdi-chili-alert-outline::before{content:"\F17EB"}.mdi-chili-hot::before{content:"\F07B2"}.mdi-chili-hot-outline::before{content:"\F17EC"}.mdi-chili-medium::before{content:"\F07B3"}.mdi-chili-medium-outline::before{content:"\F17ED"}.mdi-chili-mild::before{content:"\F07B4"}.mdi-chili-mild-outline::before{content:"\F17EE"}.mdi-chili-off::before{content:"\F1467"}.mdi-chili-off-outline::before{content:"\F17EF"}.mdi-chip::before{content:"\F061A"}.mdi-church::before{content:"\F0144"}.mdi-church-outline::before{content:"\F1B02"}.mdi-cigar::before{content:"\F1189"}.mdi-cigar-off::before{content:"\F141B"}.mdi-circle::before{content:"\F0765"}.mdi-circle-box::before{content:"\F15DC"}.mdi-circle-box-outline::before{content:"\F15DD"}.mdi-circle-double::before{content:"\F0E95"}.mdi-circle-edit-outline::before{content:"\F08D5"}.mdi-circle-expand::before{content:"\F0E96"}.mdi-circle-half::before{content:"\F1395"}.mdi-circle-half-full::before{content:"\F1396"}.mdi-circle-medium::before{content:"\F09DE"}.mdi-circle-multiple::before{content:"\F0B38"}.mdi-circle-multiple-outline::before{content:"\F0695"}.mdi-circle-off-outline::before{content:"\F10D3"}.mdi-circle-opacity::before{content:"\F1853"}.mdi-circle-outline::before{content:"\F0766"}.mdi-circle-slice-1::before{content:"\F0A9E"}.mdi-circle-slice-2::before{content:"\F0A9F"}.mdi-circle-slice-3::before{content:"\F0AA0"}.mdi-circle-slice-4::before{content:"\F0AA1"}.mdi-circle-slice-5::before{content:"\F0AA2"}.mdi-circle-slice-6::before{content:"\F0AA3"}.mdi-circle-slice-7::before{content:"\F0AA4"}.mdi-circle-slice-8::before{content:"\F0AA5"}.mdi-circle-small::before{content:"\F09DF"}.mdi-circular-saw::before{content:"\F0E22"}.mdi-city::before{content:"\F0146"}.mdi-city-switch::before{content:"\F1C28"}.mdi-city-variant::before{content:"\F0A36"}.mdi-city-variant-outline::before{content:"\F0A37"}.mdi-clipboard::before{content:"\F0147"}.mdi-clipboard-account::before{content:"\F0148"}.mdi-clipboard-account-outline::before{content:"\F0C55"}.mdi-clipboard-alert::before{content:"\F0149"}.mdi-clipboard-alert-outline::before{content:"\F0CF7"}.mdi-clipboard-arrow-down::before{content:"\F014A"}.mdi-clipboard-arrow-down-outline::before{content:"\F0C56"}.mdi-clipboard-arrow-left::before{content:"\F014B"}.mdi-clipboard-arrow-left-outline::before{content:"\F0CF8"}.mdi-clipboard-arrow-right::before{content:"\F0CF9"}.mdi-clipboard-arrow-right-outline::before{content:"\F0CFA"}.mdi-clipboard-arrow-up::before{content:"\F0C57"}.mdi-clipboard-arrow-up-outline::before{content:"\F0C58"}.mdi-clipboard-check::before{content:"\F014E"}.mdi-clipboard-check-multiple::before{content:"\F1263"}.mdi-clipboard-check-multiple-outline::before{content:"\F1264"}.mdi-clipboard-check-outline::before{content:"\F08A8"}.mdi-clipboard-clock::before{content:"\F16E2"}.mdi-clipboard-clock-outline::before{content:"\F16E3"}.mdi-clipboard-edit::before{content:"\F14E5"}.mdi-clipboard-edit-outline::before{content:"\F14E6"}.mdi-clipboard-file::before{content:"\F1265"}.mdi-clipboard-file-outline::before{content:"\F1266"}.mdi-clipboard-flow::before{content:"\F06C8"}.mdi-clipboard-flow-outline::before{content:"\F1117"}.mdi-clipboard-list::before{content:"\F10D4"}.mdi-clipboard-list-outline::before{content:"\F10D5"}.mdi-clipboard-minus::before{content:"\F1618"}.mdi-clipboard-minus-outline::before{content:"\F1619"}.mdi-clipboard-multiple::before{content:"\F1267"}.mdi-clipboard-multiple-outline::before{content:"\F1268"}.mdi-clipboard-off::before{content:"\F161A"}.mdi-clipboard-off-outline::before{content:"\F161B"}.mdi-clipboard-outline::before{content:"\F014C"}.mdi-clipboard-play::before{content:"\F0C59"}.mdi-clipboard-play-multiple::before{content:"\F1269"}.mdi-clipboard-play-multiple-outline::before{content:"\F126A"}.mdi-clipboard-play-outline::before{content:"\F0C5A"}.mdi-clipboard-plus::before{content:"\F0751"}.mdi-clipboard-plus-outline::before{content:"\F131F"}.mdi-clipboard-pulse::before{content:"\F085D"}.mdi-clipboard-pulse-outline::before{content:"\F085E"}.mdi-clipboard-remove::before{content:"\F161C"}.mdi-clipboard-remove-outline::before{content:"\F161D"}.mdi-clipboard-search::before{content:"\F161E"}.mdi-clipboard-search-outline::before{content:"\F161F"}.mdi-clipboard-text::before{content:"\F014D"}.mdi-clipboard-text-clock::before{content:"\F18F9"}.mdi-clipboard-text-clock-outline::before{content:"\F18FA"}.mdi-clipboard-text-multiple::before{content:"\F126B"}.mdi-clipboard-text-multiple-outline::before{content:"\F126C"}.mdi-clipboard-text-off::before{content:"\F1620"}.mdi-clipboard-text-off-outline::before{content:"\F1621"}.mdi-clipboard-text-outline::before{content:"\F0A38"}.mdi-clipboard-text-play::before{content:"\F0C5B"}.mdi-clipboard-text-play-outline::before{content:"\F0C5C"}.mdi-clipboard-text-search::before{content:"\F1622"}.mdi-clipboard-text-search-outline::before{content:"\F1623"}.mdi-clippy::before{content:"\F014F"}.mdi-clock::before{content:"\F0954"}.mdi-clock-alert::before{content:"\F0955"}.mdi-clock-alert-outline::before{content:"\F05CE"}.mdi-clock-check::before{content:"\F0FA8"}.mdi-clock-check-outline::before{content:"\F0FA9"}.mdi-clock-digital::before{content:"\F0E97"}.mdi-clock-edit::before{content:"\F19BA"}.mdi-clock-edit-outline::before{content:"\F19BB"}.mdi-clock-end::before{content:"\F0151"}.mdi-clock-fast::before{content:"\F0152"}.mdi-clock-in::before{content:"\F0153"}.mdi-clock-minus::before{content:"\F1863"}.mdi-clock-minus-outline::before{content:"\F1864"}.mdi-clock-out::before{content:"\F0154"}.mdi-clock-outline::before{content:"\F0150"}.mdi-clock-plus::before{content:"\F1861"}.mdi-clock-plus-outline::before{content:"\F1862"}.mdi-clock-remove::before{content:"\F1865"}.mdi-clock-remove-outline::before{content:"\F1866"}.mdi-clock-star-four-points::before{content:"\F1C29"}.mdi-clock-star-four-points-outline::before{content:"\F1C2A"}.mdi-clock-start::before{content:"\F0155"}.mdi-clock-time-eight::before{content:"\F1446"}.mdi-clock-time-eight-outline::before{content:"\F1452"}.mdi-clock-time-eleven::before{content:"\F1449"}.mdi-clock-time-eleven-outline::before{content:"\F1455"}.mdi-clock-time-five::before{content:"\F1443"}.mdi-clock-time-five-outline::before{content:"\F144F"}.mdi-clock-time-four::before{content:"\F1442"}.mdi-clock-time-four-outline::before{content:"\F144E"}.mdi-clock-time-nine::before{content:"\F1447"}.mdi-clock-time-nine-outline::before{content:"\F1453"}.mdi-clock-time-one::before{content:"\F143F"}.mdi-clock-time-one-outline::before{content:"\F144B"}.mdi-clock-time-seven::before{content:"\F1445"}.mdi-clock-time-seven-outline::before{content:"\F1451"}.mdi-clock-time-six::before{content:"\F1444"}.mdi-clock-time-six-outline::before{content:"\F1450"}.mdi-clock-time-ten::before{content:"\F1448"}.mdi-clock-time-ten-outline::before{content:"\F1454"}.mdi-clock-time-three::before{content:"\F1441"}.mdi-clock-time-three-outline::before{content:"\F144D"}.mdi-clock-time-twelve::before{content:"\F144A"}.mdi-clock-time-twelve-outline::before{content:"\F1456"}.mdi-clock-time-two::before{content:"\F1440"}.mdi-clock-time-two-outline::before{content:"\F144C"}.mdi-close::before{content:"\F0156"}.mdi-close-box::before{content:"\F0157"}.mdi-close-box-multiple::before{content:"\F0C5D"}.mdi-close-box-multiple-outline::before{content:"\F0C5E"}.mdi-close-box-outline::before{content:"\F0158"}.mdi-close-circle::before{content:"\F0159"}.mdi-close-circle-multiple::before{content:"\F062A"}.mdi-close-circle-multiple-outline::before{content:"\F0883"}.mdi-close-circle-outline::before{content:"\F015A"}.mdi-close-network::before{content:"\F015B"}.mdi-close-network-outline::before{content:"\F0C5F"}.mdi-close-octagon::before{content:"\F015C"}.mdi-close-octagon-outline::before{content:"\F015D"}.mdi-close-outline::before{content:"\F06C9"}.mdi-close-thick::before{content:"\F1398"}.mdi-closed-caption::before{content:"\F015E"}.mdi-closed-caption-outline::before{content:"\F0DBD"}.mdi-cloud::before{content:"\F015F"}.mdi-cloud-alert::before{content:"\F09E0"}.mdi-cloud-alert-outline::before{content:"\F1BE0"}.mdi-cloud-arrow-down::before{content:"\F1BE1"}.mdi-cloud-arrow-down-outline::before{content:"\F1BE2"}.mdi-cloud-arrow-left::before{content:"\F1BE3"}.mdi-cloud-arrow-left-outline::before{content:"\F1BE4"}.mdi-cloud-arrow-right::before{content:"\F1BE5"}.mdi-cloud-arrow-right-outline::before{content:"\F1BE6"}.mdi-cloud-arrow-up::before{content:"\F1BE7"}.mdi-cloud-arrow-up-outline::before{content:"\F1BE8"}.mdi-cloud-braces::before{content:"\F07B5"}.mdi-cloud-cancel::before{content:"\F1BE9"}.mdi-cloud-cancel-outline::before{content:"\F1BEA"}.mdi-cloud-check::before{content:"\F1BEB"}.mdi-cloud-check-outline::before{content:"\F1BEC"}.mdi-cloud-check-variant::before{content:"\F0160"}.mdi-cloud-check-variant-outline::before{content:"\F12CC"}.mdi-cloud-circle::before{content:"\F0161"}.mdi-cloud-circle-outline::before{content:"\F1BED"}.mdi-cloud-clock::before{content:"\F1BEE"}.mdi-cloud-clock-outline::before{content:"\F1BEF"}.mdi-cloud-cog::before{content:"\F1BF0"}.mdi-cloud-cog-outline::before{content:"\F1BF1"}.mdi-cloud-download::before{content:"\F0162"}.mdi-cloud-download-outline::before{content:"\F0B7D"}.mdi-cloud-key::before{content:"\F1CA1"}.mdi-cloud-key-outline::before{content:"\F1CA2"}.mdi-cloud-lock::before{content:"\F11F1"}.mdi-cloud-lock-open::before{content:"\F1BF2"}.mdi-cloud-lock-open-outline::before{content:"\F1BF3"}.mdi-cloud-lock-outline::before{content:"\F11F2"}.mdi-cloud-minus::before{content:"\F1BF4"}.mdi-cloud-minus-outline::before{content:"\F1BF5"}.mdi-cloud-off::before{content:"\F1BF6"}.mdi-cloud-off-outline::before{content:"\F0164"}.mdi-cloud-outline::before{content:"\F0163"}.mdi-cloud-percent::before{content:"\F1A35"}.mdi-cloud-percent-outline::before{content:"\F1A36"}.mdi-cloud-plus::before{content:"\F1BF7"}.mdi-cloud-plus-outline::before{content:"\F1BF8"}.mdi-cloud-print::before{content:"\F0165"}.mdi-cloud-print-outline::before{content:"\F0166"}.mdi-cloud-question::before{content:"\F0A39"}.mdi-cloud-question-outline::before{content:"\F1BF9"}.mdi-cloud-refresh::before{content:"\F1BFA"}.mdi-cloud-refresh-outline::before{content:"\F1BFB"}.mdi-cloud-refresh-variant::before{content:"\F052A"}.mdi-cloud-refresh-variant-outline::before{content:"\F1BFC"}.mdi-cloud-remove::before{content:"\F1BFD"}.mdi-cloud-remove-outline::before{content:"\F1BFE"}.mdi-cloud-search::before{content:"\F0956"}.mdi-cloud-search-outline::before{content:"\F0957"}.mdi-cloud-sync::before{content:"\F063F"}.mdi-cloud-sync-outline::before{content:"\F12D6"}.mdi-cloud-tags::before{content:"\F07B6"}.mdi-cloud-upload::before{content:"\F0167"}.mdi-cloud-upload-outline::before{content:"\F0B7E"}.mdi-clouds::before{content:"\F1B95"}.mdi-clover::before{content:"\F0816"}.mdi-clover-outline::before{content:"\F1C62"}.mdi-coach-lamp::before{content:"\F1020"}.mdi-coach-lamp-variant::before{content:"\F1A37"}.mdi-coat-rack::before{content:"\F109E"}.mdi-code-array::before{content:"\F0168"}.mdi-code-block-braces::before{content:"\F1C83"}.mdi-code-block-brackets::before{content:"\F1C84"}.mdi-code-block-parentheses::before{content:"\F1C85"}.mdi-code-block-tags::before{content:"\F1C86"}.mdi-code-braces::before{content:"\F0169"}.mdi-code-braces-box::before{content:"\F10D6"}.mdi-code-brackets::before{content:"\F016A"}.mdi-code-equal::before{content:"\F016B"}.mdi-code-greater-than::before{content:"\F016C"}.mdi-code-greater-than-or-equal::before{content:"\F016D"}.mdi-code-json::before{content:"\F0626"}.mdi-code-less-than::before{content:"\F016E"}.mdi-code-less-than-or-equal::before{content:"\F016F"}.mdi-code-not-equal::before{content:"\F0170"}.mdi-code-not-equal-variant::before{content:"\F0171"}.mdi-code-parentheses::before{content:"\F0172"}.mdi-code-parentheses-box::before{content:"\F10D7"}.mdi-code-string::before{content:"\F0173"}.mdi-code-tags::before{content:"\F0174"}.mdi-code-tags-check::before{content:"\F0694"}.mdi-codepen::before{content:"\F0175"}.mdi-coffee::before{content:"\F0176"}.mdi-coffee-maker::before{content:"\F109F"}.mdi-coffee-maker-check::before{content:"\F1931"}.mdi-coffee-maker-check-outline::before{content:"\F1932"}.mdi-coffee-maker-outline::before{content:"\F181B"}.mdi-coffee-off::before{content:"\F0FAA"}.mdi-coffee-off-outline::before{content:"\F0FAB"}.mdi-coffee-outline::before{content:"\F06CA"}.mdi-coffee-to-go::before{content:"\F0177"}.mdi-coffee-to-go-outline::before{content:"\F130E"}.mdi-coffin::before{content:"\F0B7F"}.mdi-cog::before{content:"\F0493"}.mdi-cog-box::before{content:"\F0494"}.mdi-cog-clockwise::before{content:"\F11DD"}.mdi-cog-counterclockwise::before{content:"\F11DE"}.mdi-cog-off::before{content:"\F13CE"}.mdi-cog-off-outline::before{content:"\F13CF"}.mdi-cog-outline::before{content:"\F08BB"}.mdi-cog-pause::before{content:"\F1933"}.mdi-cog-pause-outline::before{content:"\F1934"}.mdi-cog-play::before{content:"\F1935"}.mdi-cog-play-outline::before{content:"\F1936"}.mdi-cog-refresh::before{content:"\F145E"}.mdi-cog-refresh-outline::before{content:"\F145F"}.mdi-cog-stop::before{content:"\F1937"}.mdi-cog-stop-outline::before{content:"\F1938"}.mdi-cog-sync::before{content:"\F1460"}.mdi-cog-sync-outline::before{content:"\F1461"}.mdi-cog-transfer::before{content:"\F105B"}.mdi-cog-transfer-outline::before{content:"\F105C"}.mdi-cogs::before{content:"\F08D6"}.mdi-collage::before{content:"\F0640"}.mdi-collapse-all::before{content:"\F0AA6"}.mdi-collapse-all-outline::before{content:"\F0AA7"}.mdi-color-helper::before{content:"\F0179"}.mdi-comma::before{content:"\F0E23"}.mdi-comma-box::before{content:"\F0E2B"}.mdi-comma-box-outline::before{content:"\F0E24"}.mdi-comma-circle::before{content:"\F0E25"}.mdi-comma-circle-outline::before{content:"\F0E26"}.mdi-comment::before{content:"\F017A"}.mdi-comment-account::before{content:"\F017B"}.mdi-comment-account-outline::before{content:"\F017C"}.mdi-comment-alert::before{content:"\F017D"}.mdi-comment-alert-outline::before{content:"\F017E"}.mdi-comment-arrow-left::before{content:"\F09E1"}.mdi-comment-arrow-left-outline::before{content:"\F09E2"}.mdi-comment-arrow-right::before{content:"\F09E3"}.mdi-comment-arrow-right-outline::before{content:"\F09E4"}.mdi-comment-bookmark::before{content:"\F15AE"}.mdi-comment-bookmark-outline::before{content:"\F15AF"}.mdi-comment-check::before{content:"\F017F"}.mdi-comment-check-outline::before{content:"\F0180"}.mdi-comment-edit::before{content:"\F11BF"}.mdi-comment-edit-outline::before{content:"\F12C4"}.mdi-comment-eye::before{content:"\F0A3A"}.mdi-comment-eye-outline::before{content:"\F0A3B"}.mdi-comment-flash::before{content:"\F15B0"}.mdi-comment-flash-outline::before{content:"\F15B1"}.mdi-comment-minus::before{content:"\F15DF"}.mdi-comment-minus-outline::before{content:"\F15E0"}.mdi-comment-multiple::before{content:"\F085F"}.mdi-comment-multiple-outline::before{content:"\F0181"}.mdi-comment-off::before{content:"\F15E1"}.mdi-comment-off-outline::before{content:"\F15E2"}.mdi-comment-outline::before{content:"\F0182"}.mdi-comment-plus::before{content:"\F09E5"}.mdi-comment-plus-outline::before{content:"\F0183"}.mdi-comment-processing::before{content:"\F0184"}.mdi-comment-processing-outline::before{content:"\F0185"}.mdi-comment-question::before{content:"\F0817"}.mdi-comment-question-outline::before{content:"\F0186"}.mdi-comment-quote::before{content:"\F1021"}.mdi-comment-quote-outline::before{content:"\F1022"}.mdi-comment-remove::before{content:"\F05DE"}.mdi-comment-remove-outline::before{content:"\F0187"}.mdi-comment-search::before{content:"\F0A3C"}.mdi-comment-search-outline::before{content:"\F0A3D"}.mdi-comment-text::before{content:"\F0188"}.mdi-comment-text-multiple::before{content:"\F0860"}.mdi-comment-text-multiple-outline::before{content:"\F0861"}.mdi-comment-text-outline::before{content:"\F0189"}.mdi-compare::before{content:"\F018A"}.mdi-compare-horizontal::before{content:"\F1492"}.mdi-compare-remove::before{content:"\F18B3"}.mdi-compare-vertical::before{content:"\F1493"}.mdi-compass::before{content:"\F018B"}.mdi-compass-off::before{content:"\F0B80"}.mdi-compass-off-outline::before{content:"\F0B81"}.mdi-compass-outline::before{content:"\F018C"}.mdi-compass-rose::before{content:"\F1382"}.mdi-compost::before{content:"\F1A38"}.mdi-cone::before{content:"\F194C"}.mdi-cone-off::before{content:"\F194D"}.mdi-connection::before{content:"\F1616"}.mdi-console::before{content:"\F018D"}.mdi-console-line::before{content:"\F07B7"}.mdi-console-network::before{content:"\F08A9"}.mdi-console-network-outline::before{content:"\F0C60"}.mdi-consolidate::before{content:"\F10D8"}.mdi-contactless-payment::before{content:"\F0D6A"}.mdi-contactless-payment-circle::before{content:"\F0321"}.mdi-contactless-payment-circle-outline::before{content:"\F0408"}.mdi-contacts::before{content:"\F06CB"}.mdi-contacts-outline::before{content:"\F05B8"}.mdi-contain::before{content:"\F0A3E"}.mdi-contain-end::before{content:"\F0A3F"}.mdi-contain-start::before{content:"\F0A40"}.mdi-content-copy::before{content:"\F018F"}.mdi-content-cut::before{content:"\F0190"}.mdi-content-duplicate::before{content:"\F0191"}.mdi-content-paste::before{content:"\F0192"}.mdi-content-save::before{content:"\F0193"}.mdi-content-save-alert::before{content:"\F0F42"}.mdi-content-save-alert-outline::before{content:"\F0F43"}.mdi-content-save-all::before{content:"\F0194"}.mdi-content-save-all-outline::before{content:"\F0F44"}.mdi-content-save-check::before{content:"\F18EA"}.mdi-content-save-check-outline::before{content:"\F18EB"}.mdi-content-save-cog::before{content:"\F145B"}.mdi-content-save-cog-outline::before{content:"\F145C"}.mdi-content-save-edit::before{content:"\F0CFB"}.mdi-content-save-edit-outline::before{content:"\F0CFC"}.mdi-content-save-minus::before{content:"\F1B43"}.mdi-content-save-minus-outline::before{content:"\F1B44"}.mdi-content-save-move::before{content:"\F0E27"}.mdi-content-save-move-outline::before{content:"\F0E28"}.mdi-content-save-off::before{content:"\F1643"}.mdi-content-save-off-outline::before{content:"\F1644"}.mdi-content-save-outline::before{content:"\F0818"}.mdi-content-save-plus::before{content:"\F1B41"}.mdi-content-save-plus-outline::before{content:"\F1B42"}.mdi-content-save-settings::before{content:"\F061B"}.mdi-content-save-settings-outline::before{content:"\F0B2E"}.mdi-contrast::before{content:"\F0195"}.mdi-contrast-box::before{content:"\F0196"}.mdi-contrast-circle::before{content:"\F0197"}.mdi-controller::before{content:"\F02B4"}.mdi-controller-classic::before{content:"\F0B82"}.mdi-controller-classic-outline::before{content:"\F0B83"}.mdi-controller-off::before{content:"\F02B5"}.mdi-cookie::before{content:"\F0198"}.mdi-cookie-alert::before{content:"\F16D0"}.mdi-cookie-alert-outline::before{content:"\F16D1"}.mdi-cookie-check::before{content:"\F16D2"}.mdi-cookie-check-outline::before{content:"\F16D3"}.mdi-cookie-clock::before{content:"\F16E4"}.mdi-cookie-clock-outline::before{content:"\F16E5"}.mdi-cookie-cog::before{content:"\F16D4"}.mdi-cookie-cog-outline::before{content:"\F16D5"}.mdi-cookie-edit::before{content:"\F16E6"}.mdi-cookie-edit-outline::before{content:"\F16E7"}.mdi-cookie-lock::before{content:"\F16E8"}.mdi-cookie-lock-outline::before{content:"\F16E9"}.mdi-cookie-minus::before{content:"\F16DA"}.mdi-cookie-minus-outline::before{content:"\F16DB"}.mdi-cookie-off::before{content:"\F16EA"}.mdi-cookie-off-outline::before{content:"\F16EB"}.mdi-cookie-outline::before{content:"\F16DE"}.mdi-cookie-plus::before{content:"\F16D6"}.mdi-cookie-plus-outline::before{content:"\F16D7"}.mdi-cookie-refresh::before{content:"\F16EC"}.mdi-cookie-refresh-outline::before{content:"\F16ED"}.mdi-cookie-remove::before{content:"\F16D8"}.mdi-cookie-remove-outline::before{content:"\F16D9"}.mdi-cookie-settings::before{content:"\F16DC"}.mdi-cookie-settings-outline::before{content:"\F16DD"}.mdi-coolant-temperature::before{content:"\F03C8"}.mdi-copyleft::before{content:"\F1939"}.mdi-copyright::before{content:"\F05E6"}.mdi-cordova::before{content:"\F0958"}.mdi-corn::before{content:"\F07B8"}.mdi-corn-off::before{content:"\F13EF"}.mdi-cosine-wave::before{content:"\F1479"}.mdi-counter::before{content:"\F0199"}.mdi-countertop::before{content:"\F181C"}.mdi-countertop-outline::before{content:"\F181D"}.mdi-cow::before{content:"\F019A"}.mdi-cow-off::before{content:"\F18FC"}.mdi-cpu-32-bit::before{content:"\F0EDF"}.mdi-cpu-64-bit::before{content:"\F0EE0"}.mdi-cradle::before{content:"\F198B"}.mdi-cradle-outline::before{content:"\F1991"}.mdi-crane::before{content:"\F0862"}.mdi-creation::before{content:"\F0674"}.mdi-creation-outline::before{content:"\F1C2B"}.mdi-creative-commons::before{content:"\F0D6B"}.mdi-credit-card::before{content:"\F0FEF"}.mdi-credit-card-check::before{content:"\F13D0"}.mdi-credit-card-check-outline::before{content:"\F13D1"}.mdi-credit-card-chip::before{content:"\F190F"}.mdi-credit-card-chip-outline::before{content:"\F1910"}.mdi-credit-card-clock::before{content:"\F0EE1"}.mdi-credit-card-clock-outline::before{content:"\F0EE2"}.mdi-credit-card-edit::before{content:"\F17D7"}.mdi-credit-card-edit-outline::before{content:"\F17D8"}.mdi-credit-card-fast::before{content:"\F1911"}.mdi-credit-card-fast-outline::before{content:"\F1912"}.mdi-credit-card-lock::before{content:"\F18E7"}.mdi-credit-card-lock-outline::before{content:"\F18E8"}.mdi-credit-card-marker::before{content:"\F06A8"}.mdi-credit-card-marker-outline::before{content:"\F0DBE"}.mdi-credit-card-minus::before{content:"\F0FAC"}.mdi-credit-card-minus-outline::before{content:"\F0FAD"}.mdi-credit-card-multiple::before{content:"\F0FF0"}.mdi-credit-card-multiple-outline::before{content:"\F019C"}.mdi-credit-card-off::before{content:"\F0FF1"}.mdi-credit-card-off-outline::before{content:"\F05E4"}.mdi-credit-card-outline::before{content:"\F019B"}.mdi-credit-card-plus::before{content:"\F0FF2"}.mdi-credit-card-plus-outline::before{content:"\F0676"}.mdi-credit-card-refresh::before{content:"\F1645"}.mdi-credit-card-refresh-outline::before{content:"\F1646"}.mdi-credit-card-refund::before{content:"\F0FF3"}.mdi-credit-card-refund-outline::before{content:"\F0AA8"}.mdi-credit-card-remove::before{content:"\F0FAE"}.mdi-credit-card-remove-outline::before{content:"\F0FAF"}.mdi-credit-card-scan::before{content:"\F0FF4"}.mdi-credit-card-scan-outline::before{content:"\F019D"}.mdi-credit-card-search::before{content:"\F1647"}.mdi-credit-card-search-outline::before{content:"\F1648"}.mdi-credit-card-settings::before{content:"\F0FF5"}.mdi-credit-card-settings-outline::before{content:"\F08D7"}.mdi-credit-card-sync::before{content:"\F1649"}.mdi-credit-card-sync-outline::before{content:"\F164A"}.mdi-credit-card-wireless::before{content:"\F0802"}.mdi-credit-card-wireless-off::before{content:"\F057A"}.mdi-credit-card-wireless-off-outline::before{content:"\F057B"}.mdi-credit-card-wireless-outline::before{content:"\F0D6C"}.mdi-cricket::before{content:"\F0D6D"}.mdi-crop::before{content:"\F019E"}.mdi-crop-free::before{content:"\F019F"}.mdi-crop-landscape::before{content:"\F01A0"}.mdi-crop-portrait::before{content:"\F01A1"}.mdi-crop-rotate::before{content:"\F0696"}.mdi-crop-square::before{content:"\F01A2"}.mdi-cross::before{content:"\F0953"}.mdi-cross-bolnisi::before{content:"\F0CED"}.mdi-cross-celtic::before{content:"\F0CF5"}.mdi-cross-outline::before{content:"\F0CF6"}.mdi-crosshairs::before{content:"\F01A3"}.mdi-crosshairs-gps::before{content:"\F01A4"}.mdi-crosshairs-off::before{content:"\F0F45"}.mdi-crosshairs-question::before{content:"\F1136"}.mdi-crowd::before{content:"\F1975"}.mdi-crown::before{content:"\F01A5"}.mdi-crown-circle::before{content:"\F17DC"}.mdi-crown-circle-outline::before{content:"\F17DD"}.mdi-crown-outline::before{content:"\F11D0"}.mdi-cryengine::before{content:"\F0959"}.mdi-crystal-ball::before{content:"\F0B2F"}.mdi-cube::before{content:"\F01A6"}.mdi-cube-off::before{content:"\F141C"}.mdi-cube-off-outline::before{content:"\F141D"}.mdi-cube-outline::before{content:"\F01A7"}.mdi-cube-scan::before{content:"\F0B84"}.mdi-cube-send::before{content:"\F01A8"}.mdi-cube-unfolded::before{content:"\F01A9"}.mdi-cup::before{content:"\F01AA"}.mdi-cup-off::before{content:"\F05E5"}.mdi-cup-off-outline::before{content:"\F137D"}.mdi-cup-outline::before{content:"\F130F"}.mdi-cup-water::before{content:"\F01AB"}.mdi-cupboard::before{content:"\F0F46"}.mdi-cupboard-outline::before{content:"\F0F47"}.mdi-cupcake::before{content:"\F095A"}.mdi-curling::before{content:"\F0863"}.mdi-currency-bdt::before{content:"\F0864"}.mdi-currency-brl::before{content:"\F0B85"}.mdi-currency-btc::before{content:"\F01AC"}.mdi-currency-cny::before{content:"\F07BA"}.mdi-currency-eth::before{content:"\F07BB"}.mdi-currency-eur::before{content:"\F01AD"}.mdi-currency-eur-off::before{content:"\F1315"}.mdi-currency-fra::before{content:"\F1A39"}.mdi-currency-gbp::before{content:"\F01AE"}.mdi-currency-ils::before{content:"\F0C61"}.mdi-currency-inr::before{content:"\F01AF"}.mdi-currency-jpy::before{content:"\F07BC"}.mdi-currency-krw::before{content:"\F07BD"}.mdi-currency-kzt::before{content:"\F0865"}.mdi-currency-mnt::before{content:"\F1512"}.mdi-currency-ngn::before{content:"\F01B0"}.mdi-currency-php::before{content:"\F09E6"}.mdi-currency-rial::before{content:"\F0E9C"}.mdi-currency-rub::before{content:"\F01B1"}.mdi-currency-rupee::before{content:"\F1976"}.mdi-currency-sign::before{content:"\F07BE"}.mdi-currency-thb::before{content:"\F1C05"}.mdi-currency-try::before{content:"\F01B2"}.mdi-currency-twd::before{content:"\F07BF"}.mdi-currency-uah::before{content:"\F1B9B"}.mdi-currency-usd::before{content:"\F01C1"}.mdi-currency-usd-off::before{content:"\F067A"}.mdi-current-ac::before{content:"\F1480"}.mdi-current-dc::before{content:"\F095C"}.mdi-cursor-default::before{content:"\F01C0"}.mdi-cursor-default-click::before{content:"\F0CFD"}.mdi-cursor-default-click-outline::before{content:"\F0CFE"}.mdi-cursor-default-gesture::before{content:"\F1127"}.mdi-cursor-default-gesture-outline::before{content:"\F1128"}.mdi-cursor-default-outline::before{content:"\F01BF"}.mdi-cursor-move::before{content:"\F01BE"}.mdi-cursor-pointer::before{content:"\F01BD"}.mdi-cursor-text::before{content:"\F05E7"}.mdi-curtains::before{content:"\F1846"}.mdi-curtains-closed::before{content:"\F1847"}.mdi-cylinder::before{content:"\F194E"}.mdi-cylinder-off::before{content:"\F194F"}.mdi-dance-ballroom::before{content:"\F15FB"}.mdi-dance-pole::before{content:"\F1578"}.mdi-data-matrix::before{content:"\F153C"}.mdi-data-matrix-edit::before{content:"\F153D"}.mdi-data-matrix-minus::before{content:"\F153E"}.mdi-data-matrix-plus::before{content:"\F153F"}.mdi-data-matrix-remove::before{content:"\F1540"}.mdi-data-matrix-scan::before{content:"\F1541"}.mdi-database::before{content:"\F01BC"}.mdi-database-alert::before{content:"\F163A"}.mdi-database-alert-outline::before{content:"\F1624"}.mdi-database-arrow-down::before{content:"\F163B"}.mdi-database-arrow-down-outline::before{content:"\F1625"}.mdi-database-arrow-left::before{content:"\F163C"}.mdi-database-arrow-left-outline::before{content:"\F1626"}.mdi-database-arrow-right::before{content:"\F163D"}.mdi-database-arrow-right-outline::before{content:"\F1627"}.mdi-database-arrow-up::before{content:"\F163E"}.mdi-database-arrow-up-outline::before{content:"\F1628"}.mdi-database-check::before{content:"\F0AA9"}.mdi-database-check-outline::before{content:"\F1629"}.mdi-database-clock::before{content:"\F163F"}.mdi-database-clock-outline::before{content:"\F162A"}.mdi-database-cog::before{content:"\F164B"}.mdi-database-cog-outline::before{content:"\F164C"}.mdi-database-edit::before{content:"\F0B86"}.mdi-database-edit-outline::before{content:"\F162B"}.mdi-database-export::before{content:"\F095E"}.mdi-database-export-outline::before{content:"\F162C"}.mdi-database-eye::before{content:"\F191F"}.mdi-database-eye-off::before{content:"\F1920"}.mdi-database-eye-off-outline::before{content:"\F1921"}.mdi-database-eye-outline::before{content:"\F1922"}.mdi-database-import::before{content:"\F095D"}.mdi-database-import-outline::before{content:"\F162D"}.mdi-database-lock::before{content:"\F0AAA"}.mdi-database-lock-outline::before{content:"\F162E"}.mdi-database-marker::before{content:"\F12F6"}.mdi-database-marker-outline::before{content:"\F162F"}.mdi-database-minus::before{content:"\F01BB"}.mdi-database-minus-outline::before{content:"\F1630"}.mdi-database-off::before{content:"\F1640"}.mdi-database-off-outline::before{content:"\F1631"}.mdi-database-outline::before{content:"\F1632"}.mdi-database-plus::before{content:"\F01BA"}.mdi-database-plus-outline::before{content:"\F1633"}.mdi-database-refresh::before{content:"\F05C2"}.mdi-database-refresh-outline::before{content:"\F1634"}.mdi-database-remove::before{content:"\F0D00"}.mdi-database-remove-outline::before{content:"\F1635"}.mdi-database-search::before{content:"\F0866"}.mdi-database-search-outline::before{content:"\F1636"}.mdi-database-settings::before{content:"\F0D01"}.mdi-database-settings-outline::before{content:"\F1637"}.mdi-database-sync::before{content:"\F0CFF"}.mdi-database-sync-outline::before{content:"\F1638"}.mdi-death-star::before{content:"\F08D8"}.mdi-death-star-variant::before{content:"\F08D9"}.mdi-deathly-hallows::before{content:"\F0B87"}.mdi-debian::before{content:"\F08DA"}.mdi-debug-step-into::before{content:"\F01B9"}.mdi-debug-step-out::before{content:"\F01B8"}.mdi-debug-step-over::before{content:"\F01B7"}.mdi-decagram::before{content:"\F076C"}.mdi-decagram-outline::before{content:"\F076D"}.mdi-decimal::before{content:"\F10A1"}.mdi-decimal-comma::before{content:"\F10A2"}.mdi-decimal-comma-decrease::before{content:"\F10A3"}.mdi-decimal-comma-increase::before{content:"\F10A4"}.mdi-decimal-decrease::before{content:"\F01B6"}.mdi-decimal-increase::before{content:"\F01B5"}.mdi-delete::before{content:"\F01B4"}.mdi-delete-alert::before{content:"\F10A5"}.mdi-delete-alert-outline::before{content:"\F10A6"}.mdi-delete-circle::before{content:"\F0683"}.mdi-delete-circle-outline::before{content:"\F0B88"}.mdi-delete-clock::before{content:"\F1556"}.mdi-delete-clock-outline::before{content:"\F1557"}.mdi-delete-empty::before{content:"\F06CC"}.mdi-delete-empty-outline::before{content:"\F0E9D"}.mdi-delete-forever::before{content:"\F05E8"}.mdi-delete-forever-outline::before{content:"\F0B89"}.mdi-delete-off::before{content:"\F10A7"}.mdi-delete-off-outline::before{content:"\F10A8"}.mdi-delete-outline::before{content:"\F09E7"}.mdi-delete-restore::before{content:"\F0819"}.mdi-delete-sweep::before{content:"\F05E9"}.mdi-delete-sweep-outline::before{content:"\F0C62"}.mdi-delete-variant::before{content:"\F01B3"}.mdi-delta::before{content:"\F01C2"}.mdi-desk::before{content:"\F1239"}.mdi-desk-lamp::before{content:"\F095F"}.mdi-desk-lamp-off::before{content:"\F1B1F"}.mdi-desk-lamp-on::before{content:"\F1B20"}.mdi-deskphone::before{content:"\F01C3"}.mdi-desktop-classic::before{content:"\F07C0"}.mdi-desktop-tower::before{content:"\F01C5"}.mdi-desktop-tower-monitor::before{content:"\F0AAB"}.mdi-details::before{content:"\F01C6"}.mdi-dev-to::before{content:"\F0D6E"}.mdi-developer-board::before{content:"\F0697"}.mdi-deviantart::before{content:"\F01C7"}.mdi-devices::before{content:"\F0FB0"}.mdi-dharmachakra::before{content:"\F094B"}.mdi-diabetes::before{content:"\F1126"}.mdi-dialpad::before{content:"\F061C"}.mdi-diameter::before{content:"\F0C63"}.mdi-diameter-outline::before{content:"\F0C64"}.mdi-diameter-variant::before{content:"\F0C65"}.mdi-diamond::before{content:"\F0B8A"}.mdi-diamond-outline::before{content:"\F0B8B"}.mdi-diamond-stone::before{content:"\F01C8"}.mdi-diaper-outline::before{content:"\F1CCF"}.mdi-dice-1::before{content:"\F01CA"}.mdi-dice-1-outline::before{content:"\F114A"}.mdi-dice-2::before{content:"\F01CB"}.mdi-dice-2-outline::before{content:"\F114B"}.mdi-dice-3::before{content:"\F01CC"}.mdi-dice-3-outline::before{content:"\F114C"}.mdi-dice-4::before{content:"\F01CD"}.mdi-dice-4-outline::before{content:"\F114D"}.mdi-dice-5::before{content:"\F01CE"}.mdi-dice-5-outline::before{content:"\F114E"}.mdi-dice-6::before{content:"\F01CF"}.mdi-dice-6-outline::before{content:"\F114F"}.mdi-dice-d10::before{content:"\F1153"}.mdi-dice-d10-outline::before{content:"\F076F"}.mdi-dice-d12::before{content:"\F1154"}.mdi-dice-d12-outline::before{content:"\F0867"}.mdi-dice-d20::before{content:"\F1155"}.mdi-dice-d20-outline::before{content:"\F05EA"}.mdi-dice-d4::before{content:"\F1150"}.mdi-dice-d4-outline::before{content:"\F05EB"}.mdi-dice-d6::before{content:"\F1151"}.mdi-dice-d6-outline::before{content:"\F05ED"}.mdi-dice-d8::before{content:"\F1152"}.mdi-dice-d8-outline::before{content:"\F05EC"}.mdi-dice-multiple::before{content:"\F076E"}.mdi-dice-multiple-outline::before{content:"\F1156"}.mdi-digital-ocean::before{content:"\F1237"}.mdi-dip-switch::before{content:"\F07C1"}.mdi-directions::before{content:"\F01D0"}.mdi-directions-fork::before{content:"\F0641"}.mdi-disc::before{content:"\F05EE"}.mdi-disc-alert::before{content:"\F01D1"}.mdi-disc-player::before{content:"\F0960"}.mdi-dishwasher::before{content:"\F0AAC"}.mdi-dishwasher-alert::before{content:"\F11B8"}.mdi-dishwasher-off::before{content:"\F11B9"}.mdi-disqus::before{content:"\F01D2"}.mdi-distribute-horizontal-center::before{content:"\F11C9"}.mdi-distribute-horizontal-left::before{content:"\F11C8"}.mdi-distribute-horizontal-right::before{content:"\F11CA"}.mdi-distribute-vertical-bottom::before{content:"\F11CB"}.mdi-distribute-vertical-center::before{content:"\F11CC"}.mdi-distribute-vertical-top::before{content:"\F11CD"}.mdi-diversify::before{content:"\F1877"}.mdi-diving::before{content:"\F1977"}.mdi-diving-flippers::before{content:"\F0DBF"}.mdi-diving-helmet::before{content:"\F0DC0"}.mdi-diving-scuba::before{content:"\F1B77"}.mdi-diving-scuba-flag::before{content:"\F0DC2"}.mdi-diving-scuba-mask::before{content:"\F0DC1"}.mdi-diving-scuba-tank::before{content:"\F0DC3"}.mdi-diving-scuba-tank-multiple::before{content:"\F0DC4"}.mdi-diving-snorkel::before{content:"\F0DC5"}.mdi-division::before{content:"\F01D4"}.mdi-division-box::before{content:"\F01D5"}.mdi-dlna::before{content:"\F0A41"}.mdi-dna::before{content:"\F0684"}.mdi-dns::before{content:"\F01D6"}.mdi-dns-outline::before{content:"\F0B8C"}.mdi-dock-bottom::before{content:"\F10A9"}.mdi-dock-left::before{content:"\F10AA"}.mdi-dock-right::before{content:"\F10AB"}.mdi-dock-top::before{content:"\F1513"}.mdi-dock-window::before{content:"\F10AC"}.mdi-docker::before{content:"\F0868"}.mdi-doctor::before{content:"\F0A42"}.mdi-dog::before{content:"\F0A43"}.mdi-dog-service::before{content:"\F0AAD"}.mdi-dog-side::before{content:"\F0A44"}.mdi-dog-side-off::before{content:"\F16EE"}.mdi-dolby::before{content:"\F06B3"}.mdi-dolly::before{content:"\F0E9E"}.mdi-dolphin::before{content:"\F18B4"}.mdi-domain::before{content:"\F01D7"}.mdi-domain-off::before{content:"\F0D6F"}.mdi-domain-plus::before{content:"\F10AD"}.mdi-domain-remove::before{content:"\F10AE"}.mdi-domain-switch::before{content:"\F1C2C"}.mdi-dome-light::before{content:"\F141E"}.mdi-domino-mask::before{content:"\F1023"}.mdi-donkey::before{content:"\F07C2"}.mdi-door::before{content:"\F081A"}.mdi-door-closed::before{content:"\F081B"}.mdi-door-closed-cancel::before{content:"\F1C93"}.mdi-door-closed-lock::before{content:"\F10AF"}.mdi-door-open::before{content:"\F081C"}.mdi-door-sliding::before{content:"\F181E"}.mdi-door-sliding-lock::before{content:"\F181F"}.mdi-door-sliding-open::before{content:"\F1820"}.mdi-doorbell::before{content:"\F12E6"}.mdi-doorbell-video::before{content:"\F0869"}.mdi-dot-net::before{content:"\F0AAE"}.mdi-dots-circle::before{content:"\F1978"}.mdi-dots-grid::before{content:"\F15FC"}.mdi-dots-hexagon::before{content:"\F15FF"}.mdi-dots-horizontal::before{content:"\F01D8"}.mdi-dots-horizontal-circle::before{content:"\F07C3"}.mdi-dots-horizontal-circle-outline::before{content:"\F0B8D"}.mdi-dots-square::before{content:"\F15FD"}.mdi-dots-triangle::before{content:"\F15FE"}.mdi-dots-vertical::before{content:"\F01D9"}.mdi-dots-vertical-circle::before{content:"\F07C4"}.mdi-dots-vertical-circle-outline::before{content:"\F0B8E"}.mdi-download::before{content:"\F01DA"}.mdi-download-box::before{content:"\F1462"}.mdi-download-box-outline::before{content:"\F1463"}.mdi-download-circle::before{content:"\F1464"}.mdi-download-circle-outline::before{content:"\F1465"}.mdi-download-lock::before{content:"\F1320"}.mdi-download-lock-outline::before{content:"\F1321"}.mdi-download-multiple::before{content:"\F09E9"}.mdi-download-multiple-outline::before{content:"\F1CD0"}.mdi-download-network::before{content:"\F06F4"}.mdi-download-network-outline::before{content:"\F0C66"}.mdi-download-off::before{content:"\F10B0"}.mdi-download-off-outline::before{content:"\F10B1"}.mdi-download-outline::before{content:"\F0B8F"}.mdi-drag::before{content:"\F01DB"}.mdi-drag-horizontal::before{content:"\F01DC"}.mdi-drag-horizontal-variant::before{content:"\F12F0"}.mdi-drag-variant::before{content:"\F0B90"}.mdi-drag-vertical::before{content:"\F01DD"}.mdi-drag-vertical-variant::before{content:"\F12F1"}.mdi-drama-masks::before{content:"\F0D02"}.mdi-draw::before{content:"\F0F49"}.mdi-draw-pen::before{content:"\F19B9"}.mdi-drawing::before{content:"\F01DE"}.mdi-drawing-box::before{content:"\F01DF"}.mdi-dresser::before{content:"\F0F4A"}.mdi-dresser-outline::before{content:"\F0F4B"}.mdi-drone::before{content:"\F01E2"}.mdi-dropbox::before{content:"\F01E3"}.mdi-drupal::before{content:"\F01E4"}.mdi-duck::before{content:"\F01E5"}.mdi-dumbbell::before{content:"\F01E6"}.mdi-dump-truck::before{content:"\F0C67"}.mdi-ear-hearing::before{content:"\F07C5"}.mdi-ear-hearing-loop::before{content:"\F1AEE"}.mdi-ear-hearing-off::before{content:"\F0A45"}.mdi-earbuds::before{content:"\F184F"}.mdi-earbuds-off::before{content:"\F1850"}.mdi-earbuds-off-outline::before{content:"\F1851"}.mdi-earbuds-outline::before{content:"\F1852"}.mdi-earth::before{content:"\F01E7"}.mdi-earth-arrow-down::before{content:"\F1C87"}.mdi-earth-arrow-left::before{content:"\F1C88"}.mdi-earth-arrow-right::before{content:"\F1311"}.mdi-earth-arrow-up::before{content:"\F1C89"}.mdi-earth-box::before{content:"\F06CD"}.mdi-earth-box-minus::before{content:"\F1407"}.mdi-earth-box-off::before{content:"\F06CE"}.mdi-earth-box-plus::before{content:"\F1406"}.mdi-earth-box-remove::before{content:"\F1408"}.mdi-earth-minus::before{content:"\F1404"}.mdi-earth-off::before{content:"\F01E8"}.mdi-earth-plus::before{content:"\F1403"}.mdi-earth-remove::before{content:"\F1405"}.mdi-egg::before{content:"\F0AAF"}.mdi-egg-easter::before{content:"\F0AB0"}.mdi-egg-fried::before{content:"\F184A"}.mdi-egg-off::before{content:"\F13F0"}.mdi-egg-off-outline::before{content:"\F13F1"}.mdi-egg-outline::before{content:"\F13F2"}.mdi-eiffel-tower::before{content:"\F156B"}.mdi-eight-track::before{content:"\F09EA"}.mdi-eject::before{content:"\F01EA"}.mdi-eject-circle::before{content:"\F1B23"}.mdi-eject-circle-outline::before{content:"\F1B24"}.mdi-eject-outline::before{content:"\F0B91"}.mdi-electric-switch::before{content:"\F0E9F"}.mdi-electric-switch-closed::before{content:"\F10D9"}.mdi-electron-framework::before{content:"\F1024"}.mdi-elephant::before{content:"\F07C6"}.mdi-elevation-decline::before{content:"\F01EB"}.mdi-elevation-rise::before{content:"\F01EC"}.mdi-elevator::before{content:"\F01ED"}.mdi-elevator-down::before{content:"\F12C2"}.mdi-elevator-passenger::before{content:"\F1381"}.mdi-elevator-passenger-off::before{content:"\F1979"}.mdi-elevator-passenger-off-outline::before{content:"\F197A"}.mdi-elevator-passenger-outline::before{content:"\F197B"}.mdi-elevator-up::before{content:"\F12C1"}.mdi-ellipse::before{content:"\F0EA0"}.mdi-ellipse-outline::before{content:"\F0EA1"}.mdi-email::before{content:"\F01EE"}.mdi-email-alert::before{content:"\F06CF"}.mdi-email-alert-outline::before{content:"\F0D42"}.mdi-email-arrow-left::before{content:"\F10DA"}.mdi-email-arrow-left-outline::before{content:"\F10DB"}.mdi-email-arrow-right::before{content:"\F10DC"}.mdi-email-arrow-right-outline::before{content:"\F10DD"}.mdi-email-box::before{content:"\F0D03"}.mdi-email-check::before{content:"\F0AB1"}.mdi-email-check-outline::before{content:"\F0AB2"}.mdi-email-edit::before{content:"\F0EE3"}.mdi-email-edit-outline::before{content:"\F0EE4"}.mdi-email-fast::before{content:"\F186F"}.mdi-email-fast-outline::before{content:"\F1870"}.mdi-email-heart-outline::before{content:"\F1C5B"}.mdi-email-lock::before{content:"\F01F1"}.mdi-email-lock-outline::before{content:"\F1B61"}.mdi-email-mark-as-unread::before{content:"\F0B92"}.mdi-email-minus::before{content:"\F0EE5"}.mdi-email-minus-outline::before{content:"\F0EE6"}.mdi-email-multiple::before{content:"\F0EE7"}.mdi-email-multiple-outline::before{content:"\F0EE8"}.mdi-email-newsletter::before{content:"\F0FB1"}.mdi-email-off::before{content:"\F13E3"}.mdi-email-off-outline::before{content:"\F13E4"}.mdi-email-open::before{content:"\F01EF"}.mdi-email-open-heart-outline::before{content:"\F1C5C"}.mdi-email-open-multiple::before{content:"\F0EE9"}.mdi-email-open-multiple-outline::before{content:"\F0EEA"}.mdi-email-open-outline::before{content:"\F05EF"}.mdi-email-outline::before{content:"\F01F0"}.mdi-email-plus::before{content:"\F09EB"}.mdi-email-plus-outline::before{content:"\F09EC"}.mdi-email-remove::before{content:"\F1661"}.mdi-email-remove-outline::before{content:"\F1662"}.mdi-email-seal::before{content:"\F195B"}.mdi-email-seal-outline::before{content:"\F195C"}.mdi-email-search::before{content:"\F0961"}.mdi-email-search-outline::before{content:"\F0962"}.mdi-email-sync::before{content:"\F12C7"}.mdi-email-sync-outline::before{content:"\F12C8"}.mdi-email-variant::before{content:"\F05F0"}.mdi-ember::before{content:"\F0B30"}.mdi-emby::before{content:"\F06B4"}.mdi-emoticon::before{content:"\F0C68"}.mdi-emoticon-angry::before{content:"\F0C69"}.mdi-emoticon-angry-outline::before{content:"\F0C6A"}.mdi-emoticon-confused::before{content:"\F10DE"}.mdi-emoticon-confused-outline::before{content:"\F10DF"}.mdi-emoticon-cool::before{content:"\F0C6B"}.mdi-emoticon-cool-outline::before{content:"\F01F3"}.mdi-emoticon-cry::before{content:"\F0C6C"}.mdi-emoticon-cry-outline::before{content:"\F0C6D"}.mdi-emoticon-dead::before{content:"\F0C6E"}.mdi-emoticon-dead-outline::before{content:"\F069B"}.mdi-emoticon-devil::before{content:"\F0C6F"}.mdi-emoticon-devil-outline::before{content:"\F01F4"}.mdi-emoticon-excited::before{content:"\F0C70"}.mdi-emoticon-excited-outline::before{content:"\F069C"}.mdi-emoticon-frown::before{content:"\F0F4C"}.mdi-emoticon-frown-outline::before{content:"\F0F4D"}.mdi-emoticon-happy::before{content:"\F0C71"}.mdi-emoticon-happy-outline::before{content:"\F01F5"}.mdi-emoticon-kiss::before{content:"\F0C72"}.mdi-emoticon-kiss-outline::before{content:"\F0C73"}.mdi-emoticon-lol::before{content:"\F1214"}.mdi-emoticon-lol-outline::before{content:"\F1215"}.mdi-emoticon-minus::before{content:"\F1CB2"}.mdi-emoticon-minus-outline::before{content:"\F1CB3"}.mdi-emoticon-neutral::before{content:"\F0C74"}.mdi-emoticon-neutral-outline::before{content:"\F01F6"}.mdi-emoticon-outline::before{content:"\F01F2"}.mdi-emoticon-plus::before{content:"\F1CB4"}.mdi-emoticon-plus-outline::before{content:"\F1CB5"}.mdi-emoticon-poop::before{content:"\F01F7"}.mdi-emoticon-poop-outline::before{content:"\F0C75"}.mdi-emoticon-remove::before{content:"\F1CB6"}.mdi-emoticon-remove-outline::before{content:"\F1CB7"}.mdi-emoticon-sad::before{content:"\F0C76"}.mdi-emoticon-sad-outline::before{content:"\F01F8"}.mdi-emoticon-sick::before{content:"\F157C"}.mdi-emoticon-sick-outline::before{content:"\F157D"}.mdi-emoticon-tongue::before{content:"\F01F9"}.mdi-emoticon-tongue-outline::before{content:"\F0C77"}.mdi-emoticon-wink::before{content:"\F0C78"}.mdi-emoticon-wink-outline::before{content:"\F0C79"}.mdi-engine::before{content:"\F01FA"}.mdi-engine-off::before{content:"\F0A46"}.mdi-engine-off-outline::before{content:"\F0A47"}.mdi-engine-outline::before{content:"\F01FB"}.mdi-epsilon::before{content:"\F10E0"}.mdi-equal::before{content:"\F01FC"}.mdi-equal-box::before{content:"\F01FD"}.mdi-equalizer::before{content:"\F0EA2"}.mdi-equalizer-outline::before{content:"\F0EA3"}.mdi-eraser::before{content:"\F01FE"}.mdi-eraser-variant::before{content:"\F0642"}.mdi-escalator::before{content:"\F01FF"}.mdi-escalator-box::before{content:"\F1399"}.mdi-escalator-down::before{content:"\F12C0"}.mdi-escalator-up::before{content:"\F12BF"}.mdi-eslint::before{content:"\F0C7A"}.mdi-et::before{content:"\F0AB3"}.mdi-ethereum::before{content:"\F086A"}.mdi-ethernet::before{content:"\F0200"}.mdi-ethernet-cable::before{content:"\F0201"}.mdi-ethernet-cable-off::before{content:"\F0202"}.mdi-ethernet-off::before{content:"\F1CD1"}.mdi-ev-plug-ccs1::before{content:"\F1519"}.mdi-ev-plug-ccs2::before{content:"\F151A"}.mdi-ev-plug-chademo::before{content:"\F151B"}.mdi-ev-plug-tesla::before{content:"\F151C"}.mdi-ev-plug-type1::before{content:"\F151D"}.mdi-ev-plug-type2::before{content:"\F151E"}.mdi-ev-station::before{content:"\F05F1"}.mdi-evernote::before{content:"\F0204"}.mdi-excavator::before{content:"\F1025"}.mdi-exclamation::before{content:"\F0205"}.mdi-exclamation-thick::before{content:"\F1238"}.mdi-exit-run::before{content:"\F0A48"}.mdi-exit-to-app::before{content:"\F0206"}.mdi-expand-all::before{content:"\F0AB4"}.mdi-expand-all-outline::before{content:"\F0AB5"}.mdi-expansion-card::before{content:"\F08AE"}.mdi-expansion-card-variant::before{content:"\F0FB2"}.mdi-exponent::before{content:"\F0963"}.mdi-exponent-box::before{content:"\F0964"}.mdi-export::before{content:"\F0207"}.mdi-export-variant::before{content:"\F0B93"}.mdi-eye::before{content:"\F0208"}.mdi-eye-arrow-left::before{content:"\F18FD"}.mdi-eye-arrow-left-outline::before{content:"\F18FE"}.mdi-eye-arrow-right::before{content:"\F18FF"}.mdi-eye-arrow-right-outline::before{content:"\F1900"}.mdi-eye-check::before{content:"\F0D04"}.mdi-eye-check-outline::before{content:"\F0D05"}.mdi-eye-circle::before{content:"\F0B94"}.mdi-eye-circle-outline::before{content:"\F0B95"}.mdi-eye-closed::before{content:"\F1CA3"}.mdi-eye-lock::before{content:"\F1C06"}.mdi-eye-lock-open::before{content:"\F1C07"}.mdi-eye-lock-open-outline::before{content:"\F1C08"}.mdi-eye-lock-outline::before{content:"\F1C09"}.mdi-eye-minus::before{content:"\F1026"}.mdi-eye-minus-outline::before{content:"\F1027"}.mdi-eye-off::before{content:"\F0209"}.mdi-eye-off-outline::before{content:"\F06D1"}.mdi-eye-outline::before{content:"\F06D0"}.mdi-eye-plus::before{content:"\F086B"}.mdi-eye-plus-outline::before{content:"\F086C"}.mdi-eye-refresh::before{content:"\F197C"}.mdi-eye-refresh-outline::before{content:"\F197D"}.mdi-eye-remove::before{content:"\F15E3"}.mdi-eye-remove-outline::before{content:"\F15E4"}.mdi-eye-settings::before{content:"\F086D"}.mdi-eye-settings-outline::before{content:"\F086E"}.mdi-eyedropper::before{content:"\F020A"}.mdi-eyedropper-minus::before{content:"\F13DD"}.mdi-eyedropper-off::before{content:"\F13DF"}.mdi-eyedropper-plus::before{content:"\F13DC"}.mdi-eyedropper-remove::before{content:"\F13DE"}.mdi-eyedropper-variant::before{content:"\F020B"}.mdi-face-agent::before{content:"\F0D70"}.mdi-face-man::before{content:"\F0643"}.mdi-face-man-outline::before{content:"\F0B96"}.mdi-face-man-profile::before{content:"\F0644"}.mdi-face-man-shimmer::before{content:"\F15CC"}.mdi-face-man-shimmer-outline::before{content:"\F15CD"}.mdi-face-mask::before{content:"\F1586"}.mdi-face-mask-outline::before{content:"\F1587"}.mdi-face-recognition::before{content:"\F0C7B"}.mdi-face-woman::before{content:"\F1077"}.mdi-face-woman-outline::before{content:"\F1078"}.mdi-face-woman-profile::before{content:"\F1076"}.mdi-face-woman-shimmer::before{content:"\F15CE"}.mdi-face-woman-shimmer-outline::before{content:"\F15CF"}.mdi-facebook::before{content:"\F020C"}.mdi-facebook-gaming::before{content:"\F07DD"}.mdi-facebook-messenger::before{content:"\F020E"}.mdi-facebook-workplace::before{content:"\F0B31"}.mdi-factory::before{content:"\F020F"}.mdi-family-tree::before{content:"\F160E"}.mdi-fan::before{content:"\F0210"}.mdi-fan-alert::before{content:"\F146C"}.mdi-fan-auto::before{content:"\F171D"}.mdi-fan-chevron-down::before{content:"\F146D"}.mdi-fan-chevron-up::before{content:"\F146E"}.mdi-fan-clock::before{content:"\F1A3A"}.mdi-fan-minus::before{content:"\F1470"}.mdi-fan-off::before{content:"\F081D"}.mdi-fan-plus::before{content:"\F146F"}.mdi-fan-remove::before{content:"\F1471"}.mdi-fan-speed-1::before{content:"\F1472"}.mdi-fan-speed-2::before{content:"\F1473"}.mdi-fan-speed-3::before{content:"\F1474"}.mdi-fast-forward::before{content:"\F0211"}.mdi-fast-forward-10::before{content:"\F0D71"}.mdi-fast-forward-15::before{content:"\F193A"}.mdi-fast-forward-30::before{content:"\F0D06"}.mdi-fast-forward-45::before{content:"\F1B12"}.mdi-fast-forward-5::before{content:"\F11F8"}.mdi-fast-forward-60::before{content:"\F160B"}.mdi-fast-forward-outline::before{content:"\F06D2"}.mdi-faucet::before{content:"\F1B29"}.mdi-faucet-variant::before{content:"\F1B2A"}.mdi-fax::before{content:"\F0212"}.mdi-feather::before{content:"\F06D3"}.mdi-feature-search::before{content:"\F0A49"}.mdi-feature-search-outline::before{content:"\F0A4A"}.mdi-fedora::before{content:"\F08DB"}.mdi-fence::before{content:"\F179A"}.mdi-fence-electric::before{content:"\F17F6"}.mdi-fencing::before{content:"\F14C1"}.mdi-ferris-wheel::before{content:"\F0EA4"}.mdi-ferry::before{content:"\F0213"}.mdi-file::before{content:"\F0214"}.mdi-file-account::before{content:"\F073B"}.mdi-file-account-outline::before{content:"\F1028"}.mdi-file-alert::before{content:"\F0A4B"}.mdi-file-alert-outline::before{content:"\F0A4C"}.mdi-file-arrow-left-right::before{content:"\F1A93"}.mdi-file-arrow-left-right-outline::before{content:"\F1A94"}.mdi-file-arrow-up-down::before{content:"\F1A95"}.mdi-file-arrow-up-down-outline::before{content:"\F1A96"}.mdi-file-cabinet::before{content:"\F0AB6"}.mdi-file-cad::before{content:"\F0EEB"}.mdi-file-cad-box::before{content:"\F0EEC"}.mdi-file-cancel::before{content:"\F0DC6"}.mdi-file-cancel-outline::before{content:"\F0DC7"}.mdi-file-certificate::before{content:"\F1186"}.mdi-file-certificate-outline::before{content:"\F1187"}.mdi-file-chart::before{content:"\F0215"}.mdi-file-chart-check::before{content:"\F19C6"}.mdi-file-chart-check-outline::before{content:"\F19C7"}.mdi-file-chart-outline::before{content:"\F1029"}.mdi-file-check::before{content:"\F0216"}.mdi-file-check-outline::before{content:"\F0E29"}.mdi-file-clock::before{content:"\F12E1"}.mdi-file-clock-outline::before{content:"\F12E2"}.mdi-file-cloud::before{content:"\F0217"}.mdi-file-cloud-outline::before{content:"\F102A"}.mdi-file-code::before{content:"\F022E"}.mdi-file-code-outline::before{content:"\F102B"}.mdi-file-cog::before{content:"\F107B"}.mdi-file-cog-outline::before{content:"\F107C"}.mdi-file-compare::before{content:"\F08AA"}.mdi-file-delimited::before{content:"\F0218"}.mdi-file-delimited-outline::before{content:"\F0EA5"}.mdi-file-document::before{content:"\F0219"}.mdi-file-document-alert::before{content:"\F1A97"}.mdi-file-document-alert-outline::before{content:"\F1A98"}.mdi-file-document-arrow-right::before{content:"\F1C0F"}.mdi-file-document-arrow-right-outline::before{content:"\F1C10"}.mdi-file-document-check::before{content:"\F1A99"}.mdi-file-document-check-outline::before{content:"\F1A9A"}.mdi-file-document-edit::before{content:"\F0DC8"}.mdi-file-document-edit-outline::before{content:"\F0DC9"}.mdi-file-document-minus::before{content:"\F1A9B"}.mdi-file-document-minus-outline::before{content:"\F1A9C"}.mdi-file-document-multiple::before{content:"\F1517"}.mdi-file-document-multiple-outline::before{content:"\F1518"}.mdi-file-document-outline::before{content:"\F09EE"}.mdi-file-document-plus::before{content:"\F1A9D"}.mdi-file-document-plus-outline::before{content:"\F1A9E"}.mdi-file-document-refresh::before{content:"\F1C7A"}.mdi-file-document-refresh-outline::before{content:"\F1C7B"}.mdi-file-document-remove::before{content:"\F1A9F"}.mdi-file-document-remove-outline::before{content:"\F1AA0"}.mdi-file-download::before{content:"\F0965"}.mdi-file-download-outline::before{content:"\F0966"}.mdi-file-edit::before{content:"\F11E7"}.mdi-file-edit-outline::before{content:"\F11E8"}.mdi-file-excel::before{content:"\F021B"}.mdi-file-excel-box::before{content:"\F021C"}.mdi-file-excel-box-outline::before{content:"\F102C"}.mdi-file-excel-outline::before{content:"\F102D"}.mdi-file-export::before{content:"\F021D"}.mdi-file-export-outline::before{content:"\F102E"}.mdi-file-eye::before{content:"\F0DCA"}.mdi-file-eye-outline::before{content:"\F0DCB"}.mdi-file-find::before{content:"\F021E"}.mdi-file-find-outline::before{content:"\F0B97"}.mdi-file-gif-box::before{content:"\F0D78"}.mdi-file-hidden::before{content:"\F0613"}.mdi-file-image::before{content:"\F021F"}.mdi-file-image-marker::before{content:"\F1772"}.mdi-file-image-marker-outline::before{content:"\F1773"}.mdi-file-image-minus::before{content:"\F193B"}.mdi-file-image-minus-outline::before{content:"\F193C"}.mdi-file-image-outline::before{content:"\F0EB0"}.mdi-file-image-plus::before{content:"\F193D"}.mdi-file-image-plus-outline::before{content:"\F193E"}.mdi-file-image-remove::before{content:"\F193F"}.mdi-file-image-remove-outline::before{content:"\F1940"}.mdi-file-import::before{content:"\F0220"}.mdi-file-import-outline::before{content:"\F102F"}.mdi-file-jpg-box::before{content:"\F0225"}.mdi-file-key::before{content:"\F1184"}.mdi-file-key-outline::before{content:"\F1185"}.mdi-file-link::before{content:"\F1177"}.mdi-file-link-outline::before{content:"\F1178"}.mdi-file-lock::before{content:"\F0221"}.mdi-file-lock-open::before{content:"\F19C8"}.mdi-file-lock-open-outline::before{content:"\F19C9"}.mdi-file-lock-outline::before{content:"\F1030"}.mdi-file-marker::before{content:"\F1774"}.mdi-file-marker-outline::before{content:"\F1775"}.mdi-file-minus::before{content:"\F1AA1"}.mdi-file-minus-outline::before{content:"\F1AA2"}.mdi-file-move::before{content:"\F0AB9"}.mdi-file-move-outline::before{content:"\F1031"}.mdi-file-multiple::before{content:"\F0222"}.mdi-file-multiple-outline::before{content:"\F1032"}.mdi-file-music::before{content:"\F0223"}.mdi-file-music-outline::before{content:"\F0E2A"}.mdi-file-outline::before{content:"\F0224"}.mdi-file-pdf-box::before{content:"\F0226"}.mdi-file-percent::before{content:"\F081E"}.mdi-file-percent-outline::before{content:"\F1033"}.mdi-file-phone::before{content:"\F1179"}.mdi-file-phone-outline::before{content:"\F117A"}.mdi-file-plus::before{content:"\F0752"}.mdi-file-plus-outline::before{content:"\F0EED"}.mdi-file-png-box::before{content:"\F0E2D"}.mdi-file-powerpoint::before{content:"\F0227"}.mdi-file-powerpoint-box::before{content:"\F0228"}.mdi-file-powerpoint-box-outline::before{content:"\F1034"}.mdi-file-powerpoint-outline::before{content:"\F1035"}.mdi-file-presentation-box::before{content:"\F0229"}.mdi-file-question::before{content:"\F086F"}.mdi-file-question-outline::before{content:"\F1036"}.mdi-file-refresh::before{content:"\F0918"}.mdi-file-refresh-outline::before{content:"\F0541"}.mdi-file-remove::before{content:"\F0B98"}.mdi-file-remove-outline::before{content:"\F1037"}.mdi-file-replace::before{content:"\F0B32"}.mdi-file-replace-outline::before{content:"\F0B33"}.mdi-file-restore::before{content:"\F0670"}.mdi-file-restore-outline::before{content:"\F1038"}.mdi-file-rotate-left::before{content:"\F1A3B"}.mdi-file-rotate-left-outline::before{content:"\F1A3C"}.mdi-file-rotate-right::before{content:"\F1A3D"}.mdi-file-rotate-right-outline::before{content:"\F1A3E"}.mdi-file-search::before{content:"\F0C7C"}.mdi-file-search-outline::before{content:"\F0C7D"}.mdi-file-send::before{content:"\F022A"}.mdi-file-send-outline::before{content:"\F1039"}.mdi-file-settings::before{content:"\F1079"}.mdi-file-settings-outline::before{content:"\F107A"}.mdi-file-sign::before{content:"\F19C3"}.mdi-file-star::before{content:"\F103A"}.mdi-file-star-four-points::before{content:"\F1C2D"}.mdi-file-star-four-points-outline::before{content:"\F1C2E"}.mdi-file-star-outline::before{content:"\F103B"}.mdi-file-swap::before{content:"\F0FB4"}.mdi-file-swap-outline::before{content:"\F0FB5"}.mdi-file-sync::before{content:"\F1216"}.mdi-file-sync-outline::before{content:"\F1217"}.mdi-file-table::before{content:"\F0C7E"}.mdi-file-table-box::before{content:"\F10E1"}.mdi-file-table-box-multiple::before{content:"\F10E2"}.mdi-file-table-box-multiple-outline::before{content:"\F10E3"}.mdi-file-table-box-outline::before{content:"\F10E4"}.mdi-file-table-outline::before{content:"\F0C7F"}.mdi-file-tree::before{content:"\F0645"}.mdi-file-tree-outline::before{content:"\F13D2"}.mdi-file-undo::before{content:"\F08DC"}.mdi-file-undo-outline::before{content:"\F103C"}.mdi-file-upload::before{content:"\F0A4D"}.mdi-file-upload-outline::before{content:"\F0A4E"}.mdi-file-video::before{content:"\F022B"}.mdi-file-video-outline::before{content:"\F0E2C"}.mdi-file-word::before{content:"\F022C"}.mdi-file-word-box::before{content:"\F022D"}.mdi-file-word-box-outline::before{content:"\F103D"}.mdi-file-word-outline::before{content:"\F103E"}.mdi-file-xml-box::before{content:"\F1B4B"}.mdi-film::before{content:"\F022F"}.mdi-filmstrip::before{content:"\F0230"}.mdi-filmstrip-box::before{content:"\F0332"}.mdi-filmstrip-box-multiple::before{content:"\F0D18"}.mdi-filmstrip-off::before{content:"\F0231"}.mdi-filter::before{content:"\F0232"}.mdi-filter-check::before{content:"\F18EC"}.mdi-filter-check-outline::before{content:"\F18ED"}.mdi-filter-cog::before{content:"\F1AA3"}.mdi-filter-cog-outline::before{content:"\F1AA4"}.mdi-filter-menu::before{content:"\F10E5"}.mdi-filter-menu-outline::before{content:"\F10E6"}.mdi-filter-minus::before{content:"\F0EEE"}.mdi-filter-minus-outline::before{content:"\F0EEF"}.mdi-filter-multiple::before{content:"\F1A3F"}.mdi-filter-multiple-outline::before{content:"\F1A40"}.mdi-filter-off::before{content:"\F14EF"}.mdi-filter-off-outline::before{content:"\F14F0"}.mdi-filter-outline::before{content:"\F0233"}.mdi-filter-plus::before{content:"\F0EF0"}.mdi-filter-plus-outline::before{content:"\F0EF1"}.mdi-filter-remove::before{content:"\F0234"}.mdi-filter-remove-outline::before{content:"\F0235"}.mdi-filter-settings::before{content:"\F1AA5"}.mdi-filter-settings-outline::before{content:"\F1AA6"}.mdi-filter-variant::before{content:"\F0236"}.mdi-filter-variant-minus::before{content:"\F1112"}.mdi-filter-variant-plus::before{content:"\F1113"}.mdi-filter-variant-remove::before{content:"\F103F"}.mdi-finance::before{content:"\F081F"}.mdi-find-replace::before{content:"\F06D4"}.mdi-fingerprint::before{content:"\F0237"}.mdi-fingerprint-off::before{content:"\F0EB1"}.mdi-fire::before{content:"\F0238"}.mdi-fire-alert::before{content:"\F15D7"}.mdi-fire-circle::before{content:"\F1807"}.mdi-fire-extinguisher::before{content:"\F0EF2"}.mdi-fire-hydrant::before{content:"\F1137"}.mdi-fire-hydrant-alert::before{content:"\F1138"}.mdi-fire-hydrant-off::before{content:"\F1139"}.mdi-fire-off::before{content:"\F1722"}.mdi-fire-station::before{content:"\F1CC3"}.mdi-fire-truck::before{content:"\F08AB"}.mdi-firebase::before{content:"\F0967"}.mdi-firefox::before{content:"\F0239"}.mdi-fireplace::before{content:"\F0E2E"}.mdi-fireplace-off::before{content:"\F0E2F"}.mdi-firewire::before{content:"\F05BE"}.mdi-firework::before{content:"\F0E30"}.mdi-firework-off::before{content:"\F1723"}.mdi-fish::before{content:"\F023A"}.mdi-fish-off::before{content:"\F13F3"}.mdi-fishbowl::before{content:"\F0EF3"}.mdi-fishbowl-outline::before{content:"\F0EF4"}.mdi-fit-to-page::before{content:"\F0EF5"}.mdi-fit-to-page-outline::before{content:"\F0EF6"}.mdi-fit-to-screen::before{content:"\F18F4"}.mdi-fit-to-screen-outline::before{content:"\F18F5"}.mdi-flag::before{content:"\F023B"}.mdi-flag-checkered::before{content:"\F023C"}.mdi-flag-minus::before{content:"\F0B99"}.mdi-flag-minus-outline::before{content:"\F10B2"}.mdi-flag-off::before{content:"\F18EE"}.mdi-flag-off-outline::before{content:"\F18EF"}.mdi-flag-outline::before{content:"\F023D"}.mdi-flag-plus::before{content:"\F0B9A"}.mdi-flag-plus-outline::before{content:"\F10B3"}.mdi-flag-remove::before{content:"\F0B9B"}.mdi-flag-remove-outline::before{content:"\F10B4"}.mdi-flag-triangle::before{content:"\F023F"}.mdi-flag-variant::before{content:"\F0240"}.mdi-flag-variant-minus::before{content:"\F1BB4"}.mdi-flag-variant-minus-outline::before{content:"\F1BB5"}.mdi-flag-variant-off::before{content:"\F1BB0"}.mdi-flag-variant-off-outline::before{content:"\F1BB1"}.mdi-flag-variant-outline::before{content:"\F023E"}.mdi-flag-variant-plus::before{content:"\F1BB2"}.mdi-flag-variant-plus-outline::before{content:"\F1BB3"}.mdi-flag-variant-remove::before{content:"\F1BB6"}.mdi-flag-variant-remove-outline::before{content:"\F1BB7"}.mdi-flare::before{content:"\F0D72"}.mdi-flash::before{content:"\F0241"}.mdi-flash-alert::before{content:"\F0EF7"}.mdi-flash-alert-outline::before{content:"\F0EF8"}.mdi-flash-auto::before{content:"\F0242"}.mdi-flash-off::before{content:"\F0243"}.mdi-flash-off-outline::before{content:"\F1B45"}.mdi-flash-outline::before{content:"\F06D5"}.mdi-flash-red-eye::before{content:"\F067B"}.mdi-flash-triangle::before{content:"\F1B1D"}.mdi-flash-triangle-outline::before{content:"\F1B1E"}.mdi-flashlight::before{content:"\F0244"}.mdi-flashlight-off::before{content:"\F0245"}.mdi-flask::before{content:"\F0093"}.mdi-flask-empty::before{content:"\F0094"}.mdi-flask-empty-minus::before{content:"\F123A"}.mdi-flask-empty-minus-outline::before{content:"\F123B"}.mdi-flask-empty-off::before{content:"\F13F4"}.mdi-flask-empty-off-outline::before{content:"\F13F5"}.mdi-flask-empty-outline::before{content:"\F0095"}.mdi-flask-empty-plus::before{content:"\F123C"}.mdi-flask-empty-plus-outline::before{content:"\F123D"}.mdi-flask-empty-remove::before{content:"\F123E"}.mdi-flask-empty-remove-outline::before{content:"\F123F"}.mdi-flask-minus::before{content:"\F1240"}.mdi-flask-minus-outline::before{content:"\F1241"}.mdi-flask-off::before{content:"\F13F6"}.mdi-flask-off-outline::before{content:"\F13F7"}.mdi-flask-outline::before{content:"\F0096"}.mdi-flask-plus::before{content:"\F1242"}.mdi-flask-plus-outline::before{content:"\F1243"}.mdi-flask-remove::before{content:"\F1244"}.mdi-flask-remove-outline::before{content:"\F1245"}.mdi-flask-round-bottom::before{content:"\F124B"}.mdi-flask-round-bottom-empty::before{content:"\F124C"}.mdi-flask-round-bottom-empty-outline::before{content:"\F124D"}.mdi-flask-round-bottom-outline::before{content:"\F124E"}.mdi-fleur-de-lis::before{content:"\F1303"}.mdi-flip-horizontal::before{content:"\F10E7"}.mdi-flip-to-back::before{content:"\F0247"}.mdi-flip-to-front::before{content:"\F0248"}.mdi-flip-vertical::before{content:"\F10E8"}.mdi-floor-lamp::before{content:"\F08DD"}.mdi-floor-lamp-dual::before{content:"\F1040"}.mdi-floor-lamp-dual-outline::before{content:"\F17CE"}.mdi-floor-lamp-outline::before{content:"\F17C8"}.mdi-floor-lamp-torchiere::before{content:"\F1747"}.mdi-floor-lamp-torchiere-outline::before{content:"\F17D6"}.mdi-floor-lamp-torchiere-variant::before{content:"\F1041"}.mdi-floor-lamp-torchiere-variant-outline::before{content:"\F17CF"}.mdi-floor-plan::before{content:"\F0821"}.mdi-floppy::before{content:"\F0249"}.mdi-floppy-variant::before{content:"\F09EF"}.mdi-flower::before{content:"\F024A"}.mdi-flower-outline::before{content:"\F09F0"}.mdi-flower-pollen::before{content:"\F1885"}.mdi-flower-pollen-outline::before{content:"\F1886"}.mdi-flower-poppy::before{content:"\F0D08"}.mdi-flower-tulip::before{content:"\F09F1"}.mdi-flower-tulip-outline::before{content:"\F09F2"}.mdi-focus-auto::before{content:"\F0F4E"}.mdi-focus-field::before{content:"\F0F4F"}.mdi-focus-field-horizontal::before{content:"\F0F50"}.mdi-focus-field-vertical::before{content:"\F0F51"}.mdi-folder::before{content:"\F024B"}.mdi-folder-account::before{content:"\F024C"}.mdi-folder-account-outline::before{content:"\F0B9C"}.mdi-folder-alert::before{content:"\F0DCC"}.mdi-folder-alert-outline::before{content:"\F0DCD"}.mdi-folder-arrow-down::before{content:"\F19E8"}.mdi-folder-arrow-down-outline::before{content:"\F19E9"}.mdi-folder-arrow-left::before{content:"\F19EA"}.mdi-folder-arrow-left-outline::before{content:"\F19EB"}.mdi-folder-arrow-left-right::before{content:"\F19EC"}.mdi-folder-arrow-left-right-outline::before{content:"\F19ED"}.mdi-folder-arrow-right::before{content:"\F19EE"}.mdi-folder-arrow-right-outline::before{content:"\F19EF"}.mdi-folder-arrow-up::before{content:"\F19F0"}.mdi-folder-arrow-up-down::before{content:"\F19F1"}.mdi-folder-arrow-up-down-outline::before{content:"\F19F2"}.mdi-folder-arrow-up-outline::before{content:"\F19F3"}.mdi-folder-cancel::before{content:"\F19F4"}.mdi-folder-cancel-outline::before{content:"\F19F5"}.mdi-folder-check::before{content:"\F197E"}.mdi-folder-check-outline::before{content:"\F197F"}.mdi-folder-clock::before{content:"\F0ABA"}.mdi-folder-clock-outline::before{content:"\F0ABB"}.mdi-folder-cog::before{content:"\F107F"}.mdi-folder-cog-outline::before{content:"\F1080"}.mdi-folder-download::before{content:"\F024D"}.mdi-folder-download-outline::before{content:"\F10E9"}.mdi-folder-edit::before{content:"\F08DE"}.mdi-folder-edit-outline::before{content:"\F0DCE"}.mdi-folder-eye::before{content:"\F178A"}.mdi-folder-eye-outline::before{content:"\F178B"}.mdi-folder-file::before{content:"\F19F6"}.mdi-folder-file-outline::before{content:"\F19F7"}.mdi-folder-google-drive::before{content:"\F024E"}.mdi-folder-heart::before{content:"\F10EA"}.mdi-folder-heart-outline::before{content:"\F10EB"}.mdi-folder-hidden::before{content:"\F179E"}.mdi-folder-home::before{content:"\F10B5"}.mdi-folder-home-outline::before{content:"\F10B6"}.mdi-folder-image::before{content:"\F024F"}.mdi-folder-information::before{content:"\F10B7"}.mdi-folder-information-outline::before{content:"\F10B8"}.mdi-folder-key::before{content:"\F08AC"}.mdi-folder-key-network::before{content:"\F08AD"}.mdi-folder-key-network-outline::before{content:"\F0C80"}.mdi-folder-key-outline::before{content:"\F10EC"}.mdi-folder-lock::before{content:"\F0250"}.mdi-folder-lock-open::before{content:"\F0251"}.mdi-folder-lock-open-outline::before{content:"\F1AA7"}.mdi-folder-lock-outline::before{content:"\F1AA8"}.mdi-folder-marker::before{content:"\F126D"}.mdi-folder-marker-outline::before{content:"\F126E"}.mdi-folder-minus::before{content:"\F1B49"}.mdi-folder-minus-outline::before{content:"\F1B4A"}.mdi-folder-move::before{content:"\F0252"}.mdi-folder-move-outline::before{content:"\F1246"}.mdi-folder-multiple::before{content:"\F0253"}.mdi-folder-multiple-image::before{content:"\F0254"}.mdi-folder-multiple-outline::before{content:"\F0255"}.mdi-folder-multiple-plus::before{content:"\F147E"}.mdi-folder-multiple-plus-outline::before{content:"\F147F"}.mdi-folder-music::before{content:"\F1359"}.mdi-folder-music-outline::before{content:"\F135A"}.mdi-folder-network::before{content:"\F0870"}.mdi-folder-network-outline::before{content:"\F0C81"}.mdi-folder-off::before{content:"\F19F8"}.mdi-folder-off-outline::before{content:"\F19F9"}.mdi-folder-open::before{content:"\F0770"}.mdi-folder-open-outline::before{content:"\F0DCF"}.mdi-folder-outline::before{content:"\F0256"}.mdi-folder-play::before{content:"\F19FA"}.mdi-folder-play-outline::before{content:"\F19FB"}.mdi-folder-plus::before{content:"\F0257"}.mdi-folder-plus-outline::before{content:"\F0B9D"}.mdi-folder-pound::before{content:"\F0D09"}.mdi-folder-pound-outline::before{content:"\F0D0A"}.mdi-folder-question::before{content:"\F19CA"}.mdi-folder-question-outline::before{content:"\F19CB"}.mdi-folder-refresh::before{content:"\F0749"}.mdi-folder-refresh-outline::before{content:"\F0542"}.mdi-folder-remove::before{content:"\F0258"}.mdi-folder-remove-outline::before{content:"\F0B9E"}.mdi-folder-search::before{content:"\F0968"}.mdi-folder-search-outline::before{content:"\F0969"}.mdi-folder-settings::before{content:"\F107D"}.mdi-folder-settings-outline::before{content:"\F107E"}.mdi-folder-star::before{content:"\F069D"}.mdi-folder-star-multiple::before{content:"\F13D3"}.mdi-folder-star-multiple-outline::before{content:"\F13D4"}.mdi-folder-star-outline::before{content:"\F0B9F"}.mdi-folder-swap::before{content:"\F0FB6"}.mdi-folder-swap-outline::before{content:"\F0FB7"}.mdi-folder-sync::before{content:"\F0D0B"}.mdi-folder-sync-outline::before{content:"\F0D0C"}.mdi-folder-table::before{content:"\F12E3"}.mdi-folder-table-outline::before{content:"\F12E4"}.mdi-folder-text::before{content:"\F0C82"}.mdi-folder-text-outline::before{content:"\F0C83"}.mdi-folder-upload::before{content:"\F0259"}.mdi-folder-upload-outline::before{content:"\F10ED"}.mdi-folder-wrench::before{content:"\F19FC"}.mdi-folder-wrench-outline::before{content:"\F19FD"}.mdi-folder-zip::before{content:"\F06EB"}.mdi-folder-zip-outline::before{content:"\F07B9"}.mdi-font-awesome::before{content:"\F003A"}.mdi-food::before{content:"\F025A"}.mdi-food-apple::before{content:"\F025B"}.mdi-food-apple-outline::before{content:"\F0C84"}.mdi-food-croissant::before{content:"\F07C8"}.mdi-food-drumstick::before{content:"\F141F"}.mdi-food-drumstick-off::before{content:"\F1468"}.mdi-food-drumstick-off-outline::before{content:"\F1469"}.mdi-food-drumstick-outline::before{content:"\F1420"}.mdi-food-fork-drink::before{content:"\F05F2"}.mdi-food-halal::before{content:"\F1572"}.mdi-food-hot-dog::before{content:"\F184B"}.mdi-food-kosher::before{content:"\F1573"}.mdi-food-off::before{content:"\F05F3"}.mdi-food-off-outline::before{content:"\F1915"}.mdi-food-outline::before{content:"\F1916"}.mdi-food-steak::before{content:"\F146A"}.mdi-food-steak-off::before{content:"\F146B"}.mdi-food-takeout-box::before{content:"\F1836"}.mdi-food-takeout-box-outline::before{content:"\F1837"}.mdi-food-turkey::before{content:"\F171C"}.mdi-food-variant::before{content:"\F025C"}.mdi-food-variant-off::before{content:"\F13E5"}.mdi-foot-print::before{content:"\F0F52"}.mdi-football::before{content:"\F025D"}.mdi-football-australian::before{content:"\F025E"}.mdi-football-helmet::before{content:"\F025F"}.mdi-forest::before{content:"\F1897"}.mdi-forest-outline::before{content:"\F1C63"}.mdi-forklift::before{content:"\F07C9"}.mdi-form-dropdown::before{content:"\F1400"}.mdi-form-select::before{content:"\F1401"}.mdi-form-textarea::before{content:"\F1095"}.mdi-form-textbox::before{content:"\F060E"}.mdi-form-textbox-lock::before{content:"\F135D"}.mdi-form-textbox-password::before{content:"\F07F5"}.mdi-format-align-bottom::before{content:"\F0753"}.mdi-format-align-center::before{content:"\F0260"}.mdi-format-align-justify::before{content:"\F0261"}.mdi-format-align-left::before{content:"\F0262"}.mdi-format-align-middle::before{content:"\F0754"}.mdi-format-align-right::before{content:"\F0263"}.mdi-format-align-top::before{content:"\F0755"}.mdi-format-annotation-minus::before{content:"\F0ABC"}.mdi-format-annotation-plus::before{content:"\F0646"}.mdi-format-bold::before{content:"\F0264"}.mdi-format-clear::before{content:"\F0265"}.mdi-format-color-fill::before{content:"\F0266"}.mdi-format-color-highlight::before{content:"\F0E31"}.mdi-format-color-marker-cancel::before{content:"\F1313"}.mdi-format-color-text::before{content:"\F069E"}.mdi-format-columns::before{content:"\F08DF"}.mdi-format-float-center::before{content:"\F0267"}.mdi-format-float-left::before{content:"\F0268"}.mdi-format-float-none::before{content:"\F0269"}.mdi-format-float-right::before{content:"\F026A"}.mdi-format-font::before{content:"\F06D6"}.mdi-format-font-size-decrease::before{content:"\F09F3"}.mdi-format-font-size-increase::before{content:"\F09F4"}.mdi-format-header-1::before{content:"\F026B"}.mdi-format-header-2::before{content:"\F026C"}.mdi-format-header-3::before{content:"\F026D"}.mdi-format-header-4::before{content:"\F026E"}.mdi-format-header-5::before{content:"\F026F"}.mdi-format-header-6::before{content:"\F0270"}.mdi-format-header-decrease::before{content:"\F0271"}.mdi-format-header-equal::before{content:"\F0272"}.mdi-format-header-increase::before{content:"\F0273"}.mdi-format-header-pound::before{content:"\F0274"}.mdi-format-horizontal-align-center::before{content:"\F061E"}.mdi-format-horizontal-align-left::before{content:"\F061F"}.mdi-format-horizontal-align-right::before{content:"\F0620"}.mdi-format-indent-decrease::before{content:"\F0275"}.mdi-format-indent-increase::before{content:"\F0276"}.mdi-format-italic::before{content:"\F0277"}.mdi-format-letter-case::before{content:"\F0B34"}.mdi-format-letter-case-lower::before{content:"\F0B35"}.mdi-format-letter-case-upper::before{content:"\F0B36"}.mdi-format-letter-ends-with::before{content:"\F0FB8"}.mdi-format-letter-matches::before{content:"\F0FB9"}.mdi-format-letter-spacing::before{content:"\F1956"}.mdi-format-letter-spacing-variant::before{content:"\F1AFB"}.mdi-format-letter-starts-with::before{content:"\F0FBA"}.mdi-format-line-height::before{content:"\F1AFC"}.mdi-format-line-spacing::before{content:"\F0278"}.mdi-format-line-style::before{content:"\F05C8"}.mdi-format-line-weight::before{content:"\F05C9"}.mdi-format-list-bulleted::before{content:"\F0279"}.mdi-format-list-bulleted-square::before{content:"\F0DD0"}.mdi-format-list-bulleted-triangle::before{content:"\F0EB2"}.mdi-format-list-bulleted-type::before{content:"\F027A"}.mdi-format-list-checkbox::before{content:"\F096A"}.mdi-format-list-checks::before{content:"\F0756"}.mdi-format-list-group::before{content:"\F1860"}.mdi-format-list-group-plus::before{content:"\F1B56"}.mdi-format-list-numbered::before{content:"\F027B"}.mdi-format-list-numbered-rtl::before{content:"\F0D0D"}.mdi-format-list-text::before{content:"\F126F"}.mdi-format-overline::before{content:"\F0EB3"}.mdi-format-page-break::before{content:"\F06D7"}.mdi-format-page-split::before{content:"\F1917"}.mdi-format-paint::before{content:"\F027C"}.mdi-format-paragraph::before{content:"\F027D"}.mdi-format-paragraph-spacing::before{content:"\F1AFD"}.mdi-format-pilcrow::before{content:"\F06D8"}.mdi-format-pilcrow-arrow-left::before{content:"\F0286"}.mdi-format-pilcrow-arrow-right::before{content:"\F0285"}.mdi-format-quote-close::before{content:"\F027E"}.mdi-format-quote-close-outline::before{content:"\F11A8"}.mdi-format-quote-open::before{content:"\F0757"}.mdi-format-quote-open-outline::before{content:"\F11A7"}.mdi-format-rotate-90::before{content:"\F06AA"}.mdi-format-section::before{content:"\F069F"}.mdi-format-size::before{content:"\F027F"}.mdi-format-strikethrough::before{content:"\F0280"}.mdi-format-strikethrough-variant::before{content:"\F0281"}.mdi-format-subscript::before{content:"\F0282"}.mdi-format-superscript::before{content:"\F0283"}.mdi-format-text::before{content:"\F0284"}.mdi-format-text-rotation-angle-down::before{content:"\F0FBB"}.mdi-format-text-rotation-angle-up::before{content:"\F0FBC"}.mdi-format-text-rotation-down::before{content:"\F0D73"}.mdi-format-text-rotation-down-vertical::before{content:"\F0FBD"}.mdi-format-text-rotation-none::before{content:"\F0D74"}.mdi-format-text-rotation-up::before{content:"\F0FBE"}.mdi-format-text-rotation-vertical::before{content:"\F0FBF"}.mdi-format-text-variant::before{content:"\F0E32"}.mdi-format-text-variant-outline::before{content:"\F150F"}.mdi-format-text-wrapping-clip::before{content:"\F0D0E"}.mdi-format-text-wrapping-overflow::before{content:"\F0D0F"}.mdi-format-text-wrapping-wrap::before{content:"\F0D10"}.mdi-format-textbox::before{content:"\F0D11"}.mdi-format-title::before{content:"\F05F4"}.mdi-format-underline::before{content:"\F0287"}.mdi-format-underline-wavy::before{content:"\F18E9"}.mdi-format-vertical-align-bottom::before{content:"\F0621"}.mdi-format-vertical-align-center::before{content:"\F0622"}.mdi-format-vertical-align-top::before{content:"\F0623"}.mdi-format-wrap-inline::before{content:"\F0288"}.mdi-format-wrap-square::before{content:"\F0289"}.mdi-format-wrap-tight::before{content:"\F028A"}.mdi-format-wrap-top-bottom::before{content:"\F028B"}.mdi-forum::before{content:"\F028C"}.mdi-forum-minus::before{content:"\F1AA9"}.mdi-forum-minus-outline::before{content:"\F1AAA"}.mdi-forum-outline::before{content:"\F0822"}.mdi-forum-plus::before{content:"\F1AAB"}.mdi-forum-plus-outline::before{content:"\F1AAC"}.mdi-forum-remove::before{content:"\F1AAD"}.mdi-forum-remove-outline::before{content:"\F1AAE"}.mdi-forward::before{content:"\F028D"}.mdi-forwardburger::before{content:"\F0D75"}.mdi-fountain::before{content:"\F096B"}.mdi-fountain-pen::before{content:"\F0D12"}.mdi-fountain-pen-tip::before{content:"\F0D13"}.mdi-fraction-one-half::before{content:"\F1992"}.mdi-freebsd::before{content:"\F08E0"}.mdi-french-fries::before{content:"\F1957"}.mdi-frequently-asked-questions::before{content:"\F0EB4"}.mdi-fridge::before{content:"\F0290"}.mdi-fridge-alert::before{content:"\F11B1"}.mdi-fridge-alert-outline::before{content:"\F11B2"}.mdi-fridge-bottom::before{content:"\F0292"}.mdi-fridge-industrial::before{content:"\F15EE"}.mdi-fridge-industrial-alert::before{content:"\F15EF"}.mdi-fridge-industrial-alert-outline::before{content:"\F15F0"}.mdi-fridge-industrial-off::before{content:"\F15F1"}.mdi-fridge-industrial-off-outline::before{content:"\F15F2"}.mdi-fridge-industrial-outline::before{content:"\F15F3"}.mdi-fridge-off::before{content:"\F11AF"}.mdi-fridge-off-outline::before{content:"\F11B0"}.mdi-fridge-outline::before{content:"\F028F"}.mdi-fridge-top::before{content:"\F0291"}.mdi-fridge-variant::before{content:"\F15F4"}.mdi-fridge-variant-alert::before{content:"\F15F5"}.mdi-fridge-variant-alert-outline::before{content:"\F15F6"}.mdi-fridge-variant-off::before{content:"\F15F7"}.mdi-fridge-variant-off-outline::before{content:"\F15F8"}.mdi-fridge-variant-outline::before{content:"\F15F9"}.mdi-fruit-cherries::before{content:"\F1042"}.mdi-fruit-cherries-off::before{content:"\F13F8"}.mdi-fruit-citrus::before{content:"\F1043"}.mdi-fruit-citrus-off::before{content:"\F13F9"}.mdi-fruit-grapes::before{content:"\F1044"}.mdi-fruit-grapes-outline::before{content:"\F1045"}.mdi-fruit-pear::before{content:"\F1A0E"}.mdi-fruit-pineapple::before{content:"\F1046"}.mdi-fruit-watermelon::before{content:"\F1047"}.mdi-fuel::before{content:"\F07CA"}.mdi-fuel-cell::before{content:"\F18B5"}.mdi-fullscreen::before{content:"\F0293"}.mdi-fullscreen-exit::before{content:"\F0294"}.mdi-function::before{content:"\F0295"}.mdi-function-variant::before{content:"\F0871"}.mdi-furigana-horizontal::before{content:"\F1081"}.mdi-furigana-vertical::before{content:"\F1082"}.mdi-fuse::before{content:"\F0C85"}.mdi-fuse-alert::before{content:"\F142D"}.mdi-fuse-blade::before{content:"\F0C86"}.mdi-fuse-off::before{content:"\F142C"}.mdi-gamepad::before{content:"\F0296"}.mdi-gamepad-circle::before{content:"\F0E33"}.mdi-gamepad-circle-down::before{content:"\F0E34"}.mdi-gamepad-circle-left::before{content:"\F0E35"}.mdi-gamepad-circle-outline::before{content:"\F0E36"}.mdi-gamepad-circle-right::before{content:"\F0E37"}.mdi-gamepad-circle-up::before{content:"\F0E38"}.mdi-gamepad-down::before{content:"\F0E39"}.mdi-gamepad-left::before{content:"\F0E3A"}.mdi-gamepad-outline::before{content:"\F1919"}.mdi-gamepad-right::before{content:"\F0E3B"}.mdi-gamepad-round::before{content:"\F0E3C"}.mdi-gamepad-round-down::before{content:"\F0E3D"}.mdi-gamepad-round-left::before{content:"\F0E3E"}.mdi-gamepad-round-outline::before{content:"\F0E3F"}.mdi-gamepad-round-right::before{content:"\F0E40"}.mdi-gamepad-round-up::before{content:"\F0E41"}.mdi-gamepad-square::before{content:"\F0EB5"}.mdi-gamepad-square-outline::before{content:"\F0EB6"}.mdi-gamepad-up::before{content:"\F0E42"}.mdi-gamepad-variant::before{content:"\F0297"}.mdi-gamepad-variant-outline::before{content:"\F0EB7"}.mdi-gamma::before{content:"\F10EE"}.mdi-gantry-crane::before{content:"\F0DD1"}.mdi-garage::before{content:"\F06D9"}.mdi-garage-alert::before{content:"\F0872"}.mdi-garage-alert-variant::before{content:"\F12D5"}.mdi-garage-lock::before{content:"\F17FB"}.mdi-garage-open::before{content:"\F06DA"}.mdi-garage-open-variant::before{content:"\F12D4"}.mdi-garage-variant::before{content:"\F12D3"}.mdi-garage-variant-lock::before{content:"\F17FC"}.mdi-gas-burner::before{content:"\F1A1B"}.mdi-gas-cylinder::before{content:"\F0647"}.mdi-gas-station::before{content:"\F0298"}.mdi-gas-station-in-use::before{content:"\F1CC4"}.mdi-gas-station-in-use-outline::before{content:"\F1CC5"}.mdi-gas-station-off::before{content:"\F1409"}.mdi-gas-station-off-outline::before{content:"\F140A"}.mdi-gas-station-outline::before{content:"\F0EB8"}.mdi-gate::before{content:"\F0299"}.mdi-gate-alert::before{content:"\F17F8"}.mdi-gate-and::before{content:"\F08E1"}.mdi-gate-arrow-left::before{content:"\F17F7"}.mdi-gate-arrow-right::before{content:"\F1169"}.mdi-gate-buffer::before{content:"\F1AFE"}.mdi-gate-nand::before{content:"\F08E2"}.mdi-gate-nor::before{content:"\F08E3"}.mdi-gate-not::before{content:"\F08E4"}.mdi-gate-open::before{content:"\F116A"}.mdi-gate-or::before{content:"\F08E5"}.mdi-gate-xnor::before{content:"\F08E6"}.mdi-gate-xor::before{content:"\F08E7"}.mdi-gatsby::before{content:"\F0E43"}.mdi-gauge::before{content:"\F029A"}.mdi-gauge-empty::before{content:"\F0873"}.mdi-gauge-full::before{content:"\F0874"}.mdi-gauge-low::before{content:"\F0875"}.mdi-gavel::before{content:"\F029B"}.mdi-gender-female::before{content:"\F029C"}.mdi-gender-male::before{content:"\F029D"}.mdi-gender-male-female::before{content:"\F029E"}.mdi-gender-male-female-variant::before{content:"\F113F"}.mdi-gender-non-binary::before{content:"\F1140"}.mdi-gender-transgender::before{content:"\F029F"}.mdi-generator-mobile::before{content:"\F1C8A"}.mdi-generator-portable::before{content:"\F1C8B"}.mdi-generator-stationary::before{content:"\F1C8C"}.mdi-gentoo::before{content:"\F08E8"}.mdi-gesture::before{content:"\F07CB"}.mdi-gesture-double-tap::before{content:"\F073C"}.mdi-gesture-pinch::before{content:"\F0ABD"}.mdi-gesture-spread::before{content:"\F0ABE"}.mdi-gesture-swipe::before{content:"\F0D76"}.mdi-gesture-swipe-down::before{content:"\F073D"}.mdi-gesture-swipe-horizontal::before{content:"\F0ABF"}.mdi-gesture-swipe-left::before{content:"\F073E"}.mdi-gesture-swipe-right::before{content:"\F073F"}.mdi-gesture-swipe-up::before{content:"\F0740"}.mdi-gesture-swipe-vertical::before{content:"\F0AC0"}.mdi-gesture-tap::before{content:"\F0741"}.mdi-gesture-tap-box::before{content:"\F12A9"}.mdi-gesture-tap-button::before{content:"\F12A8"}.mdi-gesture-tap-hold::before{content:"\F0D77"}.mdi-gesture-two-double-tap::before{content:"\F0742"}.mdi-gesture-two-tap::before{content:"\F0743"}.mdi-ghost::before{content:"\F02A0"}.mdi-ghost-off::before{content:"\F09F5"}.mdi-ghost-off-outline::before{content:"\F165C"}.mdi-ghost-outline::before{content:"\F165D"}.mdi-gift::before{content:"\F0E44"}.mdi-gift-off::before{content:"\F16EF"}.mdi-gift-off-outline::before{content:"\F16F0"}.mdi-gift-open::before{content:"\F16F1"}.mdi-gift-open-outline::before{content:"\F16F2"}.mdi-gift-outline::before{content:"\F02A1"}.mdi-git::before{content:"\F02A2"}.mdi-github::before{content:"\F02A4"}.mdi-gitlab::before{content:"\F0BA0"}.mdi-glass-cocktail::before{content:"\F0356"}.mdi-glass-cocktail-off::before{content:"\F15E6"}.mdi-glass-flute::before{content:"\F02A5"}.mdi-glass-fragile::before{content:"\F1873"}.mdi-glass-mug::before{content:"\F02A6"}.mdi-glass-mug-off::before{content:"\F15E7"}.mdi-glass-mug-variant::before{content:"\F1116"}.mdi-glass-mug-variant-off::before{content:"\F15E8"}.mdi-glass-pint-outline::before{content:"\F130D"}.mdi-glass-stange::before{content:"\F02A7"}.mdi-glass-tulip::before{content:"\F02A8"}.mdi-glass-wine::before{content:"\F0876"}.mdi-glasses::before{content:"\F02AA"}.mdi-globe-light::before{content:"\F066F"}.mdi-globe-light-outline::before{content:"\F12D7"}.mdi-globe-model::before{content:"\F08E9"}.mdi-gmail::before{content:"\F02AB"}.mdi-gnome::before{content:"\F02AC"}.mdi-go-kart::before{content:"\F0D79"}.mdi-go-kart-track::before{content:"\F0D7A"}.mdi-gog::before{content:"\F0BA1"}.mdi-gold::before{content:"\F124F"}.mdi-golf::before{content:"\F0823"}.mdi-golf-cart::before{content:"\F11A4"}.mdi-golf-tee::before{content:"\F1083"}.mdi-gondola::before{content:"\F0686"}.mdi-goodreads::before{content:"\F0D7B"}.mdi-google::before{content:"\F02AD"}.mdi-google-ads::before{content:"\F0C87"}.mdi-google-analytics::before{content:"\F07CC"}.mdi-google-assistant::before{content:"\F07CD"}.mdi-google-cardboard::before{content:"\F02AE"}.mdi-google-chrome::before{content:"\F02AF"}.mdi-google-circles::before{content:"\F02B0"}.mdi-google-circles-communities::before{content:"\F02B1"}.mdi-google-circles-extended::before{content:"\F02B2"}.mdi-google-circles-group::before{content:"\F02B3"}.mdi-google-classroom::before{content:"\F02C0"}.mdi-google-cloud::before{content:"\F11F6"}.mdi-google-downasaur::before{content:"\F1362"}.mdi-google-drive::before{content:"\F02B6"}.mdi-google-earth::before{content:"\F02B7"}.mdi-google-fit::before{content:"\F096C"}.mdi-google-glass::before{content:"\F02B8"}.mdi-google-hangouts::before{content:"\F02C9"}.mdi-google-keep::before{content:"\F06DC"}.mdi-google-lens::before{content:"\F09F6"}.mdi-google-maps::before{content:"\F05F5"}.mdi-google-my-business::before{content:"\F1048"}.mdi-google-nearby::before{content:"\F02B9"}.mdi-google-play::before{content:"\F02BC"}.mdi-google-plus::before{content:"\F02BD"}.mdi-google-podcast::before{content:"\F0EB9"}.mdi-google-spreadsheet::before{content:"\F09F7"}.mdi-google-street-view::before{content:"\F0C88"}.mdi-google-translate::before{content:"\F02BF"}.mdi-gradient-horizontal::before{content:"\F174A"}.mdi-gradient-vertical::before{content:"\F06A0"}.mdi-grain::before{content:"\F0D7C"}.mdi-graph::before{content:"\F1049"}.mdi-graph-outline::before{content:"\F104A"}.mdi-graphql::before{content:"\F0877"}.mdi-grass::before{content:"\F1510"}.mdi-grave-stone::before{content:"\F0BA2"}.mdi-grease-pencil::before{content:"\F0648"}.mdi-greater-than::before{content:"\F096D"}.mdi-greater-than-or-equal::before{content:"\F096E"}.mdi-greenhouse::before{content:"\F002D"}.mdi-grid::before{content:"\F02C1"}.mdi-grid-large::before{content:"\F0758"}.mdi-grid-off::before{content:"\F02C2"}.mdi-grill::before{content:"\F0E45"}.mdi-grill-outline::before{content:"\F118A"}.mdi-group::before{content:"\F02C3"}.mdi-guitar-acoustic::before{content:"\F0771"}.mdi-guitar-electric::before{content:"\F02C4"}.mdi-guitar-pick::before{content:"\F02C5"}.mdi-guitar-pick-outline::before{content:"\F02C6"}.mdi-guy-fawkes-mask::before{content:"\F0825"}.mdi-gymnastics::before{content:"\F1A41"}.mdi-hail::before{content:"\F0AC1"}.mdi-hair-dryer::before{content:"\F10EF"}.mdi-hair-dryer-outline::before{content:"\F10F0"}.mdi-halloween::before{content:"\F0BA3"}.mdi-hamburger::before{content:"\F0685"}.mdi-hamburger-check::before{content:"\F1776"}.mdi-hamburger-minus::before{content:"\F1777"}.mdi-hamburger-off::before{content:"\F1778"}.mdi-hamburger-plus::before{content:"\F1779"}.mdi-hamburger-remove::before{content:"\F177A"}.mdi-hammer::before{content:"\F08EA"}.mdi-hammer-screwdriver::before{content:"\F1322"}.mdi-hammer-sickle::before{content:"\F1887"}.mdi-hammer-wrench::before{content:"\F1323"}.mdi-hand-back-left::before{content:"\F0E46"}.mdi-hand-back-left-off::before{content:"\F1830"}.mdi-hand-back-left-off-outline::before{content:"\F1832"}.mdi-hand-back-left-outline::before{content:"\F182C"}.mdi-hand-back-right::before{content:"\F0E47"}.mdi-hand-back-right-off::before{content:"\F1831"}.mdi-hand-back-right-off-outline::before{content:"\F1833"}.mdi-hand-back-right-outline::before{content:"\F182D"}.mdi-hand-clap::before{content:"\F194B"}.mdi-hand-clap-off::before{content:"\F1A42"}.mdi-hand-coin::before{content:"\F188F"}.mdi-hand-coin-outline::before{content:"\F1890"}.mdi-hand-cycle::before{content:"\F1B9C"}.mdi-hand-extended::before{content:"\F18B6"}.mdi-hand-extended-outline::before{content:"\F18B7"}.mdi-hand-front-left::before{content:"\F182B"}.mdi-hand-front-left-outline::before{content:"\F182E"}.mdi-hand-front-right::before{content:"\F0A4F"}.mdi-hand-front-right-outline::before{content:"\F182F"}.mdi-hand-heart::before{content:"\F10F1"}.mdi-hand-heart-outline::before{content:"\F157E"}.mdi-hand-okay::before{content:"\F0A50"}.mdi-hand-peace::before{content:"\F0A51"}.mdi-hand-peace-variant::before{content:"\F0A52"}.mdi-hand-pointing-down::before{content:"\F0A53"}.mdi-hand-pointing-left::before{content:"\F0A54"}.mdi-hand-pointing-right::before{content:"\F02C7"}.mdi-hand-pointing-up::before{content:"\F0A55"}.mdi-hand-saw::before{content:"\F0E48"}.mdi-hand-wash::before{content:"\F157F"}.mdi-hand-wash-outline::before{content:"\F1580"}.mdi-hand-water::before{content:"\F139F"}.mdi-hand-wave::before{content:"\F1821"}.mdi-hand-wave-outline::before{content:"\F1822"}.mdi-handball::before{content:"\F0F53"}.mdi-handcuffs::before{content:"\F113E"}.mdi-hands-pray::before{content:"\F0579"}.mdi-handshake::before{content:"\F1218"}.mdi-handshake-outline::before{content:"\F15A1"}.mdi-hanger::before{content:"\F02C8"}.mdi-hard-hat::before{content:"\F096F"}.mdi-harddisk::before{content:"\F02CA"}.mdi-harddisk-plus::before{content:"\F104B"}.mdi-harddisk-remove::before{content:"\F104C"}.mdi-hat-fedora::before{content:"\F0BA4"}.mdi-hazard-lights::before{content:"\F0C89"}.mdi-hdmi-port::before{content:"\F1BB8"}.mdi-hdr::before{content:"\F0D7D"}.mdi-hdr-off::before{content:"\F0D7E"}.mdi-head::before{content:"\F135E"}.mdi-head-alert::before{content:"\F1338"}.mdi-head-alert-outline::before{content:"\F1339"}.mdi-head-check::before{content:"\F133A"}.mdi-head-check-outline::before{content:"\F133B"}.mdi-head-cog::before{content:"\F133C"}.mdi-head-cog-outline::before{content:"\F133D"}.mdi-head-dots-horizontal::before{content:"\F133E"}.mdi-head-dots-horizontal-outline::before{content:"\F133F"}.mdi-head-flash::before{content:"\F1340"}.mdi-head-flash-outline::before{content:"\F1341"}.mdi-head-heart::before{content:"\F1342"}.mdi-head-heart-outline::before{content:"\F1343"}.mdi-head-lightbulb::before{content:"\F1344"}.mdi-head-lightbulb-outline::before{content:"\F1345"}.mdi-head-minus::before{content:"\F1346"}.mdi-head-minus-outline::before{content:"\F1347"}.mdi-head-outline::before{content:"\F135F"}.mdi-head-plus::before{content:"\F1348"}.mdi-head-plus-outline::before{content:"\F1349"}.mdi-head-question::before{content:"\F134A"}.mdi-head-question-outline::before{content:"\F134B"}.mdi-head-remove::before{content:"\F134C"}.mdi-head-remove-outline::before{content:"\F134D"}.mdi-head-snowflake::before{content:"\F134E"}.mdi-head-snowflake-outline::before{content:"\F134F"}.mdi-head-sync::before{content:"\F1350"}.mdi-head-sync-outline::before{content:"\F1351"}.mdi-headphones::before{content:"\F02CB"}.mdi-headphones-bluetooth::before{content:"\F0970"}.mdi-headphones-box::before{content:"\F02CC"}.mdi-headphones-off::before{content:"\F07CE"}.mdi-headphones-settings::before{content:"\F02CD"}.mdi-headset::before{content:"\F02CE"}.mdi-headset-dock::before{content:"\F02CF"}.mdi-headset-off::before{content:"\F02D0"}.mdi-heart::before{content:"\F02D1"}.mdi-heart-box::before{content:"\F02D2"}.mdi-heart-box-outline::before{content:"\F02D3"}.mdi-heart-broken::before{content:"\F02D4"}.mdi-heart-broken-outline::before{content:"\F0D14"}.mdi-heart-circle::before{content:"\F0971"}.mdi-heart-circle-outline::before{content:"\F0972"}.mdi-heart-cog::before{content:"\F1663"}.mdi-heart-cog-outline::before{content:"\F1664"}.mdi-heart-flash::before{content:"\F0EF9"}.mdi-heart-half::before{content:"\F06DF"}.mdi-heart-half-full::before{content:"\F06DE"}.mdi-heart-half-outline::before{content:"\F06E0"}.mdi-heart-minus::before{content:"\F142F"}.mdi-heart-minus-outline::before{content:"\F1432"}.mdi-heart-multiple::before{content:"\F0A56"}.mdi-heart-multiple-outline::before{content:"\F0A57"}.mdi-heart-off::before{content:"\F0759"}.mdi-heart-off-outline::before{content:"\F1434"}.mdi-heart-outline::before{content:"\F02D5"}.mdi-heart-plus::before{content:"\F142E"}.mdi-heart-plus-outline::before{content:"\F1431"}.mdi-heart-pulse::before{content:"\F05F6"}.mdi-heart-remove::before{content:"\F1430"}.mdi-heart-remove-outline::before{content:"\F1433"}.mdi-heart-search::before{content:"\F1C8D"}.mdi-heart-settings::before{content:"\F1665"}.mdi-heart-settings-outline::before{content:"\F1666"}.mdi-heat-pump::before{content:"\F1A43"}.mdi-heat-pump-outline::before{content:"\F1A44"}.mdi-heat-wave::before{content:"\F1A45"}.mdi-heating-coil::before{content:"\F1AAF"}.mdi-helicopter::before{content:"\F0AC2"}.mdi-help::before{content:"\F02D6"}.mdi-help-box::before{content:"\F078B"}.mdi-help-box-multiple::before{content:"\F1C0A"}.mdi-help-box-multiple-outline::before{content:"\F1C0B"}.mdi-help-box-outline::before{content:"\F1C0C"}.mdi-help-circle::before{content:"\F02D7"}.mdi-help-circle-outline::before{content:"\F0625"}.mdi-help-network::before{content:"\F06F5"}.mdi-help-network-outline::before{content:"\F0C8A"}.mdi-help-rhombus::before{content:"\F0BA5"}.mdi-help-rhombus-outline::before{content:"\F0BA6"}.mdi-hexadecimal::before{content:"\F12A7"}.mdi-hexagon::before{content:"\F02D8"}.mdi-hexagon-multiple::before{content:"\F06E1"}.mdi-hexagon-multiple-outline::before{content:"\F10F2"}.mdi-hexagon-outline::before{content:"\F02D9"}.mdi-hexagon-slice-1::before{content:"\F0AC3"}.mdi-hexagon-slice-2::before{content:"\F0AC4"}.mdi-hexagon-slice-3::before{content:"\F0AC5"}.mdi-hexagon-slice-4::before{content:"\F0AC6"}.mdi-hexagon-slice-5::before{content:"\F0AC7"}.mdi-hexagon-slice-6::before{content:"\F0AC8"}.mdi-hexagram::before{content:"\F0AC9"}.mdi-hexagram-outline::before{content:"\F0ACA"}.mdi-high-definition::before{content:"\F07CF"}.mdi-high-definition-box::before{content:"\F0878"}.mdi-highway::before{content:"\F05F7"}.mdi-hiking::before{content:"\F0D7F"}.mdi-history::before{content:"\F02DA"}.mdi-hockey-puck::before{content:"\F0879"}.mdi-hockey-sticks::before{content:"\F087A"}.mdi-hololens::before{content:"\F02DB"}.mdi-home::before{content:"\F02DC"}.mdi-home-account::before{content:"\F0826"}.mdi-home-alert::before{content:"\F087B"}.mdi-home-alert-outline::before{content:"\F15D0"}.mdi-home-analytics::before{content:"\F0EBA"}.mdi-home-assistant::before{content:"\F07D0"}.mdi-home-automation::before{content:"\F07D1"}.mdi-home-battery::before{content:"\F1901"}.mdi-home-battery-outline::before{content:"\F1902"}.mdi-home-circle::before{content:"\F07D2"}.mdi-home-circle-outline::before{content:"\F104D"}.mdi-home-city::before{content:"\F0D15"}.mdi-home-city-outline::before{content:"\F0D16"}.mdi-home-clock::before{content:"\F1A12"}.mdi-home-clock-outline::before{content:"\F1A13"}.mdi-home-edit::before{content:"\F1159"}.mdi-home-edit-outline::before{content:"\F115A"}.mdi-home-export-outline::before{content:"\F0F9B"}.mdi-home-flood::before{content:"\F0EFA"}.mdi-home-floor-0::before{content:"\F0DD2"}.mdi-home-floor-1::before{content:"\F0D80"}.mdi-home-floor-2::before{content:"\F0D81"}.mdi-home-floor-3::before{content:"\F0D82"}.mdi-home-floor-a::before{content:"\F0D83"}.mdi-home-floor-b::before{content:"\F0D84"}.mdi-home-floor-g::before{content:"\F0D85"}.mdi-home-floor-l::before{content:"\F0D86"}.mdi-home-floor-negative-1::before{content:"\F0DD3"}.mdi-home-group::before{content:"\F0DD4"}.mdi-home-group-minus::before{content:"\F19C1"}.mdi-home-group-plus::before{content:"\F19C0"}.mdi-home-group-remove::before{content:"\F19C2"}.mdi-home-heart::before{content:"\F0827"}.mdi-home-import-outline::before{content:"\F0F9C"}.mdi-home-lightbulb::before{content:"\F1251"}.mdi-home-lightbulb-outline::before{content:"\F1252"}.mdi-home-lightning-bolt::before{content:"\F1903"}.mdi-home-lightning-bolt-outline::before{content:"\F1904"}.mdi-home-lock::before{content:"\F08EB"}.mdi-home-lock-open::before{content:"\F08EC"}.mdi-home-map-marker::before{content:"\F05F8"}.mdi-home-minus::before{content:"\F0974"}.mdi-home-minus-outline::before{content:"\F13D5"}.mdi-home-modern::before{content:"\F02DD"}.mdi-home-off::before{content:"\F1A46"}.mdi-home-off-outline::before{content:"\F1A47"}.mdi-home-outline::before{content:"\F06A1"}.mdi-home-percent::before{content:"\F1C7C"}.mdi-home-percent-outline::before{content:"\F1C7D"}.mdi-home-plus::before{content:"\F0975"}.mdi-home-plus-outline::before{content:"\F13D6"}.mdi-home-remove::before{content:"\F1247"}.mdi-home-remove-outline::before{content:"\F13D7"}.mdi-home-roof::before{content:"\F112B"}.mdi-home-search::before{content:"\F13B0"}.mdi-home-search-outline::before{content:"\F13B1"}.mdi-home-silo::before{content:"\F1BA0"}.mdi-home-silo-outline::before{content:"\F1BA1"}.mdi-home-sound-in::before{content:"\F1C2F"}.mdi-home-sound-in-outline::before{content:"\F1C30"}.mdi-home-sound-out::before{content:"\F1C31"}.mdi-home-sound-out-outline::before{content:"\F1C32"}.mdi-home-switch::before{content:"\F1794"}.mdi-home-switch-outline::before{content:"\F1795"}.mdi-home-thermometer::before{content:"\F0F54"}.mdi-home-thermometer-outline::before{content:"\F0F55"}.mdi-home-variant::before{content:"\F02DE"}.mdi-home-variant-outline::before{content:"\F0BA7"}.mdi-hook::before{content:"\F06E2"}.mdi-hook-off::before{content:"\F06E3"}.mdi-hoop-house::before{content:"\F0E56"}.mdi-hops::before{content:"\F02DF"}.mdi-horizontal-rotate-clockwise::before{content:"\F10F3"}.mdi-horizontal-rotate-counterclockwise::before{content:"\F10F4"}.mdi-horse::before{content:"\F15BF"}.mdi-horse-human::before{content:"\F15C0"}.mdi-horse-variant::before{content:"\F15C1"}.mdi-horse-variant-fast::before{content:"\F186E"}.mdi-horseshoe::before{content:"\F0A58"}.mdi-hospital::before{content:"\F0FF6"}.mdi-hospital-box::before{content:"\F02E0"}.mdi-hospital-box-outline::before{content:"\F0FF7"}.mdi-hospital-building::before{content:"\F02E1"}.mdi-hospital-marker::before{content:"\F02E2"}.mdi-hot-tub::before{content:"\F0828"}.mdi-hours-12::before{content:"\F1C94"}.mdi-hours-24::before{content:"\F1478"}.mdi-hub::before{content:"\F1C95"}.mdi-hub-outline::before{content:"\F1C96"}.mdi-hubspot::before{content:"\F0D17"}.mdi-hulu::before{content:"\F0829"}.mdi-human::before{content:"\F02E6"}.mdi-human-baby-changing-table::before{content:"\F138B"}.mdi-human-cane::before{content:"\F1581"}.mdi-human-capacity-decrease::before{content:"\F159B"}.mdi-human-capacity-increase::before{content:"\F159C"}.mdi-human-child::before{content:"\F02E7"}.mdi-human-dolly::before{content:"\F1980"}.mdi-human-edit::before{content:"\F14E8"}.mdi-human-female::before{content:"\F0649"}.mdi-human-female-boy::before{content:"\F0A59"}.mdi-human-female-dance::before{content:"\F15C9"}.mdi-human-female-female::before{content:"\F0A5A"}.mdi-human-female-female-child::before{content:"\F1C8E"}.mdi-human-female-girl::before{content:"\F0A5B"}.mdi-human-greeting::before{content:"\F17C4"}.mdi-human-greeting-proximity::before{content:"\F159D"}.mdi-human-greeting-variant::before{content:"\F064A"}.mdi-human-handsdown::before{content:"\F064B"}.mdi-human-handsup::before{content:"\F064C"}.mdi-human-male::before{content:"\F064D"}.mdi-human-male-board::before{content:"\F0890"}.mdi-human-male-board-poll::before{content:"\F0846"}.mdi-human-male-boy::before{content:"\F0A5C"}.mdi-human-male-child::before{content:"\F138C"}.mdi-human-male-female::before{content:"\F02E8"}.mdi-human-male-female-child::before{content:"\F1823"}.mdi-human-male-girl::before{content:"\F0A5D"}.mdi-human-male-height::before{content:"\F0EFB"}.mdi-human-male-height-variant::before{content:"\F0EFC"}.mdi-human-male-male::before{content:"\F0A5E"}.mdi-human-male-male-child::before{content:"\F1C8F"}.mdi-human-non-binary::before{content:"\F1848"}.mdi-human-pregnant::before{content:"\F05CF"}.mdi-human-queue::before{content:"\F1571"}.mdi-human-scooter::before{content:"\F11E9"}.mdi-human-walker::before{content:"\F1B71"}.mdi-human-wheelchair::before{content:"\F138D"}.mdi-human-white-cane::before{content:"\F1981"}.mdi-humble-bundle::before{content:"\F0744"}.mdi-hvac::before{content:"\F1352"}.mdi-hvac-off::before{content:"\F159E"}.mdi-hydraulic-oil-level::before{content:"\F1324"}.mdi-hydraulic-oil-temperature::before{content:"\F1325"}.mdi-hydro-power::before{content:"\F12E5"}.mdi-hydrogen-station::before{content:"\F1894"}.mdi-ice-cream::before{content:"\F082A"}.mdi-ice-cream-off::before{content:"\F0E52"}.mdi-ice-pop::before{content:"\F0EFD"}.mdi-id-card::before{content:"\F0FC0"}.mdi-identifier::before{content:"\F0EFE"}.mdi-ideogram-cjk::before{content:"\F1331"}.mdi-ideogram-cjk-variant::before{content:"\F1332"}.mdi-image::before{content:"\F02E9"}.mdi-image-album::before{content:"\F02EA"}.mdi-image-area::before{content:"\F02EB"}.mdi-image-area-close::before{content:"\F02EC"}.mdi-image-auto-adjust::before{content:"\F0FC1"}.mdi-image-broken::before{content:"\F02ED"}.mdi-image-broken-variant::before{content:"\F02EE"}.mdi-image-check::before{content:"\F1B25"}.mdi-image-check-outline::before{content:"\F1B26"}.mdi-image-edit::before{content:"\F11E3"}.mdi-image-edit-outline::before{content:"\F11E4"}.mdi-image-filter-black-white::before{content:"\F02F0"}.mdi-image-filter-center-focus::before{content:"\F02F1"}.mdi-image-filter-center-focus-strong::before{content:"\F0EFF"}.mdi-image-filter-center-focus-strong-outline::before{content:"\F0F00"}.mdi-image-filter-center-focus-weak::before{content:"\F02F2"}.mdi-image-filter-drama::before{content:"\F02F3"}.mdi-image-filter-drama-outline::before{content:"\F1BFF"}.mdi-image-filter-frames::before{content:"\F02F4"}.mdi-image-filter-hdr::before{content:"\F02F5"}.mdi-image-filter-hdr-outline::before{content:"\F1C64"}.mdi-image-filter-none::before{content:"\F02F6"}.mdi-image-filter-tilt-shift::before{content:"\F02F7"}.mdi-image-filter-vintage::before{content:"\F02F8"}.mdi-image-frame::before{content:"\F0E49"}.mdi-image-lock::before{content:"\F1AB0"}.mdi-image-lock-outline::before{content:"\F1AB1"}.mdi-image-marker::before{content:"\F177B"}.mdi-image-marker-outline::before{content:"\F177C"}.mdi-image-minus::before{content:"\F1419"}.mdi-image-minus-outline::before{content:"\F1B47"}.mdi-image-move::before{content:"\F09F8"}.mdi-image-multiple::before{content:"\F02F9"}.mdi-image-multiple-outline::before{content:"\F02EF"}.mdi-image-off::before{content:"\F082B"}.mdi-image-off-outline::before{content:"\F11D1"}.mdi-image-outline::before{content:"\F0976"}.mdi-image-plus::before{content:"\F087C"}.mdi-image-plus-outline::before{content:"\F1B46"}.mdi-image-refresh::before{content:"\F19FE"}.mdi-image-refresh-outline::before{content:"\F19FF"}.mdi-image-remove::before{content:"\F1418"}.mdi-image-remove-outline::before{content:"\F1B48"}.mdi-image-search::before{content:"\F0977"}.mdi-image-search-outline::before{content:"\F0978"}.mdi-image-size-select-actual::before{content:"\F0C8D"}.mdi-image-size-select-large::before{content:"\F0C8E"}.mdi-image-size-select-small::before{content:"\F0C8F"}.mdi-image-sync::before{content:"\F1A00"}.mdi-image-sync-outline::before{content:"\F1A01"}.mdi-image-text::before{content:"\F160D"}.mdi-import::before{content:"\F02FA"}.mdi-inbox::before{content:"\F0687"}.mdi-inbox-arrow-down::before{content:"\F02FB"}.mdi-inbox-arrow-down-outline::before{content:"\F1270"}.mdi-inbox-arrow-up::before{content:"\F03D1"}.mdi-inbox-arrow-up-outline::before{content:"\F1271"}.mdi-inbox-full::before{content:"\F1272"}.mdi-inbox-full-outline::before{content:"\F1273"}.mdi-inbox-multiple::before{content:"\F08B0"}.mdi-inbox-multiple-outline::before{content:"\F0BA8"}.mdi-inbox-outline::before{content:"\F1274"}.mdi-inbox-remove::before{content:"\F159F"}.mdi-inbox-remove-outline::before{content:"\F15A0"}.mdi-incognito::before{content:"\F05F9"}.mdi-incognito-circle::before{content:"\F1421"}.mdi-incognito-circle-off::before{content:"\F1422"}.mdi-incognito-off::before{content:"\F0075"}.mdi-induction::before{content:"\F184C"}.mdi-infinity::before{content:"\F06E4"}.mdi-information::before{content:"\F02FC"}.mdi-information-box::before{content:"\F1C65"}.mdi-information-box-outline::before{content:"\F1C66"}.mdi-information-off::before{content:"\F178C"}.mdi-information-off-outline::before{content:"\F178D"}.mdi-information-outline::before{content:"\F02FD"}.mdi-information-slab-box::before{content:"\F1C67"}.mdi-information-slab-box-outline::before{content:"\F1C68"}.mdi-information-slab-circle::before{content:"\F1C69"}.mdi-information-slab-circle-outline::before{content:"\F1C6A"}.mdi-information-slab-symbol::before{content:"\F1C6B"}.mdi-information-symbol::before{content:"\F1C6C"}.mdi-information-variant::before{content:"\F064E"}.mdi-information-variant-box::before{content:"\F1C6D"}.mdi-information-variant-box-outline::before{content:"\F1C6E"}.mdi-information-variant-circle::before{content:"\F1C6F"}.mdi-information-variant-circle-outline::before{content:"\F1C70"}.mdi-instagram::before{content:"\F02FE"}.mdi-instrument-triangle::before{content:"\F104E"}.mdi-integrated-circuit-chip::before{content:"\F1913"}.mdi-invert-colors::before{content:"\F0301"}.mdi-invert-colors-off::before{content:"\F0E4A"}.mdi-invoice::before{content:"\F1CD2"}.mdi-invoice-arrow-left::before{content:"\F1CD3"}.mdi-invoice-arrow-left-outline::before{content:"\F1CD4"}.mdi-invoice-arrow-right::before{content:"\F1CD5"}.mdi-invoice-arrow-right-outline::before{content:"\F1CD6"}.mdi-invoice-check::before{content:"\F1CD7"}.mdi-invoice-check-outline::before{content:"\F1CD8"}.mdi-invoice-clock::before{content:"\F1CD9"}.mdi-invoice-clock-outline::before{content:"\F1CDA"}.mdi-invoice-edit::before{content:"\F1CDB"}.mdi-invoice-edit-outline::before{content:"\F1CDC"}.mdi-invoice-export-outline::before{content:"\F1CDD"}.mdi-invoice-fast::before{content:"\F1CDE"}.mdi-invoice-fast-outline::before{content:"\F1CDF"}.mdi-invoice-import::before{content:"\F1CE0"}.mdi-invoice-import-outline::before{content:"\F1CE1"}.mdi-invoice-list::before{content:"\F1CE2"}.mdi-invoice-list-outline::before{content:"\F1CE3"}.mdi-invoice-minus::before{content:"\F1CE4"}.mdi-invoice-minus-outline::before{content:"\F1CE5"}.mdi-invoice-multiple::before{content:"\F1CE6"}.mdi-invoice-multiple-outline::before{content:"\F1CE7"}.mdi-invoice-outline::before{content:"\F1CE8"}.mdi-invoice-plus::before{content:"\F1CE9"}.mdi-invoice-plus-outline::before{content:"\F1CEA"}.mdi-invoice-remove::before{content:"\F1CEB"}.mdi-invoice-remove-outline::before{content:"\F1CEC"}.mdi-invoice-send::before{content:"\F1CED"}.mdi-invoice-send-outline::before{content:"\F1CEE"}.mdi-invoice-text::before{content:"\F1CEF"}.mdi-invoice-text-arrow-left::before{content:"\F1CF0"}.mdi-invoice-text-arrow-left-outline::before{content:"\F1CF1"}.mdi-invoice-text-arrow-right::before{content:"\F1CF2"}.mdi-invoice-text-arrow-right-outline::before{content:"\F1CF3"}.mdi-invoice-text-check::before{content:"\F1CF4"}.mdi-invoice-text-check-outline::before{content:"\F1CF5"}.mdi-invoice-text-clock::before{content:"\F1CF6"}.mdi-invoice-text-clock-outline::before{content:"\F1CF7"}.mdi-invoice-text-edit::before{content:"\F1CF8"}.mdi-invoice-text-edit-outline::before{content:"\F1CF9"}.mdi-invoice-text-fast::before{content:"\F1CFA"}.mdi-invoice-text-fast-outline::before{content:"\F1CFB"}.mdi-invoice-text-minus::before{content:"\F1CFC"}.mdi-invoice-text-minus-outline::before{content:"\F1CFD"}.mdi-invoice-text-multiple::before{content:"\F1CFE"}.mdi-invoice-text-multiple-outline::before{content:"\F1CFF"}.mdi-invoice-text-outline::before{content:"\F1D00"}.mdi-invoice-text-plus::before{content:"\F1D01"}.mdi-invoice-text-plus-outline::before{content:"\F1D02"}.mdi-invoice-text-remove::before{content:"\F1D03"}.mdi-invoice-text-remove-outline::before{content:"\F1D04"}.mdi-invoice-text-send::before{content:"\F1D05"}.mdi-invoice-text-send-outline::before{content:"\F1D06"}.mdi-iobroker::before{content:"\F12E8"}.mdi-ip::before{content:"\F0A5F"}.mdi-ip-network::before{content:"\F0A60"}.mdi-ip-network-outline::before{content:"\F0C90"}.mdi-ip-outline::before{content:"\F1982"}.mdi-ipod::before{content:"\F0C91"}.mdi-iron::before{content:"\F1824"}.mdi-iron-board::before{content:"\F1838"}.mdi-iron-outline::before{content:"\F1825"}.mdi-island::before{content:"\F104F"}.mdi-island-variant::before{content:"\F1CC6"}.mdi-iv-bag::before{content:"\F10B9"}.mdi-jabber::before{content:"\F0DD5"}.mdi-jeepney::before{content:"\F0302"}.mdi-jellyfish::before{content:"\F0F01"}.mdi-jellyfish-outline::before{content:"\F0F02"}.mdi-jira::before{content:"\F0303"}.mdi-jquery::before{content:"\F087D"}.mdi-jsfiddle::before{content:"\F0304"}.mdi-jump-rope::before{content:"\F12FF"}.mdi-kabaddi::before{content:"\F0D87"}.mdi-kangaroo::before{content:"\F1558"}.mdi-karate::before{content:"\F082C"}.mdi-kayaking::before{content:"\F08AF"}.mdi-keg::before{content:"\F0305"}.mdi-kettle::before{content:"\F05FA"}.mdi-kettle-alert::before{content:"\F1317"}.mdi-kettle-alert-outline::before{content:"\F1318"}.mdi-kettle-off::before{content:"\F131B"}.mdi-kettle-off-outline::before{content:"\F131C"}.mdi-kettle-outline::before{content:"\F0F56"}.mdi-kettle-pour-over::before{content:"\F173C"}.mdi-kettle-steam::before{content:"\F1319"}.mdi-kettle-steam-outline::before{content:"\F131A"}.mdi-kettlebell::before{content:"\F1300"}.mdi-key::before{content:"\F0306"}.mdi-key-alert::before{content:"\F1983"}.mdi-key-alert-outline::before{content:"\F1984"}.mdi-key-arrow-right::before{content:"\F1312"}.mdi-key-chain::before{content:"\F1574"}.mdi-key-chain-variant::before{content:"\F1575"}.mdi-key-change::before{content:"\F0307"}.mdi-key-link::before{content:"\F119F"}.mdi-key-minus::before{content:"\F0308"}.mdi-key-outline::before{content:"\F0DD6"}.mdi-key-plus::before{content:"\F0309"}.mdi-key-remove::before{content:"\F030A"}.mdi-key-star::before{content:"\F119E"}.mdi-key-variant::before{content:"\F030B"}.mdi-key-wireless::before{content:"\F0FC2"}.mdi-keyboard::before{content:"\F030C"}.mdi-keyboard-backspace::before{content:"\F030D"}.mdi-keyboard-caps::before{content:"\F030E"}.mdi-keyboard-close::before{content:"\F030F"}.mdi-keyboard-close-outline::before{content:"\F1C00"}.mdi-keyboard-esc::before{content:"\F12B7"}.mdi-keyboard-f1::before{content:"\F12AB"}.mdi-keyboard-f10::before{content:"\F12B4"}.mdi-keyboard-f11::before{content:"\F12B5"}.mdi-keyboard-f12::before{content:"\F12B6"}.mdi-keyboard-f2::before{content:"\F12AC"}.mdi-keyboard-f3::before{content:"\F12AD"}.mdi-keyboard-f4::before{content:"\F12AE"}.mdi-keyboard-f5::before{content:"\F12AF"}.mdi-keyboard-f6::before{content:"\F12B0"}.mdi-keyboard-f7::before{content:"\F12B1"}.mdi-keyboard-f8::before{content:"\F12B2"}.mdi-keyboard-f9::before{content:"\F12B3"}.mdi-keyboard-off::before{content:"\F0310"}.mdi-keyboard-off-outline::before{content:"\F0E4B"}.mdi-keyboard-outline::before{content:"\F097B"}.mdi-keyboard-return::before{content:"\F0311"}.mdi-keyboard-settings::before{content:"\F09F9"}.mdi-keyboard-settings-outline::before{content:"\F09FA"}.mdi-keyboard-space::before{content:"\F1050"}.mdi-keyboard-tab::before{content:"\F0312"}.mdi-keyboard-tab-reverse::before{content:"\F0325"}.mdi-keyboard-variant::before{content:"\F0313"}.mdi-khanda::before{content:"\F10FD"}.mdi-kickstarter::before{content:"\F0745"}.mdi-kite::before{content:"\F1985"}.mdi-kite-outline::before{content:"\F1986"}.mdi-kitesurfing::before{content:"\F1744"}.mdi-klingon::before{content:"\F135B"}.mdi-knife::before{content:"\F09FB"}.mdi-knife-military::before{content:"\F09FC"}.mdi-knob::before{content:"\F1B96"}.mdi-koala::before{content:"\F173F"}.mdi-kodi::before{content:"\F0314"}.mdi-kubernetes::before{content:"\F10FE"}.mdi-label::before{content:"\F0315"}.mdi-label-multiple::before{content:"\F1375"}.mdi-label-multiple-outline::before{content:"\F1376"}.mdi-label-off::before{content:"\F0ACB"}.mdi-label-off-outline::before{content:"\F0ACC"}.mdi-label-outline::before{content:"\F0316"}.mdi-label-percent::before{content:"\F12EA"}.mdi-label-percent-outline::before{content:"\F12EB"}.mdi-label-variant::before{content:"\F0ACD"}.mdi-label-variant-outline::before{content:"\F0ACE"}.mdi-ladder::before{content:"\F15A2"}.mdi-ladybug::before{content:"\F082D"}.mdi-lambda::before{content:"\F0627"}.mdi-lamp::before{content:"\F06B5"}.mdi-lamp-outline::before{content:"\F17D0"}.mdi-lamps::before{content:"\F1576"}.mdi-lamps-outline::before{content:"\F17D1"}.mdi-lan::before{content:"\F0317"}.mdi-lan-check::before{content:"\F12AA"}.mdi-lan-connect::before{content:"\F0318"}.mdi-lan-disconnect::before{content:"\F0319"}.mdi-lan-pending::before{content:"\F031A"}.mdi-land-fields::before{content:"\F1AB2"}.mdi-land-plots::before{content:"\F1AB3"}.mdi-land-plots-circle::before{content:"\F1AB4"}.mdi-land-plots-circle-variant::before{content:"\F1AB5"}.mdi-land-plots-marker::before{content:"\F1C5D"}.mdi-land-rows-horizontal::before{content:"\F1AB6"}.mdi-land-rows-vertical::before{content:"\F1AB7"}.mdi-landslide::before{content:"\F1A48"}.mdi-landslide-outline::before{content:"\F1A49"}.mdi-language-c::before{content:"\F0671"}.mdi-language-cpp::before{content:"\F0672"}.mdi-language-csharp::before{content:"\F031B"}.mdi-language-css3::before{content:"\F031C"}.mdi-language-fortran::before{content:"\F121A"}.mdi-language-go::before{content:"\F07D3"}.mdi-language-haskell::before{content:"\F0C92"}.mdi-language-html5::before{content:"\F031D"}.mdi-language-java::before{content:"\F0B37"}.mdi-language-javascript::before{content:"\F031E"}.mdi-language-kotlin::before{content:"\F1219"}.mdi-language-lua::before{content:"\F08B1"}.mdi-language-markdown::before{content:"\F0354"}.mdi-language-markdown-outline::before{content:"\F0F5B"}.mdi-language-php::before{content:"\F031F"}.mdi-language-python::before{content:"\F0320"}.mdi-language-r::before{content:"\F07D4"}.mdi-language-ruby::before{content:"\F0D2D"}.mdi-language-ruby-on-rails::before{content:"\F0ACF"}.mdi-language-rust::before{content:"\F1617"}.mdi-language-swift::before{content:"\F06E5"}.mdi-language-typescript::before{content:"\F06E6"}.mdi-language-xaml::before{content:"\F0673"}.mdi-laptop::before{content:"\F0322"}.mdi-laptop-account::before{content:"\F1A4A"}.mdi-laptop-off::before{content:"\F06E7"}.mdi-laravel::before{content:"\F0AD0"}.mdi-laser-pointer::before{content:"\F1484"}.mdi-lasso::before{content:"\F0F03"}.mdi-lastpass::before{content:"\F0446"}.mdi-latitude::before{content:"\F0F57"}.mdi-launch::before{content:"\F0327"}.mdi-lava-lamp::before{content:"\F07D5"}.mdi-layers::before{content:"\F0328"}.mdi-layers-edit::before{content:"\F1892"}.mdi-layers-minus::before{content:"\F0E4C"}.mdi-layers-off::before{content:"\F0329"}.mdi-layers-off-outline::before{content:"\F09FD"}.mdi-layers-outline::before{content:"\F09FE"}.mdi-layers-plus::before{content:"\F0E4D"}.mdi-layers-remove::before{content:"\F0E4E"}.mdi-layers-search::before{content:"\F1206"}.mdi-layers-search-outline::before{content:"\F1207"}.mdi-layers-triple::before{content:"\F0F58"}.mdi-layers-triple-outline::before{content:"\F0F59"}.mdi-lead-pencil::before{content:"\F064F"}.mdi-leaf::before{content:"\F032A"}.mdi-leaf-circle::before{content:"\F1905"}.mdi-leaf-circle-outline::before{content:"\F1906"}.mdi-leaf-maple::before{content:"\F0C93"}.mdi-leaf-maple-off::before{content:"\F12DA"}.mdi-leaf-off::before{content:"\F12D9"}.mdi-leak::before{content:"\F0DD7"}.mdi-leak-off::before{content:"\F0DD8"}.mdi-lectern::before{content:"\F1AF0"}.mdi-led-off::before{content:"\F032B"}.mdi-led-on::before{content:"\F032C"}.mdi-led-outline::before{content:"\F032D"}.mdi-led-strip::before{content:"\F07D6"}.mdi-led-strip-variant::before{content:"\F1051"}.mdi-led-strip-variant-off::before{content:"\F1A4B"}.mdi-led-variant-off::before{content:"\F032E"}.mdi-led-variant-on::before{content:"\F032F"}.mdi-led-variant-outline::before{content:"\F0330"}.mdi-leek::before{content:"\F117D"}.mdi-less-than::before{content:"\F097C"}.mdi-less-than-or-equal::before{content:"\F097D"}.mdi-library::before{content:"\F0331"}.mdi-library-outline::before{content:"\F1A22"}.mdi-library-shelves::before{content:"\F0BA9"}.mdi-license::before{content:"\F0FC3"}.mdi-lifebuoy::before{content:"\F087E"}.mdi-light-flood-down::before{content:"\F1987"}.mdi-light-flood-up::before{content:"\F1988"}.mdi-light-recessed::before{content:"\F179B"}.mdi-light-switch::before{content:"\F097E"}.mdi-light-switch-off::before{content:"\F1A24"}.mdi-lightbulb::before{content:"\F0335"}.mdi-lightbulb-alert::before{content:"\F19E1"}.mdi-lightbulb-alert-outline::before{content:"\F19E2"}.mdi-lightbulb-auto::before{content:"\F1800"}.mdi-lightbulb-auto-outline::before{content:"\F1801"}.mdi-lightbulb-cfl::before{content:"\F1208"}.mdi-lightbulb-cfl-off::before{content:"\F1209"}.mdi-lightbulb-cfl-spiral::before{content:"\F1275"}.mdi-lightbulb-cfl-spiral-off::before{content:"\F12C3"}.mdi-lightbulb-fluorescent-tube::before{content:"\F1804"}.mdi-lightbulb-fluorescent-tube-outline::before{content:"\F1805"}.mdi-lightbulb-group::before{content:"\F1253"}.mdi-lightbulb-group-off::before{content:"\F12CD"}.mdi-lightbulb-group-off-outline::before{content:"\F12CE"}.mdi-lightbulb-group-outline::before{content:"\F1254"}.mdi-lightbulb-multiple::before{content:"\F1255"}.mdi-lightbulb-multiple-off::before{content:"\F12CF"}.mdi-lightbulb-multiple-off-outline::before{content:"\F12D0"}.mdi-lightbulb-multiple-outline::before{content:"\F1256"}.mdi-lightbulb-night::before{content:"\F1A4C"}.mdi-lightbulb-night-outline::before{content:"\F1A4D"}.mdi-lightbulb-off::before{content:"\F0E4F"}.mdi-lightbulb-off-outline::before{content:"\F0E50"}.mdi-lightbulb-on::before{content:"\F06E8"}.mdi-lightbulb-on-10::before{content:"\F1A4E"}.mdi-lightbulb-on-20::before{content:"\F1A4F"}.mdi-lightbulb-on-30::before{content:"\F1A50"}.mdi-lightbulb-on-40::before{content:"\F1A51"}.mdi-lightbulb-on-50::before{content:"\F1A52"}.mdi-lightbulb-on-60::before{content:"\F1A53"}.mdi-lightbulb-on-70::before{content:"\F1A54"}.mdi-lightbulb-on-80::before{content:"\F1A55"}.mdi-lightbulb-on-90::before{content:"\F1A56"}.mdi-lightbulb-on-outline::before{content:"\F06E9"}.mdi-lightbulb-outline::before{content:"\F0336"}.mdi-lightbulb-question::before{content:"\F19E3"}.mdi-lightbulb-question-outline::before{content:"\F19E4"}.mdi-lightbulb-spot::before{content:"\F17F4"}.mdi-lightbulb-spot-off::before{content:"\F17F5"}.mdi-lightbulb-variant::before{content:"\F1802"}.mdi-lightbulb-variant-outline::before{content:"\F1803"}.mdi-lighthouse::before{content:"\F09FF"}.mdi-lighthouse-on::before{content:"\F0A00"}.mdi-lightning-bolt::before{content:"\F140B"}.mdi-lightning-bolt-circle::before{content:"\F0820"}.mdi-lightning-bolt-outline::before{content:"\F140C"}.mdi-line-scan::before{content:"\F0624"}.mdi-lingerie::before{content:"\F1476"}.mdi-link::before{content:"\F0337"}.mdi-link-box::before{content:"\F0D1A"}.mdi-link-box-outline::before{content:"\F0D1B"}.mdi-link-box-variant::before{content:"\F0D1C"}.mdi-link-box-variant-outline::before{content:"\F0D1D"}.mdi-link-circle::before{content:"\F1CAC"}.mdi-link-circle-outline::before{content:"\F1CAD"}.mdi-link-edit::before{content:"\F1CAE"}.mdi-link-lock::before{content:"\F10BA"}.mdi-link-off::before{content:"\F0338"}.mdi-link-plus::before{content:"\F0C94"}.mdi-link-variant::before{content:"\F0339"}.mdi-link-variant-minus::before{content:"\F10FF"}.mdi-link-variant-off::before{content:"\F033A"}.mdi-link-variant-plus::before{content:"\F1100"}.mdi-link-variant-remove::before{content:"\F1101"}.mdi-linkedin::before{content:"\F033B"}.mdi-linux::before{content:"\F033D"}.mdi-linux-mint::before{content:"\F08ED"}.mdi-lipstick::before{content:"\F13B5"}.mdi-liquid-spot::before{content:"\F1826"}.mdi-liquor::before{content:"\F191E"}.mdi-list-box::before{content:"\F1B7B"}.mdi-list-box-outline::before{content:"\F1B7C"}.mdi-list-status::before{content:"\F15AB"}.mdi-litecoin::before{content:"\F0A61"}.mdi-loading::before{content:"\F0772"}.mdi-location-enter::before{content:"\F0FC4"}.mdi-location-exit::before{content:"\F0FC5"}.mdi-lock::before{content:"\F033E"}.mdi-lock-alert::before{content:"\F08EE"}.mdi-lock-alert-outline::before{content:"\F15D1"}.mdi-lock-check::before{content:"\F139A"}.mdi-lock-check-outline::before{content:"\F16A8"}.mdi-lock-clock::before{content:"\F097F"}.mdi-lock-minus::before{content:"\F16A9"}.mdi-lock-minus-outline::before{content:"\F16AA"}.mdi-lock-off::before{content:"\F1671"}.mdi-lock-off-outline::before{content:"\F1672"}.mdi-lock-open::before{content:"\F033F"}.mdi-lock-open-alert::before{content:"\F139B"}.mdi-lock-open-alert-outline::before{content:"\F15D2"}.mdi-lock-open-check::before{content:"\F139C"}.mdi-lock-open-check-outline::before{content:"\F16AB"}.mdi-lock-open-minus::before{content:"\F16AC"}.mdi-lock-open-minus-outline::before{content:"\F16AD"}.mdi-lock-open-outline::before{content:"\F0340"}.mdi-lock-open-plus::before{content:"\F16AE"}.mdi-lock-open-plus-outline::before{content:"\F16AF"}.mdi-lock-open-remove::before{content:"\F16B0"}.mdi-lock-open-remove-outline::before{content:"\F16B1"}.mdi-lock-open-variant::before{content:"\F0FC6"}.mdi-lock-open-variant-outline::before{content:"\F0FC7"}.mdi-lock-outline::before{content:"\F0341"}.mdi-lock-pattern::before{content:"\F06EA"}.mdi-lock-percent::before{content:"\F1C12"}.mdi-lock-percent-open::before{content:"\F1C13"}.mdi-lock-percent-open-outline::before{content:"\F1C14"}.mdi-lock-percent-open-variant::before{content:"\F1C15"}.mdi-lock-percent-open-variant-outline::before{content:"\F1C16"}.mdi-lock-percent-outline::before{content:"\F1C17"}.mdi-lock-plus::before{content:"\F05FB"}.mdi-lock-plus-outline::before{content:"\F16B2"}.mdi-lock-question::before{content:"\F08EF"}.mdi-lock-remove::before{content:"\F16B3"}.mdi-lock-remove-outline::before{content:"\F16B4"}.mdi-lock-reset::before{content:"\F0773"}.mdi-lock-smart::before{content:"\F08B2"}.mdi-locker::before{content:"\F07D7"}.mdi-locker-multiple::before{content:"\F07D8"}.mdi-login::before{content:"\F0342"}.mdi-login-variant::before{content:"\F05FC"}.mdi-logout::before{content:"\F0343"}.mdi-logout-variant::before{content:"\F05FD"}.mdi-longitude::before{content:"\F0F5A"}.mdi-looks::before{content:"\F0344"}.mdi-lotion::before{content:"\F1582"}.mdi-lotion-outline::before{content:"\F1583"}.mdi-lotion-plus::before{content:"\F1584"}.mdi-lotion-plus-outline::before{content:"\F1585"}.mdi-loupe::before{content:"\F0345"}.mdi-lumx::before{content:"\F0346"}.mdi-lungs::before{content:"\F1084"}.mdi-mace::before{content:"\F1843"}.mdi-magazine-pistol::before{content:"\F0324"}.mdi-magazine-rifle::before{content:"\F0323"}.mdi-magic-staff::before{content:"\F1844"}.mdi-magnet::before{content:"\F0347"}.mdi-magnet-on::before{content:"\F0348"}.mdi-magnify::before{content:"\F0349"}.mdi-magnify-close::before{content:"\F0980"}.mdi-magnify-expand::before{content:"\F1874"}.mdi-magnify-minus::before{content:"\F034A"}.mdi-magnify-minus-cursor::before{content:"\F0A62"}.mdi-magnify-minus-outline::before{content:"\F06EC"}.mdi-magnify-plus::before{content:"\F034B"}.mdi-magnify-plus-cursor::before{content:"\F0A63"}.mdi-magnify-plus-outline::before{content:"\F06ED"}.mdi-magnify-remove-cursor::before{content:"\F120C"}.mdi-magnify-remove-outline::before{content:"\F120D"}.mdi-magnify-scan::before{content:"\F1276"}.mdi-mail::before{content:"\F0EBB"}.mdi-mailbox::before{content:"\F06EE"}.mdi-mailbox-open::before{content:"\F0D88"}.mdi-mailbox-open-outline::before{content:"\F0D89"}.mdi-mailbox-open-up::before{content:"\F0D8A"}.mdi-mailbox-open-up-outline::before{content:"\F0D8B"}.mdi-mailbox-outline::before{content:"\F0D8C"}.mdi-mailbox-up::before{content:"\F0D8D"}.mdi-mailbox-up-outline::before{content:"\F0D8E"}.mdi-manjaro::before{content:"\F160A"}.mdi-map::before{content:"\F034D"}.mdi-map-check::before{content:"\F0EBC"}.mdi-map-check-outline::before{content:"\F0EBD"}.mdi-map-clock::before{content:"\F0D1E"}.mdi-map-clock-outline::before{content:"\F0D1F"}.mdi-map-legend::before{content:"\F0A01"}.mdi-map-marker::before{content:"\F034E"}.mdi-map-marker-account::before{content:"\F18E3"}.mdi-map-marker-account-outline::before{content:"\F18E4"}.mdi-map-marker-alert::before{content:"\F0F05"}.mdi-map-marker-alert-outline::before{content:"\F0F06"}.mdi-map-marker-check::before{content:"\F0C95"}.mdi-map-marker-check-outline::before{content:"\F12FB"}.mdi-map-marker-circle::before{content:"\F034F"}.mdi-map-marker-distance::before{content:"\F08F0"}.mdi-map-marker-down::before{content:"\F1102"}.mdi-map-marker-left::before{content:"\F12DB"}.mdi-map-marker-left-outline::before{content:"\F12DD"}.mdi-map-marker-minus::before{content:"\F0650"}.mdi-map-marker-minus-outline::before{content:"\F12F9"}.mdi-map-marker-multiple::before{content:"\F0350"}.mdi-map-marker-multiple-outline::before{content:"\F1277"}.mdi-map-marker-off::before{content:"\F0351"}.mdi-map-marker-off-outline::before{content:"\F12FD"}.mdi-map-marker-outline::before{content:"\F07D9"}.mdi-map-marker-path::before{content:"\F0D20"}.mdi-map-marker-plus::before{content:"\F0651"}.mdi-map-marker-plus-outline::before{content:"\F12F8"}.mdi-map-marker-question::before{content:"\F0F07"}.mdi-map-marker-question-outline::before{content:"\F0F08"}.mdi-map-marker-radius::before{content:"\F0352"}.mdi-map-marker-radius-outline::before{content:"\F12FC"}.mdi-map-marker-remove::before{content:"\F0F09"}.mdi-map-marker-remove-outline::before{content:"\F12FA"}.mdi-map-marker-remove-variant::before{content:"\F0F0A"}.mdi-map-marker-right::before{content:"\F12DC"}.mdi-map-marker-right-outline::before{content:"\F12DE"}.mdi-map-marker-star::before{content:"\F1608"}.mdi-map-marker-star-outline::before{content:"\F1609"}.mdi-map-marker-up::before{content:"\F1103"}.mdi-map-minus::before{content:"\F0981"}.mdi-map-outline::before{content:"\F0982"}.mdi-map-plus::before{content:"\F0983"}.mdi-map-search::before{content:"\F0984"}.mdi-map-search-outline::before{content:"\F0985"}.mdi-mapbox::before{content:"\F0BAA"}.mdi-margin::before{content:"\F0353"}.mdi-marker::before{content:"\F0652"}.mdi-marker-cancel::before{content:"\F0DD9"}.mdi-marker-check::before{content:"\F0355"}.mdi-mastodon::before{content:"\F0AD1"}.mdi-material-design::before{content:"\F0986"}.mdi-material-ui::before{content:"\F0357"}.mdi-math-compass::before{content:"\F0358"}.mdi-math-cos::before{content:"\F0C96"}.mdi-math-integral::before{content:"\F0FC8"}.mdi-math-integral-box::before{content:"\F0FC9"}.mdi-math-log::before{content:"\F1085"}.mdi-math-norm::before{content:"\F0FCA"}.mdi-math-norm-box::before{content:"\F0FCB"}.mdi-math-sin::before{content:"\F0C97"}.mdi-math-tan::before{content:"\F0C98"}.mdi-matrix::before{content:"\F0628"}.mdi-medal::before{content:"\F0987"}.mdi-medal-outline::before{content:"\F1326"}.mdi-medical-bag::before{content:"\F06EF"}.mdi-medical-cotton-swab::before{content:"\F1AB8"}.mdi-medication::before{content:"\F1B14"}.mdi-medication-outline::before{content:"\F1B15"}.mdi-meditation::before{content:"\F117B"}.mdi-memory::before{content:"\F035B"}.mdi-memory-arrow-down::before{content:"\F1CA6"}.mdi-menorah::before{content:"\F17D4"}.mdi-menorah-fire::before{content:"\F17D5"}.mdi-menu::before{content:"\F035C"}.mdi-menu-close::before{content:"\F1C90"}.mdi-menu-down::before{content:"\F035D"}.mdi-menu-down-outline::before{content:"\F06B6"}.mdi-menu-left::before{content:"\F035E"}.mdi-menu-left-outline::before{content:"\F0A02"}.mdi-menu-open::before{content:"\F0BAB"}.mdi-menu-right::before{content:"\F035F"}.mdi-menu-right-outline::before{content:"\F0A03"}.mdi-menu-swap::before{content:"\F0A64"}.mdi-menu-swap-outline::before{content:"\F0A65"}.mdi-menu-up::before{content:"\F0360"}.mdi-menu-up-outline::before{content:"\F06B7"}.mdi-merge::before{content:"\F0F5C"}.mdi-message::before{content:"\F0361"}.mdi-message-alert::before{content:"\F0362"}.mdi-message-alert-outline::before{content:"\F0A04"}.mdi-message-arrow-left::before{content:"\F12F2"}.mdi-message-arrow-left-outline::before{content:"\F12F3"}.mdi-message-arrow-right::before{content:"\F12F4"}.mdi-message-arrow-right-outline::before{content:"\F12F5"}.mdi-message-badge::before{content:"\F1941"}.mdi-message-badge-outline::before{content:"\F1942"}.mdi-message-bookmark::before{content:"\F15AC"}.mdi-message-bookmark-outline::before{content:"\F15AD"}.mdi-message-bulleted::before{content:"\F06A2"}.mdi-message-bulleted-off::before{content:"\F06A3"}.mdi-message-check::before{content:"\F1B8A"}.mdi-message-check-outline::before{content:"\F1B8B"}.mdi-message-cog::before{content:"\F06F1"}.mdi-message-cog-outline::before{content:"\F1172"}.mdi-message-draw::before{content:"\F0363"}.mdi-message-fast::before{content:"\F19CC"}.mdi-message-fast-outline::before{content:"\F19CD"}.mdi-message-flash::before{content:"\F15A9"}.mdi-message-flash-outline::before{content:"\F15AA"}.mdi-message-image::before{content:"\F0364"}.mdi-message-image-outline::before{content:"\F116C"}.mdi-message-lock::before{content:"\F0FCC"}.mdi-message-lock-outline::before{content:"\F116D"}.mdi-message-minus::before{content:"\F116E"}.mdi-message-minus-outline::before{content:"\F116F"}.mdi-message-off::before{content:"\F164D"}.mdi-message-off-outline::before{content:"\F164E"}.mdi-message-outline::before{content:"\F0365"}.mdi-message-plus::before{content:"\F0653"}.mdi-message-plus-outline::before{content:"\F10BB"}.mdi-message-processing::before{content:"\F0366"}.mdi-message-processing-outline::before{content:"\F1170"}.mdi-message-question::before{content:"\F173A"}.mdi-message-question-outline::before{content:"\F173B"}.mdi-message-reply::before{content:"\F0367"}.mdi-message-reply-outline::before{content:"\F173D"}.mdi-message-reply-text::before{content:"\F0368"}.mdi-message-reply-text-outline::before{content:"\F173E"}.mdi-message-settings::before{content:"\F06F0"}.mdi-message-settings-outline::before{content:"\F1171"}.mdi-message-star::before{content:"\F069A"}.mdi-message-star-outline::before{content:"\F1250"}.mdi-message-text::before{content:"\F0369"}.mdi-message-text-clock::before{content:"\F1173"}.mdi-message-text-clock-outline::before{content:"\F1174"}.mdi-message-text-fast::before{content:"\F19CE"}.mdi-message-text-fast-outline::before{content:"\F19CF"}.mdi-message-text-lock::before{content:"\F0FCD"}.mdi-message-text-lock-outline::before{content:"\F1175"}.mdi-message-text-outline::before{content:"\F036A"}.mdi-message-video::before{content:"\F036B"}.mdi-meteor::before{content:"\F0629"}.mdi-meter-electric::before{content:"\F1A57"}.mdi-meter-electric-outline::before{content:"\F1A58"}.mdi-meter-gas::before{content:"\F1A59"}.mdi-meter-gas-outline::before{content:"\F1A5A"}.mdi-metronome::before{content:"\F07DA"}.mdi-metronome-tick::before{content:"\F07DB"}.mdi-micro-sd::before{content:"\F07DC"}.mdi-microphone::before{content:"\F036C"}.mdi-microphone-message::before{content:"\F050A"}.mdi-microphone-message-off::before{content:"\F050B"}.mdi-microphone-minus::before{content:"\F08B3"}.mdi-microphone-off::before{content:"\F036D"}.mdi-microphone-outline::before{content:"\F036E"}.mdi-microphone-plus::before{content:"\F08B4"}.mdi-microphone-question::before{content:"\F1989"}.mdi-microphone-question-outline::before{content:"\F198A"}.mdi-microphone-settings::before{content:"\F036F"}.mdi-microphone-variant::before{content:"\F0370"}.mdi-microphone-variant-off::before{content:"\F0371"}.mdi-microscope::before{content:"\F0654"}.mdi-microsoft::before{content:"\F0372"}.mdi-microsoft-access::before{content:"\F138E"}.mdi-microsoft-azure::before{content:"\F0805"}.mdi-microsoft-azure-devops::before{content:"\F0FD5"}.mdi-microsoft-bing::before{content:"\F00A4"}.mdi-microsoft-dynamics-365::before{content:"\F0988"}.mdi-microsoft-edge::before{content:"\F01E9"}.mdi-microsoft-excel::before{content:"\F138F"}.mdi-microsoft-internet-explorer::before{content:"\F0300"}.mdi-microsoft-office::before{content:"\F03C6"}.mdi-microsoft-onedrive::before{content:"\F03CA"}.mdi-microsoft-onenote::before{content:"\F0747"}.mdi-microsoft-outlook::before{content:"\F0D22"}.mdi-microsoft-powerpoint::before{content:"\F1390"}.mdi-microsoft-sharepoint::before{content:"\F1391"}.mdi-microsoft-teams::before{content:"\F02BB"}.mdi-microsoft-visual-studio::before{content:"\F0610"}.mdi-microsoft-visual-studio-code::before{content:"\F0A1E"}.mdi-microsoft-windows::before{content:"\F05B3"}.mdi-microsoft-windows-classic::before{content:"\F0A21"}.mdi-microsoft-word::before{content:"\F1392"}.mdi-microsoft-xbox::before{content:"\F05B9"}.mdi-microsoft-xbox-controller::before{content:"\F05BA"}.mdi-microsoft-xbox-controller-battery-alert::before{content:"\F074B"}.mdi-microsoft-xbox-controller-battery-charging::before{content:"\F0A22"}.mdi-microsoft-xbox-controller-battery-empty::before{content:"\F074C"}.mdi-microsoft-xbox-controller-battery-full::before{content:"\F074D"}.mdi-microsoft-xbox-controller-battery-low::before{content:"\F074E"}.mdi-microsoft-xbox-controller-battery-medium::before{content:"\F074F"}.mdi-microsoft-xbox-controller-battery-unknown::before{content:"\F0750"}.mdi-microsoft-xbox-controller-menu::before{content:"\F0E6F"}.mdi-microsoft-xbox-controller-off::before{content:"\F05BB"}.mdi-microsoft-xbox-controller-view::before{content:"\F0E70"}.mdi-microwave::before{content:"\F0C99"}.mdi-microwave-off::before{content:"\F1423"}.mdi-middleware::before{content:"\F0F5D"}.mdi-middleware-outline::before{content:"\F0F5E"}.mdi-midi::before{content:"\F08F1"}.mdi-midi-port::before{content:"\F08F2"}.mdi-mine::before{content:"\F0DDA"}.mdi-minecraft::before{content:"\F0373"}.mdi-mini-sd::before{content:"\F0A05"}.mdi-minidisc::before{content:"\F0A06"}.mdi-minus::before{content:"\F0374"}.mdi-minus-box::before{content:"\F0375"}.mdi-minus-box-multiple::before{content:"\F1141"}.mdi-minus-box-multiple-outline::before{content:"\F1142"}.mdi-minus-box-outline::before{content:"\F06F2"}.mdi-minus-circle::before{content:"\F0376"}.mdi-minus-circle-multiple::before{content:"\F035A"}.mdi-minus-circle-multiple-outline::before{content:"\F0AD3"}.mdi-minus-circle-off::before{content:"\F1459"}.mdi-minus-circle-off-outline::before{content:"\F145A"}.mdi-minus-circle-outline::before{content:"\F0377"}.mdi-minus-network::before{content:"\F0378"}.mdi-minus-network-outline::before{content:"\F0C9A"}.mdi-minus-thick::before{content:"\F1639"}.mdi-mirror::before{content:"\F11FD"}.mdi-mirror-rectangle::before{content:"\F179F"}.mdi-mirror-variant::before{content:"\F17A0"}.mdi-mixed-martial-arts::before{content:"\F0D8F"}.mdi-mixed-reality::before{content:"\F087F"}.mdi-molecule::before{content:"\F0BAC"}.mdi-molecule-co::before{content:"\F12FE"}.mdi-molecule-co2::before{content:"\F07E4"}.mdi-monitor::before{content:"\F0379"}.mdi-monitor-account::before{content:"\F1A5B"}.mdi-monitor-arrow-down::before{content:"\F19D0"}.mdi-monitor-arrow-down-variant::before{content:"\F19D1"}.mdi-monitor-cellphone::before{content:"\F0989"}.mdi-monitor-cellphone-star::before{content:"\F098A"}.mdi-monitor-dashboard::before{content:"\F0A07"}.mdi-monitor-edit::before{content:"\F12C6"}.mdi-monitor-eye::before{content:"\F13B4"}.mdi-monitor-lock::before{content:"\F0DDB"}.mdi-monitor-multiple::before{content:"\F037A"}.mdi-monitor-off::before{content:"\F0D90"}.mdi-monitor-screenshot::before{content:"\F0E51"}.mdi-monitor-share::before{content:"\F1483"}.mdi-monitor-shimmer::before{content:"\F1104"}.mdi-monitor-small::before{content:"\F1876"}.mdi-monitor-speaker::before{content:"\F0F5F"}.mdi-monitor-speaker-off::before{content:"\F0F60"}.mdi-monitor-star::before{content:"\F0DDC"}.mdi-monitor-vertical::before{content:"\F1C33"}.mdi-moon-first-quarter::before{content:"\F0F61"}.mdi-moon-full::before{content:"\F0F62"}.mdi-moon-last-quarter::before{content:"\F0F63"}.mdi-moon-new::before{content:"\F0F64"}.mdi-moon-waning-crescent::before{content:"\F0F65"}.mdi-moon-waning-gibbous::before{content:"\F0F66"}.mdi-moon-waxing-crescent::before{content:"\F0F67"}.mdi-moon-waxing-gibbous::before{content:"\F0F68"}.mdi-moped::before{content:"\F1086"}.mdi-moped-electric::before{content:"\F15B7"}.mdi-moped-electric-outline::before{content:"\F15B8"}.mdi-moped-outline::before{content:"\F15B9"}.mdi-more::before{content:"\F037B"}.mdi-mortar-pestle::before{content:"\F1748"}.mdi-mortar-pestle-plus::before{content:"\F03F1"}.mdi-mosque::before{content:"\F0D45"}.mdi-mosque-outline::before{content:"\F1827"}.mdi-mother-heart::before{content:"\F1314"}.mdi-mother-nurse::before{content:"\F0D21"}.mdi-motion::before{content:"\F15B2"}.mdi-motion-outline::before{content:"\F15B3"}.mdi-motion-pause::before{content:"\F1590"}.mdi-motion-pause-outline::before{content:"\F1592"}.mdi-motion-play::before{content:"\F158F"}.mdi-motion-play-outline::before{content:"\F1591"}.mdi-motion-sensor::before{content:"\F0D91"}.mdi-motion-sensor-off::before{content:"\F1435"}.mdi-motorbike::before{content:"\F037C"}.mdi-motorbike-electric::before{content:"\F15BA"}.mdi-motorbike-off::before{content:"\F1B16"}.mdi-mouse::before{content:"\F037D"}.mdi-mouse-bluetooth::before{content:"\F098B"}.mdi-mouse-left-click::before{content:"\F1D07"}.mdi-mouse-left-click-outline::before{content:"\F1D08"}.mdi-mouse-move-down::before{content:"\F1550"}.mdi-mouse-move-up::before{content:"\F1551"}.mdi-mouse-move-vertical::before{content:"\F1552"}.mdi-mouse-off::before{content:"\F037E"}.mdi-mouse-outline::before{content:"\F1D09"}.mdi-mouse-right-click::before{content:"\F1D0A"}.mdi-mouse-right-click-outline::before{content:"\F1D0B"}.mdi-mouse-scroll-wheel::before{content:"\F1D0C"}.mdi-mouse-variant::before{content:"\F037F"}.mdi-mouse-variant-off::before{content:"\F0380"}.mdi-move-resize::before{content:"\F0655"}.mdi-move-resize-variant::before{content:"\F0656"}.mdi-movie::before{content:"\F0381"}.mdi-movie-check::before{content:"\F16F3"}.mdi-movie-check-outline::before{content:"\F16F4"}.mdi-movie-cog::before{content:"\F16F5"}.mdi-movie-cog-outline::before{content:"\F16F6"}.mdi-movie-edit::before{content:"\F1122"}.mdi-movie-edit-outline::before{content:"\F1123"}.mdi-movie-filter::before{content:"\F1124"}.mdi-movie-filter-outline::before{content:"\F1125"}.mdi-movie-minus::before{content:"\F16F7"}.mdi-movie-minus-outline::before{content:"\F16F8"}.mdi-movie-off::before{content:"\F16F9"}.mdi-movie-off-outline::before{content:"\F16FA"}.mdi-movie-open::before{content:"\F0FCE"}.mdi-movie-open-check::before{content:"\F16FB"}.mdi-movie-open-check-outline::before{content:"\F16FC"}.mdi-movie-open-cog::before{content:"\F16FD"}.mdi-movie-open-cog-outline::before{content:"\F16FE"}.mdi-movie-open-edit::before{content:"\F16FF"}.mdi-movie-open-edit-outline::before{content:"\F1700"}.mdi-movie-open-minus::before{content:"\F1701"}.mdi-movie-open-minus-outline::before{content:"\F1702"}.mdi-movie-open-off::before{content:"\F1703"}.mdi-movie-open-off-outline::before{content:"\F1704"}.mdi-movie-open-outline::before{content:"\F0FCF"}.mdi-movie-open-play::before{content:"\F1705"}.mdi-movie-open-play-outline::before{content:"\F1706"}.mdi-movie-open-plus::before{content:"\F1707"}.mdi-movie-open-plus-outline::before{content:"\F1708"}.mdi-movie-open-remove::before{content:"\F1709"}.mdi-movie-open-remove-outline::before{content:"\F170A"}.mdi-movie-open-settings::before{content:"\F170B"}.mdi-movie-open-settings-outline::before{content:"\F170C"}.mdi-movie-open-star::before{content:"\F170D"}.mdi-movie-open-star-outline::before{content:"\F170E"}.mdi-movie-outline::before{content:"\F0DDD"}.mdi-movie-play::before{content:"\F170F"}.mdi-movie-play-outline::before{content:"\F1710"}.mdi-movie-plus::before{content:"\F1711"}.mdi-movie-plus-outline::before{content:"\F1712"}.mdi-movie-remove::before{content:"\F1713"}.mdi-movie-remove-outline::before{content:"\F1714"}.mdi-movie-roll::before{content:"\F07DE"}.mdi-movie-search::before{content:"\F11D2"}.mdi-movie-search-outline::before{content:"\F11D3"}.mdi-movie-settings::before{content:"\F1715"}.mdi-movie-settings-outline::before{content:"\F1716"}.mdi-movie-star::before{content:"\F1717"}.mdi-movie-star-outline::before{content:"\F1718"}.mdi-mower::before{content:"\F166F"}.mdi-mower-bag::before{content:"\F1670"}.mdi-mower-bag-on::before{content:"\F1B60"}.mdi-mower-on::before{content:"\F1B5F"}.mdi-muffin::before{content:"\F098C"}.mdi-multicast::before{content:"\F1893"}.mdi-multimedia::before{content:"\F1B97"}.mdi-multiplication::before{content:"\F0382"}.mdi-multiplication-box::before{content:"\F0383"}.mdi-mushroom::before{content:"\F07DF"}.mdi-mushroom-off::before{content:"\F13FA"}.mdi-mushroom-off-outline::before{content:"\F13FB"}.mdi-mushroom-outline::before{content:"\F07E0"}.mdi-music::before{content:"\F075A"}.mdi-music-accidental-double-flat::before{content:"\F0F69"}.mdi-music-accidental-double-sharp::before{content:"\F0F6A"}.mdi-music-accidental-flat::before{content:"\F0F6B"}.mdi-music-accidental-natural::before{content:"\F0F6C"}.mdi-music-accidental-sharp::before{content:"\F0F6D"}.mdi-music-box::before{content:"\F0384"}.mdi-music-box-multiple::before{content:"\F0333"}.mdi-music-box-multiple-outline::before{content:"\F0F04"}.mdi-music-box-outline::before{content:"\F0385"}.mdi-music-circle::before{content:"\F0386"}.mdi-music-circle-outline::before{content:"\F0AD4"}.mdi-music-clef-alto::before{content:"\F0F6E"}.mdi-music-clef-bass::before{content:"\F0F6F"}.mdi-music-clef-treble::before{content:"\F0F70"}.mdi-music-note::before{content:"\F0387"}.mdi-music-note-bluetooth::before{content:"\F05FE"}.mdi-music-note-bluetooth-off::before{content:"\F05FF"}.mdi-music-note-eighth::before{content:"\F0388"}.mdi-music-note-eighth-dotted::before{content:"\F0F71"}.mdi-music-note-half::before{content:"\F0389"}.mdi-music-note-half-dotted::before{content:"\F0F72"}.mdi-music-note-minus::before{content:"\F1B89"}.mdi-music-note-off::before{content:"\F038A"}.mdi-music-note-off-outline::before{content:"\F0F73"}.mdi-music-note-outline::before{content:"\F0F74"}.mdi-music-note-plus::before{content:"\F0DDE"}.mdi-music-note-quarter::before{content:"\F038B"}.mdi-music-note-quarter-dotted::before{content:"\F0F75"}.mdi-music-note-sixteenth::before{content:"\F038C"}.mdi-music-note-sixteenth-dotted::before{content:"\F0F76"}.mdi-music-note-whole::before{content:"\F038D"}.mdi-music-note-whole-dotted::before{content:"\F0F77"}.mdi-music-off::before{content:"\F075B"}.mdi-music-rest-eighth::before{content:"\F0F78"}.mdi-music-rest-half::before{content:"\F0F79"}.mdi-music-rest-quarter::before{content:"\F0F7A"}.mdi-music-rest-sixteenth::before{content:"\F0F7B"}.mdi-music-rest-whole::before{content:"\F0F7C"}.mdi-mustache::before{content:"\F15DE"}.mdi-nail::before{content:"\F0DDF"}.mdi-nas::before{content:"\F08F3"}.mdi-nativescript::before{content:"\F0880"}.mdi-nature::before{content:"\F038E"}.mdi-nature-outline::before{content:"\F1C71"}.mdi-nature-people::before{content:"\F038F"}.mdi-nature-people-outline::before{content:"\F1C72"}.mdi-navigation::before{content:"\F0390"}.mdi-navigation-outline::before{content:"\F1607"}.mdi-navigation-variant::before{content:"\F18F0"}.mdi-navigation-variant-outline::before{content:"\F18F1"}.mdi-near-me::before{content:"\F05CD"}.mdi-necklace::before{content:"\F0F0B"}.mdi-needle::before{content:"\F0391"}.mdi-needle-off::before{content:"\F19D2"}.mdi-netflix::before{content:"\F0746"}.mdi-network::before{content:"\F06F3"}.mdi-network-off::before{content:"\F0C9B"}.mdi-network-off-outline::before{content:"\F0C9C"}.mdi-network-outline::before{content:"\F0C9D"}.mdi-network-pos::before{content:"\F1ACB"}.mdi-network-strength-1::before{content:"\F08F4"}.mdi-network-strength-1-alert::before{content:"\F08F5"}.mdi-network-strength-2::before{content:"\F08F6"}.mdi-network-strength-2-alert::before{content:"\F08F7"}.mdi-network-strength-3::before{content:"\F08F8"}.mdi-network-strength-3-alert::before{content:"\F08F9"}.mdi-network-strength-4::before{content:"\F08FA"}.mdi-network-strength-4-alert::before{content:"\F08FB"}.mdi-network-strength-4-cog::before{content:"\F191A"}.mdi-network-strength-off::before{content:"\F08FC"}.mdi-network-strength-off-outline::before{content:"\F08FD"}.mdi-network-strength-outline::before{content:"\F08FE"}.mdi-new-box::before{content:"\F0394"}.mdi-newspaper::before{content:"\F0395"}.mdi-newspaper-check::before{content:"\F1943"}.mdi-newspaper-minus::before{content:"\F0F0C"}.mdi-newspaper-plus::before{content:"\F0F0D"}.mdi-newspaper-remove::before{content:"\F1944"}.mdi-newspaper-variant::before{content:"\F1001"}.mdi-newspaper-variant-multiple::before{content:"\F1002"}.mdi-newspaper-variant-multiple-outline::before{content:"\F1003"}.mdi-newspaper-variant-outline::before{content:"\F1004"}.mdi-nfc::before{content:"\F0396"}.mdi-nfc-search-variant::before{content:"\F0E53"}.mdi-nfc-tap::before{content:"\F0397"}.mdi-nfc-variant::before{content:"\F0398"}.mdi-nfc-variant-off::before{content:"\F0E54"}.mdi-ninja::before{content:"\F0774"}.mdi-nintendo-game-boy::before{content:"\F1393"}.mdi-nintendo-switch::before{content:"\F07E1"}.mdi-nintendo-wii::before{content:"\F05AB"}.mdi-nintendo-wiiu::before{content:"\F072D"}.mdi-nix::before{content:"\F1105"}.mdi-nodejs::before{content:"\F0399"}.mdi-noodles::before{content:"\F117E"}.mdi-not-equal::before{content:"\F098D"}.mdi-not-equal-variant::before{content:"\F098E"}.mdi-note::before{content:"\F039A"}.mdi-note-alert::before{content:"\F177D"}.mdi-note-alert-outline::before{content:"\F177E"}.mdi-note-check::before{content:"\F177F"}.mdi-note-check-outline::before{content:"\F1780"}.mdi-note-edit::before{content:"\F1781"}.mdi-note-edit-outline::before{content:"\F1782"}.mdi-note-minus::before{content:"\F164F"}.mdi-note-minus-outline::before{content:"\F1650"}.mdi-note-multiple::before{content:"\F06B8"}.mdi-note-multiple-outline::before{content:"\F06B9"}.mdi-note-off::before{content:"\F1783"}.mdi-note-off-outline::before{content:"\F1784"}.mdi-note-outline::before{content:"\F039B"}.mdi-note-plus::before{content:"\F039C"}.mdi-note-plus-outline::before{content:"\F039D"}.mdi-note-remove::before{content:"\F1651"}.mdi-note-remove-outline::before{content:"\F1652"}.mdi-note-search::before{content:"\F1653"}.mdi-note-search-outline::before{content:"\F1654"}.mdi-note-text::before{content:"\F039E"}.mdi-note-text-outline::before{content:"\F11D7"}.mdi-notebook::before{content:"\F082E"}.mdi-notebook-check::before{content:"\F14F5"}.mdi-notebook-check-outline::before{content:"\F14F6"}.mdi-notebook-edit::before{content:"\F14E7"}.mdi-notebook-edit-outline::before{content:"\F14E9"}.mdi-notebook-heart::before{content:"\F1A0B"}.mdi-notebook-heart-outline::before{content:"\F1A0C"}.mdi-notebook-minus::before{content:"\F1610"}.mdi-notebook-minus-outline::before{content:"\F1611"}.mdi-notebook-multiple::before{content:"\F0E55"}.mdi-notebook-outline::before{content:"\F0EBF"}.mdi-notebook-plus::before{content:"\F1612"}.mdi-notebook-plus-outline::before{content:"\F1613"}.mdi-notebook-remove::before{content:"\F1614"}.mdi-notebook-remove-outline::before{content:"\F1615"}.mdi-notification-clear-all::before{content:"\F039F"}.mdi-npm::before{content:"\F06F7"}.mdi-nuke::before{content:"\F06A4"}.mdi-null::before{content:"\F07E2"}.mdi-numeric::before{content:"\F03A0"}.mdi-numeric-0::before{content:"\F0B39"}.mdi-numeric-0-box::before{content:"\F03A1"}.mdi-numeric-0-box-multiple::before{content:"\F0F0E"}.mdi-numeric-0-box-multiple-outline::before{content:"\F03A2"}.mdi-numeric-0-box-outline::before{content:"\F03A3"}.mdi-numeric-0-circle::before{content:"\F0C9E"}.mdi-numeric-0-circle-outline::before{content:"\F0C9F"}.mdi-numeric-1::before{content:"\F0B3A"}.mdi-numeric-1-box::before{content:"\F03A4"}.mdi-numeric-1-box-multiple::before{content:"\F0F0F"}.mdi-numeric-1-box-multiple-outline::before{content:"\F03A5"}.mdi-numeric-1-box-outline::before{content:"\F03A6"}.mdi-numeric-1-circle::before{content:"\F0CA0"}.mdi-numeric-1-circle-outline::before{content:"\F0CA1"}.mdi-numeric-10::before{content:"\F0FE9"}.mdi-numeric-10-box::before{content:"\F0F7D"}.mdi-numeric-10-box-multiple::before{content:"\F0FEA"}.mdi-numeric-10-box-multiple-outline::before{content:"\F0FEB"}.mdi-numeric-10-box-outline::before{content:"\F0F7E"}.mdi-numeric-10-circle::before{content:"\F0FEC"}.mdi-numeric-10-circle-outline::before{content:"\F0FED"}.mdi-numeric-2::before{content:"\F0B3B"}.mdi-numeric-2-box::before{content:"\F03A7"}.mdi-numeric-2-box-multiple::before{content:"\F0F10"}.mdi-numeric-2-box-multiple-outline::before{content:"\F03A8"}.mdi-numeric-2-box-outline::before{content:"\F03A9"}.mdi-numeric-2-circle::before{content:"\F0CA2"}.mdi-numeric-2-circle-outline::before{content:"\F0CA3"}.mdi-numeric-3::before{content:"\F0B3C"}.mdi-numeric-3-box::before{content:"\F03AA"}.mdi-numeric-3-box-multiple::before{content:"\F0F11"}.mdi-numeric-3-box-multiple-outline::before{content:"\F03AB"}.mdi-numeric-3-box-outline::before{content:"\F03AC"}.mdi-numeric-3-circle::before{content:"\F0CA4"}.mdi-numeric-3-circle-outline::before{content:"\F0CA5"}.mdi-numeric-4::before{content:"\F0B3D"}.mdi-numeric-4-box::before{content:"\F03AD"}.mdi-numeric-4-box-multiple::before{content:"\F0F12"}.mdi-numeric-4-box-multiple-outline::before{content:"\F03B2"}.mdi-numeric-4-box-outline::before{content:"\F03AE"}.mdi-numeric-4-circle::before{content:"\F0CA6"}.mdi-numeric-4-circle-outline::before{content:"\F0CA7"}.mdi-numeric-5::before{content:"\F0B3E"}.mdi-numeric-5-box::before{content:"\F03B1"}.mdi-numeric-5-box-multiple::before{content:"\F0F13"}.mdi-numeric-5-box-multiple-outline::before{content:"\F03AF"}.mdi-numeric-5-box-outline::before{content:"\F03B0"}.mdi-numeric-5-circle::before{content:"\F0CA8"}.mdi-numeric-5-circle-outline::before{content:"\F0CA9"}.mdi-numeric-6::before{content:"\F0B3F"}.mdi-numeric-6-box::before{content:"\F03B3"}.mdi-numeric-6-box-multiple::before{content:"\F0F14"}.mdi-numeric-6-box-multiple-outline::before{content:"\F03B4"}.mdi-numeric-6-box-outline::before{content:"\F03B5"}.mdi-numeric-6-circle::before{content:"\F0CAA"}.mdi-numeric-6-circle-outline::before{content:"\F0CAB"}.mdi-numeric-7::before{content:"\F0B40"}.mdi-numeric-7-box::before{content:"\F03B6"}.mdi-numeric-7-box-multiple::before{content:"\F0F15"}.mdi-numeric-7-box-multiple-outline::before{content:"\F03B7"}.mdi-numeric-7-box-outline::before{content:"\F03B8"}.mdi-numeric-7-circle::before{content:"\F0CAC"}.mdi-numeric-7-circle-outline::before{content:"\F0CAD"}.mdi-numeric-8::before{content:"\F0B41"}.mdi-numeric-8-box::before{content:"\F03B9"}.mdi-numeric-8-box-multiple::before{content:"\F0F16"}.mdi-numeric-8-box-multiple-outline::before{content:"\F03BA"}.mdi-numeric-8-box-outline::before{content:"\F03BB"}.mdi-numeric-8-circle::before{content:"\F0CAE"}.mdi-numeric-8-circle-outline::before{content:"\F0CAF"}.mdi-numeric-9::before{content:"\F0B42"}.mdi-numeric-9-box::before{content:"\F03BC"}.mdi-numeric-9-box-multiple::before{content:"\F0F17"}.mdi-numeric-9-box-multiple-outline::before{content:"\F03BD"}.mdi-numeric-9-box-outline::before{content:"\F03BE"}.mdi-numeric-9-circle::before{content:"\F0CB0"}.mdi-numeric-9-circle-outline::before{content:"\F0CB1"}.mdi-numeric-9-plus::before{content:"\F0FEE"}.mdi-numeric-9-plus-box::before{content:"\F03BF"}.mdi-numeric-9-plus-box-multiple::before{content:"\F0F18"}.mdi-numeric-9-plus-box-multiple-outline::before{content:"\F03C0"}.mdi-numeric-9-plus-box-outline::before{content:"\F03C1"}.mdi-numeric-9-plus-circle::before{content:"\F0CB2"}.mdi-numeric-9-plus-circle-outline::before{content:"\F0CB3"}.mdi-numeric-negative-1::before{content:"\F1052"}.mdi-numeric-off::before{content:"\F19D3"}.mdi-numeric-positive-1::before{content:"\F15CB"}.mdi-nut::before{content:"\F06F8"}.mdi-nutrition::before{content:"\F03C2"}.mdi-nuxt::before{content:"\F1106"}.mdi-oar::before{content:"\F067C"}.mdi-ocarina::before{content:"\F0DE0"}.mdi-oci::before{content:"\F12E9"}.mdi-ocr::before{content:"\F113A"}.mdi-octagon::before{content:"\F03C3"}.mdi-octagon-outline::before{content:"\F03C4"}.mdi-octagram::before{content:"\F06F9"}.mdi-octagram-edit::before{content:"\F1C34"}.mdi-octagram-edit-outline::before{content:"\F1C35"}.mdi-octagram-minus::before{content:"\F1C36"}.mdi-octagram-minus-outline::before{content:"\F1C37"}.mdi-octagram-outline::before{content:"\F0775"}.mdi-octagram-plus::before{content:"\F1C38"}.mdi-octagram-plus-outline::before{content:"\F1C39"}.mdi-octahedron::before{content:"\F1950"}.mdi-octahedron-off::before{content:"\F1951"}.mdi-odnoklassniki::before{content:"\F03C5"}.mdi-offer::before{content:"\F121B"}.mdi-office-building::before{content:"\F0991"}.mdi-office-building-cog::before{content:"\F1949"}.mdi-office-building-cog-outline::before{content:"\F194A"}.mdi-office-building-marker::before{content:"\F1520"}.mdi-office-building-marker-outline::before{content:"\F1521"}.mdi-office-building-minus::before{content:"\F1BAA"}.mdi-office-building-minus-outline::before{content:"\F1BAB"}.mdi-office-building-outline::before{content:"\F151F"}.mdi-office-building-plus::before{content:"\F1BA8"}.mdi-office-building-plus-outline::before{content:"\F1BA9"}.mdi-office-building-remove::before{content:"\F1BAC"}.mdi-office-building-remove-outline::before{content:"\F1BAD"}.mdi-oil::before{content:"\F03C7"}.mdi-oil-lamp::before{content:"\F0F19"}.mdi-oil-level::before{content:"\F1053"}.mdi-oil-temperature::before{content:"\F0FF8"}.mdi-om::before{content:"\F0973"}.mdi-omega::before{content:"\F03C9"}.mdi-one-up::before{content:"\F0BAD"}.mdi-onepassword::before{content:"\F0881"}.mdi-opacity::before{content:"\F05CC"}.mdi-open-in-app::before{content:"\F03CB"}.mdi-open-in-new::before{content:"\F03CC"}.mdi-open-source-initiative::before{content:"\F0BAE"}.mdi-openid::before{content:"\F03CD"}.mdi-opera::before{content:"\F03CE"}.mdi-orbit::before{content:"\F0018"}.mdi-orbit-variant::before{content:"\F15DB"}.mdi-order-alphabetical-ascending::before{content:"\F020D"}.mdi-order-alphabetical-descending::before{content:"\F0D07"}.mdi-order-bool-ascending::before{content:"\F02BE"}.mdi-order-bool-ascending-variant::before{content:"\F098F"}.mdi-order-bool-descending::before{content:"\F1384"}.mdi-order-bool-descending-variant::before{content:"\F0990"}.mdi-order-numeric-ascending::before{content:"\F0545"}.mdi-order-numeric-descending::before{content:"\F0546"}.mdi-origin::before{content:"\F0B43"}.mdi-ornament::before{content:"\F03CF"}.mdi-ornament-variant::before{content:"\F03D0"}.mdi-outdoor-lamp::before{content:"\F1054"}.mdi-overscan::before{content:"\F1005"}.mdi-owl::before{content:"\F03D2"}.mdi-pac-man::before{content:"\F0BAF"}.mdi-package::before{content:"\F03D3"}.mdi-package-check::before{content:"\F1B51"}.mdi-package-down::before{content:"\F03D4"}.mdi-package-up::before{content:"\F03D5"}.mdi-package-variant::before{content:"\F03D6"}.mdi-package-variant-closed::before{content:"\F03D7"}.mdi-package-variant-closed-check::before{content:"\F1B52"}.mdi-package-variant-closed-minus::before{content:"\F19D4"}.mdi-package-variant-closed-plus::before{content:"\F19D5"}.mdi-package-variant-closed-remove::before{content:"\F19D6"}.mdi-package-variant-minus::before{content:"\F19D7"}.mdi-package-variant-plus::before{content:"\F19D8"}.mdi-package-variant-remove::before{content:"\F19D9"}.mdi-page-first::before{content:"\F0600"}.mdi-page-last::before{content:"\F0601"}.mdi-page-layout-body::before{content:"\F06FA"}.mdi-page-layout-footer::before{content:"\F06FB"}.mdi-page-layout-header::before{content:"\F06FC"}.mdi-page-layout-header-footer::before{content:"\F0F7F"}.mdi-page-layout-sidebar-left::before{content:"\F06FD"}.mdi-page-layout-sidebar-right::before{content:"\F06FE"}.mdi-page-next::before{content:"\F0BB0"}.mdi-page-next-outline::before{content:"\F0BB1"}.mdi-page-previous::before{content:"\F0BB2"}.mdi-page-previous-outline::before{content:"\F0BB3"}.mdi-pail::before{content:"\F1417"}.mdi-pail-minus::before{content:"\F1437"}.mdi-pail-minus-outline::before{content:"\F143C"}.mdi-pail-off::before{content:"\F1439"}.mdi-pail-off-outline::before{content:"\F143E"}.mdi-pail-outline::before{content:"\F143A"}.mdi-pail-plus::before{content:"\F1436"}.mdi-pail-plus-outline::before{content:"\F143B"}.mdi-pail-remove::before{content:"\F1438"}.mdi-pail-remove-outline::before{content:"\F143D"}.mdi-palette::before{content:"\F03D8"}.mdi-palette-advanced::before{content:"\F03D9"}.mdi-palette-outline::before{content:"\F0E0C"}.mdi-palette-swatch::before{content:"\F08B5"}.mdi-palette-swatch-outline::before{content:"\F135C"}.mdi-palette-swatch-variant::before{content:"\F195A"}.mdi-palm-tree::before{content:"\F1055"}.mdi-pan::before{content:"\F0BB4"}.mdi-pan-bottom-left::before{content:"\F0BB5"}.mdi-pan-bottom-right::before{content:"\F0BB6"}.mdi-pan-down::before{content:"\F0BB7"}.mdi-pan-horizontal::before{content:"\F0BB8"}.mdi-pan-left::before{content:"\F0BB9"}.mdi-pan-right::before{content:"\F0BBA"}.mdi-pan-top-left::before{content:"\F0BBB"}.mdi-pan-top-right::before{content:"\F0BBC"}.mdi-pan-up::before{content:"\F0BBD"}.mdi-pan-vertical::before{content:"\F0BBE"}.mdi-panda::before{content:"\F03DA"}.mdi-pandora::before{content:"\F03DB"}.mdi-panorama::before{content:"\F03DC"}.mdi-panorama-fisheye::before{content:"\F03DD"}.mdi-panorama-horizontal::before{content:"\F1928"}.mdi-panorama-horizontal-outline::before{content:"\F03DE"}.mdi-panorama-outline::before{content:"\F198C"}.mdi-panorama-sphere::before{content:"\F198D"}.mdi-panorama-sphere-outline::before{content:"\F198E"}.mdi-panorama-variant::before{content:"\F198F"}.mdi-panorama-variant-outline::before{content:"\F1990"}.mdi-panorama-vertical::before{content:"\F1929"}.mdi-panorama-vertical-outline::before{content:"\F03DF"}.mdi-panorama-wide-angle::before{content:"\F195F"}.mdi-panorama-wide-angle-outline::before{content:"\F03E0"}.mdi-paper-cut-vertical::before{content:"\F03E1"}.mdi-paper-roll::before{content:"\F1157"}.mdi-paper-roll-outline::before{content:"\F1158"}.mdi-paperclip::before{content:"\F03E2"}.mdi-paperclip-check::before{content:"\F1AC6"}.mdi-paperclip-lock::before{content:"\F19DA"}.mdi-paperclip-minus::before{content:"\F1AC7"}.mdi-paperclip-off::before{content:"\F1AC8"}.mdi-paperclip-plus::before{content:"\F1AC9"}.mdi-paperclip-remove::before{content:"\F1ACA"}.mdi-parachute::before{content:"\F0CB4"}.mdi-parachute-outline::before{content:"\F0CB5"}.mdi-paragliding::before{content:"\F1745"}.mdi-parking::before{content:"\F03E3"}.mdi-party-popper::before{content:"\F1056"}.mdi-passport::before{content:"\F07E3"}.mdi-passport-alert::before{content:"\F1CB8"}.mdi-passport-biometric::before{content:"\F0DE1"}.mdi-passport-cancel::before{content:"\F1CB9"}.mdi-passport-check::before{content:"\F1CBA"}.mdi-passport-minus::before{content:"\F1CBB"}.mdi-passport-plus::before{content:"\F1CBC"}.mdi-passport-remove::before{content:"\F1CBD"}.mdi-pasta::before{content:"\F1160"}.mdi-patio-heater::before{content:"\F0F80"}.mdi-patreon::before{content:"\F0882"}.mdi-pause::before{content:"\F03E4"}.mdi-pause-box::before{content:"\F00BC"}.mdi-pause-box-outline::before{content:"\F1B7A"}.mdi-pause-circle::before{content:"\F03E5"}.mdi-pause-circle-outline::before{content:"\F03E6"}.mdi-pause-octagon::before{content:"\F03E7"}.mdi-pause-octagon-outline::before{content:"\F03E8"}.mdi-paw::before{content:"\F03E9"}.mdi-paw-off::before{content:"\F0657"}.mdi-paw-off-outline::before{content:"\F1676"}.mdi-paw-outline::before{content:"\F1675"}.mdi-peace::before{content:"\F0884"}.mdi-peanut::before{content:"\F0FFC"}.mdi-peanut-off::before{content:"\F0FFD"}.mdi-peanut-off-outline::before{content:"\F0FFF"}.mdi-peanut-outline::before{content:"\F0FFE"}.mdi-pen::before{content:"\F03EA"}.mdi-pen-lock::before{content:"\F0DE2"}.mdi-pen-minus::before{content:"\F0DE3"}.mdi-pen-off::before{content:"\F0DE4"}.mdi-pen-plus::before{content:"\F0DE5"}.mdi-pen-remove::before{content:"\F0DE6"}.mdi-pencil::before{content:"\F03EB"}.mdi-pencil-box::before{content:"\F03EC"}.mdi-pencil-box-multiple::before{content:"\F1144"}.mdi-pencil-box-multiple-outline::before{content:"\F1145"}.mdi-pencil-box-outline::before{content:"\F03ED"}.mdi-pencil-circle::before{content:"\F06FF"}.mdi-pencil-circle-outline::before{content:"\F0776"}.mdi-pencil-lock::before{content:"\F03EE"}.mdi-pencil-lock-outline::before{content:"\F0DE7"}.mdi-pencil-minus::before{content:"\F0DE8"}.mdi-pencil-minus-outline::before{content:"\F0DE9"}.mdi-pencil-off::before{content:"\F03EF"}.mdi-pencil-off-outline::before{content:"\F0DEA"}.mdi-pencil-outline::before{content:"\F0CB6"}.mdi-pencil-plus::before{content:"\F0DEB"}.mdi-pencil-plus-outline::before{content:"\F0DEC"}.mdi-pencil-remove::before{content:"\F0DED"}.mdi-pencil-remove-outline::before{content:"\F0DEE"}.mdi-pencil-ruler::before{content:"\F1353"}.mdi-pencil-ruler-outline::before{content:"\F1C11"}.mdi-penguin::before{content:"\F0EC0"}.mdi-pentagon::before{content:"\F0701"}.mdi-pentagon-outline::before{content:"\F0700"}.mdi-pentagram::before{content:"\F1667"}.mdi-percent::before{content:"\F03F0"}.mdi-percent-box::before{content:"\F1A02"}.mdi-percent-box-outline::before{content:"\F1A03"}.mdi-percent-circle::before{content:"\F1A04"}.mdi-percent-circle-outline::before{content:"\F1A05"}.mdi-percent-outline::before{content:"\F1278"}.mdi-periodic-table::before{content:"\F08B6"}.mdi-perspective-less::before{content:"\F0D23"}.mdi-perspective-more::before{content:"\F0D24"}.mdi-ph::before{content:"\F17C5"}.mdi-phone::before{content:"\F03F2"}.mdi-phone-alert::before{content:"\F0F1A"}.mdi-phone-alert-outline::before{content:"\F118E"}.mdi-phone-bluetooth::before{content:"\F03F3"}.mdi-phone-bluetooth-outline::before{content:"\F118F"}.mdi-phone-cancel::before{content:"\F10BC"}.mdi-phone-cancel-outline::before{content:"\F1190"}.mdi-phone-check::before{content:"\F11A9"}.mdi-phone-check-outline::before{content:"\F11AA"}.mdi-phone-classic::before{content:"\F0602"}.mdi-phone-classic-off::before{content:"\F1279"}.mdi-phone-clock::before{content:"\F19DB"}.mdi-phone-dial::before{content:"\F1559"}.mdi-phone-dial-outline::before{content:"\F155A"}.mdi-phone-forward::before{content:"\F03F4"}.mdi-phone-forward-outline::before{content:"\F1191"}.mdi-phone-hangup::before{content:"\F03F5"}.mdi-phone-hangup-outline::before{content:"\F1192"}.mdi-phone-in-talk::before{content:"\F03F6"}.mdi-phone-in-talk-outline::before{content:"\F1182"}.mdi-phone-incoming::before{content:"\F03F7"}.mdi-phone-incoming-outgoing::before{content:"\F1B3F"}.mdi-phone-incoming-outgoing-outline::before{content:"\F1B40"}.mdi-phone-incoming-outline::before{content:"\F1193"}.mdi-phone-lock::before{content:"\F03F8"}.mdi-phone-lock-outline::before{content:"\F1194"}.mdi-phone-log::before{content:"\F03F9"}.mdi-phone-log-outline::before{content:"\F1195"}.mdi-phone-message::before{content:"\F1196"}.mdi-phone-message-outline::before{content:"\F1197"}.mdi-phone-minus::before{content:"\F0658"}.mdi-phone-minus-outline::before{content:"\F1198"}.mdi-phone-missed::before{content:"\F03FA"}.mdi-phone-missed-outline::before{content:"\F11A5"}.mdi-phone-off::before{content:"\F0DEF"}.mdi-phone-off-outline::before{content:"\F11A6"}.mdi-phone-outgoing::before{content:"\F03FB"}.mdi-phone-outgoing-outline::before{content:"\F1199"}.mdi-phone-outline::before{content:"\F0DF0"}.mdi-phone-paused::before{content:"\F03FC"}.mdi-phone-paused-outline::before{content:"\F119A"}.mdi-phone-plus::before{content:"\F0659"}.mdi-phone-plus-outline::before{content:"\F119B"}.mdi-phone-refresh::before{content:"\F1993"}.mdi-phone-refresh-outline::before{content:"\F1994"}.mdi-phone-remove::before{content:"\F152F"}.mdi-phone-remove-outline::before{content:"\F1530"}.mdi-phone-return::before{content:"\F082F"}.mdi-phone-return-outline::before{content:"\F119C"}.mdi-phone-ring::before{content:"\F11AB"}.mdi-phone-ring-outline::before{content:"\F11AC"}.mdi-phone-rotate-landscape::before{content:"\F0885"}.mdi-phone-rotate-portrait::before{content:"\F0886"}.mdi-phone-settings::before{content:"\F03FD"}.mdi-phone-settings-outline::before{content:"\F119D"}.mdi-phone-sync::before{content:"\F1995"}.mdi-phone-sync-outline::before{content:"\F1996"}.mdi-phone-voip::before{content:"\F03FE"}.mdi-pi::before{content:"\F03FF"}.mdi-pi-box::before{content:"\F0400"}.mdi-pi-hole::before{content:"\F0DF1"}.mdi-piano::before{content:"\F067D"}.mdi-piano-off::before{content:"\F0698"}.mdi-pickaxe::before{content:"\F08B7"}.mdi-picture-in-picture-bottom-right::before{content:"\F0E57"}.mdi-picture-in-picture-bottom-right-outline::before{content:"\F0E58"}.mdi-picture-in-picture-top-right::before{content:"\F0E59"}.mdi-picture-in-picture-top-right-outline::before{content:"\F0E5A"}.mdi-pier::before{content:"\F0887"}.mdi-pier-crane::before{content:"\F0888"}.mdi-pig::before{content:"\F0401"}.mdi-pig-variant::before{content:"\F1006"}.mdi-pig-variant-outline::before{content:"\F1678"}.mdi-piggy-bank::before{content:"\F1007"}.mdi-piggy-bank-outline::before{content:"\F1679"}.mdi-pill::before{content:"\F0402"}.mdi-pill-multiple::before{content:"\F1B4C"}.mdi-pill-off::before{content:"\F1A5C"}.mdi-pillar::before{content:"\F0702"}.mdi-pin::before{content:"\F0403"}.mdi-pin-off::before{content:"\F0404"}.mdi-pin-off-outline::before{content:"\F0930"}.mdi-pin-outline::before{content:"\F0931"}.mdi-pine-tree::before{content:"\F0405"}.mdi-pine-tree-box::before{content:"\F0406"}.mdi-pine-tree-fire::before{content:"\F141A"}.mdi-pine-tree-variant::before{content:"\F1C73"}.mdi-pine-tree-variant-outline::before{content:"\F1C74"}.mdi-pinterest::before{content:"\F0407"}.mdi-pinwheel::before{content:"\F0AD5"}.mdi-pinwheel-outline::before{content:"\F0AD6"}.mdi-pipe::before{content:"\F07E5"}.mdi-pipe-disconnected::before{content:"\F07E6"}.mdi-pipe-leak::before{content:"\F0889"}.mdi-pipe-valve::before{content:"\F184D"}.mdi-pipe-wrench::before{content:"\F1354"}.mdi-pirate::before{content:"\F0A08"}.mdi-pistol::before{content:"\F0703"}.mdi-piston::before{content:"\F088A"}.mdi-pitchfork::before{content:"\F1553"}.mdi-pizza::before{content:"\F0409"}.mdi-plane-car::before{content:"\F1AFF"}.mdi-plane-train::before{content:"\F1B00"}.mdi-play::before{content:"\F040A"}.mdi-play-box::before{content:"\F127A"}.mdi-play-box-edit-outline::before{content:"\F1C3A"}.mdi-play-box-lock::before{content:"\F1A16"}.mdi-play-box-lock-open::before{content:"\F1A17"}.mdi-play-box-lock-open-outline::before{content:"\F1A18"}.mdi-play-box-lock-outline::before{content:"\F1A19"}.mdi-play-box-multiple::before{content:"\F0D19"}.mdi-play-box-multiple-outline::before{content:"\F13E6"}.mdi-play-box-outline::before{content:"\F040B"}.mdi-play-circle::before{content:"\F040C"}.mdi-play-circle-outline::before{content:"\F040D"}.mdi-play-network::before{content:"\F088B"}.mdi-play-network-outline::before{content:"\F0CB7"}.mdi-play-outline::before{content:"\F0F1B"}.mdi-play-pause::before{content:"\F040E"}.mdi-play-protected-content::before{content:"\F040F"}.mdi-play-speed::before{content:"\F08FF"}.mdi-playlist-check::before{content:"\F05C7"}.mdi-playlist-edit::before{content:"\F0900"}.mdi-playlist-minus::before{content:"\F0410"}.mdi-playlist-music::before{content:"\F0CB8"}.mdi-playlist-music-outline::before{content:"\F0CB9"}.mdi-playlist-play::before{content:"\F0411"}.mdi-playlist-plus::before{content:"\F0412"}.mdi-playlist-remove::before{content:"\F0413"}.mdi-playlist-star::before{content:"\F0DF2"}.mdi-plex::before{content:"\F06BA"}.mdi-pliers::before{content:"\F19A4"}.mdi-plus::before{content:"\F0415"}.mdi-plus-box::before{content:"\F0416"}.mdi-plus-box-multiple::before{content:"\F0334"}.mdi-plus-box-multiple-outline::before{content:"\F1143"}.mdi-plus-box-outline::before{content:"\F0704"}.mdi-plus-circle::before{content:"\F0417"}.mdi-plus-circle-multiple::before{content:"\F034C"}.mdi-plus-circle-multiple-outline::before{content:"\F0418"}.mdi-plus-circle-outline::before{content:"\F0419"}.mdi-plus-lock::before{content:"\F1A5D"}.mdi-plus-lock-open::before{content:"\F1A5E"}.mdi-plus-minus::before{content:"\F0992"}.mdi-plus-minus-box::before{content:"\F0993"}.mdi-plus-minus-variant::before{content:"\F14C9"}.mdi-plus-network::before{content:"\F041A"}.mdi-plus-network-outline::before{content:"\F0CBA"}.mdi-plus-outline::before{content:"\F0705"}.mdi-plus-thick::before{content:"\F11EC"}.mdi-pocket::before{content:"\F1CBE"}.mdi-podcast::before{content:"\F0994"}.mdi-podium::before{content:"\F0D25"}.mdi-podium-bronze::before{content:"\F0D26"}.mdi-podium-gold::before{content:"\F0D27"}.mdi-podium-silver::before{content:"\F0D28"}.mdi-point-of-sale::before{content:"\F0D92"}.mdi-pokeball::before{content:"\F041D"}.mdi-pokemon-go::before{content:"\F0A09"}.mdi-poker-chip::before{content:"\F0830"}.mdi-polaroid::before{content:"\F041E"}.mdi-police-badge::before{content:"\F1167"}.mdi-police-badge-outline::before{content:"\F1168"}.mdi-police-station::before{content:"\F1839"}.mdi-poll::before{content:"\F041F"}.mdi-polo::before{content:"\F14C3"}.mdi-polymer::before{content:"\F0421"}.mdi-pool::before{content:"\F0606"}.mdi-pool-thermometer::before{content:"\F1A5F"}.mdi-popcorn::before{content:"\F0422"}.mdi-post::before{content:"\F1008"}.mdi-post-lamp::before{content:"\F1A60"}.mdi-post-outline::before{content:"\F1009"}.mdi-postage-stamp::before{content:"\F0CBB"}.mdi-pot::before{content:"\F02E5"}.mdi-pot-mix::before{content:"\F065B"}.mdi-pot-mix-outline::before{content:"\F0677"}.mdi-pot-outline::before{content:"\F02FF"}.mdi-pot-steam::before{content:"\F065A"}.mdi-pot-steam-outline::before{content:"\F0326"}.mdi-pound::before{content:"\F0423"}.mdi-pound-box::before{content:"\F0424"}.mdi-pound-box-outline::before{content:"\F117F"}.mdi-power::before{content:"\F0425"}.mdi-power-cycle::before{content:"\F0901"}.mdi-power-off::before{content:"\F0902"}.mdi-power-on::before{content:"\F0903"}.mdi-power-plug::before{content:"\F06A5"}.mdi-power-plug-battery::before{content:"\F1C3B"}.mdi-power-plug-battery-outline::before{content:"\F1C3C"}.mdi-power-plug-off::before{content:"\F06A6"}.mdi-power-plug-off-outline::before{content:"\F1424"}.mdi-power-plug-outline::before{content:"\F1425"}.mdi-power-settings::before{content:"\F0426"}.mdi-power-sleep::before{content:"\F0904"}.mdi-power-socket::before{content:"\F0427"}.mdi-power-socket-au::before{content:"\F0905"}.mdi-power-socket-ch::before{content:"\F0FB3"}.mdi-power-socket-de::before{content:"\F1107"}.mdi-power-socket-eu::before{content:"\F07E7"}.mdi-power-socket-fr::before{content:"\F1108"}.mdi-power-socket-it::before{content:"\F14FF"}.mdi-power-socket-jp::before{content:"\F1109"}.mdi-power-socket-uk::before{content:"\F07E8"}.mdi-power-socket-us::before{content:"\F07E9"}.mdi-power-standby::before{content:"\F0906"}.mdi-powershell::before{content:"\F0A0A"}.mdi-prescription::before{content:"\F0706"}.mdi-presentation::before{content:"\F0428"}.mdi-presentation-play::before{content:"\F0429"}.mdi-pretzel::before{content:"\F1562"}.mdi-printer::before{content:"\F042A"}.mdi-printer-3d::before{content:"\F042B"}.mdi-printer-3d-nozzle::before{content:"\F0E5B"}.mdi-printer-3d-nozzle-alert::before{content:"\F11C0"}.mdi-printer-3d-nozzle-alert-outline::before{content:"\F11C1"}.mdi-printer-3d-nozzle-heat::before{content:"\F18B8"}.mdi-printer-3d-nozzle-heat-outline::before{content:"\F18B9"}.mdi-printer-3d-nozzle-off::before{content:"\F1B19"}.mdi-printer-3d-nozzle-off-outline::before{content:"\F1B1A"}.mdi-printer-3d-nozzle-outline::before{content:"\F0E5C"}.mdi-printer-3d-off::before{content:"\F1B0E"}.mdi-printer-alert::before{content:"\F042C"}.mdi-printer-check::before{content:"\F1146"}.mdi-printer-eye::before{content:"\F1458"}.mdi-printer-off::before{content:"\F0E5D"}.mdi-printer-off-outline::before{content:"\F1785"}.mdi-printer-outline::before{content:"\F1786"}.mdi-printer-pos::before{content:"\F1057"}.mdi-printer-pos-alert::before{content:"\F1BBC"}.mdi-printer-pos-alert-outline::before{content:"\F1BBD"}.mdi-printer-pos-cancel::before{content:"\F1BBE"}.mdi-printer-pos-cancel-outline::before{content:"\F1BBF"}.mdi-printer-pos-check::before{content:"\F1BC0"}.mdi-printer-pos-check-outline::before{content:"\F1BC1"}.mdi-printer-pos-cog::before{content:"\F1BC2"}.mdi-printer-pos-cog-outline::before{content:"\F1BC3"}.mdi-printer-pos-edit::before{content:"\F1BC4"}.mdi-printer-pos-edit-outline::before{content:"\F1BC5"}.mdi-printer-pos-minus::before{content:"\F1BC6"}.mdi-printer-pos-minus-outline::before{content:"\F1BC7"}.mdi-printer-pos-network::before{content:"\F1BC8"}.mdi-printer-pos-network-outline::before{content:"\F1BC9"}.mdi-printer-pos-off::before{content:"\F1BCA"}.mdi-printer-pos-off-outline::before{content:"\F1BCB"}.mdi-printer-pos-outline::before{content:"\F1BCC"}.mdi-printer-pos-pause::before{content:"\F1BCD"}.mdi-printer-pos-pause-outline::before{content:"\F1BCE"}.mdi-printer-pos-play::before{content:"\F1BCF"}.mdi-printer-pos-play-outline::before{content:"\F1BD0"}.mdi-printer-pos-plus::before{content:"\F1BD1"}.mdi-printer-pos-plus-outline::before{content:"\F1BD2"}.mdi-printer-pos-refresh::before{content:"\F1BD3"}.mdi-printer-pos-refresh-outline::before{content:"\F1BD4"}.mdi-printer-pos-remove::before{content:"\F1BD5"}.mdi-printer-pos-remove-outline::before{content:"\F1BD6"}.mdi-printer-pos-star::before{content:"\F1BD7"}.mdi-printer-pos-star-outline::before{content:"\F1BD8"}.mdi-printer-pos-stop::before{content:"\F1BD9"}.mdi-printer-pos-stop-outline::before{content:"\F1BDA"}.mdi-printer-pos-sync::before{content:"\F1BDB"}.mdi-printer-pos-sync-outline::before{content:"\F1BDC"}.mdi-printer-pos-wrench::before{content:"\F1BDD"}.mdi-printer-pos-wrench-outline::before{content:"\F1BDE"}.mdi-printer-search::before{content:"\F1457"}.mdi-printer-settings::before{content:"\F0707"}.mdi-printer-wireless::before{content:"\F0A0B"}.mdi-priority-high::before{content:"\F0603"}.mdi-priority-low::before{content:"\F0604"}.mdi-professional-hexagon::before{content:"\F042D"}.mdi-progress-alert::before{content:"\F0CBC"}.mdi-progress-check::before{content:"\F0995"}.mdi-progress-clock::before{content:"\F0996"}.mdi-progress-close::before{content:"\F110A"}.mdi-progress-download::before{content:"\F0997"}.mdi-progress-helper::before{content:"\F1BA2"}.mdi-progress-pencil::before{content:"\F1787"}.mdi-progress-question::before{content:"\F1522"}.mdi-progress-star::before{content:"\F1788"}.mdi-progress-star-four-points::before{content:"\F1C3D"}.mdi-progress-tag::before{content:"\F1D0D"}.mdi-progress-upload::before{content:"\F0998"}.mdi-progress-wrench::before{content:"\F0CBD"}.mdi-projector::before{content:"\F042E"}.mdi-projector-off::before{content:"\F1A23"}.mdi-projector-screen::before{content:"\F042F"}.mdi-projector-screen-off::before{content:"\F180D"}.mdi-projector-screen-off-outline::before{content:"\F180E"}.mdi-projector-screen-outline::before{content:"\F1724"}.mdi-projector-screen-variant::before{content:"\F180F"}.mdi-projector-screen-variant-off::before{content:"\F1810"}.mdi-projector-screen-variant-off-outline::before{content:"\F1811"}.mdi-projector-screen-variant-outline::before{content:"\F1812"}.mdi-propane-tank::before{content:"\F1357"}.mdi-propane-tank-outline::before{content:"\F1358"}.mdi-protocol::before{content:"\F0FD8"}.mdi-publish::before{content:"\F06A7"}.mdi-publish-off::before{content:"\F1945"}.mdi-pulse::before{content:"\F0430"}.mdi-pump::before{content:"\F1402"}.mdi-pump-off::before{content:"\F1B22"}.mdi-pumpkin::before{content:"\F0BBF"}.mdi-purse::before{content:"\F0F1C"}.mdi-purse-outline::before{content:"\F0F1D"}.mdi-puzzle::before{content:"\F0431"}.mdi-puzzle-check::before{content:"\F1426"}.mdi-puzzle-check-outline::before{content:"\F1427"}.mdi-puzzle-edit::before{content:"\F14D3"}.mdi-puzzle-edit-outline::before{content:"\F14D9"}.mdi-puzzle-heart::before{content:"\F14D4"}.mdi-puzzle-heart-outline::before{content:"\F14DA"}.mdi-puzzle-minus::before{content:"\F14D1"}.mdi-puzzle-minus-outline::before{content:"\F14D7"}.mdi-puzzle-outline::before{content:"\F0A66"}.mdi-puzzle-plus::before{content:"\F14D0"}.mdi-puzzle-plus-outline::before{content:"\F14D6"}.mdi-puzzle-remove::before{content:"\F14D2"}.mdi-puzzle-remove-outline::before{content:"\F14D8"}.mdi-puzzle-star::before{content:"\F14D5"}.mdi-puzzle-star-outline::before{content:"\F14DB"}.mdi-pyramid::before{content:"\F1952"}.mdi-pyramid-off::before{content:"\F1953"}.mdi-qi::before{content:"\F0999"}.mdi-qqchat::before{content:"\F0605"}.mdi-qrcode::before{content:"\F0432"}.mdi-qrcode-edit::before{content:"\F08B8"}.mdi-qrcode-minus::before{content:"\F118C"}.mdi-qrcode-plus::before{content:"\F118B"}.mdi-qrcode-remove::before{content:"\F118D"}.mdi-qrcode-scan::before{content:"\F0433"}.mdi-quadcopter::before{content:"\F0434"}.mdi-quality-high::before{content:"\F0435"}.mdi-quality-low::before{content:"\F0A0C"}.mdi-quality-medium::before{content:"\F0A0D"}.mdi-queue-first-in-last-out::before{content:"\F1CAF"}.mdi-quora::before{content:"\F0D29"}.mdi-rabbit::before{content:"\F0907"}.mdi-rabbit-variant::before{content:"\F1A61"}.mdi-rabbit-variant-outline::before{content:"\F1A62"}.mdi-racing-helmet::before{content:"\F0D93"}.mdi-racquetball::before{content:"\F0D94"}.mdi-radar::before{content:"\F0437"}.mdi-radiator::before{content:"\F0438"}.mdi-radiator-disabled::before{content:"\F0AD7"}.mdi-radiator-off::before{content:"\F0AD8"}.mdi-radio::before{content:"\F0439"}.mdi-radio-am::before{content:"\F0CBE"}.mdi-radio-fm::before{content:"\F0CBF"}.mdi-radio-handheld::before{content:"\F043A"}.mdi-radio-off::before{content:"\F121C"}.mdi-radio-tower::before{content:"\F043B"}.mdi-radioactive::before{content:"\F043C"}.mdi-radioactive-circle::before{content:"\F185D"}.mdi-radioactive-circle-outline::before{content:"\F185E"}.mdi-radioactive-off::before{content:"\F0EC1"}.mdi-radiobox-blank::before{content:"\F043D"}.mdi-radiobox-indeterminate-variant::before{content:"\F1C5E"}.mdi-radiobox-marked::before{content:"\F043E"}.mdi-radiology-box::before{content:"\F14C5"}.mdi-radiology-box-outline::before{content:"\F14C6"}.mdi-radius::before{content:"\F0CC0"}.mdi-radius-outline::before{content:"\F0CC1"}.mdi-railroad-light::before{content:"\F0F1E"}.mdi-rake::before{content:"\F1544"}.mdi-raspberry-pi::before{content:"\F043F"}.mdi-raw::before{content:"\F1A0F"}.mdi-raw-off::before{content:"\F1A10"}.mdi-ray-end::before{content:"\F0440"}.mdi-ray-end-arrow::before{content:"\F0441"}.mdi-ray-start::before{content:"\F0442"}.mdi-ray-start-arrow::before{content:"\F0443"}.mdi-ray-start-end::before{content:"\F0444"}.mdi-ray-start-vertex-end::before{content:"\F15D8"}.mdi-ray-vertex::before{content:"\F0445"}.mdi-razor-double-edge::before{content:"\F1997"}.mdi-razor-single-edge::before{content:"\F1998"}.mdi-react::before{content:"\F0708"}.mdi-read::before{content:"\F0447"}.mdi-receipt::before{content:"\F0824"}.mdi-receipt-clock::before{content:"\F1C3E"}.mdi-receipt-clock-outline::before{content:"\F1C3F"}.mdi-receipt-outline::before{content:"\F04F7"}.mdi-receipt-send::before{content:"\F1C40"}.mdi-receipt-send-outline::before{content:"\F1C41"}.mdi-receipt-text::before{content:"\F0449"}.mdi-receipt-text-arrow-left::before{content:"\F1C42"}.mdi-receipt-text-arrow-left-outline::before{content:"\F1C43"}.mdi-receipt-text-arrow-right::before{content:"\F1C44"}.mdi-receipt-text-arrow-right-outline::before{content:"\F1C45"}.mdi-receipt-text-check::before{content:"\F1A63"}.mdi-receipt-text-check-outline::before{content:"\F1A64"}.mdi-receipt-text-clock::before{content:"\F1C46"}.mdi-receipt-text-clock-outline::before{content:"\F1C47"}.mdi-receipt-text-edit::before{content:"\F1C48"}.mdi-receipt-text-edit-outline::before{content:"\F1C49"}.mdi-receipt-text-minus::before{content:"\F1A65"}.mdi-receipt-text-minus-outline::before{content:"\F1A66"}.mdi-receipt-text-outline::before{content:"\F19DC"}.mdi-receipt-text-plus::before{content:"\F1A67"}.mdi-receipt-text-plus-outline::before{content:"\F1A68"}.mdi-receipt-text-remove::before{content:"\F1A69"}.mdi-receipt-text-remove-outline::before{content:"\F1A6A"}.mdi-receipt-text-send::before{content:"\F1C4A"}.mdi-receipt-text-send-outline::before{content:"\F1C4B"}.mdi-record::before{content:"\F044A"}.mdi-record-circle::before{content:"\F0EC2"}.mdi-record-circle-outline::before{content:"\F0EC3"}.mdi-record-player::before{content:"\F099A"}.mdi-record-rec::before{content:"\F044B"}.mdi-rectangle::before{content:"\F0E5E"}.mdi-rectangle-outline::before{content:"\F0E5F"}.mdi-recycle::before{content:"\F044C"}.mdi-recycle-variant::before{content:"\F139D"}.mdi-reddit::before{content:"\F044D"}.mdi-redhat::before{content:"\F111B"}.mdi-redo::before{content:"\F044E"}.mdi-redo-variant::before{content:"\F044F"}.mdi-reflect-horizontal::before{content:"\F0A0E"}.mdi-reflect-vertical::before{content:"\F0A0F"}.mdi-refresh::before{content:"\F0450"}.mdi-refresh-auto::before{content:"\F18F2"}.mdi-refresh-circle::before{content:"\F1377"}.mdi-regex::before{content:"\F0451"}.mdi-registered-trademark::before{content:"\F0A67"}.mdi-reiterate::before{content:"\F1588"}.mdi-relation-many-to-many::before{content:"\F1496"}.mdi-relation-many-to-one::before{content:"\F1497"}.mdi-relation-many-to-one-or-many::before{content:"\F1498"}.mdi-relation-many-to-only-one::before{content:"\F1499"}.mdi-relation-many-to-zero-or-many::before{content:"\F149A"}.mdi-relation-many-to-zero-or-one::before{content:"\F149B"}.mdi-relation-one-or-many-to-many::before{content:"\F149C"}.mdi-relation-one-or-many-to-one::before{content:"\F149D"}.mdi-relation-one-or-many-to-one-or-many::before{content:"\F149E"}.mdi-relation-one-or-many-to-only-one::before{content:"\F149F"}.mdi-relation-one-or-many-to-zero-or-many::before{content:"\F14A0"}.mdi-relation-one-or-many-to-zero-or-one::before{content:"\F14A1"}.mdi-relation-one-to-many::before{content:"\F14A2"}.mdi-relation-one-to-one::before{content:"\F14A3"}.mdi-relation-one-to-one-or-many::before{content:"\F14A4"}.mdi-relation-one-to-only-one::before{content:"\F14A5"}.mdi-relation-one-to-zero-or-many::before{content:"\F14A6"}.mdi-relation-one-to-zero-or-one::before{content:"\F14A7"}.mdi-relation-only-one-to-many::before{content:"\F14A8"}.mdi-relation-only-one-to-one::before{content:"\F14A9"}.mdi-relation-only-one-to-one-or-many::before{content:"\F14AA"}.mdi-relation-only-one-to-only-one::before{content:"\F14AB"}.mdi-relation-only-one-to-zero-or-many::before{content:"\F14AC"}.mdi-relation-only-one-to-zero-or-one::before{content:"\F14AD"}.mdi-relation-zero-or-many-to-many::before{content:"\F14AE"}.mdi-relation-zero-or-many-to-one::before{content:"\F14AF"}.mdi-relation-zero-or-many-to-one-or-many::before{content:"\F14B0"}.mdi-relation-zero-or-many-to-only-one::before{content:"\F14B1"}.mdi-relation-zero-or-many-to-zero-or-many::before{content:"\F14B2"}.mdi-relation-zero-or-many-to-zero-or-one::before{content:"\F14B3"}.mdi-relation-zero-or-one-to-many::before{content:"\F14B4"}.mdi-relation-zero-or-one-to-one::before{content:"\F14B5"}.mdi-relation-zero-or-one-to-one-or-many::before{content:"\F14B6"}.mdi-relation-zero-or-one-to-only-one::before{content:"\F14B7"}.mdi-relation-zero-or-one-to-zero-or-many::before{content:"\F14B8"}.mdi-relation-zero-or-one-to-zero-or-one::before{content:"\F14B9"}.mdi-relative-scale::before{content:"\F0452"}.mdi-reload::before{content:"\F0453"}.mdi-reload-alert::before{content:"\F110B"}.mdi-reminder::before{content:"\F088C"}.mdi-remote::before{content:"\F0454"}.mdi-remote-desktop::before{content:"\F08B9"}.mdi-remote-off::before{content:"\F0EC4"}.mdi-remote-tv::before{content:"\F0EC5"}.mdi-remote-tv-off::before{content:"\F0EC6"}.mdi-rename::before{content:"\F1C18"}.mdi-rename-box::before{content:"\F0455"}.mdi-rename-box-outline::before{content:"\F1C19"}.mdi-rename-outline::before{content:"\F1C1A"}.mdi-reorder-horizontal::before{content:"\F0688"}.mdi-reorder-vertical::before{content:"\F0689"}.mdi-repeat::before{content:"\F0456"}.mdi-repeat-off::before{content:"\F0457"}.mdi-repeat-once::before{content:"\F0458"}.mdi-repeat-variant::before{content:"\F0547"}.mdi-replay::before{content:"\F0459"}.mdi-reply::before{content:"\F045A"}.mdi-reply-all::before{content:"\F045B"}.mdi-reply-all-outline::before{content:"\F0F1F"}.mdi-reply-circle::before{content:"\F11AE"}.mdi-reply-outline::before{content:"\F0F20"}.mdi-reproduction::before{content:"\F045C"}.mdi-resistor::before{content:"\F0B44"}.mdi-resistor-nodes::before{content:"\F0B45"}.mdi-resize::before{content:"\F0A68"}.mdi-resize-bottom-right::before{content:"\F045D"}.mdi-responsive::before{content:"\F045E"}.mdi-restart::before{content:"\F0709"}.mdi-restart-alert::before{content:"\F110C"}.mdi-restart-off::before{content:"\F0D95"}.mdi-restore::before{content:"\F099B"}.mdi-restore-alert::before{content:"\F110D"}.mdi-rewind::before{content:"\F045F"}.mdi-rewind-10::before{content:"\F0D2A"}.mdi-rewind-15::before{content:"\F1946"}.mdi-rewind-30::before{content:"\F0D96"}.mdi-rewind-45::before{content:"\F1B13"}.mdi-rewind-5::before{content:"\F11F9"}.mdi-rewind-60::before{content:"\F160C"}.mdi-rewind-outline::before{content:"\F070A"}.mdi-rhombus::before{content:"\F070B"}.mdi-rhombus-medium::before{content:"\F0A10"}.mdi-rhombus-medium-outline::before{content:"\F14DC"}.mdi-rhombus-outline::before{content:"\F070C"}.mdi-rhombus-split::before{content:"\F0A11"}.mdi-rhombus-split-outline::before{content:"\F14DD"}.mdi-ribbon::before{content:"\F0460"}.mdi-rice::before{content:"\F07EA"}.mdi-rickshaw::before{content:"\F15BB"}.mdi-rickshaw-electric::before{content:"\F15BC"}.mdi-ring::before{content:"\F07EB"}.mdi-rivet::before{content:"\F0E60"}.mdi-road::before{content:"\F0461"}.mdi-road-variant::before{content:"\F0462"}.mdi-robber::before{content:"\F1058"}.mdi-robot::before{content:"\F06A9"}.mdi-robot-angry::before{content:"\F169D"}.mdi-robot-angry-outline::before{content:"\F169E"}.mdi-robot-confused::before{content:"\F169F"}.mdi-robot-confused-outline::before{content:"\F16A0"}.mdi-robot-dead::before{content:"\F16A1"}.mdi-robot-dead-outline::before{content:"\F16A2"}.mdi-robot-excited::before{content:"\F16A3"}.mdi-robot-excited-outline::before{content:"\F16A4"}.mdi-robot-happy::before{content:"\F1719"}.mdi-robot-happy-outline::before{content:"\F171A"}.mdi-robot-industrial::before{content:"\F0B46"}.mdi-robot-industrial-outline::before{content:"\F1A1A"}.mdi-robot-love::before{content:"\F16A5"}.mdi-robot-love-outline::before{content:"\F16A6"}.mdi-robot-mower::before{content:"\F11F7"}.mdi-robot-mower-outline::before{content:"\F11F3"}.mdi-robot-off::before{content:"\F16A7"}.mdi-robot-off-outline::before{content:"\F167B"}.mdi-robot-outline::before{content:"\F167A"}.mdi-robot-vacuum::before{content:"\F070D"}.mdi-robot-vacuum-alert::before{content:"\F1B5D"}.mdi-robot-vacuum-off::before{content:"\F1C01"}.mdi-robot-vacuum-variant::before{content:"\F0908"}.mdi-robot-vacuum-variant-alert::before{content:"\F1B5E"}.mdi-robot-vacuum-variant-off::before{content:"\F1C02"}.mdi-rocket::before{content:"\F0463"}.mdi-rocket-launch::before{content:"\F14DE"}.mdi-rocket-launch-outline::before{content:"\F14DF"}.mdi-rocket-outline::before{content:"\F13AF"}.mdi-rodent::before{content:"\F1327"}.mdi-roller-shade::before{content:"\F1A6B"}.mdi-roller-shade-closed::before{content:"\F1A6C"}.mdi-roller-skate::before{content:"\F0D2B"}.mdi-roller-skate-off::before{content:"\F0145"}.mdi-rollerblade::before{content:"\F0D2C"}.mdi-rollerblade-off::before{content:"\F002E"}.mdi-rollupjs::before{content:"\F0BC0"}.mdi-rolodex::before{content:"\F1AB9"}.mdi-rolodex-outline::before{content:"\F1ABA"}.mdi-roman-numeral-1::before{content:"\F1088"}.mdi-roman-numeral-10::before{content:"\F1091"}.mdi-roman-numeral-2::before{content:"\F1089"}.mdi-roman-numeral-3::before{content:"\F108A"}.mdi-roman-numeral-4::before{content:"\F108B"}.mdi-roman-numeral-5::before{content:"\F108C"}.mdi-roman-numeral-6::before{content:"\F108D"}.mdi-roman-numeral-7::before{content:"\F108E"}.mdi-roman-numeral-8::before{content:"\F108F"}.mdi-roman-numeral-9::before{content:"\F1090"}.mdi-room-service::before{content:"\F088D"}.mdi-room-service-outline::before{content:"\F0D97"}.mdi-rotate-360::before{content:"\F1999"}.mdi-rotate-3d::before{content:"\F0EC7"}.mdi-rotate-3d-variant::before{content:"\F0464"}.mdi-rotate-left::before{content:"\F0465"}.mdi-rotate-left-variant::before{content:"\F0466"}.mdi-rotate-orbit::before{content:"\F0D98"}.mdi-rotate-right::before{content:"\F0467"}.mdi-rotate-right-variant::before{content:"\F0468"}.mdi-rounded-corner::before{content:"\F0607"}.mdi-router::before{content:"\F11E2"}.mdi-router-network::before{content:"\F1087"}.mdi-router-network-wireless::before{content:"\F1C97"}.mdi-router-wireless::before{content:"\F0469"}.mdi-router-wireless-off::before{content:"\F15A3"}.mdi-router-wireless-settings::before{content:"\F0A69"}.mdi-routes::before{content:"\F046A"}.mdi-routes-clock::before{content:"\F1059"}.mdi-rowing::before{content:"\F0608"}.mdi-rss::before{content:"\F046B"}.mdi-rss-box::before{content:"\F046C"}.mdi-rss-off::before{content:"\F0F21"}.mdi-rug::before{content:"\F1475"}.mdi-rugby::before{content:"\F0D99"}.mdi-ruler::before{content:"\F046D"}.mdi-ruler-square::before{content:"\F0CC2"}.mdi-ruler-square-compass::before{content:"\F0EBE"}.mdi-run::before{content:"\F070E"}.mdi-run-fast::before{content:"\F046E"}.mdi-rv-truck::before{content:"\F11D4"}.mdi-sack::before{content:"\F0D2E"}.mdi-sack-outline::before{content:"\F1C4C"}.mdi-sack-percent::before{content:"\F0D2F"}.mdi-safe::before{content:"\F0A6A"}.mdi-safe-square::before{content:"\F127C"}.mdi-safe-square-outline::before{content:"\F127D"}.mdi-safety-goggles::before{content:"\F0D30"}.mdi-sail-boat::before{content:"\F0EC8"}.mdi-sail-boat-sink::before{content:"\F1AEF"}.mdi-sale::before{content:"\F046F"}.mdi-sale-outline::before{content:"\F1A06"}.mdi-salesforce::before{content:"\F088E"}.mdi-sass::before{content:"\F07EC"}.mdi-satellite::before{content:"\F0470"}.mdi-satellite-uplink::before{content:"\F0909"}.mdi-satellite-variant::before{content:"\F0471"}.mdi-sausage::before{content:"\F08BA"}.mdi-sausage-off::before{content:"\F1789"}.mdi-saw-blade::before{content:"\F0E61"}.mdi-sawtooth-wave::before{content:"\F147A"}.mdi-saxophone::before{content:"\F0609"}.mdi-scale::before{content:"\F0472"}.mdi-scale-balance::before{content:"\F05D1"}.mdi-scale-bathroom::before{content:"\F0473"}.mdi-scale-off::before{content:"\F105A"}.mdi-scale-unbalanced::before{content:"\F19B8"}.mdi-scan-helper::before{content:"\F13D8"}.mdi-scanner::before{content:"\F06AB"}.mdi-scanner-off::before{content:"\F090A"}.mdi-scatter-plot::before{content:"\F0EC9"}.mdi-scatter-plot-outline::before{content:"\F0ECA"}.mdi-scent::before{content:"\F1958"}.mdi-scent-off::before{content:"\F1959"}.mdi-school::before{content:"\F0474"}.mdi-school-outline::before{content:"\F1180"}.mdi-scissors-cutting::before{content:"\F0A6B"}.mdi-scooter::before{content:"\F15BD"}.mdi-scooter-electric::before{content:"\F15BE"}.mdi-scoreboard::before{content:"\F127E"}.mdi-scoreboard-outline::before{content:"\F127F"}.mdi-screen-rotation::before{content:"\F0475"}.mdi-screen-rotation-lock::before{content:"\F0478"}.mdi-screw-flat-top::before{content:"\F0DF3"}.mdi-screw-lag::before{content:"\F0DF4"}.mdi-screw-machine-flat-top::before{content:"\F0DF5"}.mdi-screw-machine-round-top::before{content:"\F0DF6"}.mdi-screw-round-top::before{content:"\F0DF7"}.mdi-screwdriver::before{content:"\F0476"}.mdi-script::before{content:"\F0BC1"}.mdi-script-outline::before{content:"\F0477"}.mdi-script-text::before{content:"\F0BC2"}.mdi-script-text-key::before{content:"\F1725"}.mdi-script-text-key-outline::before{content:"\F1726"}.mdi-script-text-outline::before{content:"\F0BC3"}.mdi-script-text-play::before{content:"\F1727"}.mdi-script-text-play-outline::before{content:"\F1728"}.mdi-sd::before{content:"\F0479"}.mdi-seal::before{content:"\F047A"}.mdi-seal-variant::before{content:"\F0FD9"}.mdi-search-web::before{content:"\F070F"}.mdi-seat::before{content:"\F0CC3"}.mdi-seat-flat::before{content:"\F047B"}.mdi-seat-flat-angled::before{content:"\F047C"}.mdi-seat-individual-suite::before{content:"\F047D"}.mdi-seat-legroom-extra::before{content:"\F047E"}.mdi-seat-legroom-normal::before{content:"\F047F"}.mdi-seat-legroom-reduced::before{content:"\F0480"}.mdi-seat-outline::before{content:"\F0CC4"}.mdi-seat-passenger::before{content:"\F1249"}.mdi-seat-recline-extra::before{content:"\F0481"}.mdi-seat-recline-normal::before{content:"\F0482"}.mdi-seatbelt::before{content:"\F0CC5"}.mdi-security::before{content:"\F0483"}.mdi-security-network::before{content:"\F0484"}.mdi-seed::before{content:"\F0E62"}.mdi-seed-off::before{content:"\F13FD"}.mdi-seed-off-outline::before{content:"\F13FE"}.mdi-seed-outline::before{content:"\F0E63"}.mdi-seed-plus::before{content:"\F1A6D"}.mdi-seed-plus-outline::before{content:"\F1A6E"}.mdi-seesaw::before{content:"\F15A4"}.mdi-segment::before{content:"\F0ECB"}.mdi-select::before{content:"\F0485"}.mdi-select-all::before{content:"\F0486"}.mdi-select-arrow-down::before{content:"\F1B59"}.mdi-select-arrow-up::before{content:"\F1B58"}.mdi-select-color::before{content:"\F0D31"}.mdi-select-compare::before{content:"\F0AD9"}.mdi-select-drag::before{content:"\F0A6C"}.mdi-select-group::before{content:"\F0F82"}.mdi-select-inverse::before{content:"\F0487"}.mdi-select-marker::before{content:"\F1280"}.mdi-select-multiple::before{content:"\F1281"}.mdi-select-multiple-marker::before{content:"\F1282"}.mdi-select-off::before{content:"\F0488"}.mdi-select-place::before{content:"\F0FDA"}.mdi-select-remove::before{content:"\F17C1"}.mdi-select-search::before{content:"\F1204"}.mdi-selection::before{content:"\F0489"}.mdi-selection-drag::before{content:"\F0A6D"}.mdi-selection-ellipse::before{content:"\F0D32"}.mdi-selection-ellipse-arrow-inside::before{content:"\F0F22"}.mdi-selection-ellipse-remove::before{content:"\F17C2"}.mdi-selection-marker::before{content:"\F1283"}.mdi-selection-multiple::before{content:"\F1285"}.mdi-selection-multiple-marker::before{content:"\F1284"}.mdi-selection-off::before{content:"\F0777"}.mdi-selection-remove::before{content:"\F17C3"}.mdi-selection-search::before{content:"\F1205"}.mdi-semantic-web::before{content:"\F1316"}.mdi-send::before{content:"\F048A"}.mdi-send-check::before{content:"\F1161"}.mdi-send-check-outline::before{content:"\F1162"}.mdi-send-circle::before{content:"\F0DF8"}.mdi-send-circle-outline::before{content:"\F0DF9"}.mdi-send-clock::before{content:"\F1163"}.mdi-send-clock-outline::before{content:"\F1164"}.mdi-send-lock::before{content:"\F07ED"}.mdi-send-lock-outline::before{content:"\F1166"}.mdi-send-outline::before{content:"\F1165"}.mdi-send-variant::before{content:"\F1C4D"}.mdi-send-variant-clock::before{content:"\F1C7E"}.mdi-send-variant-clock-outline::before{content:"\F1C7F"}.mdi-send-variant-outline::before{content:"\F1C4E"}.mdi-serial-port::before{content:"\F065C"}.mdi-server::before{content:"\F048B"}.mdi-server-minus::before{content:"\F048C"}.mdi-server-minus-outline::before{content:"\F1C98"}.mdi-server-network::before{content:"\F048D"}.mdi-server-network-off::before{content:"\F048E"}.mdi-server-network-outline::before{content:"\F1C99"}.mdi-server-off::before{content:"\F048F"}.mdi-server-outline::before{content:"\F1C9A"}.mdi-server-plus::before{content:"\F0490"}.mdi-server-plus-outline::before{content:"\F1C9B"}.mdi-server-remove::before{content:"\F0491"}.mdi-server-security::before{content:"\F0492"}.mdi-set-all::before{content:"\F0778"}.mdi-set-center::before{content:"\F0779"}.mdi-set-center-right::before{content:"\F077A"}.mdi-set-left::before{content:"\F077B"}.mdi-set-left-center::before{content:"\F077C"}.mdi-set-left-right::before{content:"\F077D"}.mdi-set-merge::before{content:"\F14E0"}.mdi-set-none::before{content:"\F077E"}.mdi-set-right::before{content:"\F077F"}.mdi-set-split::before{content:"\F14E1"}.mdi-set-square::before{content:"\F145D"}.mdi-set-top-box::before{content:"\F099F"}.mdi-settings-helper::before{content:"\F0A6E"}.mdi-shaker::before{content:"\F110E"}.mdi-shaker-outline::before{content:"\F110F"}.mdi-shape::before{content:"\F0831"}.mdi-shape-circle-plus::before{content:"\F065D"}.mdi-shape-outline::before{content:"\F0832"}.mdi-shape-oval-plus::before{content:"\F11FA"}.mdi-shape-plus::before{content:"\F0495"}.mdi-shape-plus-outline::before{content:"\F1C4F"}.mdi-shape-polygon-plus::before{content:"\F065E"}.mdi-shape-rectangle-plus::before{content:"\F065F"}.mdi-shape-square-plus::before{content:"\F0660"}.mdi-shape-square-rounded-plus::before{content:"\F14FA"}.mdi-share::before{content:"\F0496"}.mdi-share-all::before{content:"\F11F4"}.mdi-share-all-outline::before{content:"\F11F5"}.mdi-share-circle::before{content:"\F11AD"}.mdi-share-off::before{content:"\F0F23"}.mdi-share-off-outline::before{content:"\F0F24"}.mdi-share-outline::before{content:"\F0932"}.mdi-share-variant::before{content:"\F0497"}.mdi-share-variant-outline::before{content:"\F1514"}.mdi-shark::before{content:"\F18BA"}.mdi-shark-fin::before{content:"\F1673"}.mdi-shark-fin-outline::before{content:"\F1674"}.mdi-shark-off::before{content:"\F18BB"}.mdi-sheep::before{content:"\F0CC6"}.mdi-shield::before{content:"\F0498"}.mdi-shield-account::before{content:"\F088F"}.mdi-shield-account-outline::before{content:"\F0A12"}.mdi-shield-account-variant::before{content:"\F15A7"}.mdi-shield-account-variant-outline::before{content:"\F15A8"}.mdi-shield-airplane::before{content:"\F06BB"}.mdi-shield-airplane-outline::before{content:"\F0CC7"}.mdi-shield-alert::before{content:"\F0ECC"}.mdi-shield-alert-outline::before{content:"\F0ECD"}.mdi-shield-bug::before{content:"\F13DA"}.mdi-shield-bug-outline::before{content:"\F13DB"}.mdi-shield-car::before{content:"\F0F83"}.mdi-shield-check::before{content:"\F0565"}.mdi-shield-check-outline::before{content:"\F0CC8"}.mdi-shield-cross::before{content:"\F0CC9"}.mdi-shield-cross-outline::before{content:"\F0CCA"}.mdi-shield-crown::before{content:"\F18BC"}.mdi-shield-crown-outline::before{content:"\F18BD"}.mdi-shield-edit::before{content:"\F11A0"}.mdi-shield-edit-outline::before{content:"\F11A1"}.mdi-shield-half::before{content:"\F1360"}.mdi-shield-half-full::before{content:"\F0780"}.mdi-shield-home::before{content:"\F068A"}.mdi-shield-home-outline::before{content:"\F0CCB"}.mdi-shield-key::before{content:"\F0BC4"}.mdi-shield-key-outline::before{content:"\F0BC5"}.mdi-shield-link-variant::before{content:"\F0D33"}.mdi-shield-link-variant-outline::before{content:"\F0D34"}.mdi-shield-lock::before{content:"\F099D"}.mdi-shield-lock-open::before{content:"\F199A"}.mdi-shield-lock-open-outline::before{content:"\F199B"}.mdi-shield-lock-outline::before{content:"\F0CCC"}.mdi-shield-moon::before{content:"\F1828"}.mdi-shield-moon-outline::before{content:"\F1829"}.mdi-shield-off::before{content:"\F099E"}.mdi-shield-off-outline::before{content:"\F099C"}.mdi-shield-outline::before{content:"\F0499"}.mdi-shield-plus::before{content:"\F0ADA"}.mdi-shield-plus-outline::before{content:"\F0ADB"}.mdi-shield-refresh::before{content:"\F00AA"}.mdi-shield-refresh-outline::before{content:"\F01E0"}.mdi-shield-remove::before{content:"\F0ADC"}.mdi-shield-remove-outline::before{content:"\F0ADD"}.mdi-shield-search::before{content:"\F0D9A"}.mdi-shield-star::before{content:"\F113B"}.mdi-shield-star-outline::before{content:"\F113C"}.mdi-shield-sun::before{content:"\F105D"}.mdi-shield-sun-outline::before{content:"\F105E"}.mdi-shield-sword::before{content:"\F18BE"}.mdi-shield-sword-outline::before{content:"\F18BF"}.mdi-shield-sync::before{content:"\F11A2"}.mdi-shield-sync-outline::before{content:"\F11A3"}.mdi-shimmer::before{content:"\F1545"}.mdi-ship-wheel::before{content:"\F0833"}.mdi-shipping-pallet::before{content:"\F184E"}.mdi-shoe-ballet::before{content:"\F15CA"}.mdi-shoe-cleat::before{content:"\F15C7"}.mdi-shoe-formal::before{content:"\F0B47"}.mdi-shoe-heel::before{content:"\F0B48"}.mdi-shoe-print::before{content:"\F0DFA"}.mdi-shoe-sneaker::before{content:"\F15C8"}.mdi-shopping::before{content:"\F049A"}.mdi-shopping-music::before{content:"\F049B"}.mdi-shopping-outline::before{content:"\F11D5"}.mdi-shopping-search::before{content:"\F0F84"}.mdi-shopping-search-outline::before{content:"\F1A6F"}.mdi-shore::before{content:"\F14F9"}.mdi-shovel::before{content:"\F0710"}.mdi-shovel-off::before{content:"\F0711"}.mdi-shower::before{content:"\F09A0"}.mdi-shower-head::before{content:"\F09A1"}.mdi-shredder::before{content:"\F049C"}.mdi-shuffle::before{content:"\F049D"}.mdi-shuffle-disabled::before{content:"\F049E"}.mdi-shuffle-variant::before{content:"\F049F"}.mdi-shuriken::before{content:"\F137F"}.mdi-sickle::before{content:"\F18C0"}.mdi-sigma::before{content:"\F04A0"}.mdi-sigma-lower::before{content:"\F062B"}.mdi-sign-caution::before{content:"\F04A1"}.mdi-sign-direction::before{content:"\F0781"}.mdi-sign-direction-minus::before{content:"\F1000"}.mdi-sign-direction-plus::before{content:"\F0FDC"}.mdi-sign-direction-remove::before{content:"\F0FDD"}.mdi-sign-language::before{content:"\F1B4D"}.mdi-sign-language-outline::before{content:"\F1B4E"}.mdi-sign-pole::before{content:"\F14F8"}.mdi-sign-real-estate::before{content:"\F1118"}.mdi-sign-text::before{content:"\F0782"}.mdi-sign-yield::before{content:"\F1BAF"}.mdi-signal::before{content:"\F04A2"}.mdi-signal-2g::before{content:"\F0712"}.mdi-signal-3g::before{content:"\F0713"}.mdi-signal-4g::before{content:"\F0714"}.mdi-signal-5g::before{content:"\F0A6F"}.mdi-signal-cellular-1::before{content:"\F08BC"}.mdi-signal-cellular-2::before{content:"\F08BD"}.mdi-signal-cellular-3::before{content:"\F08BE"}.mdi-signal-cellular-outline::before{content:"\F08BF"}.mdi-signal-distance-variant::before{content:"\F0E64"}.mdi-signal-hspa::before{content:"\F0715"}.mdi-signal-hspa-plus::before{content:"\F0716"}.mdi-signal-off::before{content:"\F0783"}.mdi-signal-variant::before{content:"\F060A"}.mdi-signature::before{content:"\F0DFB"}.mdi-signature-freehand::before{content:"\F0DFC"}.mdi-signature-image::before{content:"\F0DFD"}.mdi-signature-text::before{content:"\F0DFE"}.mdi-silo::before{content:"\F1B9F"}.mdi-silo-outline::before{content:"\F0B49"}.mdi-silverware::before{content:"\F04A3"}.mdi-silverware-clean::before{content:"\F0FDE"}.mdi-silverware-fork::before{content:"\F04A4"}.mdi-silverware-fork-knife::before{content:"\F0A70"}.mdi-silverware-spoon::before{content:"\F04A5"}.mdi-silverware-variant::before{content:"\F04A6"}.mdi-sim::before{content:"\F04A7"}.mdi-sim-alert::before{content:"\F04A8"}.mdi-sim-alert-outline::before{content:"\F15D3"}.mdi-sim-off::before{content:"\F04A9"}.mdi-sim-off-outline::before{content:"\F15D4"}.mdi-sim-outline::before{content:"\F15D5"}.mdi-simple-icons::before{content:"\F131D"}.mdi-sina-weibo::before{content:"\F0ADF"}.mdi-sine-wave::before{content:"\F095B"}.mdi-sitemap::before{content:"\F04AA"}.mdi-sitemap-outline::before{content:"\F199C"}.mdi-size-l::before{content:"\F13A6"}.mdi-size-m::before{content:"\F13A5"}.mdi-size-s::before{content:"\F13A4"}.mdi-size-xl::before{content:"\F13A7"}.mdi-size-xs::before{content:"\F13A3"}.mdi-size-xxl::before{content:"\F13A8"}.mdi-size-xxs::before{content:"\F13A2"}.mdi-size-xxxl::before{content:"\F13A9"}.mdi-skate::before{content:"\F0D35"}.mdi-skate-off::before{content:"\F0699"}.mdi-skateboard::before{content:"\F14C2"}.mdi-skateboarding::before{content:"\F0501"}.mdi-skew-less::before{content:"\F0D36"}.mdi-skew-more::before{content:"\F0D37"}.mdi-ski::before{content:"\F1304"}.mdi-ski-cross-country::before{content:"\F1305"}.mdi-ski-water::before{content:"\F1306"}.mdi-skip-backward::before{content:"\F04AB"}.mdi-skip-backward-outline::before{content:"\F0F25"}.mdi-skip-forward::before{content:"\F04AC"}.mdi-skip-forward-outline::before{content:"\F0F26"}.mdi-skip-next::before{content:"\F04AD"}.mdi-skip-next-circle::before{content:"\F0661"}.mdi-skip-next-circle-outline::before{content:"\F0662"}.mdi-skip-next-outline::before{content:"\F0F27"}.mdi-skip-previous::before{content:"\F04AE"}.mdi-skip-previous-circle::before{content:"\F0663"}.mdi-skip-previous-circle-outline::before{content:"\F0664"}.mdi-skip-previous-outline::before{content:"\F0F28"}.mdi-skull::before{content:"\F068C"}.mdi-skull-crossbones::before{content:"\F0BC6"}.mdi-skull-crossbones-outline::before{content:"\F0BC7"}.mdi-skull-outline::before{content:"\F0BC8"}.mdi-skull-scan::before{content:"\F14C7"}.mdi-skull-scan-outline::before{content:"\F14C8"}.mdi-skype::before{content:"\F04AF"}.mdi-skype-business::before{content:"\F04B0"}.mdi-slack::before{content:"\F04B1"}.mdi-slash-forward::before{content:"\F0FDF"}.mdi-slash-forward-box::before{content:"\F0FE0"}.mdi-sledding::before{content:"\F041B"}.mdi-sleep::before{content:"\F04B2"}.mdi-sleep-off::before{content:"\F04B3"}.mdi-slide::before{content:"\F15A5"}.mdi-slope-downhill::before{content:"\F0DFF"}.mdi-slope-uphill::before{content:"\F0E00"}.mdi-slot-machine::before{content:"\F1114"}.mdi-slot-machine-outline::before{content:"\F1115"}.mdi-smart-card::before{content:"\F10BD"}.mdi-smart-card-off::before{content:"\F18F7"}.mdi-smart-card-off-outline::before{content:"\F18F8"}.mdi-smart-card-outline::before{content:"\F10BE"}.mdi-smart-card-reader::before{content:"\F10BF"}.mdi-smart-card-reader-outline::before{content:"\F10C0"}.mdi-smog::before{content:"\F0A71"}.mdi-smoke::before{content:"\F1799"}.mdi-smoke-detector::before{content:"\F0392"}.mdi-smoke-detector-alert::before{content:"\F192E"}.mdi-smoke-detector-alert-outline::before{content:"\F192F"}.mdi-smoke-detector-off::before{content:"\F1809"}.mdi-smoke-detector-off-outline::before{content:"\F180A"}.mdi-smoke-detector-outline::before{content:"\F1808"}.mdi-smoke-detector-variant::before{content:"\F180B"}.mdi-smoke-detector-variant-alert::before{content:"\F1930"}.mdi-smoke-detector-variant-off::before{content:"\F180C"}.mdi-smoking::before{content:"\F04B4"}.mdi-smoking-off::before{content:"\F04B5"}.mdi-smoking-pipe::before{content:"\F140D"}.mdi-smoking-pipe-off::before{content:"\F1428"}.mdi-snail::before{content:"\F1677"}.mdi-snake::before{content:"\F150E"}.mdi-snapchat::before{content:"\F04B6"}.mdi-snowboard::before{content:"\F1307"}.mdi-snowflake::before{content:"\F0717"}.mdi-snowflake-alert::before{content:"\F0F29"}.mdi-snowflake-check::before{content:"\F1A70"}.mdi-snowflake-melt::before{content:"\F12CB"}.mdi-snowflake-off::before{content:"\F14E3"}.mdi-snowflake-thermometer::before{content:"\F1A71"}.mdi-snowflake-variant::before{content:"\F0F2A"}.mdi-snowman::before{content:"\F04B7"}.mdi-snowmobile::before{content:"\F06DD"}.mdi-snowshoeing::before{content:"\F1A72"}.mdi-soccer::before{content:"\F04B8"}.mdi-soccer-field::before{content:"\F0834"}.mdi-social-distance-2-meters::before{content:"\F1579"}.mdi-social-distance-6-feet::before{content:"\F157A"}.mdi-sofa::before{content:"\F04B9"}.mdi-sofa-outline::before{content:"\F156D"}.mdi-sofa-single::before{content:"\F156E"}.mdi-sofa-single-outline::before{content:"\F156F"}.mdi-solar-panel::before{content:"\F0D9B"}.mdi-solar-panel-large::before{content:"\F0D9C"}.mdi-solar-power::before{content:"\F0A72"}.mdi-solar-power-variant::before{content:"\F1A73"}.mdi-solar-power-variant-outline::before{content:"\F1A74"}.mdi-soldering-iron::before{content:"\F1092"}.mdi-solid::before{content:"\F068D"}.mdi-sony-playstation::before{content:"\F0414"}.mdi-sort::before{content:"\F04BA"}.mdi-sort-alphabetical-ascending::before{content:"\F05BD"}.mdi-sort-alphabetical-ascending-variant::before{content:"\F1148"}.mdi-sort-alphabetical-descending::before{content:"\F05BF"}.mdi-sort-alphabetical-descending-variant::before{content:"\F1149"}.mdi-sort-alphabetical-variant::before{content:"\F04BB"}.mdi-sort-ascending::before{content:"\F04BC"}.mdi-sort-bool-ascending::before{content:"\F1385"}.mdi-sort-bool-ascending-variant::before{content:"\F1386"}.mdi-sort-bool-descending::before{content:"\F1387"}.mdi-sort-bool-descending-variant::before{content:"\F1388"}.mdi-sort-calendar-ascending::before{content:"\F1547"}.mdi-sort-calendar-descending::before{content:"\F1548"}.mdi-sort-clock-ascending::before{content:"\F1549"}.mdi-sort-clock-ascending-outline::before{content:"\F154A"}.mdi-sort-clock-descending::before{content:"\F154B"}.mdi-sort-clock-descending-outline::before{content:"\F154C"}.mdi-sort-descending::before{content:"\F04BD"}.mdi-sort-numeric-ascending::before{content:"\F1389"}.mdi-sort-numeric-ascending-variant::before{content:"\F090D"}.mdi-sort-numeric-descending::before{content:"\F138A"}.mdi-sort-numeric-descending-variant::before{content:"\F0AD2"}.mdi-sort-numeric-variant::before{content:"\F04BE"}.mdi-sort-reverse-variant::before{content:"\F033C"}.mdi-sort-variant::before{content:"\F04BF"}.mdi-sort-variant-lock::before{content:"\F0CCD"}.mdi-sort-variant-lock-open::before{content:"\F0CCE"}.mdi-sort-variant-off::before{content:"\F1ABB"}.mdi-sort-variant-remove::before{content:"\F1147"}.mdi-soundbar::before{content:"\F17DB"}.mdi-soundcloud::before{content:"\F04C0"}.mdi-source-branch::before{content:"\F062C"}.mdi-source-branch-check::before{content:"\F14CF"}.mdi-source-branch-minus::before{content:"\F14CB"}.mdi-source-branch-plus::before{content:"\F14CA"}.mdi-source-branch-refresh::before{content:"\F14CD"}.mdi-source-branch-remove::before{content:"\F14CC"}.mdi-source-branch-sync::before{content:"\F14CE"}.mdi-source-commit::before{content:"\F0718"}.mdi-source-commit-end::before{content:"\F0719"}.mdi-source-commit-end-local::before{content:"\F071A"}.mdi-source-commit-local::before{content:"\F071B"}.mdi-source-commit-next-local::before{content:"\F071C"}.mdi-source-commit-start::before{content:"\F071D"}.mdi-source-commit-start-next-local::before{content:"\F071E"}.mdi-source-fork::before{content:"\F04C1"}.mdi-source-merge::before{content:"\F062D"}.mdi-source-pull::before{content:"\F04C2"}.mdi-source-repository::before{content:"\F0CCF"}.mdi-source-repository-multiple::before{content:"\F0CD0"}.mdi-soy-sauce::before{content:"\F07EE"}.mdi-soy-sauce-off::before{content:"\F13FC"}.mdi-spa::before{content:"\F0CD1"}.mdi-spa-outline::before{content:"\F0CD2"}.mdi-space-invaders::before{content:"\F0BC9"}.mdi-space-station::before{content:"\F1383"}.mdi-spade::before{content:"\F0E65"}.mdi-speaker::before{content:"\F04C3"}.mdi-speaker-bluetooth::before{content:"\F09A2"}.mdi-speaker-message::before{content:"\F1B11"}.mdi-speaker-multiple::before{content:"\F0D38"}.mdi-speaker-off::before{content:"\F04C4"}.mdi-speaker-pause::before{content:"\F1B73"}.mdi-speaker-play::before{content:"\F1B72"}.mdi-speaker-stop::before{content:"\F1B74"}.mdi-speaker-wireless::before{content:"\F071F"}.mdi-spear::before{content:"\F1845"}.mdi-speedometer::before{content:"\F04C5"}.mdi-speedometer-medium::before{content:"\F0F85"}.mdi-speedometer-slow::before{content:"\F0F86"}.mdi-spellcheck::before{content:"\F04C6"}.mdi-sphere::before{content:"\F1954"}.mdi-sphere-off::before{content:"\F1955"}.mdi-spider::before{content:"\F11EA"}.mdi-spider-outline::before{content:"\F1C75"}.mdi-spider-thread::before{content:"\F11EB"}.mdi-spider-web::before{content:"\F0BCA"}.mdi-spirit-level::before{content:"\F14F1"}.mdi-spoon-sugar::before{content:"\F1429"}.mdi-spotify::before{content:"\F04C7"}.mdi-spotlight::before{content:"\F04C8"}.mdi-spotlight-beam::before{content:"\F04C9"}.mdi-spray::before{content:"\F0665"}.mdi-spray-bottle::before{content:"\F0AE0"}.mdi-sprinkler::before{content:"\F105F"}.mdi-sprinkler-fire::before{content:"\F199D"}.mdi-sprinkler-variant::before{content:"\F1060"}.mdi-sprout::before{content:"\F0E66"}.mdi-sprout-outline::before{content:"\F0E67"}.mdi-square::before{content:"\F0764"}.mdi-square-circle::before{content:"\F1500"}.mdi-square-circle-outline::before{content:"\F1C50"}.mdi-square-edit-outline::before{content:"\F090C"}.mdi-square-medium::before{content:"\F0A13"}.mdi-square-medium-outline::before{content:"\F0A14"}.mdi-square-off::before{content:"\F12EE"}.mdi-square-off-outline::before{content:"\F12EF"}.mdi-square-opacity::before{content:"\F1854"}.mdi-square-outline::before{content:"\F0763"}.mdi-square-root::before{content:"\F0784"}.mdi-square-root-box::before{content:"\F09A3"}.mdi-square-rounded::before{content:"\F14FB"}.mdi-square-rounded-badge::before{content:"\F1A07"}.mdi-square-rounded-badge-outline::before{content:"\F1A08"}.mdi-square-rounded-outline::before{content:"\F14FC"}.mdi-square-small::before{content:"\F0A15"}.mdi-square-wave::before{content:"\F147B"}.mdi-squeegee::before{content:"\F0AE1"}.mdi-ssh::before{content:"\F08C0"}.mdi-stack-exchange::before{content:"\F060B"}.mdi-stack-overflow::before{content:"\F04CC"}.mdi-stackpath::before{content:"\F0359"}.mdi-stadium::before{content:"\F0FF9"}.mdi-stadium-outline::before{content:"\F1B03"}.mdi-stadium-variant::before{content:"\F0720"}.mdi-stairs::before{content:"\F04CD"}.mdi-stairs-box::before{content:"\F139E"}.mdi-stairs-down::before{content:"\F12BE"}.mdi-stairs-up::before{content:"\F12BD"}.mdi-stamper::before{content:"\F0D39"}.mdi-standard-definition::before{content:"\F07EF"}.mdi-star::before{content:"\F04CE"}.mdi-star-box::before{content:"\F0A73"}.mdi-star-box-multiple::before{content:"\F1286"}.mdi-star-box-multiple-outline::before{content:"\F1287"}.mdi-star-box-outline::before{content:"\F0A74"}.mdi-star-check::before{content:"\F1566"}.mdi-star-check-outline::before{content:"\F156A"}.mdi-star-circle::before{content:"\F04CF"}.mdi-star-circle-outline::before{content:"\F09A4"}.mdi-star-cog::before{content:"\F1668"}.mdi-star-cog-outline::before{content:"\F1669"}.mdi-star-crescent::before{content:"\F0979"}.mdi-star-david::before{content:"\F097A"}.mdi-star-face::before{content:"\F09A5"}.mdi-star-four-points::before{content:"\F0AE2"}.mdi-star-four-points-box::before{content:"\F1C51"}.mdi-star-four-points-box-outline::before{content:"\F1C52"}.mdi-star-four-points-circle::before{content:"\F1C53"}.mdi-star-four-points-circle-outline::before{content:"\F1C54"}.mdi-star-four-points-outline::before{content:"\F0AE3"}.mdi-star-four-points-small::before{content:"\F1C55"}.mdi-star-half::before{content:"\F0246"}.mdi-star-half-full::before{content:"\F04D0"}.mdi-star-minus::before{content:"\F1564"}.mdi-star-minus-outline::before{content:"\F1568"}.mdi-star-off::before{content:"\F04D1"}.mdi-star-off-outline::before{content:"\F155B"}.mdi-star-outline::before{content:"\F04D2"}.mdi-star-plus::before{content:"\F1563"}.mdi-star-plus-outline::before{content:"\F1567"}.mdi-star-remove::before{content:"\F1565"}.mdi-star-remove-outline::before{content:"\F1569"}.mdi-star-settings::before{content:"\F166A"}.mdi-star-settings-outline::before{content:"\F166B"}.mdi-star-shooting::before{content:"\F1741"}.mdi-star-shooting-outline::before{content:"\F1742"}.mdi-star-three-points::before{content:"\F0AE4"}.mdi-star-three-points-outline::before{content:"\F0AE5"}.mdi-state-machine::before{content:"\F11EF"}.mdi-steam::before{content:"\F04D3"}.mdi-steering::before{content:"\F04D4"}.mdi-steering-off::before{content:"\F090E"}.mdi-step-backward::before{content:"\F04D5"}.mdi-step-backward-2::before{content:"\F04D6"}.mdi-step-forward::before{content:"\F04D7"}.mdi-step-forward-2::before{content:"\F04D8"}.mdi-stethoscope::before{content:"\F04D9"}.mdi-sticker::before{content:"\F1364"}.mdi-sticker-alert::before{content:"\F1365"}.mdi-sticker-alert-outline::before{content:"\F1366"}.mdi-sticker-check::before{content:"\F1367"}.mdi-sticker-check-outline::before{content:"\F1368"}.mdi-sticker-circle-outline::before{content:"\F05D0"}.mdi-sticker-emoji::before{content:"\F0785"}.mdi-sticker-minus::before{content:"\F1369"}.mdi-sticker-minus-outline::before{content:"\F136A"}.mdi-sticker-outline::before{content:"\F136B"}.mdi-sticker-plus::before{content:"\F136C"}.mdi-sticker-plus-outline::before{content:"\F136D"}.mdi-sticker-remove::before{content:"\F136E"}.mdi-sticker-remove-outline::before{content:"\F136F"}.mdi-sticker-text::before{content:"\F178E"}.mdi-sticker-text-outline::before{content:"\F178F"}.mdi-stocking::before{content:"\F04DA"}.mdi-stomach::before{content:"\F1093"}.mdi-stool::before{content:"\F195D"}.mdi-stool-outline::before{content:"\F195E"}.mdi-stop::before{content:"\F04DB"}.mdi-stop-circle::before{content:"\F0666"}.mdi-stop-circle-outline::before{content:"\F0667"}.mdi-storage-tank::before{content:"\F1A75"}.mdi-storage-tank-outline::before{content:"\F1A76"}.mdi-store::before{content:"\F04DC"}.mdi-store-24-hour::before{content:"\F04DD"}.mdi-store-alert::before{content:"\F18C1"}.mdi-store-alert-outline::before{content:"\F18C2"}.mdi-store-check::before{content:"\F18C3"}.mdi-store-check-outline::before{content:"\F18C4"}.mdi-store-clock::before{content:"\F18C5"}.mdi-store-clock-outline::before{content:"\F18C6"}.mdi-store-cog::before{content:"\F18C7"}.mdi-store-cog-outline::before{content:"\F18C8"}.mdi-store-edit::before{content:"\F18C9"}.mdi-store-edit-outline::before{content:"\F18CA"}.mdi-store-marker::before{content:"\F18CB"}.mdi-store-marker-outline::before{content:"\F18CC"}.mdi-store-minus::before{content:"\F165E"}.mdi-store-minus-outline::before{content:"\F18CD"}.mdi-store-off::before{content:"\F18CE"}.mdi-store-off-outline::before{content:"\F18CF"}.mdi-store-outline::before{content:"\F1361"}.mdi-store-plus::before{content:"\F165F"}.mdi-store-plus-outline::before{content:"\F18D0"}.mdi-store-remove::before{content:"\F1660"}.mdi-store-remove-outline::before{content:"\F18D1"}.mdi-store-search::before{content:"\F18D2"}.mdi-store-search-outline::before{content:"\F18D3"}.mdi-store-settings::before{content:"\F18D4"}.mdi-store-settings-outline::before{content:"\F18D5"}.mdi-storefront::before{content:"\F07C7"}.mdi-storefront-check::before{content:"\F1B7D"}.mdi-storefront-check-outline::before{content:"\F1B7E"}.mdi-storefront-edit::before{content:"\F1B7F"}.mdi-storefront-edit-outline::before{content:"\F1B80"}.mdi-storefront-minus::before{content:"\F1B83"}.mdi-storefront-minus-outline::before{content:"\F1B84"}.mdi-storefront-outline::before{content:"\F10C1"}.mdi-storefront-plus::before{content:"\F1B81"}.mdi-storefront-plus-outline::before{content:"\F1B82"}.mdi-storefront-remove::before{content:"\F1B85"}.mdi-storefront-remove-outline::before{content:"\F1B86"}.mdi-stove::before{content:"\F04DE"}.mdi-strategy::before{content:"\F11D6"}.mdi-stretch-to-page::before{content:"\F0F2B"}.mdi-stretch-to-page-outline::before{content:"\F0F2C"}.mdi-string-lights::before{content:"\F12BA"}.mdi-string-lights-off::before{content:"\F12BB"}.mdi-subdirectory-arrow-left::before{content:"\F060C"}.mdi-subdirectory-arrow-right::before{content:"\F060D"}.mdi-submarine::before{content:"\F156C"}.mdi-subtitles::before{content:"\F0A16"}.mdi-subtitles-outline::before{content:"\F0A17"}.mdi-subway::before{content:"\F06AC"}.mdi-subway-alert-variant::before{content:"\F0D9D"}.mdi-subway-variant::before{content:"\F04DF"}.mdi-summit::before{content:"\F0786"}.mdi-sun-angle::before{content:"\F1B27"}.mdi-sun-angle-outline::before{content:"\F1B28"}.mdi-sun-clock::before{content:"\F1A77"}.mdi-sun-clock-outline::before{content:"\F1A78"}.mdi-sun-compass::before{content:"\F19A5"}.mdi-sun-snowflake::before{content:"\F1796"}.mdi-sun-snowflake-variant::before{content:"\F1A79"}.mdi-sun-thermometer::before{content:"\F18D6"}.mdi-sun-thermometer-outline::before{content:"\F18D7"}.mdi-sun-wireless::before{content:"\F17FE"}.mdi-sun-wireless-outline::before{content:"\F17FF"}.mdi-sunglasses::before{content:"\F04E0"}.mdi-surfing::before{content:"\F1746"}.mdi-surround-sound::before{content:"\F05C5"}.mdi-surround-sound-2-0::before{content:"\F07F0"}.mdi-surround-sound-2-1::before{content:"\F1729"}.mdi-surround-sound-3-1::before{content:"\F07F1"}.mdi-surround-sound-5-1::before{content:"\F07F2"}.mdi-surround-sound-5-1-2::before{content:"\F172A"}.mdi-surround-sound-7-1::before{content:"\F07F3"}.mdi-svg::before{content:"\F0721"}.mdi-swap-horizontal::before{content:"\F04E1"}.mdi-swap-horizontal-bold::before{content:"\F0BCD"}.mdi-swap-horizontal-circle::before{content:"\F0FE1"}.mdi-swap-horizontal-circle-outline::before{content:"\F0FE2"}.mdi-swap-horizontal-hidden::before{content:"\F1D0E"}.mdi-swap-horizontal-variant::before{content:"\F08C1"}.mdi-swap-vertical::before{content:"\F04E2"}.mdi-swap-vertical-bold::before{content:"\F0BCE"}.mdi-swap-vertical-circle::before{content:"\F0FE3"}.mdi-swap-vertical-circle-outline::before{content:"\F0FE4"}.mdi-swap-vertical-variant::before{content:"\F08C2"}.mdi-swim::before{content:"\F04E3"}.mdi-switch::before{content:"\F04E4"}.mdi-sword::before{content:"\F04E5"}.mdi-sword-cross::before{content:"\F0787"}.mdi-syllabary-hangul::before{content:"\F1333"}.mdi-syllabary-hiragana::before{content:"\F1334"}.mdi-syllabary-katakana::before{content:"\F1335"}.mdi-syllabary-katakana-halfwidth::before{content:"\F1336"}.mdi-symbol::before{content:"\F1501"}.mdi-symfony::before{content:"\F0AE6"}.mdi-synagogue::before{content:"\F1B04"}.mdi-synagogue-outline::before{content:"\F1B05"}.mdi-sync::before{content:"\F04E6"}.mdi-sync-alert::before{content:"\F04E7"}.mdi-sync-circle::before{content:"\F1378"}.mdi-sync-off::before{content:"\F04E8"}.mdi-tab::before{content:"\F04E9"}.mdi-tab-minus::before{content:"\F0B4B"}.mdi-tab-plus::before{content:"\F075C"}.mdi-tab-remove::before{content:"\F0B4C"}.mdi-tab-search::before{content:"\F199E"}.mdi-tab-unselected::before{content:"\F04EA"}.mdi-table::before{content:"\F04EB"}.mdi-table-account::before{content:"\F13B9"}.mdi-table-alert::before{content:"\F13BA"}.mdi-table-arrow-down::before{content:"\F13BB"}.mdi-table-arrow-left::before{content:"\F13BC"}.mdi-table-arrow-right::before{content:"\F13BD"}.mdi-table-arrow-up::before{content:"\F13BE"}.mdi-table-border::before{content:"\F0A18"}.mdi-table-cancel::before{content:"\F13BF"}.mdi-table-chair::before{content:"\F1061"}.mdi-table-check::before{content:"\F13C0"}.mdi-table-clock::before{content:"\F13C1"}.mdi-table-cog::before{content:"\F13C2"}.mdi-table-column::before{content:"\F0835"}.mdi-table-column-plus-after::before{content:"\F04EC"}.mdi-table-column-plus-before::before{content:"\F04ED"}.mdi-table-column-remove::before{content:"\F04EE"}.mdi-table-column-width::before{content:"\F04EF"}.mdi-table-edit::before{content:"\F04F0"}.mdi-table-eye::before{content:"\F1094"}.mdi-table-eye-off::before{content:"\F13C3"}.mdi-table-filter::before{content:"\F1B8C"}.mdi-table-furniture::before{content:"\F05BC"}.mdi-table-headers-eye::before{content:"\F121D"}.mdi-table-headers-eye-off::before{content:"\F121E"}.mdi-table-heart::before{content:"\F13C4"}.mdi-table-key::before{content:"\F13C5"}.mdi-table-large::before{content:"\F04F1"}.mdi-table-large-plus::before{content:"\F0F87"}.mdi-table-large-remove::before{content:"\F0F88"}.mdi-table-lock::before{content:"\F13C6"}.mdi-table-merge-cells::before{content:"\F09A6"}.mdi-table-minus::before{content:"\F13C7"}.mdi-table-multiple::before{content:"\F13C8"}.mdi-table-network::before{content:"\F13C9"}.mdi-table-of-contents::before{content:"\F0836"}.mdi-table-off::before{content:"\F13CA"}.mdi-table-picnic::before{content:"\F1743"}.mdi-table-pivot::before{content:"\F183C"}.mdi-table-plus::before{content:"\F0A75"}.mdi-table-question::before{content:"\F1B21"}.mdi-table-refresh::before{content:"\F13A0"}.mdi-table-remove::before{content:"\F0A76"}.mdi-table-row::before{content:"\F0837"}.mdi-table-row-height::before{content:"\F04F2"}.mdi-table-row-plus-after::before{content:"\F04F3"}.mdi-table-row-plus-before::before{content:"\F04F4"}.mdi-table-row-remove::before{content:"\F04F5"}.mdi-table-search::before{content:"\F090F"}.mdi-table-settings::before{content:"\F0838"}.mdi-table-split-cell::before{content:"\F142A"}.mdi-table-star::before{content:"\F13CB"}.mdi-table-sync::before{content:"\F13A1"}.mdi-table-tennis::before{content:"\F0E68"}.mdi-tablet::before{content:"\F04F6"}.mdi-tablet-cellphone::before{content:"\F09A7"}.mdi-tablet-dashboard::before{content:"\F0ECE"}.mdi-taco::before{content:"\F0762"}.mdi-tag::before{content:"\F04F9"}.mdi-tag-arrow-down::before{content:"\F172B"}.mdi-tag-arrow-down-outline::before{content:"\F172C"}.mdi-tag-arrow-left::before{content:"\F172D"}.mdi-tag-arrow-left-outline::before{content:"\F172E"}.mdi-tag-arrow-right::before{content:"\F172F"}.mdi-tag-arrow-right-outline::before{content:"\F1730"}.mdi-tag-arrow-up::before{content:"\F1731"}.mdi-tag-arrow-up-outline::before{content:"\F1732"}.mdi-tag-check::before{content:"\F1A7A"}.mdi-tag-check-outline::before{content:"\F1A7B"}.mdi-tag-edit::before{content:"\F1C9C"}.mdi-tag-edit-outline::before{content:"\F1C9D"}.mdi-tag-faces::before{content:"\F04FA"}.mdi-tag-heart::before{content:"\F068B"}.mdi-tag-heart-outline::before{content:"\F0BCF"}.mdi-tag-hidden::before{content:"\F1C76"}.mdi-tag-minus::before{content:"\F0910"}.mdi-tag-minus-outline::before{content:"\F121F"}.mdi-tag-multiple::before{content:"\F04FB"}.mdi-tag-multiple-outline::before{content:"\F12F7"}.mdi-tag-off::before{content:"\F1220"}.mdi-tag-off-outline::before{content:"\F1221"}.mdi-tag-outline::before{content:"\F04FC"}.mdi-tag-plus::before{content:"\F0722"}.mdi-tag-plus-outline::before{content:"\F1222"}.mdi-tag-remove::before{content:"\F0723"}.mdi-tag-remove-outline::before{content:"\F1223"}.mdi-tag-search::before{content:"\F1907"}.mdi-tag-search-outline::before{content:"\F1908"}.mdi-tag-text::before{content:"\F1224"}.mdi-tag-text-outline::before{content:"\F04FD"}.mdi-tailwind::before{content:"\F13FF"}.mdi-tally-mark-1::before{content:"\F1ABC"}.mdi-tally-mark-2::before{content:"\F1ABD"}.mdi-tally-mark-3::before{content:"\F1ABE"}.mdi-tally-mark-4::before{content:"\F1ABF"}.mdi-tally-mark-5::before{content:"\F1AC0"}.mdi-tangram::before{content:"\F04F8"}.mdi-tank::before{content:"\F0D3A"}.mdi-tanker-truck::before{content:"\F0FE5"}.mdi-tape-drive::before{content:"\F16DF"}.mdi-tape-measure::before{content:"\F0B4D"}.mdi-target::before{content:"\F04FE"}.mdi-target-account::before{content:"\F0BD0"}.mdi-target-variant::before{content:"\F0A77"}.mdi-taxi::before{content:"\F04FF"}.mdi-tea::before{content:"\F0D9E"}.mdi-tea-outline::before{content:"\F0D9F"}.mdi-teamviewer::before{content:"\F0500"}.mdi-teddy-bear::before{content:"\F18FB"}.mdi-telescope::before{content:"\F0B4E"}.mdi-television::before{content:"\F0502"}.mdi-television-ambient-light::before{content:"\F1356"}.mdi-television-box::before{content:"\F0839"}.mdi-television-classic::before{content:"\F07F4"}.mdi-television-classic-off::before{content:"\F083A"}.mdi-television-guide::before{content:"\F0503"}.mdi-television-off::before{content:"\F083B"}.mdi-television-pause::before{content:"\F0F89"}.mdi-television-play::before{content:"\F0ECF"}.mdi-television-shimmer::before{content:"\F1110"}.mdi-television-speaker::before{content:"\F1B1B"}.mdi-television-speaker-off::before{content:"\F1B1C"}.mdi-television-stop::before{content:"\F0F8A"}.mdi-temperature-celsius::before{content:"\F0504"}.mdi-temperature-fahrenheit::before{content:"\F0505"}.mdi-temperature-kelvin::before{content:"\F0506"}.mdi-temple-buddhist::before{content:"\F1B06"}.mdi-temple-buddhist-outline::before{content:"\F1B07"}.mdi-temple-hindu::before{content:"\F1B08"}.mdi-temple-hindu-outline::before{content:"\F1B09"}.mdi-tennis::before{content:"\F0DA0"}.mdi-tennis-ball::before{content:"\F0507"}.mdi-tennis-ball-outline::before{content:"\F1C5F"}.mdi-tent::before{content:"\F0508"}.mdi-terraform::before{content:"\F1062"}.mdi-terrain::before{content:"\F0509"}.mdi-test-tube::before{content:"\F0668"}.mdi-test-tube-empty::before{content:"\F0911"}.mdi-test-tube-off::before{content:"\F0912"}.mdi-text::before{content:"\F09A8"}.mdi-text-account::before{content:"\F1570"}.mdi-text-box::before{content:"\F021A"}.mdi-text-box-check::before{content:"\F0EA6"}.mdi-text-box-check-outline::before{content:"\F0EA7"}.mdi-text-box-edit::before{content:"\F1A7C"}.mdi-text-box-edit-outline::before{content:"\F1A7D"}.mdi-text-box-minus::before{content:"\F0EA8"}.mdi-text-box-minus-outline::before{content:"\F0EA9"}.mdi-text-box-multiple::before{content:"\F0AB7"}.mdi-text-box-multiple-outline::before{content:"\F0AB8"}.mdi-text-box-outline::before{content:"\F09ED"}.mdi-text-box-plus::before{content:"\F0EAA"}.mdi-text-box-plus-outline::before{content:"\F0EAB"}.mdi-text-box-remove::before{content:"\F0EAC"}.mdi-text-box-remove-outline::before{content:"\F0EAD"}.mdi-text-box-search::before{content:"\F0EAE"}.mdi-text-box-search-outline::before{content:"\F0EAF"}.mdi-text-long::before{content:"\F09AA"}.mdi-text-recognition::before{content:"\F113D"}.mdi-text-search::before{content:"\F13B8"}.mdi-text-search-variant::before{content:"\F1A7E"}.mdi-text-shadow::before{content:"\F0669"}.mdi-text-short::before{content:"\F09A9"}.mdi-texture::before{content:"\F050C"}.mdi-texture-box::before{content:"\F0FE6"}.mdi-theater::before{content:"\F050D"}.mdi-theme-light-dark::before{content:"\F050E"}.mdi-thermometer::before{content:"\F050F"}.mdi-thermometer-alert::before{content:"\F0E01"}.mdi-thermometer-auto::before{content:"\F1B0F"}.mdi-thermometer-bluetooth::before{content:"\F1895"}.mdi-thermometer-check::before{content:"\F1A7F"}.mdi-thermometer-chevron-down::before{content:"\F0E02"}.mdi-thermometer-chevron-up::before{content:"\F0E03"}.mdi-thermometer-high::before{content:"\F10C2"}.mdi-thermometer-lines::before{content:"\F0510"}.mdi-thermometer-low::before{content:"\F10C3"}.mdi-thermometer-minus::before{content:"\F0E04"}.mdi-thermometer-off::before{content:"\F1531"}.mdi-thermometer-plus::before{content:"\F0E05"}.mdi-thermometer-probe::before{content:"\F1B2B"}.mdi-thermometer-probe-off::before{content:"\F1B2C"}.mdi-thermometer-water::before{content:"\F1A80"}.mdi-thermostat::before{content:"\F0393"}.mdi-thermostat-auto::before{content:"\F1B17"}.mdi-thermostat-box::before{content:"\F0891"}.mdi-thermostat-box-auto::before{content:"\F1B18"}.mdi-thermostat-cog::before{content:"\F1C80"}.mdi-thought-bubble::before{content:"\F07F6"}.mdi-thought-bubble-outline::before{content:"\F07F7"}.mdi-thumb-down::before{content:"\F0511"}.mdi-thumb-down-outline::before{content:"\F0512"}.mdi-thumb-up::before{content:"\F0513"}.mdi-thumb-up-outline::before{content:"\F0514"}.mdi-thumbs-up-down::before{content:"\F0515"}.mdi-thumbs-up-down-outline::before{content:"\F1914"}.mdi-ticket::before{content:"\F0516"}.mdi-ticket-account::before{content:"\F0517"}.mdi-ticket-confirmation::before{content:"\F0518"}.mdi-ticket-confirmation-outline::before{content:"\F13AA"}.mdi-ticket-outline::before{content:"\F0913"}.mdi-ticket-percent::before{content:"\F0724"}.mdi-ticket-percent-outline::before{content:"\F142B"}.mdi-tie::before{content:"\F0519"}.mdi-tilde::before{content:"\F0725"}.mdi-tilde-off::before{content:"\F18F3"}.mdi-timelapse::before{content:"\F051A"}.mdi-timeline::before{content:"\F0BD1"}.mdi-timeline-alert::before{content:"\F0F95"}.mdi-timeline-alert-outline::before{content:"\F0F98"}.mdi-timeline-check::before{content:"\F1532"}.mdi-timeline-check-outline::before{content:"\F1533"}.mdi-timeline-clock::before{content:"\F11FB"}.mdi-timeline-clock-outline::before{content:"\F11FC"}.mdi-timeline-minus::before{content:"\F1534"}.mdi-timeline-minus-outline::before{content:"\F1535"}.mdi-timeline-outline::before{content:"\F0BD2"}.mdi-timeline-plus::before{content:"\F0F96"}.mdi-timeline-plus-outline::before{content:"\F0F97"}.mdi-timeline-question::before{content:"\F0F99"}.mdi-timeline-question-outline::before{content:"\F0F9A"}.mdi-timeline-remove::before{content:"\F1536"}.mdi-timeline-remove-outline::before{content:"\F1537"}.mdi-timeline-text::before{content:"\F0BD3"}.mdi-timeline-text-outline::before{content:"\F0BD4"}.mdi-timer::before{content:"\F13AB"}.mdi-timer-10::before{content:"\F051C"}.mdi-timer-3::before{content:"\F051D"}.mdi-timer-alert::before{content:"\F1ACC"}.mdi-timer-alert-outline::before{content:"\F1ACD"}.mdi-timer-cancel::before{content:"\F1ACE"}.mdi-timer-cancel-outline::before{content:"\F1ACF"}.mdi-timer-check::before{content:"\F1AD0"}.mdi-timer-check-outline::before{content:"\F1AD1"}.mdi-timer-cog::before{content:"\F1925"}.mdi-timer-cog-outline::before{content:"\F1926"}.mdi-timer-edit::before{content:"\F1AD2"}.mdi-timer-edit-outline::before{content:"\F1AD3"}.mdi-timer-lock::before{content:"\F1AD4"}.mdi-timer-lock-open::before{content:"\F1AD5"}.mdi-timer-lock-open-outline::before{content:"\F1AD6"}.mdi-timer-lock-outline::before{content:"\F1AD7"}.mdi-timer-marker::before{content:"\F1AD8"}.mdi-timer-marker-outline::before{content:"\F1AD9"}.mdi-timer-minus::before{content:"\F1ADA"}.mdi-timer-minus-outline::before{content:"\F1ADB"}.mdi-timer-music::before{content:"\F1ADC"}.mdi-timer-music-outline::before{content:"\F1ADD"}.mdi-timer-off::before{content:"\F13AC"}.mdi-timer-off-outline::before{content:"\F051E"}.mdi-timer-outline::before{content:"\F051B"}.mdi-timer-pause::before{content:"\F1ADE"}.mdi-timer-pause-outline::before{content:"\F1ADF"}.mdi-timer-play::before{content:"\F1AE0"}.mdi-timer-play-outline::before{content:"\F1AE1"}.mdi-timer-plus::before{content:"\F1AE2"}.mdi-timer-plus-outline::before{content:"\F1AE3"}.mdi-timer-refresh::before{content:"\F1AE4"}.mdi-timer-refresh-outline::before{content:"\F1AE5"}.mdi-timer-remove::before{content:"\F1AE6"}.mdi-timer-remove-outline::before{content:"\F1AE7"}.mdi-timer-sand::before{content:"\F051F"}.mdi-timer-sand-complete::before{content:"\F199F"}.mdi-timer-sand-empty::before{content:"\F06AD"}.mdi-timer-sand-full::before{content:"\F078C"}.mdi-timer-sand-paused::before{content:"\F19A0"}.mdi-timer-settings::before{content:"\F1923"}.mdi-timer-settings-outline::before{content:"\F1924"}.mdi-timer-star::before{content:"\F1AE8"}.mdi-timer-star-outline::before{content:"\F1AE9"}.mdi-timer-stop::before{content:"\F1AEA"}.mdi-timer-stop-outline::before{content:"\F1AEB"}.mdi-timer-sync::before{content:"\F1AEC"}.mdi-timer-sync-outline::before{content:"\F1AED"}.mdi-timetable::before{content:"\F0520"}.mdi-tire::before{content:"\F1896"}.mdi-toaster::before{content:"\F1063"}.mdi-toaster-off::before{content:"\F11B7"}.mdi-toaster-oven::before{content:"\F0CD3"}.mdi-toggle-switch::before{content:"\F0521"}.mdi-toggle-switch-off::before{content:"\F0522"}.mdi-toggle-switch-off-outline::before{content:"\F0A19"}.mdi-toggle-switch-outline::before{content:"\F0A1A"}.mdi-toggle-switch-variant::before{content:"\F1A25"}.mdi-toggle-switch-variant-off::before{content:"\F1A26"}.mdi-toilet::before{content:"\F09AB"}.mdi-toolbox::before{content:"\F09AC"}.mdi-toolbox-outline::before{content:"\F09AD"}.mdi-tools::before{content:"\F1064"}.mdi-tooltip::before{content:"\F0523"}.mdi-tooltip-account::before{content:"\F000C"}.mdi-tooltip-cellphone::before{content:"\F183B"}.mdi-tooltip-check::before{content:"\F155C"}.mdi-tooltip-check-outline::before{content:"\F155D"}.mdi-tooltip-edit::before{content:"\F0524"}.mdi-tooltip-edit-outline::before{content:"\F12C5"}.mdi-tooltip-image::before{content:"\F0525"}.mdi-tooltip-image-outline::before{content:"\F0BD5"}.mdi-tooltip-minus::before{content:"\F155E"}.mdi-tooltip-minus-outline::before{content:"\F155F"}.mdi-tooltip-outline::before{content:"\F0526"}.mdi-tooltip-plus::before{content:"\F0BD6"}.mdi-tooltip-plus-outline::before{content:"\F0527"}.mdi-tooltip-question::before{content:"\F1BBA"}.mdi-tooltip-question-outline::before{content:"\F1BBB"}.mdi-tooltip-remove::before{content:"\F1560"}.mdi-tooltip-remove-outline::before{content:"\F1561"}.mdi-tooltip-text::before{content:"\F0528"}.mdi-tooltip-text-outline::before{content:"\F0BD7"}.mdi-tooth::before{content:"\F08C3"}.mdi-tooth-outline::before{content:"\F0529"}.mdi-toothbrush::before{content:"\F1129"}.mdi-toothbrush-electric::before{content:"\F112C"}.mdi-toothbrush-paste::before{content:"\F112A"}.mdi-torch::before{content:"\F1606"}.mdi-tortoise::before{content:"\F0D3B"}.mdi-toslink::before{content:"\F12B8"}.mdi-touch-text-outline::before{content:"\F1C60"}.mdi-tournament::before{content:"\F09AE"}.mdi-tow-truck::before{content:"\F083C"}.mdi-tower-beach::before{content:"\F0681"}.mdi-tower-fire::before{content:"\F0682"}.mdi-town-hall::before{content:"\F1875"}.mdi-toy-brick::before{content:"\F1288"}.mdi-toy-brick-marker::before{content:"\F1289"}.mdi-toy-brick-marker-outline::before{content:"\F128A"}.mdi-toy-brick-minus::before{content:"\F128B"}.mdi-toy-brick-minus-outline::before{content:"\F128C"}.mdi-toy-brick-outline::before{content:"\F128D"}.mdi-toy-brick-plus::before{content:"\F128E"}.mdi-toy-brick-plus-outline::before{content:"\F128F"}.mdi-toy-brick-remove::before{content:"\F1290"}.mdi-toy-brick-remove-outline::before{content:"\F1291"}.mdi-toy-brick-search::before{content:"\F1292"}.mdi-toy-brick-search-outline::before{content:"\F1293"}.mdi-track-light::before{content:"\F0914"}.mdi-track-light-off::before{content:"\F1B01"}.mdi-trackpad::before{content:"\F07F8"}.mdi-trackpad-lock::before{content:"\F0933"}.mdi-tractor::before{content:"\F0892"}.mdi-tractor-variant::before{content:"\F14C4"}.mdi-trademark::before{content:"\F0A78"}.mdi-traffic-cone::before{content:"\F137C"}.mdi-traffic-light::before{content:"\F052B"}.mdi-traffic-light-outline::before{content:"\F182A"}.mdi-train::before{content:"\F052C"}.mdi-train-bus::before{content:"\F1CC7"}.mdi-train-car::before{content:"\F0BD8"}.mdi-train-car-autorack::before{content:"\F1B2D"}.mdi-train-car-box::before{content:"\F1B2E"}.mdi-train-car-box-full::before{content:"\F1B2F"}.mdi-train-car-box-open::before{content:"\F1B30"}.mdi-train-car-caboose::before{content:"\F1B31"}.mdi-train-car-centerbeam::before{content:"\F1B32"}.mdi-train-car-centerbeam-full::before{content:"\F1B33"}.mdi-train-car-container::before{content:"\F1B34"}.mdi-train-car-flatbed::before{content:"\F1B35"}.mdi-train-car-flatbed-car::before{content:"\F1B36"}.mdi-train-car-flatbed-tank::before{content:"\F1B37"}.mdi-train-car-gondola::before{content:"\F1B38"}.mdi-train-car-gondola-full::before{content:"\F1B39"}.mdi-train-car-hopper::before{content:"\F1B3A"}.mdi-train-car-hopper-covered::before{content:"\F1B3B"}.mdi-train-car-hopper-full::before{content:"\F1B3C"}.mdi-train-car-intermodal::before{content:"\F1B3D"}.mdi-train-car-passenger::before{content:"\F1733"}.mdi-train-car-passenger-door::before{content:"\F1734"}.mdi-train-car-passenger-door-open::before{content:"\F1735"}.mdi-train-car-passenger-variant::before{content:"\F1736"}.mdi-train-car-tank::before{content:"\F1B3E"}.mdi-train-variant::before{content:"\F08C4"}.mdi-tram::before{content:"\F052D"}.mdi-tram-side::before{content:"\F0FE7"}.mdi-transcribe::before{content:"\F052E"}.mdi-transcribe-close::before{content:"\F052F"}.mdi-transfer::before{content:"\F1065"}.mdi-transfer-down::before{content:"\F0DA1"}.mdi-transfer-left::before{content:"\F0DA2"}.mdi-transfer-right::before{content:"\F0530"}.mdi-transfer-up::before{content:"\F0DA3"}.mdi-transit-connection::before{content:"\F0D3C"}.mdi-transit-connection-horizontal::before{content:"\F1546"}.mdi-transit-connection-variant::before{content:"\F0D3D"}.mdi-transit-detour::before{content:"\F0F8B"}.mdi-transit-skip::before{content:"\F1515"}.mdi-transit-transfer::before{content:"\F06AE"}.mdi-transition::before{content:"\F0915"}.mdi-transition-masked::before{content:"\F0916"}.mdi-translate::before{content:"\F05CA"}.mdi-translate-off::before{content:"\F0E06"}.mdi-translate-variant::before{content:"\F1B99"}.mdi-transmission-tower::before{content:"\F0D3E"}.mdi-transmission-tower-export::before{content:"\F192C"}.mdi-transmission-tower-import::before{content:"\F192D"}.mdi-transmission-tower-off::before{content:"\F19DD"}.mdi-trash-can::before{content:"\F0A79"}.mdi-trash-can-outline::before{content:"\F0A7A"}.mdi-tray::before{content:"\F1294"}.mdi-tray-alert::before{content:"\F1295"}.mdi-tray-arrow-down::before{content:"\F0120"}.mdi-tray-arrow-up::before{content:"\F011D"}.mdi-tray-full::before{content:"\F1296"}.mdi-tray-minus::before{content:"\F1297"}.mdi-tray-plus::before{content:"\F1298"}.mdi-tray-remove::before{content:"\F1299"}.mdi-treasure-chest::before{content:"\F0726"}.mdi-treasure-chest-outline::before{content:"\F1C77"}.mdi-tree::before{content:"\F0531"}.mdi-tree-outline::before{content:"\F0E69"}.mdi-trello::before{content:"\F0532"}.mdi-trending-down::before{content:"\F0533"}.mdi-trending-neutral::before{content:"\F0534"}.mdi-trending-up::before{content:"\F0535"}.mdi-triangle::before{content:"\F0536"}.mdi-triangle-down::before{content:"\F1C56"}.mdi-triangle-down-outline::before{content:"\F1C57"}.mdi-triangle-outline::before{content:"\F0537"}.mdi-triangle-small-down::before{content:"\F1A09"}.mdi-triangle-small-up::before{content:"\F1A0A"}.mdi-triangle-wave::before{content:"\F147C"}.mdi-triforce::before{content:"\F0BD9"}.mdi-trophy::before{content:"\F0538"}.mdi-trophy-award::before{content:"\F0539"}.mdi-trophy-broken::before{content:"\F0DA4"}.mdi-trophy-outline::before{content:"\F053A"}.mdi-trophy-variant::before{content:"\F053B"}.mdi-trophy-variant-outline::before{content:"\F053C"}.mdi-truck::before{content:"\F053D"}.mdi-truck-alert::before{content:"\F19DE"}.mdi-truck-alert-outline::before{content:"\F19DF"}.mdi-truck-cargo-container::before{content:"\F18D8"}.mdi-truck-check::before{content:"\F0CD4"}.mdi-truck-check-outline::before{content:"\F129A"}.mdi-truck-delivery::before{content:"\F053E"}.mdi-truck-delivery-outline::before{content:"\F129B"}.mdi-truck-fast::before{content:"\F0788"}.mdi-truck-fast-outline::before{content:"\F129C"}.mdi-truck-flatbed::before{content:"\F1891"}.mdi-truck-minus::before{content:"\F19AE"}.mdi-truck-minus-outline::before{content:"\F19BD"}.mdi-truck-off-road::before{content:"\F1C9E"}.mdi-truck-off-road-off::before{content:"\F1C9F"}.mdi-truck-outline::before{content:"\F129D"}.mdi-truck-plus::before{content:"\F19AD"}.mdi-truck-plus-outline::before{content:"\F19BC"}.mdi-truck-remove::before{content:"\F19AF"}.mdi-truck-remove-outline::before{content:"\F19BE"}.mdi-truck-snowflake::before{content:"\F19A6"}.mdi-truck-trailer::before{content:"\F0727"}.mdi-trumpet::before{content:"\F1096"}.mdi-tshirt-crew::before{content:"\F0A7B"}.mdi-tshirt-crew-outline::before{content:"\F053F"}.mdi-tshirt-v::before{content:"\F0A7C"}.mdi-tshirt-v-outline::before{content:"\F0540"}.mdi-tsunami::before{content:"\F1A81"}.mdi-tumble-dryer::before{content:"\F0917"}.mdi-tumble-dryer-alert::before{content:"\F11BA"}.mdi-tumble-dryer-off::before{content:"\F11BB"}.mdi-tune::before{content:"\F062E"}.mdi-tune-variant::before{content:"\F1542"}.mdi-tune-vertical::before{content:"\F066A"}.mdi-tune-vertical-variant::before{content:"\F1543"}.mdi-tunnel::before{content:"\F183D"}.mdi-tunnel-outline::before{content:"\F183E"}.mdi-turbine::before{content:"\F1A82"}.mdi-turkey::before{content:"\F171B"}.mdi-turnstile::before{content:"\F0CD5"}.mdi-turnstile-outline::before{content:"\F0CD6"}.mdi-turtle::before{content:"\F0CD7"}.mdi-twitch::before{content:"\F0543"}.mdi-twitter::before{content:"\F0544"}.mdi-two-factor-authentication::before{content:"\F09AF"}.mdi-typewriter::before{content:"\F0F2D"}.mdi-ubisoft::before{content:"\F0BDA"}.mdi-ubuntu::before{content:"\F0548"}.mdi-ufo::before{content:"\F10C4"}.mdi-ufo-outline::before{content:"\F10C5"}.mdi-ultra-high-definition::before{content:"\F07F9"}.mdi-umbraco::before{content:"\F0549"}.mdi-umbrella::before{content:"\F054A"}.mdi-umbrella-beach::before{content:"\F188A"}.mdi-umbrella-beach-outline::before{content:"\F188B"}.mdi-umbrella-closed::before{content:"\F09B0"}.mdi-umbrella-closed-outline::before{content:"\F13E2"}.mdi-umbrella-closed-variant::before{content:"\F13E1"}.mdi-umbrella-outline::before{content:"\F054B"}.mdi-underwear-outline::before{content:"\F1D0F"}.mdi-undo::before{content:"\F054C"}.mdi-undo-variant::before{content:"\F054D"}.mdi-unfold-less-horizontal::before{content:"\F054E"}.mdi-unfold-less-vertical::before{content:"\F0760"}.mdi-unfold-more-horizontal::before{content:"\F054F"}.mdi-unfold-more-vertical::before{content:"\F0761"}.mdi-ungroup::before{content:"\F0550"}.mdi-unicode::before{content:"\F0ED0"}.mdi-unicorn::before{content:"\F15C2"}.mdi-unicorn-variant::before{content:"\F15C3"}.mdi-unicycle::before{content:"\F15E5"}.mdi-unity::before{content:"\F06AF"}.mdi-unreal::before{content:"\F09B1"}.mdi-update::before{content:"\F06B0"}.mdi-upload::before{content:"\F0552"}.mdi-upload-box::before{content:"\F1D10"}.mdi-upload-box-outline::before{content:"\F1D11"}.mdi-upload-circle::before{content:"\F1D12"}.mdi-upload-circle-outline::before{content:"\F1D13"}.mdi-upload-lock::before{content:"\F1373"}.mdi-upload-lock-outline::before{content:"\F1374"}.mdi-upload-multiple::before{content:"\F083D"}.mdi-upload-multiple-outline::before{content:"\F1D14"}.mdi-upload-network::before{content:"\F06F6"}.mdi-upload-network-outline::before{content:"\F0CD8"}.mdi-upload-off::before{content:"\F10C6"}.mdi-upload-off-outline::before{content:"\F10C7"}.mdi-upload-outline::before{content:"\F0E07"}.mdi-usb::before{content:"\F0553"}.mdi-usb-c-port::before{content:"\F1CBF"}.mdi-usb-flash-drive::before{content:"\F129E"}.mdi-usb-flash-drive-outline::before{content:"\F129F"}.mdi-usb-port::before{content:"\F11F0"}.mdi-vacuum::before{content:"\F19A1"}.mdi-vacuum-outline::before{content:"\F19A2"}.mdi-valve::before{content:"\F1066"}.mdi-valve-closed::before{content:"\F1067"}.mdi-valve-open::before{content:"\F1068"}.mdi-van-passenger::before{content:"\F07FA"}.mdi-van-utility::before{content:"\F07FB"}.mdi-vanish::before{content:"\F07FC"}.mdi-vanish-quarter::before{content:"\F1554"}.mdi-vanity-light::before{content:"\F11E1"}.mdi-variable::before{content:"\F0AE7"}.mdi-variable-box::before{content:"\F1111"}.mdi-vector-arrange-above::before{content:"\F0554"}.mdi-vector-arrange-below::before{content:"\F0555"}.mdi-vector-bezier::before{content:"\F0AE8"}.mdi-vector-circle::before{content:"\F0556"}.mdi-vector-circle-variant::before{content:"\F0557"}.mdi-vector-combine::before{content:"\F0558"}.mdi-vector-curve::before{content:"\F0559"}.mdi-vector-difference::before{content:"\F055A"}.mdi-vector-difference-ab::before{content:"\F055B"}.mdi-vector-difference-ba::before{content:"\F055C"}.mdi-vector-ellipse::before{content:"\F0893"}.mdi-vector-intersection::before{content:"\F055D"}.mdi-vector-line::before{content:"\F055E"}.mdi-vector-link::before{content:"\F0FE8"}.mdi-vector-point::before{content:"\F01C4"}.mdi-vector-point-edit::before{content:"\F09E8"}.mdi-vector-point-minus::before{content:"\F1B78"}.mdi-vector-point-plus::before{content:"\F1B79"}.mdi-vector-point-select::before{content:"\F055F"}.mdi-vector-polygon::before{content:"\F0560"}.mdi-vector-polygon-variant::before{content:"\F1856"}.mdi-vector-polyline::before{content:"\F0561"}.mdi-vector-polyline-edit::before{content:"\F1225"}.mdi-vector-polyline-minus::before{content:"\F1226"}.mdi-vector-polyline-plus::before{content:"\F1227"}.mdi-vector-polyline-remove::before{content:"\F1228"}.mdi-vector-radius::before{content:"\F074A"}.mdi-vector-rectangle::before{content:"\F05C6"}.mdi-vector-selection::before{content:"\F0562"}.mdi-vector-square::before{content:"\F0001"}.mdi-vector-square-close::before{content:"\F1857"}.mdi-vector-square-edit::before{content:"\F18D9"}.mdi-vector-square-minus::before{content:"\F18DA"}.mdi-vector-square-open::before{content:"\F1858"}.mdi-vector-square-plus::before{content:"\F18DB"}.mdi-vector-square-remove::before{content:"\F18DC"}.mdi-vector-triangle::before{content:"\F0563"}.mdi-vector-union::before{content:"\F0564"}.mdi-vhs::before{content:"\F0A1B"}.mdi-vibrate::before{content:"\F0566"}.mdi-vibrate-off::before{content:"\F0CD9"}.mdi-video::before{content:"\F0567"}.mdi-video-2d::before{content:"\F1A1C"}.mdi-video-3d::before{content:"\F07FD"}.mdi-video-3d-off::before{content:"\F13D9"}.mdi-video-3d-variant::before{content:"\F0ED1"}.mdi-video-4k-box::before{content:"\F083E"}.mdi-video-account::before{content:"\F0919"}.mdi-video-box::before{content:"\F00FD"}.mdi-video-box-off::before{content:"\F00FE"}.mdi-video-check::before{content:"\F1069"}.mdi-video-check-outline::before{content:"\F106A"}.mdi-video-high-definition::before{content:"\F152E"}.mdi-video-image::before{content:"\F091A"}.mdi-video-input-antenna::before{content:"\F083F"}.mdi-video-input-component::before{content:"\F0840"}.mdi-video-input-hdmi::before{content:"\F0841"}.mdi-video-input-scart::before{content:"\F0F8C"}.mdi-video-input-svideo::before{content:"\F0842"}.mdi-video-marker::before{content:"\F19A9"}.mdi-video-marker-outline::before{content:"\F19AA"}.mdi-video-minus::before{content:"\F09B2"}.mdi-video-minus-outline::before{content:"\F02BA"}.mdi-video-off::before{content:"\F0568"}.mdi-video-off-outline::before{content:"\F0BDB"}.mdi-video-outline::before{content:"\F0BDC"}.mdi-video-plus::before{content:"\F09B3"}.mdi-video-plus-outline::before{content:"\F01D3"}.mdi-video-stabilization::before{content:"\F091B"}.mdi-video-standard-definition::before{content:"\F1CA0"}.mdi-video-switch::before{content:"\F0569"}.mdi-video-switch-outline::before{content:"\F0790"}.mdi-video-vintage::before{content:"\F0A1C"}.mdi-video-wireless::before{content:"\F0ED2"}.mdi-video-wireless-outline::before{content:"\F0ED3"}.mdi-view-agenda::before{content:"\F056A"}.mdi-view-agenda-outline::before{content:"\F11D8"}.mdi-view-array::before{content:"\F056B"}.mdi-view-array-outline::before{content:"\F1485"}.mdi-view-carousel::before{content:"\F056C"}.mdi-view-carousel-outline::before{content:"\F1486"}.mdi-view-column::before{content:"\F056D"}.mdi-view-column-outline::before{content:"\F1487"}.mdi-view-comfy::before{content:"\F0E6A"}.mdi-view-comfy-outline::before{content:"\F1488"}.mdi-view-compact::before{content:"\F0E6B"}.mdi-view-compact-outline::before{content:"\F0E6C"}.mdi-view-dashboard::before{content:"\F056E"}.mdi-view-dashboard-edit::before{content:"\F1947"}.mdi-view-dashboard-edit-outline::before{content:"\F1948"}.mdi-view-dashboard-outline::before{content:"\F0A1D"}.mdi-view-dashboard-variant::before{content:"\F0843"}.mdi-view-dashboard-variant-outline::before{content:"\F1489"}.mdi-view-day::before{content:"\F056F"}.mdi-view-day-outline::before{content:"\F148A"}.mdi-view-gallery::before{content:"\F1888"}.mdi-view-gallery-outline::before{content:"\F1889"}.mdi-view-grid::before{content:"\F0570"}.mdi-view-grid-compact::before{content:"\F1C61"}.mdi-view-grid-outline::before{content:"\F11D9"}.mdi-view-grid-plus::before{content:"\F0F8D"}.mdi-view-grid-plus-outline::before{content:"\F11DA"}.mdi-view-headline::before{content:"\F0571"}.mdi-view-list::before{content:"\F0572"}.mdi-view-list-outline::before{content:"\F148B"}.mdi-view-module::before{content:"\F0573"}.mdi-view-module-outline::before{content:"\F148C"}.mdi-view-parallel::before{content:"\F0728"}.mdi-view-parallel-outline::before{content:"\F148D"}.mdi-view-quilt::before{content:"\F0574"}.mdi-view-quilt-outline::before{content:"\F148E"}.mdi-view-sequential::before{content:"\F0729"}.mdi-view-sequential-outline::before{content:"\F148F"}.mdi-view-split-horizontal::before{content:"\F0BCB"}.mdi-view-split-vertical::before{content:"\F0BCC"}.mdi-view-stream::before{content:"\F0575"}.mdi-view-stream-outline::before{content:"\F1490"}.mdi-view-week::before{content:"\F0576"}.mdi-view-week-outline::before{content:"\F1491"}.mdi-vimeo::before{content:"\F0577"}.mdi-violin::before{content:"\F060F"}.mdi-virtual-reality::before{content:"\F0894"}.mdi-virus::before{content:"\F13B6"}.mdi-virus-off::before{content:"\F18E1"}.mdi-virus-off-outline::before{content:"\F18E2"}.mdi-virus-outline::before{content:"\F13B7"}.mdi-vlc::before{content:"\F057C"}.mdi-voicemail::before{content:"\F057D"}.mdi-volcano::before{content:"\F1A83"}.mdi-volcano-outline::before{content:"\F1A84"}.mdi-volleyball::before{content:"\F09B4"}.mdi-volume-equal::before{content:"\F1B10"}.mdi-volume-high::before{content:"\F057E"}.mdi-volume-low::before{content:"\F057F"}.mdi-volume-medium::before{content:"\F0580"}.mdi-volume-minus::before{content:"\F075E"}.mdi-volume-mute::before{content:"\F075F"}.mdi-volume-off::before{content:"\F0581"}.mdi-volume-plus::before{content:"\F075D"}.mdi-volume-source::before{content:"\F1120"}.mdi-volume-variant-off::before{content:"\F0E08"}.mdi-volume-vibrate::before{content:"\F1121"}.mdi-vote::before{content:"\F0A1F"}.mdi-vote-outline::before{content:"\F0A20"}.mdi-vpn::before{content:"\F0582"}.mdi-vuejs::before{content:"\F0844"}.mdi-vuetify::before{content:"\F0E6D"}.mdi-walk::before{content:"\F0583"}.mdi-wall::before{content:"\F07FE"}.mdi-wall-fire::before{content:"\F1A11"}.mdi-wall-sconce::before{content:"\F091C"}.mdi-wall-sconce-flat::before{content:"\F091D"}.mdi-wall-sconce-flat-outline::before{content:"\F17C9"}.mdi-wall-sconce-flat-variant::before{content:"\F041C"}.mdi-wall-sconce-flat-variant-outline::before{content:"\F17CA"}.mdi-wall-sconce-outline::before{content:"\F17CB"}.mdi-wall-sconce-round::before{content:"\F0748"}.mdi-wall-sconce-round-outline::before{content:"\F17CC"}.mdi-wall-sconce-round-variant::before{content:"\F091E"}.mdi-wall-sconce-round-variant-outline::before{content:"\F17CD"}.mdi-wallet::before{content:"\F0584"}.mdi-wallet-bifold::before{content:"\F1C58"}.mdi-wallet-bifold-outline::before{content:"\F1C59"}.mdi-wallet-giftcard::before{content:"\F0585"}.mdi-wallet-membership::before{content:"\F0586"}.mdi-wallet-outline::before{content:"\F0BDD"}.mdi-wallet-plus::before{content:"\F0F8E"}.mdi-wallet-plus-outline::before{content:"\F0F8F"}.mdi-wallet-travel::before{content:"\F0587"}.mdi-wallpaper::before{content:"\F0E09"}.mdi-wan::before{content:"\F0588"}.mdi-wardrobe::before{content:"\F0F90"}.mdi-wardrobe-outline::before{content:"\F0F91"}.mdi-warehouse::before{content:"\F0F81"}.mdi-washing-machine::before{content:"\F072A"}.mdi-washing-machine-alert::before{content:"\F11BC"}.mdi-washing-machine-off::before{content:"\F11BD"}.mdi-watch::before{content:"\F0589"}.mdi-watch-export::before{content:"\F058A"}.mdi-watch-export-variant::before{content:"\F0895"}.mdi-watch-import::before{content:"\F058B"}.mdi-watch-import-variant::before{content:"\F0896"}.mdi-watch-variant::before{content:"\F0897"}.mdi-watch-vibrate::before{content:"\F06B1"}.mdi-watch-vibrate-off::before{content:"\F0CDA"}.mdi-water::before{content:"\F058C"}.mdi-water-alert::before{content:"\F1502"}.mdi-water-alert-outline::before{content:"\F1503"}.mdi-water-boiler::before{content:"\F0F92"}.mdi-water-boiler-alert::before{content:"\F11B3"}.mdi-water-boiler-auto::before{content:"\F1B98"}.mdi-water-boiler-off::before{content:"\F11B4"}.mdi-water-check::before{content:"\F1504"}.mdi-water-check-outline::before{content:"\F1505"}.mdi-water-circle::before{content:"\F1806"}.mdi-water-minus::before{content:"\F1506"}.mdi-water-minus-outline::before{content:"\F1507"}.mdi-water-off::before{content:"\F058D"}.mdi-water-off-outline::before{content:"\F1508"}.mdi-water-opacity::before{content:"\F1855"}.mdi-water-outline::before{content:"\F0E0A"}.mdi-water-percent::before{content:"\F058E"}.mdi-water-percent-alert::before{content:"\F1509"}.mdi-water-plus::before{content:"\F150A"}.mdi-water-plus-outline::before{content:"\F150B"}.mdi-water-polo::before{content:"\F12A0"}.mdi-water-pump::before{content:"\F058F"}.mdi-water-pump-off::before{content:"\F0F93"}.mdi-water-remove::before{content:"\F150C"}.mdi-water-remove-outline::before{content:"\F150D"}.mdi-water-sync::before{content:"\F17C6"}.mdi-water-thermometer::before{content:"\F1A85"}.mdi-water-thermometer-outline::before{content:"\F1A86"}.mdi-water-well::before{content:"\F106B"}.mdi-water-well-outline::before{content:"\F106C"}.mdi-waterfall::before{content:"\F1849"}.mdi-watering-can::before{content:"\F1481"}.mdi-watering-can-outline::before{content:"\F1482"}.mdi-watermark::before{content:"\F0612"}.mdi-wave::before{content:"\F0F2E"}.mdi-wave-arrow-down::before{content:"\F1CB0"}.mdi-wave-arrow-up::before{content:"\F1CB1"}.mdi-wave-undercurrent::before{content:"\F1CC0"}.mdi-waveform::before{content:"\F147D"}.mdi-waves::before{content:"\F078D"}.mdi-waves-arrow-left::before{content:"\F1859"}.mdi-waves-arrow-right::before{content:"\F185A"}.mdi-waves-arrow-up::before{content:"\F185B"}.mdi-waze::before{content:"\F0BDE"}.mdi-weather-cloudy::before{content:"\F0590"}.mdi-weather-cloudy-alert::before{content:"\F0F2F"}.mdi-weather-cloudy-arrow-right::before{content:"\F0E6E"}.mdi-weather-cloudy-clock::before{content:"\F18F6"}.mdi-weather-dust::before{content:"\F1B5A"}.mdi-weather-fog::before{content:"\F0591"}.mdi-weather-hail::before{content:"\F0592"}.mdi-weather-hazy::before{content:"\F0F30"}.mdi-weather-hurricane::before{content:"\F0898"}.mdi-weather-hurricane-outline::before{content:"\F1C78"}.mdi-weather-lightning::before{content:"\F0593"}.mdi-weather-lightning-rainy::before{content:"\F067E"}.mdi-weather-moonset::before{content:"\F1D15"}.mdi-weather-moonset-down::before{content:"\F1D16"}.mdi-weather-moonset-up::before{content:"\F1D17"}.mdi-weather-night::before{content:"\F0594"}.mdi-weather-night-partly-cloudy::before{content:"\F0F31"}.mdi-weather-partly-cloudy::before{content:"\F0595"}.mdi-weather-partly-lightning::before{content:"\F0F32"}.mdi-weather-partly-rainy::before{content:"\F0F33"}.mdi-weather-partly-snowy::before{content:"\F0F34"}.mdi-weather-partly-snowy-rainy::before{content:"\F0F35"}.mdi-weather-pouring::before{content:"\F0596"}.mdi-weather-rainy::before{content:"\F0597"}.mdi-weather-snowy::before{content:"\F0598"}.mdi-weather-snowy-heavy::before{content:"\F0F36"}.mdi-weather-snowy-rainy::before{content:"\F067F"}.mdi-weather-sunny::before{content:"\F0599"}.mdi-weather-sunny-alert::before{content:"\F0F37"}.mdi-weather-sunny-off::before{content:"\F14E4"}.mdi-weather-sunset::before{content:"\F059A"}.mdi-weather-sunset-down::before{content:"\F059B"}.mdi-weather-sunset-up::before{content:"\F059C"}.mdi-weather-tornado::before{content:"\F0F38"}.mdi-weather-windy::before{content:"\F059D"}.mdi-weather-windy-variant::before{content:"\F059E"}.mdi-web::before{content:"\F059F"}.mdi-web-box::before{content:"\F0F94"}.mdi-web-cancel::before{content:"\F1790"}.mdi-web-check::before{content:"\F0789"}.mdi-web-clock::before{content:"\F124A"}.mdi-web-minus::before{content:"\F10A0"}.mdi-web-off::before{content:"\F0A8E"}.mdi-web-plus::before{content:"\F0033"}.mdi-web-refresh::before{content:"\F1791"}.mdi-web-remove::before{content:"\F0551"}.mdi-web-sync::before{content:"\F1792"}.mdi-webcam::before{content:"\F05A0"}.mdi-webcam-off::before{content:"\F1737"}.mdi-webhook::before{content:"\F062F"}.mdi-webpack::before{content:"\F072B"}.mdi-webrtc::before{content:"\F1248"}.mdi-wechat::before{content:"\F0611"}.mdi-weight::before{content:"\F05A1"}.mdi-weight-gram::before{content:"\F0D3F"}.mdi-weight-kilogram::before{content:"\F05A2"}.mdi-weight-lifter::before{content:"\F115D"}.mdi-weight-pound::before{content:"\F09B5"}.mdi-whatsapp::before{content:"\F05A3"}.mdi-wheel-barrow::before{content:"\F14F2"}.mdi-wheelchair::before{content:"\F1A87"}.mdi-wheelchair-accessibility::before{content:"\F05A4"}.mdi-whistle::before{content:"\F09B6"}.mdi-whistle-outline::before{content:"\F12BC"}.mdi-white-balance-auto::before{content:"\F05A5"}.mdi-white-balance-incandescent::before{content:"\F05A6"}.mdi-white-balance-iridescent::before{content:"\F05A7"}.mdi-white-balance-sunny::before{content:"\F05A8"}.mdi-widgets::before{content:"\F072C"}.mdi-widgets-outline::before{content:"\F1355"}.mdi-wifi::before{content:"\F05A9"}.mdi-wifi-alert::before{content:"\F16B5"}.mdi-wifi-arrow-down::before{content:"\F16B6"}.mdi-wifi-arrow-left::before{content:"\F16B7"}.mdi-wifi-arrow-left-right::before{content:"\F16B8"}.mdi-wifi-arrow-right::before{content:"\F16B9"}.mdi-wifi-arrow-up::before{content:"\F16BA"}.mdi-wifi-arrow-up-down::before{content:"\F16BB"}.mdi-wifi-cancel::before{content:"\F16BC"}.mdi-wifi-check::before{content:"\F16BD"}.mdi-wifi-cog::before{content:"\F16BE"}.mdi-wifi-lock::before{content:"\F16BF"}.mdi-wifi-lock-open::before{content:"\F16C0"}.mdi-wifi-marker::before{content:"\F16C1"}.mdi-wifi-minus::before{content:"\F16C2"}.mdi-wifi-off::before{content:"\F05AA"}.mdi-wifi-plus::before{content:"\F16C3"}.mdi-wifi-refresh::before{content:"\F16C4"}.mdi-wifi-remove::before{content:"\F16C5"}.mdi-wifi-settings::before{content:"\F16C6"}.mdi-wifi-star::before{content:"\F0E0B"}.mdi-wifi-strength-1::before{content:"\F091F"}.mdi-wifi-strength-1-alert::before{content:"\F0920"}.mdi-wifi-strength-1-lock::before{content:"\F0921"}.mdi-wifi-strength-1-lock-open::before{content:"\F16CB"}.mdi-wifi-strength-2::before{content:"\F0922"}.mdi-wifi-strength-2-alert::before{content:"\F0923"}.mdi-wifi-strength-2-lock::before{content:"\F0924"}.mdi-wifi-strength-2-lock-open::before{content:"\F16CC"}.mdi-wifi-strength-3::before{content:"\F0925"}.mdi-wifi-strength-3-alert::before{content:"\F0926"}.mdi-wifi-strength-3-lock::before{content:"\F0927"}.mdi-wifi-strength-3-lock-open::before{content:"\F16CD"}.mdi-wifi-strength-4::before{content:"\F0928"}.mdi-wifi-strength-4-alert::before{content:"\F0929"}.mdi-wifi-strength-4-lock::before{content:"\F092A"}.mdi-wifi-strength-4-lock-open::before{content:"\F16CE"}.mdi-wifi-strength-alert-outline::before{content:"\F092B"}.mdi-wifi-strength-lock-open-outline::before{content:"\F16CF"}.mdi-wifi-strength-lock-outline::before{content:"\F092C"}.mdi-wifi-strength-off::before{content:"\F092D"}.mdi-wifi-strength-off-outline::before{content:"\F092E"}.mdi-wifi-strength-outline::before{content:"\F092F"}.mdi-wifi-sync::before{content:"\F16C7"}.mdi-wikipedia::before{content:"\F05AC"}.mdi-wind-power::before{content:"\F1A88"}.mdi-wind-power-outline::before{content:"\F1A89"}.mdi-wind-turbine::before{content:"\F0DA5"}.mdi-wind-turbine-alert::before{content:"\F19AB"}.mdi-wind-turbine-check::before{content:"\F19AC"}.mdi-window-close::before{content:"\F05AD"}.mdi-window-closed::before{content:"\F05AE"}.mdi-window-closed-variant::before{content:"\F11DB"}.mdi-window-maximize::before{content:"\F05AF"}.mdi-window-minimize::before{content:"\F05B0"}.mdi-window-open::before{content:"\F05B1"}.mdi-window-open-variant::before{content:"\F11DC"}.mdi-window-restore::before{content:"\F05B2"}.mdi-window-shutter::before{content:"\F111C"}.mdi-window-shutter-alert::before{content:"\F111D"}.mdi-window-shutter-auto::before{content:"\F1BA3"}.mdi-window-shutter-cog::before{content:"\F1A8A"}.mdi-window-shutter-open::before{content:"\F111E"}.mdi-window-shutter-settings::before{content:"\F1A8B"}.mdi-windsock::before{content:"\F15FA"}.mdi-wiper::before{content:"\F0AE9"}.mdi-wiper-wash::before{content:"\F0DA6"}.mdi-wiper-wash-alert::before{content:"\F18DF"}.mdi-wizard-hat::before{content:"\F1477"}.mdi-wordpress::before{content:"\F05B4"}.mdi-wrap::before{content:"\F05B6"}.mdi-wrap-disabled::before{content:"\F0BDF"}.mdi-wrench::before{content:"\F05B7"}.mdi-wrench-check::before{content:"\F1B8F"}.mdi-wrench-check-outline::before{content:"\F1B90"}.mdi-wrench-clock::before{content:"\F19A3"}.mdi-wrench-clock-outline::before{content:"\F1B93"}.mdi-wrench-cog::before{content:"\F1B91"}.mdi-wrench-cog-outline::before{content:"\F1B92"}.mdi-wrench-outline::before{content:"\F0BE0"}.mdi-xamarin::before{content:"\F0845"}.mdi-xml::before{content:"\F05C0"}.mdi-xmpp::before{content:"\F07FF"}.mdi-yahoo::before{content:"\F0B4F"}.mdi-yeast::before{content:"\F05C1"}.mdi-yin-yang::before{content:"\F0680"}.mdi-yoga::before{content:"\F117C"}.mdi-youtube::before{content:"\F05C3"}.mdi-youtube-gaming::before{content:"\F0848"}.mdi-youtube-studio::before{content:"\F0847"}.mdi-youtube-subscription::before{content:"\F0D40"}.mdi-youtube-tv::before{content:"\F0448"}.mdi-yurt::before{content:"\F1516"}.mdi-z-wave::before{content:"\F0AEA"}.mdi-zend::before{content:"\F0AEB"}.mdi-zigbee::before{content:"\F0D41"}.mdi-zip-box::before{content:"\F05C4"}.mdi-zip-box-outline::before{content:"\F0FFA"}.mdi-zip-disk::before{content:"\F0A23"}.mdi-zodiac-aquarius::before{content:"\F0A7D"}.mdi-zodiac-aries::before{content:"\F0A7E"}.mdi-zodiac-cancer::before{content:"\F0A7F"}.mdi-zodiac-capricorn::before{content:"\F0A80"}.mdi-zodiac-gemini::before{content:"\F0A81"}.mdi-zodiac-leo::before{content:"\F0A82"}.mdi-zodiac-libra::before{content:"\F0A83"}.mdi-zodiac-pisces::before{content:"\F0A84"}.mdi-zodiac-sagittarius::before{content:"\F0A85"}.mdi-zodiac-scorpio::before{content:"\F0A86"}.mdi-zodiac-taurus::before{content:"\F0A87"}.mdi-zodiac-virgo::before{content:"\F0A88"}.mdi-blank::before{content:"\F68C";visibility:hidden}.mdi-18px.mdi-set,.mdi-18px.mdi:before{font-size:18px}.mdi-24px.mdi-set,.mdi-24px.mdi:before{font-size:24px}.mdi-36px.mdi-set,.mdi-36px.mdi:before{font-size:36px}.mdi-48px.mdi-set,.mdi-48px.mdi:before{font-size:48px}.mdi-dark:before{color:rgba(0,0,0,0.54)}.mdi-dark.mdi-inactive:before{color:rgba(0,0,0,0.26)}.mdi-light:before{color:#fff}.mdi-light.mdi-inactive:before{color:rgba(255,255,255,0.3)}.mdi-rotate-45:before{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.mdi-rotate-90:before{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.mdi-rotate-135:before{-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg)}.mdi-rotate-180:before{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.mdi-rotate-225:before{-webkit-transform:rotate(225deg);-ms-transform:rotate(225deg);transform:rotate(225deg)}.mdi-rotate-270:before{-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.mdi-rotate-315:before{-webkit-transform:rotate(315deg);-ms-transform:rotate(315deg);transform:rotate(315deg)}.mdi-flip-h:before{-webkit-transform:scaleX(-1);transform:scaleX(-1);filter:FlipH;-ms-filter:"FlipH"}.mdi-flip-v:before{-webkit-transform:scaleY(-1);transform:scaleY(-1);filter:FlipV;-ms-filter:"FlipV"}.mdi-spin:before{-webkit-animation:mdi-spin 2s infinite linear;animation:mdi-spin 2s infinite linear}@-webkit-keyframes mdi-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes mdi-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}} - -/*# sourceMappingURL=materialdesignicons.css.map */ \ No newline at end of file diff --git a/web/scripts/api.js b/web/scripts/api.js deleted file mode 100644 index eee1c9a15..000000000 --- a/web/scripts/api.js +++ /dev/null @@ -1,482 +0,0 @@ -class ComfyApi extends EventTarget { - #registered = new Set(); - - constructor() { - super(); - this.api_host = location.host; - this.api_base = location.pathname.split('/').slice(0, -1).join('/'); - this.initialClientId = sessionStorage.getItem("clientId"); - } - - apiURL(route) { - return this.api_base + route; - } - - fetchApi(route, options) { - if (!options) { - options = {}; - } - if (!options.headers) { - options.headers = {}; - } - options.headers["Comfy-User"] = this.user; - return fetch(this.apiURL(route), options); - } - - addEventListener(type, callback, options) { - super.addEventListener(type, callback, options); - this.#registered.add(type); - } - - /** - * Poll status for colab and other things that don't support websockets. - */ - #pollQueue() { - setInterval(async () => { - try { - const resp = await this.fetchApi("/prompt"); - const status = await resp.json(); - this.dispatchEvent(new CustomEvent("status", { detail: status })); - } catch (error) { - this.dispatchEvent(new CustomEvent("status", { detail: null })); - } - }, 1000); - } - - /** - * Creates and connects a WebSocket for realtime updates - * @param {boolean} isReconnect If the socket is connection is a reconnect attempt - */ - #createSocket(isReconnect) { - if (this.socket) { - return; - } - - let opened = false; - let existingSession = window.name; - if (existingSession) { - existingSession = "?clientId=" + existingSession; - } - this.socket = new WebSocket( - `ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}` - ); - this.socket.binaryType = "arraybuffer"; - - this.socket.addEventListener("open", () => { - opened = true; - if (isReconnect) { - this.dispatchEvent(new CustomEvent("reconnected")); - } - }); - - this.socket.addEventListener("error", () => { - if (this.socket) this.socket.close(); - if (!isReconnect && !opened) { - this.#pollQueue(); - } - }); - - this.socket.addEventListener("close", () => { - setTimeout(() => { - this.socket = null; - this.#createSocket(true); - }, 300); - if (opened) { - this.dispatchEvent(new CustomEvent("status", { detail: null })); - this.dispatchEvent(new CustomEvent("reconnecting")); - } - }); - - this.socket.addEventListener("message", (event) => { - try { - if (event.data instanceof ArrayBuffer) { - const view = new DataView(event.data); - const eventType = view.getUint32(0); - const buffer = event.data.slice(4); - switch (eventType) { - case 1: - const view2 = new DataView(event.data); - const imageType = view2.getUint32(0) - let imageMime - switch (imageType) { - case 1: - default: - imageMime = "image/jpeg"; - break; - case 2: - imageMime = "image/png" - } - const imageBlob = new Blob([buffer.slice(4)], { type: imageMime }); - this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob })); - break; - default: - throw new Error(`Unknown binary websocket message of type ${eventType}`); - } - } - else { - const msg = JSON.parse(event.data); - switch (msg.type) { - case "status": - if (msg.data.sid) { - this.clientId = msg.data.sid; - window.name = this.clientId; // use window name so it isnt reused when duplicating tabs - sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow - } - this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status })); - break; - case "progress": - this.dispatchEvent(new CustomEvent("progress", { detail: msg.data })); - break; - case "executing": - this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.display_node })); - break; - case "executed": - this.dispatchEvent(new CustomEvent("executed", { detail: msg.data })); - break; - case "execution_start": - this.dispatchEvent(new CustomEvent("execution_start", { detail: msg.data })); - break; - case "execution_success": - this.dispatchEvent(new CustomEvent("execution_success", { detail: msg.data })); - break; - case "execution_error": - this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data })); - break; - case "execution_cached": - this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data })); - break; - default: - if (this.#registered.has(msg.type)) { - this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data })); - } else { - throw new Error(`Unknown message type ${msg.type}`); - } - } - } - } catch (error) { - console.warn("Unhandled message:", event.data, error); - } - }); - } - - /** - * Initialises sockets and realtime updates - */ - init() { - this.#createSocket(); - } - - /** - * Gets a list of extension urls - * @returns An array of script urls to import - */ - async getExtensions() { - const resp = await this.fetchApi("/extensions", { cache: "no-store" }); - return await resp.json(); - } - - /** - * Gets a list of embedding names - * @returns An array of script urls to import - */ - async getEmbeddings() { - const resp = await this.fetchApi("/embeddings", { cache: "no-store" }); - return await resp.json(); - } - - /** - * Loads node object definitions for the graph - * @returns The node definitions - */ - async getNodeDefs() { - const resp = await this.fetchApi("/object_info", { cache: "no-store" }); - return await resp.json(); - } - - /** - * - * @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue - * @param {object} prompt The prompt data to queue - */ - async queuePrompt(number, { output, workflow }) { - const body = { - client_id: this.clientId, - prompt: output, - extra_data: { extra_pnginfo: { workflow } }, - }; - - if (number === -1) { - body.front = true; - } else if (number != 0) { - body.number = number; - } - - const res = await this.fetchApi("/prompt", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - - if (res.status !== 200) { - throw { - response: await res.json(), - }; - } - - return await res.json(); - } - - /** - * Loads a list of items (queue or history) - * @param {string} type The type of items to load, queue or history - * @returns The items of the specified type grouped by their status - */ - async getItems(type) { - if (type === "queue") { - return this.getQueue(); - } - return this.getHistory(); - } - - /** - * Gets the current state of the queue - * @returns The currently running and queued items - */ - async getQueue() { - try { - const res = await this.fetchApi("/queue"); - const data = await res.json(); - return { - // Running action uses a different endpoint for cancelling - Running: data.queue_running.map((prompt) => ({ - prompt, - remove: { name: "Cancel", cb: () => api.interrupt() }, - })), - Pending: data.queue_pending.map((prompt) => ({ prompt })), - }; - } catch (error) { - console.error(error); - return { Running: [], Pending: [] }; - } - } - - /** - * Gets the prompt execution history - * @returns Prompt history including node outputs - */ - async getHistory(max_items=200) { - try { - const res = await this.fetchApi(`/history?max_items=${max_items}`); - return { History: Object.values(await res.json()) }; - } catch (error) { - console.error(error); - return { History: [] }; - } - } - - /** - * Gets system & device stats - * @returns System stats such as python version, OS, per device info - */ - async getSystemStats() { - const res = await this.fetchApi("/system_stats"); - return await res.json(); - } - - /** - * Sends a POST request to the API - * @param {*} type The endpoint to post to - * @param {*} body Optional POST data - */ - async #postItem(type, body) { - try { - await this.fetchApi("/" + type, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: body ? JSON.stringify(body) : undefined, - }); - } catch (error) { - console.error(error); - } - } - - /** - * Deletes an item from the specified list - * @param {string} type The type of item to delete, queue or history - * @param {number} id The id of the item to delete - */ - async deleteItem(type, id) { - await this.#postItem(type, { delete: [id] }); - } - - /** - * Clears the specified list - * @param {string} type The type of list to clear, queue or history - */ - async clearItems(type) { - await this.#postItem(type, { clear: true }); - } - - /** - * Interrupts the execution of the running prompt - */ - async interrupt() { - await this.#postItem("interrupt", null); - } - - /** - * Gets user configuration data and where data should be stored - * @returns { Promise<{ storage: "server" | "browser", users?: Promise, migrated?: boolean }> } - */ - async getUserConfig() { - return (await this.fetchApi("/users")).json(); - } - - /** - * Creates a new user - * @param { string } username - * @returns The fetch response - */ - createUser(username) { - return this.fetchApi("/users", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ username }), - }); - } - - /** - * Gets all setting values for the current user - * @returns { Promise } A dictionary of id -> value - */ - async getSettings() { - return (await this.fetchApi("/settings")).json(); - } - - /** - * Gets a setting for the current user - * @param { string } id The id of the setting to fetch - * @returns { Promise } The setting value - */ - async getSetting(id) { - return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json(); - } - - /** - * Stores a dictionary of settings for the current user - * @param { Record } settings Dictionary of setting id -> value to save - * @returns { Promise } - */ - async storeSettings(settings) { - return this.fetchApi(`/settings`, { - method: "POST", - body: JSON.stringify(settings) - }); - } - - /** - * Stores a setting for the current user - * @param { string } id The id of the setting to update - * @param { unknown } value The value of the setting - * @returns { Promise } - */ - async storeSetting(id, value) { - return this.fetchApi(`/settings/${encodeURIComponent(id)}`, { - method: "POST", - body: JSON.stringify(value) - }); - } - - /** - * Gets a user data file for the current user - * @param { string } file The name of the userdata file to load - * @param { RequestInit } [options] - * @returns { Promise } The fetch response object - */ - async getUserData(file, options) { - return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options); - } - - /** - * Stores a user data file for the current user - * @param { string } file The name of the userdata file to save - * @param { unknown } data The data to save to the file - * @param { RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } } [options] - * @returns { Promise } - */ - async storeUserData(file, data, options = { overwrite: true, stringify: true, throwOnError: true }) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options?.overwrite}`, { - method: "POST", - body: options?.stringify ? JSON.stringify(data) : data, - ...options, - }); - if (resp.status !== 200 && options?.throwOnError !== false) { - throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`); - } - return resp; - } - - /** - * Deletes a user data file for the current user - * @param { string } file The name of the userdata file to delete - */ - async deleteUserData(file) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, { - method: "DELETE", - }); - if (resp.status !== 204) { - throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`); - } - } - - /** - * Move a user data file for the current user - * @param { string } source The userdata file to move - * @param { string } dest The destination for the file - */ - async moveUserData(source, dest, options = { overwrite: false }) { - const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, { - method: "POST", - }); - return resp; - } - - /** - * @overload - * Lists user data files for the current user - * @param { string } dir The directory in which to list files - * @param { boolean } [recurse] If the listing should be recursive - * @param { true } [split] If the paths should be split based on the os path separator - * @returns { Promise> } The list of split file paths in the format [fullPath, ...splitPath] - */ - /** - * @overload - * Lists user data files for the current user - * @param { string } dir The directory in which to list files - * @param { boolean } [recurse] If the listing should be recursive - * @param { false | undefined } [split] If the paths should be split based on the os path separator - * @returns { Promise> } The list of files - */ - async listUserData(dir, recurse, split) { - const resp = await this.fetchApi( - `/userdata?${new URLSearchParams({ - recurse, - dir, - split, - })}` - ); - if (resp.status === 404) return []; - if (resp.status !== 200) { - throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`); - } - return resp.json(); - } -} - -export const api = new ComfyApi(); diff --git a/web/scripts/app.js b/web/scripts/app.js deleted file mode 100644 index 6e2b39538..000000000 --- a/web/scripts/app.js +++ /dev/null @@ -1,2460 +0,0 @@ -import { ComfyLogging } from "./logging.js"; -import { ComfyWidgets, initWidgets } from "./widgets.js"; -import { ComfyUI, $el } from "./ui.js"; -import { api } from "./api.js"; -import { defaultGraph } from "./defaultGraph.js"; -import { getPngMetadata, getWebpMetadata, getFlacMetadata, importA1111, getLatentMetadata } from "./pnginfo.js"; -import { addDomClippingSetting } from "./domWidget.js"; -import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js"; -import { ComfyAppMenu } from "./ui/menu/index.js"; -import { getStorageValue, setStorageValue } from "./utils.js"; -import { ComfyWorkflowManager } from "./workflows.js"; -export const ANIM_PREVIEW_WIDGET = "$$comfy_animation_preview"; - -function sanitizeNodeName(string) { - let entityMap = { - '&': '', - '<': '', - '>': '', - '"': '', - "'": '', - '`': '', - '=': '' - }; - return String(string).replace(/[&<>"'`=]/g, function fromEntityMap (s) { - return entityMap[s]; - }); -} - -/** - * @typedef {import("types/comfy").ComfyExtension} ComfyExtension - */ - -export class ComfyApp { - /** - * List of entries to queue - * @type {{number: number, batchCount: number}[]} - */ - #queueItems = []; - /** - * If the queue is currently being processed - * @type {boolean} - */ - #processingQueue = false; - - /** - * Content Clipboard - * @type {serialized node object} - */ - static clipspace = null; - static clipspace_invalidate_handler = null; - static open_maskeditor = null; - static clipspace_return_node = null; - - constructor() { - this.ui = new ComfyUI(this); - this.logging = new ComfyLogging(this); - this.workflowManager = new ComfyWorkflowManager(this); - this.bodyTop = $el("div.comfyui-body-top", { parent: document.body }); - this.bodyLeft = $el("div.comfyui-body-left", { parent: document.body }); - this.bodyRight = $el("div.comfyui-body-right", { parent: document.body }); - this.bodyBottom = $el("div.comfyui-body-bottom", { parent: document.body }); - this.menu = new ComfyAppMenu(this); - - /** - * List of extensions that are registered with the app - * @type {ComfyExtension[]} - */ - this.extensions = []; - - /** - * Stores the execution output data for each node - * @type {Record} - */ - this._nodeOutputs = {}; - - /** - * Stores the preview image data for each node - * @type {Record} - */ - this.nodePreviewImages = {}; - - /** - * If the shift key on the keyboard is pressed - * @type {boolean} - */ - this.shiftDown = false; - } - - get nodeOutputs() { - return this._nodeOutputs; - } - - set nodeOutputs(value) { - this._nodeOutputs = value; - this.#invokeExtensions("onNodeOutputsUpdated", value); - } - - getPreviewFormatParam() { - let preview_format = this.ui.settings.getSettingValue("Comfy.PreviewFormat"); - if(preview_format) - return `&preview=${preview_format}`; - else - return ""; - } - - getRandParam() { - return "&rand=" + Math.random(); - } - - static isImageNode(node) { - return node.imgs || (node && node.widgets && node.widgets.findIndex(obj => obj.name === 'image') >= 0); - } - - static onClipspaceEditorSave() { - if(ComfyApp.clipspace_return_node) { - ComfyApp.pasteFromClipspace(ComfyApp.clipspace_return_node); - } - } - - static onClipspaceEditorClosed() { - ComfyApp.clipspace_return_node = null; - } - - static copyToClipspace(node) { - var widgets = null; - if(node.widgets) { - widgets = node.widgets.map(({ type, name, value }) => ({ type, name, value })); - } - - var imgs = undefined; - var orig_imgs = undefined; - if(node.imgs != undefined) { - imgs = []; - orig_imgs = []; - - for (let i = 0; i < node.imgs.length; i++) { - imgs[i] = new Image(); - imgs[i].src = node.imgs[i].src; - orig_imgs[i] = imgs[i]; - } - } - - var selectedIndex = 0; - if(node.imageIndex) { - selectedIndex = node.imageIndex; - } - - ComfyApp.clipspace = { - 'widgets': widgets, - 'imgs': imgs, - 'original_imgs': orig_imgs, - 'images': node.images, - 'selectedIndex': selectedIndex, - 'img_paste_mode': 'selected' // reset to default im_paste_mode state on copy action - }; - - ComfyApp.clipspace_return_node = null; - - if(ComfyApp.clipspace_invalidate_handler) { - ComfyApp.clipspace_invalidate_handler(); - } - } - - static pasteFromClipspace(node) { - if(ComfyApp.clipspace) { - // image paste - if(ComfyApp.clipspace.imgs && node.imgs) { - if(node.images && ComfyApp.clipspace.images) { - if(ComfyApp.clipspace['img_paste_mode'] == 'selected') { - node.images = [ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']]]; - } - else { - node.images = ComfyApp.clipspace.images; - } - - if(app.nodeOutputs[node.id + ""]) - app.nodeOutputs[node.id + ""].images = node.images; - } - - if(ComfyApp.clipspace.imgs) { - // deep-copy to cut link with clipspace - if(ComfyApp.clipspace['img_paste_mode'] == 'selected') { - const img = new Image(); - img.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src; - node.imgs = [img]; - node.imageIndex = 0; - } - else { - const imgs = []; - for(let i=0; i obj.name === 'image'); - if(index >= 0) { - if(node.widgets[index].type != 'image' && typeof node.widgets[index].value == "string" && clip_image.filename) { - node.widgets[index].value = (clip_image.subfolder?clip_image.subfolder+'/':'') + clip_image.filename + (clip_image.type?` [${clip_image.type}]`:''); - } - else { - node.widgets[index].value = clip_image; - } - } - } - if(ComfyApp.clipspace.widgets) { - ComfyApp.clipspace.widgets.forEach(({ type, name, value }) => { - const prop = Object.values(node.widgets).find(obj => obj.type === type && obj.name === name); - if (prop && prop.type != 'button') { - if(prop.type != 'image' && typeof prop.value == "string" && value.filename) { - prop.value = (value.subfolder?value.subfolder+'/':'') + value.filename + (value.type?` [${value.type}]`:''); - } - else { - prop.value = value; - prop.callback(value); - } - } - }); - } - } - - app.graph.setDirtyCanvas(true); - } - } - - /** - * Invoke an extension callback - * @param {keyof ComfyExtension} method The extension callback to execute - * @param {any[]} args Any arguments to pass to the callback - * @returns - */ - #invokeExtensions(method, ...args) { - let results = []; - for (const ext of this.extensions) { - if (method in ext) { - try { - results.push(ext[method](...args, this)); - } catch (error) { - console.error( - `Error calling extension '${ext.name}' method '${method}'`, - { error }, - { extension: ext }, - { args } - ); - } - } - } - return results; - } - - /** - * Invoke an async extension callback - * Each callback will be invoked concurrently - * @param {string} method The extension callback to execute - * @param {...any} args Any arguments to pass to the callback - * @returns - */ - async #invokeExtensionsAsync(method, ...args) { - return await Promise.all( - this.extensions.map(async (ext) => { - if (method in ext) { - try { - return await ext[method](...args, this); - } catch (error) { - console.error( - `Error calling extension '${ext.name}' method '${method}'`, - { error }, - { extension: ext }, - { args } - ); - } - } - }) - ); - } - - #addRestoreWorkflowView() { - const serialize = LGraph.prototype.serialize; - const self = this; - LGraph.prototype.serialize = function() { - const workflow = serialize.apply(this, arguments); - - // Store the drag & scale info in the serialized workflow if the setting is enabled - if (self.enableWorkflowViewRestore.value) { - if (!workflow.extra) { - workflow.extra = {}; - } - workflow.extra.ds = { - scale: self.canvas.ds.scale, - offset: self.canvas.ds.offset, - }; - } else if (workflow.extra?.ds) { - // Clear any old view data - delete workflow.extra.ds; - } - - return workflow; - } - this.enableWorkflowViewRestore = this.ui.settings.addSetting({ - id: "Comfy.EnableWorkflowViewRestore", - name: "Save and restore canvas position and zoom level in workflows", - type: "boolean", - defaultValue: true - }); - } - - /** - * Adds special context menu handling for nodes - * e.g. this adds Open Image functionality for nodes that show images - * @param {*} node The node to add the menu handler - */ - #addNodeContextMenuHandler(node) { - function getCopyImageOption(img) { - if (typeof window.ClipboardItem === "undefined") return []; - return [ - { - content: "Copy Image", - callback: async () => { - const url = new URL(img.src); - url.searchParams.delete("preview"); - - const writeImage = async (blob) => { - await navigator.clipboard.write([ - new ClipboardItem({ - [blob.type]: blob, - }), - ]); - }; - - try { - const data = await fetch(url); - const blob = await data.blob(); - try { - await writeImage(blob); - } catch (error) { - // Chrome seems to only support PNG on write, convert and try again - if (blob.type !== "image/png") { - const canvas = $el("canvas", { - width: img.naturalWidth, - height: img.naturalHeight, - }); - const ctx = canvas.getContext("2d"); - let image; - if (typeof window.createImageBitmap === "undefined") { - image = new Image(); - const p = new Promise((resolve, reject) => { - image.onload = resolve; - image.onerror = reject; - }).finally(() => { - URL.revokeObjectURL(image.src); - }); - image.src = URL.createObjectURL(blob); - await p; - } else { - image = await createImageBitmap(blob); - } - try { - ctx.drawImage(image, 0, 0); - canvas.toBlob(writeImage, "image/png"); - } finally { - if (typeof image.close === "function") { - image.close(); - } - } - - return; - } - throw error; - } - } catch (error) { - alert("Error copying image: " + (error.message ?? error)); - } - }, - }, - ]; - } - - node.prototype.getExtraMenuOptions = function (_, options) { - if (this.imgs) { - // If this node has images then we add an open in new tab item - let img; - if (this.imageIndex != null) { - // An image is selected so select that - img = this.imgs[this.imageIndex]; - } else if (this.overIndex != null) { - // No image is selected but one is hovered - img = this.imgs[this.overIndex]; - } - if (img) { - options.unshift( - { - content: "Open Image", - callback: () => { - let url = new URL(img.src); - url.searchParams.delete("preview"); - window.open(url, "_blank"); - }, - }, - ...getCopyImageOption(img), - { - content: "Save Image", - callback: () => { - const a = document.createElement("a"); - let url = new URL(img.src); - url.searchParams.delete("preview"); - a.href = url; - a.setAttribute("download", new URLSearchParams(url.search).get("filename")); - document.body.append(a); - a.click(); - requestAnimationFrame(() => a.remove()); - }, - } - ); - } - } - - options.push({ - content: "Bypass", - callback: (obj) => { - if (this.mode === 4) this.mode = 0; - else this.mode = 4; - this.graph.change(); - }, - }); - - // prevent conflict of clipspace content - if (!ComfyApp.clipspace_return_node) { - options.push({ - content: "Copy (Clipspace)", - callback: (obj) => { - ComfyApp.copyToClipspace(this); - }, - }); - - if (ComfyApp.clipspace != null) { - options.push({ - content: "Paste (Clipspace)", - callback: () => { - ComfyApp.pasteFromClipspace(this); - }, - }); - } - - if (ComfyApp.isImageNode(this)) { - options.push({ - content: "Open in MaskEditor", - callback: (obj) => { - ComfyApp.copyToClipspace(this); - ComfyApp.clipspace_return_node = this; - ComfyApp.open_maskeditor(); - }, - }); - } - } - }; - } - - #addNodeKeyHandler(node) { - const app = this; - const origNodeOnKeyDown = node.prototype.onKeyDown; - - node.prototype.onKeyDown = function(e) { - if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) { - return false; - } - - if (this.flags.collapsed || !this.imgs || this.imageIndex === null) { - return; - } - - let handled = false; - - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - if (e.key === "ArrowLeft") { - this.imageIndex -= 1; - } else if (e.key === "ArrowRight") { - this.imageIndex += 1; - } - this.imageIndex %= this.imgs.length; - - if (this.imageIndex < 0) { - this.imageIndex = this.imgs.length + this.imageIndex; - } - handled = true; - } else if (e.key === "Escape") { - this.imageIndex = null; - handled = true; - } - - if (handled === true) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - } - } - - /** - * Adds Custom drawing logic for nodes - * e.g. Draws images and handles thumbnail navigation on nodes that output images - * @param {*} node The node to add the draw handler - */ - #addDrawBackgroundHandler(node) { - const app = this; - - function getImageTop(node) { - let shiftY; - if (node.imageOffset != null) { - shiftY = node.imageOffset; - } else { - if (node.widgets?.length) { - const w = node.widgets[node.widgets.length - 1]; - shiftY = w.last_y; - if (w.computeSize) { - shiftY += w.computeSize()[1] + 4; - } - else if(w.computedHeight) { - shiftY += w.computedHeight; - } - else { - shiftY += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } else { - shiftY = node.computeSize()[1]; - } - } - return shiftY; - } - - node.prototype.setSizeForImage = function (force) { - if(!force && this.animatedImages) return; - - if (this.inputHeight || this.freeWidgetSpace > 210) { - this.setSize(this.size); - return; - } - const minHeight = getImageTop(this) + 220; - if (this.size[1] < minHeight) { - this.setSize([this.size[0], minHeight]); - } - }; - - node.prototype.onDrawBackground = function (ctx) { - if (!this.flags.collapsed) { - let imgURLs = [] - let imagesChanged = false - - const output = app.nodeOutputs[this.id + ""]; - if (output?.images) { - this.animatedImages = output?.animated?.find(Boolean); - if (this.images !== output.images) { - this.images = output.images; - imagesChanged = true; - imgURLs = imgURLs.concat( - output.images.map((params) => { - return api.apiURL( - "/view?" + - new URLSearchParams(params).toString() + - (this.animatedImages ? "" : app.getPreviewFormatParam()) + app.getRandParam() - ); - }) - ); - } - } - - const preview = app.nodePreviewImages[this.id + ""] - if (this.preview !== preview) { - this.preview = preview - imagesChanged = true; - if (preview != null) { - imgURLs.push(preview); - } - } - - if (imagesChanged) { - this.imageIndex = null; - if (imgURLs.length > 0) { - Promise.all( - imgURLs.map((src) => { - return new Promise((r) => { - const img = new Image(); - img.onload = () => r(img); - img.onerror = () => r(null); - img.src = src - }); - }) - ).then((imgs) => { - if ((!output || this.images === output.images) && (!preview || this.preview === preview)) { - this.imgs = imgs.filter(Boolean); - this.setSizeForImage?.(); - app.graph.setDirtyCanvas(true); - } - }); - } - else { - this.imgs = null; - } - } - - function calculateGrid(w, h, n) { - let columns, rows, cellsize; - - if (w > h) { - cellsize = h; - columns = Math.ceil(w / cellsize); - rows = Math.ceil(n / columns); - } else { - cellsize = w; - rows = Math.ceil(h / cellsize); - columns = Math.ceil(n / rows); - } - - while (columns * rows < n) { - cellsize++; - if (w >= h) { - columns = Math.ceil(w / cellsize); - rows = Math.ceil(n / columns); - } else { - rows = Math.ceil(h / cellsize); - columns = Math.ceil(n / rows); - } - } - - const cell_size = Math.min(w/columns, h/rows); - return {cell_size, columns, rows}; - } - - function is_all_same_aspect_ratio(imgs) { - // assume: imgs.length >= 2 - let ratio = imgs[0].naturalWidth/imgs[0].naturalHeight; - - for(let i=1; i w.name === ANIM_PREVIEW_WIDGET); - - if(this.animatedImages) { - // Instead of using the canvas we'll use a IMG - if(widgetIdx > -1) { - // Replace content - const widget = this.widgets[widgetIdx]; - widget.options.host.updateImages(this.imgs); - } else { - const host = createImageHost(this); - this.setSizeForImage(true); - const widget = this.addDOMWidget(ANIM_PREVIEW_WIDGET, "img", host.el, { - host, - getHeight: host.getHeight, - onDraw: host.onDraw, - hideOnZoom: false - }); - widget.serializeValue = () => undefined; - widget.options.host.updateImages(this.imgs); - } - return; - } - - if (widgetIdx > -1) { - this.widgets[widgetIdx].onRemove?.(); - this.widgets.splice(widgetIdx, 1); - } - - const canvas = app.graph.list_of_graphcanvas[0]; - const mouse = canvas.graph_mouse; - if (!canvas.pointer_is_down && this.pointerDown) { - if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) { - this.imageIndex = this.pointerDown.index; - } - this.pointerDown = null; - } - - let imageIndex = this.imageIndex; - const numImages = this.imgs.length; - if (numImages === 1 && !imageIndex) { - this.imageIndex = imageIndex = 0; - } - - const top = getImageTop(this); - var shiftY = top; - - let dw = this.size[0]; - let dh = this.size[1]; - dh -= shiftY; - - if (imageIndex == null) { - var cellWidth, cellHeight, shiftX, cell_padding, cols; - - const compact_mode = is_all_same_aspect_ratio(this.imgs); - if(!compact_mode) { - // use rectangle cell style and border line - cell_padding = 2; - const { cell_size, columns, rows } = calculateGrid(dw, dh, numImages); - cols = columns; - - cellWidth = cell_size; - cellHeight = cell_size; - shiftX = (dw-cell_size*cols)/2; - shiftY = (dh-cell_size*rows)/2 + top; - } - else { - cell_padding = 0; - ({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(this.imgs, dw, dh)); - } - - let anyHovered = false; - this.imageRects = []; - for (let i = 0; i < numImages; i++) { - const img = this.imgs[i]; - const row = Math.floor(i / cols); - const col = i % cols; - const x = col * cellWidth + shiftX; - const y = row * cellHeight + shiftY; - if (!anyHovered) { - anyHovered = LiteGraph.isInsideRectangle( - mouse[0], - mouse[1], - x + this.pos[0], - y + this.pos[1], - cellWidth, - cellHeight - ); - if (anyHovered) { - this.overIndex = i; - let value = 110; - if (canvas.pointer_is_down) { - if (!this.pointerDown || this.pointerDown.index !== i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - value = 125; - } - ctx.filter = `contrast(${value}%) brightness(${value}%)`; - canvas.canvas.style.cursor = "pointer"; - } - } - this.imageRects.push([x, y, cellWidth, cellHeight]); - - let wratio = cellWidth/img.width; - let hratio = cellHeight/img.height; - var ratio = Math.min(wratio, hratio); - - let imgHeight = ratio * img.height; - let imgY = row * cellHeight + shiftY + (cellHeight - imgHeight)/2; - let imgWidth = ratio * img.width; - let imgX = col * cellWidth + shiftX + (cellWidth - imgWidth)/2; - - ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2); - if(!compact_mode) { - // rectangle cell and border line style - ctx.strokeStyle = "#8F8F8F"; - ctx.lineWidth = 1; - ctx.strokeRect(x+cell_padding, y+cell_padding, cellWidth-cell_padding*2, cellHeight-cell_padding*2); - } - - ctx.filter = "none"; - } - - if (!anyHovered) { - this.pointerDown = null; - this.overIndex = null; - } - } else { - // Draw individual - let w = this.imgs[imageIndex].naturalWidth; - let h = this.imgs[imageIndex].naturalHeight; - - const scaleX = dw / w; - const scaleY = dh / h; - const scale = Math.min(scaleX, scaleY, 1); - - w *= scale; - h *= scale; - - let x = (dw - w) / 2; - let y = (dh - h) / 2 + shiftY; - ctx.drawImage(this.imgs[imageIndex], x, y, w, h); - - const drawButton = (x, y, sz, text) => { - const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz); - let fill = "#333"; - let textFill = "#fff"; - let isClicking = false; - if (hovered) { - canvas.canvas.style.cursor = "pointer"; - if (canvas.pointer_is_down) { - fill = "#1e90ff"; - isClicking = true; - } else { - fill = "#eee"; - textFill = "#000"; - } - } else { - this.pointerWasDown = null; - } - - ctx.fillStyle = fill; - ctx.beginPath(); - ctx.roundRect(x, y, sz, sz, [4]); - ctx.fill(); - ctx.fillStyle = textFill; - ctx.font = "12px Arial"; - ctx.textAlign = "center"; - ctx.fillText(text, x + 15, y + 20); - - return isClicking; - }; - - if (numImages > 1) { - if (drawButton(dw - 40, dh + top - 40, 30, `${this.imageIndex + 1}/${numImages}`)) { - let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1; - if (!this.pointerDown || !this.pointerDown.index === i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - } - - if (drawButton(dw - 40, top + 10, 30, `x`)) { - if (!this.pointerDown || !this.pointerDown.index === null) { - this.pointerDown = { index: null, pos: [...mouse] }; - } - } - } - } - } - } - }; - } - - /** - * Adds a handler allowing drag+drop of files onto the window to load workflows - */ - #addDropHandler() { - // Get prompt from dropped PNG or json - document.addEventListener("drop", async (event) => { - event.preventDefault(); - event.stopPropagation(); - - const n = this.dragOverNode; - this.dragOverNode = null; - // Node handles file drop, we dont use the built in onDropFile handler as its buggy - // If you drag multiple files it will call it multiple times with the same file - if (n && n.onDragDrop && (await n.onDragDrop(event))) { - return; - } - // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that - if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") { - await this.handleFile(event.dataTransfer.files[0]); - } else { - // Try loading the first URI in the transfer list - const validTypes = ["text/uri-list", "text/x-moz-url"]; - const match = [...event.dataTransfer.types].find((t) => validTypes.find(v => t === v)); - if (match) { - const uri = event.dataTransfer.getData(match)?.split("\n")?.[0]; - if (uri) { - await this.handleFile(await (await fetch(uri)).blob()); - } - } - } - }); - - // Always clear over node on drag leave - this.canvasEl.addEventListener("dragleave", async () => { - if (this.dragOverNode) { - this.dragOverNode = null; - this.graph.setDirtyCanvas(false, true); - } - }); - - // Add handler for dropping onto a specific node - this.canvasEl.addEventListener( - "dragover", - (e) => { - this.canvas.adjustMouseEvent(e); - const node = this.graph.getNodeOnPos(e.canvasX, e.canvasY); - if (node) { - if (node.onDragOver && node.onDragOver(e)) { - this.dragOverNode = node; - - // dragover event is fired very frequently, run this on an animation frame - requestAnimationFrame(() => { - this.graph.setDirtyCanvas(false, true); - }); - return; - } - } - this.dragOverNode = null; - }, - false - ); - } - - /** - * Adds a handler on paste that extracts and loads images or workflows from pasted JSON data - */ - #addPasteHandler() { - document.addEventListener("paste", async (e) => { - // ctrl+shift+v is used to paste nodes with connections - // this is handled by litegraph - if(this.shiftDown) return; - - let data = (e.clipboardData || window.clipboardData); - const items = data.items; - - // Look for image paste data - for (const item of items) { - if (item.type.startsWith('image/')) { - var imageNode = null; - - // If an image node is selected, paste into it - if (this.canvas.current_node && - this.canvas.current_node.is_selected && - ComfyApp.isImageNode(this.canvas.current_node)) { - imageNode = this.canvas.current_node; - } - - // No image node selected: add a new one - if (!imageNode) { - const newNode = LiteGraph.createNode("LoadImage"); - newNode.pos = [...this.canvas.graph_mouse]; - imageNode = this.graph.add(newNode); - this.graph.change(); - } - const blob = item.getAsFile(); - imageNode.pasteFile(blob); - return; - } - } - - // No image found. Look for node data - data = data.getData("text/plain"); - let workflow; - try { - data = data.slice(data.indexOf("{")); - workflow = JSON.parse(data); - } catch (err) { - try { - data = data.slice(data.indexOf("workflow\n")); - data = data.slice(data.indexOf("{")); - workflow = JSON.parse(data); - } catch (error) {} - } - - if (workflow && workflow.version && workflow.nodes && workflow.extra) { - await this.loadGraphData(workflow); - } - else { - if (e.target.type === "text" || e.target.type === "textarea") { - return; - } - - // Litegraph default paste - this.canvas.pasteFromClipboard(); - } - - - }); - } - - - /** - * Adds a handler on copy that serializes selected nodes to JSON - */ - #addCopyHandler() { - document.addEventListener("copy", (e) => { - if (e.target.type === "text" || e.target.type === "textarea") { - // Default system copy - return; - } - - // copy nodes and clear clipboard - if (e.target.className === "litegraph" && this.canvas.selected_nodes) { - this.canvas.copyToClipboard(); - e.clipboardData.setData('text', ' '); //clearData doesn't remove images from clipboard - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - }); - } - - - /** - * Handle mouse - * - * Move group by header - */ - #addProcessMouseHandler() { - const self = this; - - const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown; - LGraphCanvas.prototype.processMouseDown = function(e) { - // prepare for ctrl+shift drag: zoom start - if(e.ctrlKey && e.shiftKey && e.buttons) { - self.zoom_drag_start = [e.x, e.y, this.ds.scale]; - return; - } - - const res = origProcessMouseDown.apply(this, arguments); - - this.selected_group_moving = false; - - if (this.selected_group && !this.selected_group_resizing) { - var font_size = - this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - var height = font_size * 1.4; - - // Move group by header - if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) { - this.selected_group_moving = true; - } - } - - return res; - } - - const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove; - LGraphCanvas.prototype.processMouseMove = function(e) { - // handle ctrl+shift drag - if(e.ctrlKey && e.shiftKey && self.zoom_drag_start) { - // stop canvas zoom action - if(!e.buttons) { - self.zoom_drag_start = null; - return; - } - - // calculate delta - let deltaY = e.y - self.zoom_drag_start[1]; - let startScale = self.zoom_drag_start[2]; - - let scale = startScale - deltaY/100; - - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - - return; - } - - const orig_selected_group = this.selected_group; - - if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = null; - } - - const res = origProcessMouseMove.apply(this, arguments); - - if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = orig_selected_group; - } - - return res; - }; - } - - /** - * Handle keypress - * - * Ctrl + M mute/unmute selected nodes - */ - #addProcessKeyHandler() { - const self = this; - const origProcessKey = LGraphCanvas.prototype.processKey; - LGraphCanvas.prototype.processKey = function(e) { - if (!this.graph) { - return; - } - - var block_default = false; - - if (e.target.localName == "input") { - return; - } - - if (e.type == "keydown" && !e.repeat) { - - // Ctrl + M mute/unmute - if (e.key === 'm' && (e.metaKey || e.ctrlKey)) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 2) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 2; // never - } - } - } - block_default = true; - } - - // Ctrl + B bypass - if (e.key === 'b' && (e.metaKey || e.ctrlKey)) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 4) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 4; // never - } - } - } - block_default = true; - } - - // Alt + C collapse/uncollapse - if (e.key === 'c' && e.altKey) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - this.selected_nodes[i].collapse() - } - } - block_default = true; - } - - // Ctrl+C Copy - if ((e.key === 'c') && (e.metaKey || e.ctrlKey)) { - // Trigger onCopy - return true; - } - - // Ctrl+V Paste - if ((e.key === 'v' || e.key == 'V') && (e.metaKey || e.ctrlKey) && !e.shiftKey) { - // Trigger onPaste - return true; - } - - if((e.key === '+') && e.altKey) { - block_default = true; - let scale = this.ds.scale * 1.1; - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - } - - if((e.key === '-') && e.altKey) { - block_default = true; - let scale = this.ds.scale * 1 / 1.1; - this.ds.changeScale(scale, [this.ds.element.width/2, this.ds.element.height/2]); - this.graph.change(); - } - } - - this.graph.change(); - - if (block_default) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - - // Fall through to Litegraph defaults - return origProcessKey.apply(this, arguments); - }; - } - - /** - * Draws group header bar - */ - #addDrawGroupsHandler() { - const self = this; - - const origDrawGroups = LGraphCanvas.prototype.drawGroups; - LGraphCanvas.prototype.drawGroups = function(canvas, ctx) { - if (!this.graph) { - return; - } - - var groups = this.graph._groups; - - ctx.save(); - ctx.globalAlpha = 0.7 * this.editor_alpha; - - for (var i = 0; i < groups.length; ++i) { - var group = groups[i]; - - if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) { - continue; - } //out of the visible area - - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25 * this.editor_alpha; - ctx.beginPath(); - var font_size = - group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4); - ctx.fill(); - ctx.globalAlpha = this.editor_alpha; - } - - ctx.restore(); - - const res = origDrawGroups.apply(this, arguments); - return res; - } - } - - /** - * Draws node highlights (executing, drag drop) and progress bar - */ - #addDrawNodeHandler() { - const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape; - const self = this; - - LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { - const res = origDrawNodeShape.apply(this, arguments); - - const nodeErrors = self.lastNodeErrors?.[node.id]; - - let color = null; - let lineWidth = 1; - if (node.id === +self.runningNodeId) { - color = "#0f0"; - } else if (self.dragOverNode && node.id === self.dragOverNode.id) { - color = "dodgerblue"; - } - else if (nodeErrors?.errors) { - color = "red"; - lineWidth = 2; - } - else if (self.lastExecutionError && +self.lastExecutionError.node_id === node.id) { - color = "#f0f"; - lineWidth = 2; - } - - if (color) { - const shape = node._shape || node.constructor.shape || LiteGraph.ROUND_SHAPE; - ctx.lineWidth = lineWidth; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - if (shape == LiteGraph.BOX_SHAPE) - ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT); - else if (shape == LiteGraph.ROUND_SHAPE || (shape == LiteGraph.CARD_SHAPE && node.flags.collapsed)) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - this.round_radius * 2 - ); - else if (shape == LiteGraph.CARD_SHAPE) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - [this.round_radius * 2, this.round_radius * 2, 2, 2] - ); - else if (shape == LiteGraph.CIRCLE_SHAPE) - ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2); - ctx.strokeStyle = color; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - ctx.globalAlpha = 1; - } - - if (self.progress && node.id === +self.runningNodeId) { - ctx.fillStyle = "green"; - ctx.fillRect(0, 0, size[0] * (self.progress.value / self.progress.max), 6); - ctx.fillStyle = bgcolor; - } - - // Highlight inputs that failed validation - if (nodeErrors) { - ctx.lineWidth = 2; - ctx.strokeStyle = "red"; - for (const error of nodeErrors.errors) { - if (error.extra_info && error.extra_info.input_name) { - const inputIndex = node.findInputSlot(error.extra_info.input_name) - if (inputIndex !== -1) { - let pos = node.getConnectionPos(true, inputIndex); - ctx.beginPath(); - ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) - ctx.stroke(); - } - } - } - } - - return res; - }; - - const origDrawNode = LGraphCanvas.prototype.drawNode; - LGraphCanvas.prototype.drawNode = function (node, ctx) { - var editor_alpha = this.editor_alpha; - var old_color = node.bgcolor; - - if (node.mode === 2) { // never - this.editor_alpha = 0.4; - } - - if (node.mode === 4) { // never - node.bgcolor = "#FF00FF"; - this.editor_alpha = 0.2; - } - - const res = origDrawNode.apply(this, arguments); - - this.editor_alpha = editor_alpha; - node.bgcolor = old_color; - - return res; - }; - } - - /** - * Handles updates from the API socket - */ - #addApiUpdateHandlers() { - api.addEventListener("status", ({ detail }) => { - this.ui.setStatus(detail); - }); - - api.addEventListener("reconnecting", () => { - this.ui.dialog.show("Reconnecting..."); - }); - - api.addEventListener("reconnected", () => { - this.ui.dialog.close(); - }); - - api.addEventListener("progress", ({ detail }) => { - if (this.workflowManager.activePrompt?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - this.progress = detail; - this.graph.setDirtyCanvas(true, false); - }); - - api.addEventListener("executing", ({ detail }) => { - if (this.workflowManager.activePrompt ?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - this.progress = null; - this.runningNodeId = detail; - this.graph.setDirtyCanvas(true, false); - delete this.nodePreviewImages[this.runningNodeId] - }); - - api.addEventListener("executed", ({ detail }) => { - if (this.workflowManager.activePrompt ?.workflow - && this.workflowManager.activePrompt.workflow !== this.workflowManager.activeWorkflow) return; - const output = this.nodeOutputs[detail.display_node]; - if (detail.merge && output) { - for (const k in detail.output ?? {}) { - const v = output[k]; - if (v instanceof Array) { - output[k] = v.concat(detail.output[k]); - } else { - output[k] = detail.output[k]; - } - } - } else { - this.nodeOutputs[detail.display_node] = detail.output; - } - const node = this.graph.getNodeById(detail.display_node); - if (node) { - if (node.onExecuted) - node.onExecuted(detail.output); - } - }); - - api.addEventListener("execution_start", ({ detail }) => { - this.runningNodeId = null; - this.lastExecutionError = null - this.graph._nodes.forEach((node) => { - if (node.onExecutionStart) - node.onExecutionStart() - }) - }); - - api.addEventListener("execution_error", ({ detail }) => { - this.lastExecutionError = detail; - const formattedError = this.#formatExecutionError(detail); - this.ui.dialog.show(formattedError); - this.canvas.draw(true, true); - }); - - api.addEventListener("b_preview", ({ detail }) => { - const id = this.runningNodeId - if (id == null) - return; - - const blob = detail - const blobUrl = URL.createObjectURL(blob) - this.nodePreviewImages[id] = [blobUrl] - }); - - api.init(); - } - - #addKeyboardHandler() { - window.addEventListener("keydown", (e) => { - this.shiftDown = e.shiftKey; - }); - window.addEventListener("keyup", (e) => { - this.shiftDown = e.shiftKey; - }); - } - - #addConfigureHandler() { - const app = this; - const configure = LGraph.prototype.configure; - // Flag that the graph is configuring to prevent nodes from running checks while its still loading - LGraph.prototype.configure = function () { - app.configuringGraph = true; - try { - return configure.apply(this, arguments); - } finally { - app.configuringGraph = false; - } - }; - } - - #addAfterConfigureHandler() { - const app = this; - const onConfigure = app.graph.onConfigure; - app.graph.onConfigure = function () { - // Fire callbacks before the onConfigure, this is used by widget inputs to setup the config - for (const node of app.graph._nodes) { - node.onGraphConfigured?.(); - } - - const r = onConfigure?.apply(this, arguments); - - // Fire after onConfigure, used by primitves to generate widget using input nodes config - for (const node of app.graph._nodes) { - node.onAfterGraphConfigured?.(); - } - - return r; - }; - } - - /** - * Loads all extensions from the API into the window in parallel - */ - async #loadExtensions() { - const extensions = await api.getExtensions(); - this.logging.addEntry("Comfy.App", "debug", { Extensions: extensions }); - - const extensionPromises = extensions.map(async ext => { - try { - await import(api.apiURL(ext)); - } catch (error) { - console.error("Error loading extension", ext, error); - } - }); - - await Promise.all(extensionPromises); - try { - this.menu.workflows.registerExtension(this); - } catch (error) { - console.error(error); - } - } - - async #migrateSettings() { - this.isNewUserSession = true; - // Store all current settings - const settings = Object.keys(this.ui.settings).reduce((p, n) => { - const v = localStorage[`Comfy.Settings.${n}`]; - if (v) { - try { - p[n] = JSON.parse(v); - } catch (error) {} - } - return p; - }, {}); - - await api.storeSettings(settings); - } - - async #setUser() { - const userConfig = await api.getUserConfig(); - this.storageLocation = userConfig.storage; - if (typeof userConfig.migrated == "boolean") { - // Single user mode migrated true/false for if the default user is created - if (!userConfig.migrated && this.storageLocation === "server") { - // Default user not created yet - await this.#migrateSettings(); - } - return; - } - - this.multiUserServer = true; - let user = localStorage["Comfy.userId"]; - const users = userConfig.users ?? {}; - if (!user || !users[user]) { - // This will rarely be hit so move the loading to on demand - const { UserSelectionScreen } = await import("./ui/userSelection.js"); - - this.ui.menuContainer.style.display = "none"; - const { userId, username, created } = await new UserSelectionScreen().show(users, user); - this.ui.menuContainer.style.display = ""; - - user = userId; - localStorage["Comfy.userName"] = username; - localStorage["Comfy.userId"] = user; - - if (created) { - api.user = user; - await this.#migrateSettings(); - } - } - - api.user = user; - - this.ui.settings.addSetting({ - id: "Comfy.SwitchUser", - name: "Switch User", - type: (name) => { - let currentUser = localStorage["Comfy.userName"]; - if (currentUser) { - currentUser = ` (${currentUser})`; - } - return $el("tr", [ - $el("td", [ - $el("label", { - textContent: name, - }), - ]), - $el("td", [ - $el("button", { - textContent: name + (currentUser ?? ""), - onclick: () => { - delete localStorage["Comfy.userId"]; - delete localStorage["Comfy.userName"]; - window.location.reload(); - }, - }), - ]), - ]); - }, - }); - } - - /** - * Set up the app on the page - */ - async setup() { - await this.#setUser(); - - // Create and mount the LiteGraph in the DOM - const mainCanvas = document.createElement("canvas") - mainCanvas.style.touchAction = "none" - const canvasEl = (this.canvasEl = Object.assign(mainCanvas, { id: "graph-canvas" })); - canvasEl.tabIndex = "1"; - document.body.append(canvasEl); - this.resizeCanvas(); - - await Promise.all([this.workflowManager.loadWorkflows(), this.ui.settings.load()]); - await this.#loadExtensions(); - - addDomClippingSetting(); - this.#addProcessMouseHandler(); - this.#addProcessKeyHandler(); - this.#addConfigureHandler(); - this.#addApiUpdateHandlers(); - this.#addRestoreWorkflowView(); - - this.graph = new LGraph(); - - this.#addAfterConfigureHandler(); - - this.canvas = new LGraphCanvas(canvasEl, this.graph); - this.ctx = canvasEl.getContext("2d"); - - LiteGraph.release_link_on_empty_shows_menu = true; - LiteGraph.alt_drag_do_clone_nodes = true; - - this.graph.start(); - - // Ensure the canvas fills the window - this.resizeCanvas(); - window.addEventListener("resize", () => this.resizeCanvas()); - const ro = new ResizeObserver(() => this.resizeCanvas()); - ro.observe(this.bodyTop); - ro.observe(this.bodyLeft); - ro.observe(this.bodyRight); - ro.observe(this.bodyBottom); - - await this.#invokeExtensionsAsync("init"); - await this.registerNodes(); - initWidgets(this); - - // Load previous workflow - let restored = false; - try { - const loadWorkflow = async (json) => { - if (json) { - const workflow = JSON.parse(json); - const workflowName = getStorageValue("Comfy.PreviousWorkflow"); - await this.loadGraphData(workflow, true, true, workflowName); - return true; - } - }; - const clientId = api.initialClientId ?? api.clientId; - restored = - (clientId && (await loadWorkflow(sessionStorage.getItem(`workflow:${clientId}`)))) || - (await loadWorkflow(localStorage.getItem("workflow"))); - } catch (err) { - console.error("Error loading previous workflow", err); - } - - // We failed to restore a workflow so load the default - if (!restored) { - await this.loadGraphData(); - } - - // Save current workflow automatically - setInterval(() => { - const workflow = JSON.stringify(this.graph.serialize()); - localStorage.setItem("workflow", workflow); - if (api.clientId) { - sessionStorage.setItem(`workflow:${api.clientId}`, workflow); - } - }, 1000); - - this.#addDrawNodeHandler(); - this.#addDrawGroupsHandler(); - this.#addDropHandler(); - this.#addCopyHandler(); - this.#addPasteHandler(); - this.#addKeyboardHandler(); - - await this.#invokeExtensionsAsync("setup"); - } - - resizeCanvas() { - // Limit minimal scale to 1, see https://github.com/comfyanonymous/ComfyUI/pull/845 - const scale = Math.max(window.devicePixelRatio, 1); - - // Clear fixed width and height while calculating rect so it uses 100% instead - this.canvasEl.height = this.canvasEl.width = ""; - const { width, height } = this.canvasEl.getBoundingClientRect(); - this.canvasEl.width = Math.round(width * scale); - this.canvasEl.height = Math.round(height * scale); - this.canvasEl.getContext("2d").scale(scale, scale); - this.canvas?.draw(true, true); - } - - /** - * Registers nodes with the graph - */ - async registerNodes() { - const app = this; - // Load node definitions from the backend - const defs = await api.getNodeDefs(); - await this.registerNodesFromDefs(defs); - await this.#invokeExtensionsAsync("registerCustomNodes"); - } - - getWidgetType(inputData, inputName) { - const type = inputData[0]; - - if (Array.isArray(type)) { - return "COMBO"; - } else if (`${type}:${inputName}` in this.widgets) { - return `${type}:${inputName}`; - } else if (type in this.widgets) { - return type; - } else { - return null; - } - } - - async registerNodeDef(nodeId, nodeData) { - const self = this; - const node = Object.assign( - function ComfyNode() { - var inputs = nodeData["input"]["required"]; - if (nodeData["input"]["optional"] != undefined) { - inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]); - } - const config = { minWidth: 1, minHeight: 1 }; - for (const inputName in inputs) { - const inputData = inputs[inputName]; - const type = inputData[0]; - - let widgetCreated = true; - const widgetType = self.getWidgetType(inputData, inputName); - if(widgetType) { - if(widgetType === "COMBO") { - Object.assign(config, self.widgets.COMBO(this, inputName, inputData, app) || {}); - } else { - Object.assign(config, self.widgets[widgetType](this, inputName, inputData, app) || {}); - } - } else { - // Node connection inputs - this.addInput(inputName, type); - widgetCreated = false; - } - - if(widgetCreated && inputData[1]?.forceInput && config?.widget) { - if (!config.widget.options) config.widget.options = {}; - config.widget.options.forceInput = inputData[1].forceInput; - } - if(widgetCreated && inputData[1]?.defaultInput && config?.widget) { - if (!config.widget.options) config.widget.options = {}; - config.widget.options.defaultInput = inputData[1].defaultInput; - } - } - - for (const o in nodeData["output"]) { - let output = nodeData["output"][o]; - if(output instanceof Array) output = "COMBO"; - const outputTooltip = nodeData["output_tooltips"]?.[o]; - const outputName = nodeData["output_name"][o] || output; - const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ; - this.addOutput(outputName, output, { shape: outputShape, tooltip: outputTooltip }); - } - - const s = this.computeSize(); - s[0] = Math.max(config.minWidth, s[0] * 1.5); - s[1] = Math.max(config.minHeight, s[1]); - this.size = s; - this.serialize_widgets = true; - - app.#invokeExtensionsAsync("nodeCreated", this); - }, - { - title: nodeData.display_name || nodeData.name, - comfyClass: nodeData.name, - nodeData - } - ); - node.prototype.comfyClass = nodeData.name; - - this.#addNodeContextMenuHandler(node); - this.#addDrawBackgroundHandler(node, app); - this.#addNodeKeyHandler(node); - - await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData); - LiteGraph.registerNodeType(nodeId, node); - node.category = nodeData.category; - } - - async registerNodesFromDefs(defs) { - await this.#invokeExtensionsAsync("addCustomNodeDefs", defs); - - // Generate list of known widgets - this.widgets = Object.assign( - {}, - ComfyWidgets, - ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean) - ); - - // Register a node for each definition - for (const nodeId in defs) { - this.registerNodeDef(nodeId, defs[nodeId]); - } - } - - loadTemplateData(templateData) { - if (!templateData?.templates) { - return; - } - - const old = localStorage.getItem("litegrapheditor_clipboard"); - - var maxY, nodeBottom, node; - - for (const template of templateData.templates) { - if (!template?.data) { - continue; - } - - localStorage.setItem("litegrapheditor_clipboard", template.data); - app.canvas.pasteFromClipboard(); - - // Move mouse position down to paste the next template below - - maxY = false; - - for (const i in app.canvas.selected_nodes) { - node = app.canvas.selected_nodes[i]; - - nodeBottom = node.pos[1] + node.size[1]; - - if (maxY === false || nodeBottom > maxY) { - maxY = nodeBottom; - } - } - - app.canvas.graph_mouse[1] = maxY + 50; - } - - localStorage.setItem("litegrapheditor_clipboard", old); - } - - showMissingNodesError(missingNodeTypes, hasAddedNodes = true) { - let seenTypes = new Set(); - - this.ui.dialog.show( - $el("div.comfy-missing-nodes", [ - $el("span", { textContent: "When loading the graph, the following node types were not found: " }), - $el( - "ul", - Array.from(new Set(missingNodeTypes)).map((t) => { - let children = []; - if (typeof t === "object") { - if(seenTypes.has(t.type)) return null; - seenTypes.add(t.type); - children.push($el("span", { textContent: t.type })); - if (t.hint) { - children.push($el("span", { textContent: t.hint })); - } - if (t.action) { - children.push($el("button", { onclick: t.action.callback, textContent: t.action.text })); - } - } else { - if(seenTypes.has(t)) return null; - seenTypes.add(t); - children.push($el("span", { textContent: t })); - } - return $el("li", children); - }).filter(Boolean) - ), - ...(hasAddedNodes - ? [$el("span", { textContent: "Nodes that have failed to load will show as red on the graph." })] - : []), - ]) - ); - this.logging.addEntry("Comfy.App", "warn", { - MissingNodes: missingNodeTypes, - }); - } - - async changeWorkflow(callback, workflow = null) { - try { - this.workflowManager.activeWorkflow?.changeTracker?.store() - } catch (error) { - console.error(error); - } - await callback(); - try { - this.workflowManager.setWorkflow(workflow); - this.workflowManager.activeWorkflow?.track() - } catch (error) { - console.error(error); - } - } - - /** - * Populates the graph with the specified workflow data - * @param {*} graphData A serialized graph object - * @param { boolean } clean If the graph state, e.g. images, should be cleared - * @param { boolean } restore_view If the graph position should be restored - * @param { import("./workflows.js").ComfyWorkflowInstance | null } workflow The workflow - */ - async loadGraphData(graphData, clean = true, restore_view = true, workflow = null) { - if (clean !== false) { - this.clean(); - } - - let reset_invalid_values = false; - if (!graphData) { - graphData = defaultGraph; - reset_invalid_values = true; - } - - if (typeof structuredClone === "undefined") - { - graphData = JSON.parse(JSON.stringify(graphData)); - }else - { - graphData = structuredClone(graphData); - } - - try { - this.workflowManager.setWorkflow(workflow); - } catch (error) { - console.error(error); - } - - const missingNodeTypes = []; - await this.#invokeExtensionsAsync("beforeConfigureGraph", graphData, missingNodeTypes); - for (let n of graphData.nodes) { - // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now - if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; - if (n.type == "ConditioningAverage ") n.type = "ConditioningAverage"; //typo fix - if (n.type == "SDV_img2vid_Conditioning") n.type = "SVD_img2vid_Conditioning"; //typo fix - - // Find missing node types - if (!(n.type in LiteGraph.registered_node_types)) { - missingNodeTypes.push(n.type); - n.type = sanitizeNodeName(n.type); - } - } - - try { - this.graph.configure(graphData); - if (restore_view && this.enableWorkflowViewRestore.value && graphData.extra?.ds) { - this.canvas.ds.offset = graphData.extra.ds.offset; - this.canvas.ds.scale = graphData.extra.ds.scale; - } - - try { - this.workflowManager.activeWorkflow?.track() - } catch (error) { - } - } catch (error) { - let errorHint = []; - // Try extracting filename to see if it was caused by an extension script - const filename = error.fileName || (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1]; - const pos = (filename || "").indexOf("/extensions/"); - if (pos > -1) { - errorHint.push( - $el("span", { textContent: "This may be due to the following script:" }), - $el("br"), - $el("span", { - style: { - fontWeight: "bold", - }, - textContent: filename.substring(pos), - }) - ); - } - - // Show dialog to let the user know something went wrong loading the data - this.ui.dialog.show( - $el("div", [ - $el("p", { textContent: "Loading aborted due to error reloading workflow data" }), - $el("pre", { - style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" }, - textContent: error.toString(), - }), - $el("pre", { - style: { - padding: "5px", - color: "#ccc", - fontSize: "10px", - maxHeight: "50vh", - overflow: "auto", - backgroundColor: "rgba(0,0,0,0.2)", - }, - textContent: error.stack || "No stacktrace available", - }), - ...errorHint, - ]).outerHTML - ); - - return; - } - - for (const node of this.graph._nodes) { - const size = node.computeSize(); - size[0] = Math.max(node.size[0], size[0]); - size[1] = Math.max(node.size[1], size[1]); - node.size = size; - - if (node.widgets) { - // If you break something in the backend and want to patch workflows in the frontend - // This is the place to do this - for (let widget of node.widgets) { - if (node.type == "KSampler" || node.type == "KSamplerAdvanced") { - if (widget.name == "sampler_name") { - if (widget.value.startsWith("sample_")) { - widget.value = widget.value.slice(7); - } - if (widget.value === "euler_pp" || widget.value === "euler_ancestral_pp") { - widget.value = widget.value.slice(0, -3); - for (let w of node.widgets) { - if (w.name == "cfg") { - w.value *= 2.0; - } - } - } - } - } - if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") { - if (widget.name == "control_after_generate") { - if (widget.value === true) { - widget.value = "randomize"; - } else if (widget.value === false) { - widget.value = "fixed"; - } - } - } - if (reset_invalid_values) { - if (widget.type == "combo") { - if (!widget.options.values.includes(widget.value) && widget.options.values.length > 0) { - widget.value = widget.options.values[0]; - } - } - } - } - } - - this.#invokeExtensions("loadedGraphNode", node); - } - - if (missingNodeTypes.length) { - this.showMissingNodesError(missingNodeTypes); - } - await this.#invokeExtensionsAsync("afterConfigureGraph", missingNodeTypes); - requestAnimationFrame(() => { - this.graph.setDirtyCanvas(true, true); - }); - } - - /** - * Converts the current graph workflow for sending to the API - * @returns The workflow and node links - */ - async graphToPrompt(graph = this.graph, clean = true) { - for (const outerNode of graph.computeExecutionOrder(false)) { - if (outerNode.widgets) { - for (const widget of outerNode.widgets) { - // Allow widgets to run callbacks before a prompt has been queued - // e.g. random seed before every gen - widget.beforeQueued?.(); - } - } - - const innerNodes = outerNode.getInnerNodes ? outerNode.getInnerNodes() : [outerNode]; - for (const node of innerNodes) { - if (node.isVirtualNode) { - // Don't serialize frontend only nodes but let them make changes - if (node.applyToGraph) { - node.applyToGraph(); - } - } - } - } - - const workflow = graph.serialize(); - const output = {}; - // Process nodes in order of execution - for (const outerNode of graph.computeExecutionOrder(false)) { - const skipNode = outerNode.mode === 2 || outerNode.mode === 4; - const innerNodes = (!skipNode && outerNode.getInnerNodes) ? outerNode.getInnerNodes() : [outerNode]; - for (const node of innerNodes) { - if (node.isVirtualNode) { - continue; - } - - if (node.mode === 2 || node.mode === 4) { - // Don't serialize muted nodes - continue; - } - - const inputs = {}; - const widgets = node.widgets; - - // Store all widget values - if (widgets) { - for (const i in widgets) { - const widget = widgets[i]; - if (!widget.options || widget.options.serialize !== false) { - inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(node, i) : widget.value; - } - } - } - - // Store all node links - for (let i in node.inputs) { - let parent = node.getInputNode(i); - if (parent) { - let link = node.getInputLink(i); - while (parent.mode === 4 || parent.isVirtualNode) { - let found = false; - if (parent.isVirtualNode) { - link = parent.getInputLink(link.origin_slot); - if (link) { - parent = parent.getInputNode(link.target_slot); - if (parent) { - found = true; - } - } - } else if (link && parent.mode === 4) { - let all_inputs = [link.origin_slot]; - if (parent.inputs) { - all_inputs = all_inputs.concat(Object.keys(parent.inputs)) - for (let parent_input in all_inputs) { - parent_input = all_inputs[parent_input]; - if (parent.inputs[parent_input]?.type === node.inputs[i].type) { - link = parent.getInputLink(parent_input); - if (link) { - parent = parent.getInputNode(parent_input); - } - found = true; - break; - } - } - } - } - - if (!found) { - break; - } - } - - if (link) { - if (parent?.updateLink) { - link = parent.updateLink(link); - } - if (link) { - inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)]; - } - } - } - } - - let node_data = { - inputs, - class_type: node.comfyClass, - }; - - if (this.ui.settings.getSettingValue("Comfy.DevMode")) { - // Ignored by the backend. - node_data["_meta"] = { - title: node.title, - } - } - - output[String(node.id)] = node_data; - } - } - - // Remove inputs connected to removed nodes - if(clean) { - for (const o in output) { - for (const i in output[o].inputs) { - if (Array.isArray(output[o].inputs[i]) - && output[o].inputs[i].length === 2 - && !output[output[o].inputs[i][0]]) { - delete output[o].inputs[i]; - } - } - } - } - - return { workflow, output }; - } - - #formatPromptError(error) { - if (error == null) { - return "(unknown error)" - } - else if (typeof error === "string") { - return error; - } - else if (error.stack && error.message) { - return error.toString() - } - else if (error.response) { - let message = error.response.error.message; - if (error.response.error.details) - message += ": " + error.response.error.details; - for (const [nodeID, nodeError] of Object.entries(error.response.node_errors)) { - message += "\n" + nodeError.class_type + ":" - for (const errorReason of nodeError.errors) { - message += "\n - " + errorReason.message + ": " + errorReason.details - } - } - return message - } - return "(unknown error)" - } - - #formatExecutionError(error) { - if (error == null) { - return "(unknown error)" - } - - const traceback = error.traceback.join("") - const nodeId = error.node_id - const nodeType = error.node_type - - return `Error occurred when executing ${nodeType}:\n\n${error.exception_message}\n\n${traceback}` - } - - async queuePrompt(number, batchCount = 1) { - this.#queueItems.push({ number, batchCount }); - - // Only have one action process the items so each one gets a unique seed correctly - if (this.#processingQueue) { - return; - } - - this.#processingQueue = true; - this.lastNodeErrors = null; - - try { - while (this.#queueItems.length) { - ({ number, batchCount } = this.#queueItems.pop()); - - for (let i = 0; i < batchCount; i++) { - const p = await this.graphToPrompt(); - - try { - const res = await api.queuePrompt(number, p); - this.lastNodeErrors = res.node_errors; - if (this.lastNodeErrors.length > 0) { - this.canvas.draw(true, true); - } else { - try { - this.workflowManager.storePrompt({ - id: res.prompt_id, - nodes: Object.keys(p.output) - }); - } catch (error) { - } - } - } catch (error) { - const formattedError = this.#formatPromptError(error) - this.ui.dialog.show(formattedError); - if (error.response) { - this.lastNodeErrors = error.response.node_errors; - this.canvas.draw(true, true); - } - break; - } - - for (const n of p.workflow.nodes) { - const node = graph.getNodeById(n.id); - if (node.widgets) { - for (const widget of node.widgets) { - // Allow widgets to run callbacks after a prompt has been queued - // e.g. random seed after every gen - if (widget.afterQueued) { - widget.afterQueued(); - } - } - } - } - - this.canvas.draw(true, true); - await this.ui.queue.update(); - } - } - } finally { - this.#processingQueue = false; - } - api.dispatchEvent(new CustomEvent("promptQueued", { detail: { number, batchCount } })); - return !this.lastNodeErrors; - } - - showErrorOnFileLoad(file) { - this.ui.dialog.show( - $el("div", [ - $el("p", {textContent: `Unable to find workflow in ${file.name}`}) - ]).outerHTML - ); - } - - /** - * Loads workflow data from the specified file - * @param {File} file - */ - async handleFile(file) { - const removeExt = f => { - if(!f) return f; - const p = f.lastIndexOf("."); - if(p === -1) return f; - return f.substring(0, p); - }; - - const fileName = removeExt(file.name); - if (file.type === "image/png") { - const pngInfo = await getPngMetadata(file); - if (pngInfo?.workflow) { - await this.loadGraphData(JSON.parse(pngInfo.workflow), true, true, fileName); - } else if (pngInfo?.prompt) { - this.loadApiJson(JSON.parse(pngInfo.prompt), fileName); - } else if (pngInfo?.parameters) { - this.changeWorkflow(() => { - importA1111(this.graph, pngInfo.parameters); - }, fileName) - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "image/webp") { - const pngInfo = await getWebpMetadata(file); - // Support loading workflows from that webp custom node. - const workflow = pngInfo?.workflow || pngInfo?.Workflow; - const prompt = pngInfo?.prompt || pngInfo?.Prompt; - - if (workflow) { - this.loadGraphData(JSON.parse(workflow), true, true, fileName); - } else if (prompt) { - this.loadApiJson(JSON.parse(prompt), fileName); - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "audio/flac" || file.type === "audio/x-flac") { - const pngInfo = await getFlacMetadata(file); - // Support loading workflows from that webp custom node. - const workflow = pngInfo?.workflow; - const prompt = pngInfo?.prompt; - - if (workflow) { - this.loadGraphData(JSON.parse(workflow), true, true, fileName); - } else if (prompt) { - this.loadApiJson(JSON.parse(prompt), fileName); - } else { - this.showErrorOnFileLoad(file); - } - } else if (file.type === "application/json" || file.name?.endsWith(".json")) { - const reader = new FileReader(); - reader.onload = async () => { - const jsonContent = JSON.parse(reader.result); - if (jsonContent?.templates) { - this.loadTemplateData(jsonContent); - } else if(this.isApiJson(jsonContent)) { - this.loadApiJson(jsonContent, fileName); - } else { - await this.loadGraphData(jsonContent, true, true, fileName); - } - }; - reader.readAsText(file); - } else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) { - const info = await getLatentMetadata(file); - if (info.workflow) { - await this.loadGraphData(JSON.parse(info.workflow), true, true, fileName); - } else if (info.prompt) { - this.loadApiJson(JSON.parse(info.prompt)); - } else { - this.showErrorOnFileLoad(file); - } - } else { - this.showErrorOnFileLoad(file); - } - } - - isApiJson(data) { - return Object.values(data).every((v) => v.class_type); - } - - loadApiJson(apiData, fileName) { - const missingNodeTypes = Object.values(apiData).filter((n) => !LiteGraph.registered_node_types[n.class_type]); - if (missingNodeTypes.length) { - this.showMissingNodesError(missingNodeTypes.map(t => t.class_type), false); - return; - } - - const ids = Object.keys(apiData); - app.graph.clear(); - for (const id of ids) { - const data = apiData[id]; - const node = LiteGraph.createNode(data.class_type); - node.id = isNaN(+id) ? id : +id; - node.title = data._meta?.title ?? node.title - app.graph.add(node); - } - - this.changeWorkflow(() => { - for (const id of ids) { - const data = apiData[id]; - const node = app.graph.getNodeById(id); - for (const input in data.inputs ?? {}) { - const value = data.inputs[input]; - if (value instanceof Array) { - const [fromId, fromSlot] = value; - const fromNode = app.graph.getNodeById(fromId); - let toSlot = node.inputs?.findIndex((inp) => inp.name === input); - if (toSlot == null || toSlot === -1) { - try { - // Target has no matching input, most likely a converted widget - const widget = node.widgets?.find((w) => w.name === input); - if (widget && node.convertWidgetToInput?.(widget)) { - toSlot = node.inputs?.length - 1; - } - } catch (error) {} - } - if (toSlot != null || toSlot !== -1) { - fromNode.connect(fromSlot, node, toSlot); - } - } else { - const widget = node.widgets?.find((w) => w.name === input); - if (widget) { - widget.value = value; - widget.callback?.(value); - } - } - } - } - app.graph.arrange(); - }, fileName); - } - - /** - * Registers a Comfy web extension with the app - * @param {ComfyExtension} extension - */ - registerExtension(extension) { - if (!extension.name) { - throw new Error("Extensions must have a 'name' property."); - } - if (this.extensions.find((ext) => ext.name === extension.name)) { - throw new Error(`Extension named '${extension.name}' already registered.`); - } - this.extensions.push(extension); - } - - /** - * Refresh combo list on whole nodes - */ - async refreshComboInNodes() { - const defs = await api.getNodeDefs(); - - for (const nodeId in defs) { - this.registerNodeDef(nodeId, defs[nodeId]); - } - - for(let nodeNum in this.graph._nodes) { - const node = this.graph._nodes[nodeNum]; - const def = defs[node.type]; - - // Allow primitive nodes to handle refresh - node.refreshComboInNode?.(defs); - - if(!def) - continue; - - for(const widgetNum in node.widgets) { - const widget = node.widgets[widgetNum] - if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) { - widget.options.values = def["input"]["required"][widget.name][0]; - - if(widget.name != 'image' && !widget.options.values.includes(widget.value)) { - widget.value = widget.options.values[0]; - widget.callback(widget.value); - } - } - } - } - - await this.#invokeExtensionsAsync("refreshComboInNodes", defs); - } - - resetView() { - app.canvas.ds.scale = 1; - app.canvas.ds.offset = [0, 0] - app.graph.setDirtyCanvas(true, true); - } - - /** - * Clean current state - */ - clean() { - this.nodeOutputs = {}; - this.nodePreviewImages = {} - this.lastNodeErrors = null; - this.lastExecutionError = null; - this.runningNodeId = null; - } -} - -export const app = new ComfyApp(); diff --git a/web/scripts/changeTracker.js b/web/scripts/changeTracker.js deleted file mode 100644 index d7798dbe9..000000000 --- a/web/scripts/changeTracker.js +++ /dev/null @@ -1,255 +0,0 @@ -// @ts-check - -import { api } from "./api.js"; -import { clone } from "./utils.js"; - -export class ChangeTracker { - static MAX_HISTORY = 50; - #app; - undo = []; - redo = []; - activeState = null; - isOurLoad = false; - /** @type { import("./workflows").ComfyWorkflow | null } */ - workflow; - - ds; - nodeOutputs; - - get app() { - return this.#app ?? this.workflow.manager.app; - } - - constructor(workflow) { - this.workflow = workflow; - } - - #setApp(app) { - this.#app = app; - } - - store() { - this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] }; - } - - restore() { - if (this.ds) { - this.app.canvas.ds.scale = this.ds.scale; - this.app.canvas.ds.offset = this.ds.offset; - } - if (this.nodeOutputs) { - this.app.nodeOutputs = this.nodeOutputs; - } - } - - checkState() { - if (!this.app.graph) return; - - const currentState = this.app.graph.serialize(); - if (!this.activeState) { - this.activeState = clone(currentState); - return; - } - if (!ChangeTracker.graphEqual(this.activeState, currentState)) { - this.undo.push(this.activeState); - if (this.undo.length > ChangeTracker.MAX_HISTORY) { - this.undo.shift(); - } - this.activeState = clone(currentState); - this.redo.length = 0; - this.workflow.unsaved = true; - api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState })); - } - } - - async updateState(source, target) { - const prevState = source.pop(); - if (prevState) { - target.push(this.activeState); - this.isOurLoad = true; - await this.app.loadGraphData(prevState, false, false, this.workflow); - this.activeState = prevState; - } - } - - async undoRedo(e) { - if (e.ctrlKey || e.metaKey) { - if (e.key === "y") { - this.updateState(this.redo, this.undo); - return true; - } else if (e.key === "z") { - this.updateState(this.undo, this.redo); - return true; - } - } - } - - /** @param { import("./app.js").ComfyApp } app */ - static init(app) { - const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker; - globalTracker.#setApp(app); - - const loadGraphData = app.loadGraphData; - app.loadGraphData = async function () { - const v = await loadGraphData.apply(this, arguments); - const ct = changeTracker(); - if (ct.isOurLoad) { - ct.isOurLoad = false; - } else { - ct.checkState(); - } - return v; - }; - - let keyIgnored = false; - window.addEventListener( - "keydown", - (e) => { - const activeEl = document.activeElement; - requestAnimationFrame(async () => { - let bindInputEl; - // If we are auto queue in change mode then we do want to trigger on inputs - if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { - if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") { - // Ignore events on inputs, they have their native history - return; - } - bindInputEl = activeEl; - } - - keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta"; - if (keyIgnored) return; - - // Check if this is a ctrl+z ctrl+y - if (await changeTracker().undoRedo(e)) return; - - // If our active element is some type of input then handle changes after they're done - if (ChangeTracker.bindInput(bindInputEl)) return; - changeTracker().checkState(); - }); - }, - true - ); - - window.addEventListener("keyup", (e) => { - if (keyIgnored) { - keyIgnored = false; - changeTracker().checkState(); - } - }); - - // Handle clicking DOM elements (e.g. widgets) - window.addEventListener("mouseup", () => { - changeTracker().checkState(); - }); - - // Handle prompt queue event for dynamic widget changes - api.addEventListener("promptQueued", () => { - changeTracker().checkState(); - }); - - // Handle litegraph clicks - const processMouseUp = LGraphCanvas.prototype.processMouseUp; - LGraphCanvas.prototype.processMouseUp = function (e) { - const v = processMouseUp.apply(this, arguments); - changeTracker().checkState(); - return v; - }; - const processMouseDown = LGraphCanvas.prototype.processMouseDown; - LGraphCanvas.prototype.processMouseDown = function (e) { - const v = processMouseDown.apply(this, arguments); - changeTracker().checkState(); - return v; - }; - - // Handle litegraph context menu for COMBO widgets - const close = LiteGraph.ContextMenu.prototype.close; - LiteGraph.ContextMenu.prototype.close = function (e) { - const v = close.apply(this, arguments); - changeTracker().checkState(); - return v; - }; - - // Detects nodes being added via the node search dialog - const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded; - LiteGraph.LGraph.prototype.onNodeAdded = function () { - const v = onNodeAdded?.apply(this, arguments); - if (!app?.configuringGraph) { - const ct = changeTracker(); - if (!ct.isOurLoad) { - ct.checkState(); - } - } - return v; - }; - - // Store node outputs - api.addEventListener("executed", ({ detail }) => { - const prompt = app.workflowManager.queuedPrompts[detail.prompt_id]; - if (!prompt?.workflow) return; - const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {}); - const output = nodeOutputs[detail.node]; - if (detail.merge && output) { - for (const k in detail.output ?? {}) { - const v = output[k]; - if (v instanceof Array) { - output[k] = v.concat(detail.output[k]); - } else { - output[k] = detail.output[k]; - } - } - } else { - nodeOutputs[detail.node] = detail.output; - } - }); - } - - static bindInput(app, activeEl) { - if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") { - for (const evt of ["change", "input", "blur"]) { - if (`on${evt}` in activeEl) { - const listener = () => { - app.workflowManager.activeWorkflow.changeTracker.checkState(); - activeEl.removeEventListener(evt, listener); - }; - activeEl.addEventListener(evt, listener); - return true; - } - } - } - } - - static graphEqual(a, b, path = "") { - if (a === b) return true; - - if (typeof a == "object" && a && typeof b == "object" && b) { - const keys = Object.getOwnPropertyNames(a); - - if (keys.length != Object.getOwnPropertyNames(b).length) { - return false; - } - - for (const key of keys) { - let av = a[key]; - let bv = b[key]; - if (!path && key === "nodes") { - // Nodes need to be sorted as the order changes when selecting nodes - av = [...av].sort((a, b) => a.id - b.id); - bv = [...bv].sort((a, b) => a.id - b.id); - } else if (path === "extra.ds") { - // Ignore view changes - continue; - } - if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) { - return false; - } - } - - return true; - } - - return false; - } -} - -const globalTracker = new ChangeTracker({}); \ No newline at end of file diff --git a/web/scripts/defaultGraph.js b/web/scripts/defaultGraph.js deleted file mode 100644 index 9b3cb4a7e..000000000 --- a/web/scripts/defaultGraph.js +++ /dev/null @@ -1,119 +0,0 @@ -export const defaultGraph = { - last_node_id: 9, - last_link_id: 9, - nodes: [ - { - id: 7, - type: "CLIPTextEncode", - pos: [413, 389], - size: { 0: 425.27801513671875, 1: 180.6060791015625 }, - flags: {}, - order: 3, - mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: 5 }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }], - properties: {}, - widgets_values: ["text, watermark"], - }, - { - id: 6, - type: "CLIPTextEncode", - pos: [415, 186], - size: { 0: 422.84503173828125, 1: 164.31304931640625 }, - flags: {}, - order: 2, - mode: 0, - inputs: [{ name: "clip", type: "CLIP", link: 3 }], - outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }], - properties: {}, - widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"], - }, - { - id: 5, - type: "EmptyLatentImage", - pos: [473, 609], - size: { 0: 315, 1: 106 }, - flags: {}, - order: 1, - mode: 0, - outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }], - properties: {}, - widgets_values: [512, 512, 1], - }, - { - id: 3, - type: "KSampler", - pos: [863, 186], - size: { 0: 315, 1: 262 }, - flags: {}, - order: 4, - mode: 0, - inputs: [ - { name: "model", type: "MODEL", link: 1 }, - { name: "positive", type: "CONDITIONING", link: 4 }, - { name: "negative", type: "CONDITIONING", link: 6 }, - { name: "latent_image", type: "LATENT", link: 2 }, - ], - outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }], - properties: {}, - widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1], - }, - { - id: 8, - type: "VAEDecode", - pos: [1209, 188], - size: { 0: 210, 1: 46 }, - flags: {}, - order: 5, - mode: 0, - inputs: [ - { name: "samples", type: "LATENT", link: 7 }, - { name: "vae", type: "VAE", link: 8 }, - ], - outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }], - properties: {}, - }, - { - id: 9, - type: "SaveImage", - pos: [1451, 189], - size: { 0: 210, 1: 26 }, - flags: {}, - order: 6, - mode: 0, - inputs: [{ name: "images", type: "IMAGE", link: 9 }], - properties: {}, - }, - { - id: 4, - type: "CheckpointLoaderSimple", - pos: [26, 474], - size: { 0: 315, 1: 98 }, - flags: {}, - order: 0, - mode: 0, - outputs: [ - { name: "MODEL", type: "MODEL", links: [1], slot_index: 0 }, - { name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 }, - { name: "VAE", type: "VAE", links: [8], slot_index: 2 }, - ], - properties: {}, - widgets_values: ["v1-5-pruned-emaonly.ckpt"], - }, - ], - links: [ - [1, 4, 0, 3, 0, "MODEL"], - [2, 5, 0, 3, 3, "LATENT"], - [3, 4, 1, 6, 0, "CLIP"], - [4, 6, 0, 3, 1, "CONDITIONING"], - [5, 4, 1, 7, 0, "CLIP"], - [6, 7, 0, 3, 2, "CONDITIONING"], - [7, 3, 0, 8, 0, "LATENT"], - [8, 4, 2, 8, 1, "VAE"], - [9, 8, 0, 9, 0, "IMAGE"], - ], - groups: [], - config: {}, - extra: {}, - version: 0.4, -}; diff --git a/web/scripts/domWidget.js b/web/scripts/domWidget.js deleted file mode 100644 index 03ed2ff10..000000000 --- a/web/scripts/domWidget.js +++ /dev/null @@ -1,335 +0,0 @@ -import { app, ANIM_PREVIEW_WIDGET } from "./app.js"; - -const SIZE = Symbol(); - -function intersect(a, b) { - const x = Math.max(a.x, b.x); - const num1 = Math.min(a.x + a.width, b.x + b.width); - const y = Math.max(a.y, b.y); - const num2 = Math.min(a.y + a.height, b.y + b.height); - if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]; - else return null; -} - -function getClipPath(node, element) { - const selectedNode = Object.values(app.canvas.selected_nodes)[0]; - if (selectedNode && selectedNode !== node) { - const elRect = element.getBoundingClientRect(); - const MARGIN = 7; - const scale = app.canvas.ds.scale; - - const bounding = selectedNode.getBounding(); - const intersection = intersect( - { x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale }, - { - x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN, - y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN, - width: bounding[2] + MARGIN + MARGIN, - height: bounding[3] + MARGIN + MARGIN, - } - ); - - if (!intersection) { - return ""; - } - - const widgetRect = element.getBoundingClientRect(); - const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px"; - const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px"; - const clipWidth = intersection[2] + "px"; - const clipHeight = intersection[3] + "px"; - const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`; - return path; - } - return ""; -} - -function computeSize(size) { - if (this.widgets?.[0]?.last_y == null) return; - - let y = this.widgets[0].last_y; - let freeSpace = size[1] - y; - - let widgetHeight = 0; - let dom = []; - for (const w of this.widgets) { - if (w.type === "converted-widget") { - // Ignore - delete w.computedHeight; - } else if (w.computeSize) { - widgetHeight += w.computeSize()[1] + 4; - } else if (w.element) { - // Extract DOM widget size info - const styles = getComputedStyle(w.element); - let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height")); - let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height")); - - let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height"); - if (prefHeight.endsWith?.("%")) { - prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100); - } else { - prefHeight = parseInt(prefHeight); - if (isNaN(minHeight)) { - minHeight = prefHeight; - } - } - if (isNaN(minHeight)) { - minHeight = 50; - } - if (!isNaN(maxHeight)) { - if (!isNaN(prefHeight)) { - prefHeight = Math.min(prefHeight, maxHeight); - } else { - prefHeight = maxHeight; - } - } - dom.push({ - minHeight, - prefHeight, - w, - }); - } else { - widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } - - freeSpace -= widgetHeight; - - // Calculate sizes with all widgets at their min height - const prefGrow = []; // Nodes that want to grow to their prefd size - const canGrow = []; // Nodes that can grow to auto size - let growBy = 0; - for (const d of dom) { - freeSpace -= d.minHeight; - if (isNaN(d.prefHeight)) { - canGrow.push(d); - d.w.computedHeight = d.minHeight; - } else { - const diff = d.prefHeight - d.minHeight; - if (diff > 0) { - prefGrow.push(d); - growBy += diff; - d.diff = diff; - } else { - d.w.computedHeight = d.minHeight; - } - } - } - - if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) { - // Allocate space for image - freeSpace -= 220; - } - - this.freeWidgetSpace = freeSpace; - - if (freeSpace < 0) { - // Not enough space for all widgets so we need to grow - size[1] -= freeSpace; - this.graph.setDirtyCanvas(true); - } else { - // Share the space between each - const growDiff = freeSpace - growBy; - if (growDiff > 0) { - // All pref sizes can be fulfilled - freeSpace = growDiff; - for (const d of prefGrow) { - d.w.computedHeight = d.prefHeight; - } - } else { - // We need to grow evenly - const shared = -growDiff / prefGrow.length; - for (const d of prefGrow) { - d.w.computedHeight = d.prefHeight - shared; - } - freeSpace = 0; - } - - if (freeSpace > 0 && canGrow.length) { - // Grow any that are auto height - const shared = freeSpace / canGrow.length; - for (const d of canGrow) { - d.w.computedHeight += shared; - } - } - } - - // Position each of the widgets - for (const w of this.widgets) { - w.y = y; - if (w.computedHeight) { - y += w.computedHeight; - } else if (w.computeSize) { - y += w.computeSize()[1] + 4; - } else { - y += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } -} - -// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen -const elementWidgets = new Set(); -const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes; -LGraphCanvas.prototype.computeVisibleNodes = function () { - const visibleNodes = computeVisibleNodes.apply(this, arguments); - for (const node of app.graph._nodes) { - if (elementWidgets.has(node)) { - const hidden = visibleNodes.indexOf(node) === -1; - for (const w of node.widgets) { - if (w.element) { - w.element.hidden = hidden; - w.element.style.display = hidden ? "none" : undefined; - if (hidden) { - w.options.onHide?.(w); - } - } - } - } - } - - return visibleNodes; -}; - -let enableDomClipping = true; - -export function addDomClippingSetting() { - app.ui.settings.addSetting({ - id: "Comfy.DOMClippingEnabled", - name: "Enable DOM element clipping (enabling may reduce performance)", - type: "boolean", - defaultValue: enableDomClipping, - onChange(value) { - enableDomClipping = !!value; - }, - }); -} - -LGraphNode.prototype.addDOMWidget = function (name, type, element, options) { - options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options }; - - if (!element.parentElement) { - document.body.append(element); - } - element.hidden = true; - element.style.display = "none"; - - let mouseDownHandler; - if (element.blur) { - mouseDownHandler = (event) => { - if (!element.contains(event.target)) { - element.blur(); - } - }; - document.addEventListener("mousedown", mouseDownHandler); - } - - const { nodeData } = this.constructor; - const tooltip = (nodeData?.input.required?.[name] ?? nodeData?.input.optional?.[name])?.[1]?.tooltip; - if (tooltip && !element.title) { - element.title = tooltip; - } - - const widget = { - type, - name, - get value() { - return options.getValue?.() ?? undefined; - }, - set value(v) { - options.setValue?.(v); - widget.callback?.(widget.value); - }, - draw: function (ctx, node, widgetWidth, y, widgetHeight) { - if (widget.computedHeight == null) { - computeSize.call(node, node.size); - } - - const hidden = - node.flags?.collapsed || - (!!options.hideOnZoom && app.canvas.ds.scale < 0.5) || - widget.computedHeight <= 0 || - widget.type === "converted-widget"|| - widget.type === "hidden"; - element.hidden = hidden; - element.style.display = hidden ? "none" : null; - if (hidden) { - widget.options.onHide?.(widget); - return; - } - - const margin = 10; - const elRect = ctx.canvas.getBoundingClientRect(); - const transform = new DOMMatrix() - .scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height) - .multiplySelf(ctx.getTransform()) - .translateSelf(margin, margin + y ); - - const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); - - Object.assign(element.style, { - transformOrigin: "0 0", - transform: scale, - left: `${transform.a + transform.e + elRect.left}px`, - top: `${transform.d + transform.f + elRect.top}px`, - width: `${widgetWidth - margin * 2}px`, - height: `${(widget.computedHeight ?? 50) - margin * 2}px`, - position: "absolute", - zIndex: app.graph._nodes.indexOf(node), - }); - - if (enableDomClipping) { - element.style.clipPath = getClipPath(node, element); - element.style.willChange = "clip-path"; - } - - this.options.onDraw?.(widget); - }, - element, - options, - onRemove() { - if (mouseDownHandler) { - document.removeEventListener("mousedown", mouseDownHandler); - } - element.remove(); - }, - }; - - for (const evt of options.selectOn) { - element.addEventListener(evt, () => { - app.canvas.selectNode(this); - app.canvas.bringToFront(this); - }); - } - - this.addCustomWidget(widget); - elementWidgets.add(this); - - const collapse = this.collapse; - this.collapse = function() { - collapse.apply(this, arguments); - if(this.flags?.collapsed) { - element.hidden = true; - element.style.display = "none"; - } - } - - const onRemoved = this.onRemoved; - this.onRemoved = function () { - element.remove(); - elementWidgets.delete(this); - onRemoved?.apply(this, arguments); - }; - - if (!this[SIZE]) { - this[SIZE] = true; - const onResize = this.onResize; - this.onResize = function (size) { - options.beforeResize?.call(widget, this); - computeSize.call(this, size); - onResize?.apply(this, arguments); - options.afterResize?.call(widget, this); - }; - } - - return widget; -}; diff --git a/web/scripts/logging.js b/web/scripts/logging.js deleted file mode 100644 index 875dd970b..000000000 --- a/web/scripts/logging.js +++ /dev/null @@ -1,370 +0,0 @@ -import { $el, ComfyDialog } from "./ui.js"; -import { api } from "./api.js"; - -$el("style", { - textContent: ` - .comfy-logging-logs { - display: grid; - color: var(--fg-color); - white-space: pre-wrap; - } - .comfy-logging-log { - display: contents; - } - .comfy-logging-title { - background: var(--tr-even-bg-color); - font-weight: bold; - margin-bottom: 5px; - text-align: center; - } - .comfy-logging-log div { - background: var(--row-bg); - padding: 5px; - } - `, - parent: document.body, -}); - -// Stringify function supporting max depth and removal of circular references -// https://stackoverflow.com/a/57193345 -function stringify(val, depth, replacer, space, onGetObjID) { - depth = isNaN(+depth) ? 1 : depth; - var recursMap = new WeakMap(); - function _build(val, depth, o, a, r) { - // (JSON.stringify() has it's own rules, which we respect here by using it for property iteration) - return !val || typeof val != "object" - ? val - : ((r = recursMap.has(val)), - recursMap.set(val, true), - (a = Array.isArray(val)), - r - ? (o = (onGetObjID && onGetObjID(val)) || null) - : JSON.stringify(val, function (k, v) { - if (a || depth > 0) { - if (replacer) v = replacer(k, v); - if (!k) return (a = Array.isArray(v)), (val = v); - !o && (o = a ? [] : {}); - o[k] = _build(v, a ? depth : depth - 1); - } - }), - o === void 0 ? (a ? [] : {}) : o); - } - return JSON.stringify(_build(val, depth), null, space); -} - -const jsonReplacer = (k, v, ui) => { - if (v instanceof Array && v.length === 1) { - v = v[0]; - } - if (v instanceof Date) { - v = v.toISOString(); - if (ui) { - v = v.split("T")[1]; - } - } - if (v instanceof Error) { - let err = ""; - if (v.name) err += v.name + "\n"; - if (v.message) err += v.message + "\n"; - if (v.stack) err += v.stack + "\n"; - if (!err) { - err = v.toString(); - } - v = err; - } - return v; -}; - -const fileInput = $el("input", { - type: "file", - accept: ".json", - style: { display: "none" }, - parent: document.body, -}); - -class ComfyLoggingDialog extends ComfyDialog { - constructor(logging) { - super(); - this.logging = logging; - } - - clear() { - this.logging.clear(); - this.show(); - } - - export() { - const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: `comfyui-logs-${Date.now()}.json`, - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - } - - import() { - fileInput.onchange = () => { - const reader = new FileReader(); - reader.onload = () => { - fileInput.remove(); - try { - const obj = JSON.parse(reader.result); - if (obj instanceof Array) { - this.show(obj); - } else { - throw new Error("Invalid file selected."); - } - } catch (error) { - alert("Unable to load logs: " + error.message); - } - }; - reader.readAsText(fileInput.files[0]); - }; - fileInput.click(); - } - - createButtons() { - return [ - $el("button", { - type: "button", - textContent: "Clear", - onclick: () => this.clear(), - }), - $el("button", { - type: "button", - textContent: "Export logs...", - onclick: () => this.export(), - }), - $el("button", { - type: "button", - textContent: "View exported logs...", - onclick: () => this.import(), - }), - ...super.createButtons(), - ]; - } - - getTypeColor(type) { - switch (type) { - case "error": - return "red"; - case "warn": - return "orange"; - case "debug": - return "dodgerblue"; - } - } - - show(entries) { - if (!entries) entries = this.logging.entries; - this.element.style.width = "100%"; - const cols = { - source: "Source", - type: "Type", - timestamp: "Timestamp", - message: "Message", - }; - const keys = Object.keys(cols); - const headers = Object.values(cols).map((title) => - $el("div.comfy-logging-title", { - textContent: title, - }) - ); - const rows = entries.map((entry, i) => { - return $el( - "div.comfy-logging-log", - { - $: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`), - }, - keys.map((key) => { - let v = entry[key]; - let color; - if (key === "type") { - color = this.getTypeColor(v); - } else { - v = jsonReplacer(key, v, true); - - if (typeof v === "object") { - v = stringify(v, 5, jsonReplacer, " "); - } - } - - return $el("div", { - style: { - color, - }, - textContent: v, - }); - }) - ); - }); - - const grid = $el( - "div.comfy-logging-logs", - { - style: { - gridTemplateColumns: `repeat(${headers.length}, 1fr)`, - }, - }, - [...headers, ...rows] - ); - const els = [grid]; - if (!this.logging.enabled) { - els.unshift( - $el("h3", { - style: { textAlign: "center" }, - textContent: "Logging is disabled", - }) - ); - } - super.show($el("div", els)); - } -} - -export class ComfyLogging { - /** - * @type Array<{ source: string, type: string, timestamp: Date, message: any }> - */ - entries = []; - - #enabled; - #console = {}; - - get enabled() { - return this.#enabled; - } - - set enabled(value) { - if (value === this.#enabled) return; - if (value) { - this.patchConsole(); - } else { - this.unpatchConsole(); - } - this.#enabled = value; - } - - constructor(app) { - this.app = app; - - this.dialog = new ComfyLoggingDialog(this); - this.addSetting(); - this.catchUnhandled(); - this.addInitData(); - } - - addSetting() { - const settingId = "Comfy.Logging.Enabled"; - const htmlSettingId = settingId.replaceAll(".", "-"); - const setting = this.app.ui.settings.addSetting({ - id: settingId, - name: settingId, - defaultValue: true, - onChange: (value) => { - this.enabled = value; - }, - type: (name, setter, value) => { - return $el("tr", [ - $el("td", [ - $el("label", { - textContent: "Logging", - for: htmlSettingId, - }), - ]), - $el("td", [ - $el("input", { - id: htmlSettingId, - type: "checkbox", - checked: value, - onchange: (event) => { - setter(event.target.checked); - }, - }), - $el("button", { - textContent: "View Logs", - onclick: () => { - this.app.ui.settings.element.close(); - this.dialog.show(); - }, - style: { - fontSize: "14px", - display: "block", - marginTop: "5px", - }, - }), - ]), - ]); - }, - }); - this.enabled = setting.value; - } - - patchConsole() { - // Capture common console outputs - const self = this; - for (const type of ["log", "warn", "error", "debug"]) { - const orig = console[type]; - this.#console[type] = orig; - console[type] = function () { - orig.apply(console, arguments); - self.addEntry("console", type, ...arguments); - }; - } - } - - unpatchConsole() { - // Restore original console functions - for (const type of Object.keys(this.#console)) { - console[type] = this.#console[type]; - } - this.#console = {}; - } - - catchUnhandled() { - // Capture uncaught errors - window.addEventListener("error", (e) => { - this.addEntry("window", "error", e.error ?? "Unknown error"); - return false; - }); - - window.addEventListener("unhandledrejection", (e) => { - this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error"); - }); - } - - clear() { - this.entries = []; - } - - addEntry(source, type, ...args) { - if (this.enabled) { - this.entries.push({ - source, - type, - timestamp: new Date(), - message: args, - }); - } - } - - log(source, ...args) { - this.addEntry(source, "log", ...args); - } - - async addInitData() { - if (!this.enabled) return; - const source = "ComfyUI.Logging"; - this.addEntry(source, "debug", { UserAgent: navigator.userAgent }); - const systemStats = await api.getSystemStats(); - this.addEntry(source, "debug", systemStats); - } -} diff --git a/web/scripts/pnginfo.js b/web/scripts/pnginfo.js deleted file mode 100644 index 4477ed7a1..000000000 --- a/web/scripts/pnginfo.js +++ /dev/null @@ -1,507 +0,0 @@ -import { api } from "./api.js"; - -export function getPngMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = (event) => { - // Get the PNG data as a Uint8Array - const pngData = new Uint8Array(event.target.result); - const dataView = new DataView(pngData.buffer); - - // Check that the PNG signature is present - if (dataView.getUint32(0) !== 0x89504e47) { - console.error("Not a valid PNG file"); - r(); - return; - } - - // Start searching for chunks after the PNG signature - let offset = 8; - let txt_chunks = {}; - // Loop through the chunks in the PNG file - while (offset < pngData.length) { - // Get the length of the chunk - const length = dataView.getUint32(offset); - // Get the chunk type - const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8)); - if (type === "tEXt" || type == "comf" || type === "iTXt") { - // Get the keyword - let keyword_end = offset + 8; - while (pngData[keyword_end] !== 0) { - keyword_end++; - } - const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end)); - // Get the text - const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length); - const contentJson = new TextDecoder("utf-8").decode(contentArraySegment); - txt_chunks[keyword] = contentJson; - } - - offset += 12 + length; - } - - r(txt_chunks); - }; - - reader.readAsArrayBuffer(file); - }); -} - -function parseExifData(exifData) { - // Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian) - const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === "II"; - - // Function to read 16-bit and 32-bit integers from binary data - function readInt(offset, isLittleEndian, length) { - let arr = exifData.slice(offset, offset + length) - if (length === 2) { - return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(0, isLittleEndian); - } else if (length === 4) { - return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(0, isLittleEndian); - } - } - - // Read the offset to the first IFD (Image File Directory) - const ifdOffset = readInt(4, isLittleEndian, 4); - - function parseIFD(offset) { - const numEntries = readInt(offset, isLittleEndian, 2); - const result = {}; - - for (let i = 0; i < numEntries; i++) { - const entryOffset = offset + 2 + i * 12; - const tag = readInt(entryOffset, isLittleEndian, 2); - const type = readInt(entryOffset + 2, isLittleEndian, 2); - const numValues = readInt(entryOffset + 4, isLittleEndian, 4); - const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4); - - // Read the value(s) based on the data type - let value; - if (type === 2) { - // ASCII string - value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1)); - } - - result[tag] = value; - } - - return result; - } - - // Parse the first IFD - const ifdData = parseIFD(ifdOffset); - return ifdData; -} - -function splitValues(input) { - var output = {}; - for (var key in input) { - var value = input[key]; - var splitValues = value.split(':', 2); - output[splitValues[0]] = splitValues[1]; - } - return output; -} - -export function getWebpMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = (event) => { - const webp = new Uint8Array(event.target.result); - const dataView = new DataView(webp.buffer); - - // Check that the WEBP signature is present - if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) { - console.error("Not a valid WEBP file"); - r(); - return; - } - - // Start searching for chunks after the WEBP signature - let offset = 12; - let txt_chunks = {}; - // Loop through the chunks in the WEBP file - while (offset < webp.length) { - const chunk_length = dataView.getUint32(offset + 4, true); - const chunk_type = String.fromCharCode(...webp.slice(offset, offset + 4)); - if (chunk_type === "EXIF") { - if (String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == "Exif\0\0") { - offset += 6; - } - let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length)); - for (var key in data) { - var value = data[key]; - let index = value.indexOf(':'); - txt_chunks[value.slice(0, index)] = value.slice(index + 1); - } - break; - } - - offset += 8 + chunk_length; - } - - r(txt_chunks); - }; - - reader.readAsArrayBuffer(file); - }); -} - -export function getLatentMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = (event) => { - const safetensorsData = new Uint8Array(event.target.result); - const dataView = new DataView(safetensorsData.buffer); - let header_size = dataView.getUint32(0, true); - let offset = 8; - let header = JSON.parse(new TextDecoder().decode(safetensorsData.slice(offset, offset + header_size))); - r(header.__metadata__); - }; - - var slice = file.slice(0, 1024 * 1024 * 4); - reader.readAsArrayBuffer(slice); - }); -} - - -function getString(dataView, offset, length) { - let string = ''; - for (let i = 0; i < length; i++) { - string += String.fromCharCode(dataView.getUint8(offset + i)); - } - return string; -} - -// Function to parse the Vorbis Comment block -function parseVorbisComment(dataView) { - let offset = 0; - const vendorLength = dataView.getUint32(offset, true); - offset += 4; - const vendorString = getString(dataView, offset, vendorLength); - offset += vendorLength; - - const userCommentListLength = dataView.getUint32(offset, true); - offset += 4; - const comments = {}; - for (let i = 0; i < userCommentListLength; i++) { - const commentLength = dataView.getUint32(offset, true); - offset += 4; - const comment = getString(dataView, offset, commentLength); - offset += commentLength; - - const ind = comment.indexOf('=') - const key = comment.substring(0, ind); - - comments[key] = comment.substring(ind+1); - } - - return comments; -} - -// Function to read a FLAC file and parse Vorbis comments -export function getFlacMetadata(file) { - return new Promise((r) => { - const reader = new FileReader(); - reader.onload = function(event) { - const arrayBuffer = event.target.result; - const dataView = new DataView(arrayBuffer); - - // Verify the FLAC signature - const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4)); - if (signature !== 'fLaC') { - console.error('Not a valid FLAC file'); - return; - } - - // Parse metadata blocks - let offset = 4; - let vorbisComment = null; - while (offset < dataView.byteLength) { - const isLastBlock = dataView.getUint8(offset) & 0x80; - const blockType = dataView.getUint8(offset) & 0x7F; - const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF; - offset += 4; - - if (blockType === 4) { // Vorbis Comment block type - vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize)); - } - - offset += blockSize; - if (isLastBlock) break; - } - - r(vorbisComment); - }; - reader.readAsArrayBuffer(file); - }); -} - -export async function importA1111(graph, parameters) { - const p = parameters.lastIndexOf("\nSteps:"); - if (p > -1) { - const embeddings = await api.getEmbeddings(); - const opts = parameters - .substr(p) - .split("\n")[1] - .match(new RegExp("\\s*([^:]+:\\s*([^\"\\{].*?|\".*?\"|\\{.*?\\}))\\s*(,|$)", "g")) - .reduce((p, n) => { - const s = n.split(":"); - if (s[1].endsWith(',')) { - s[1] = s[1].substr(0, s[1].length -1); - } - p[s[0].trim().toLowerCase()] = s[1].trim(); - return p; - }, {}); - const p2 = parameters.lastIndexOf("\nNegative prompt:", p); - if (p2 > -1) { - let positive = parameters.substr(0, p2).trim(); - let negative = parameters.substring(p2 + 18, p).trim(); - - const ckptNode = LiteGraph.createNode("CheckpointLoaderSimple"); - const clipSkipNode = LiteGraph.createNode("CLIPSetLastLayer"); - const positiveNode = LiteGraph.createNode("CLIPTextEncode"); - const negativeNode = LiteGraph.createNode("CLIPTextEncode"); - const samplerNode = LiteGraph.createNode("KSampler"); - const imageNode = LiteGraph.createNode("EmptyLatentImage"); - const vaeNode = LiteGraph.createNode("VAEDecode"); - const vaeLoaderNode = LiteGraph.createNode("VAELoader"); - const saveNode = LiteGraph.createNode("SaveImage"); - let hrSamplerNode = null; - let hrSteps = null; - - const ceil64 = (v) => Math.ceil(v / 64) * 64; - - function getWidget(node, name) { - return node.widgets.find((w) => w.name === name); - } - - function setWidgetValue(node, name, value, isOptionPrefix) { - const w = getWidget(node, name); - if (isOptionPrefix) { - const o = w.options.values.find((w) => w.startsWith(value)); - if (o) { - w.value = o; - } else { - console.warn(`Unknown value '${value}' for widget '${name}'`, node); - w.value = value; - } - } else { - w.value = value; - } - } - - function createLoraNodes(clipNode, text, prevClip, prevModel) { - const loras = []; - text = text.replace(/]+)>/g, function (m, c) { - const s = c.split(":"); - const weight = parseFloat(s[1]); - if (isNaN(weight)) { - console.warn("Invalid LORA", m); - } else { - loras.push({ name: s[0], weight }); - } - return ""; - }); - - for (const l of loras) { - const loraNode = LiteGraph.createNode("LoraLoader"); - graph.add(loraNode); - setWidgetValue(loraNode, "lora_name", l.name, true); - setWidgetValue(loraNode, "strength_model", l.weight); - setWidgetValue(loraNode, "strength_clip", l.weight); - prevModel.node.connect(prevModel.index, loraNode, 0); - prevClip.node.connect(prevClip.index, loraNode, 1); - prevModel = { node: loraNode, index: 0 }; - prevClip = { node: loraNode, index: 1 }; - } - - prevClip.node.connect(1, clipNode, 0); - prevModel.node.connect(0, samplerNode, 0); - if (hrSamplerNode) { - prevModel.node.connect(0, hrSamplerNode, 0); - } - - return { text, prevModel, prevClip }; - } - - function replaceEmbeddings(text) { - if(!embeddings.length) return text; - return text.replaceAll( - new RegExp( - "\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b", - "ig" - ), - "embedding:$1" - ); - } - - function popOpt(name) { - const v = opts[name]; - delete opts[name]; - return v; - } - - graph.clear(); - graph.add(ckptNode); - graph.add(clipSkipNode); - graph.add(positiveNode); - graph.add(negativeNode); - graph.add(samplerNode); - graph.add(imageNode); - graph.add(vaeNode); - graph.add(vaeLoaderNode); - graph.add(saveNode); - - ckptNode.connect(1, clipSkipNode, 0); - clipSkipNode.connect(0, positiveNode, 0); - clipSkipNode.connect(0, negativeNode, 0); - ckptNode.connect(0, samplerNode, 0); - positiveNode.connect(0, samplerNode, 1); - negativeNode.connect(0, samplerNode, 2); - imageNode.connect(0, samplerNode, 3); - vaeNode.connect(0, saveNode, 0); - samplerNode.connect(0, vaeNode, 0); - vaeLoaderNode.connect(0, vaeNode, 1); - - const handlers = { - model(v) { - setWidgetValue(ckptNode, "ckpt_name", v, true); - }, - "vae"(v) { - setWidgetValue(vaeLoaderNode, "vae_name", v, true); - }, - "cfg scale"(v) { - setWidgetValue(samplerNode, "cfg", +v); - }, - "clip skip"(v) { - setWidgetValue(clipSkipNode, "stop_at_clip_layer", -v); - }, - sampler(v) { - let name = v.toLowerCase().replace("++", "pp").replaceAll(" ", "_"); - if (name.includes("karras")) { - name = name.replace("karras", "").replace(/_+$/, ""); - setWidgetValue(samplerNode, "scheduler", "karras"); - } else { - setWidgetValue(samplerNode, "scheduler", "normal"); - } - const w = getWidget(samplerNode, "sampler_name"); - const o = w.options.values.find((w) => w === name || w === "sample_" + name); - if (o) { - setWidgetValue(samplerNode, "sampler_name", o); - } - }, - size(v) { - const wxh = v.split("x"); - const w = ceil64(+wxh[0]); - const h = ceil64(+wxh[1]); - const hrUp = popOpt("hires upscale"); - const hrSz = popOpt("hires resize"); - hrSteps = popOpt("hires steps"); - let hrMethod = popOpt("hires upscaler"); - - setWidgetValue(imageNode, "width", w); - setWidgetValue(imageNode, "height", h); - - if (hrUp || hrSz) { - let uw, uh; - if (hrUp) { - uw = w * hrUp; - uh = h * hrUp; - } else { - const s = hrSz.split("x"); - uw = +s[0]; - uh = +s[1]; - } - - let upscaleNode; - let latentNode; - - if (hrMethod.startsWith("Latent")) { - latentNode = upscaleNode = LiteGraph.createNode("LatentUpscale"); - graph.add(upscaleNode); - samplerNode.connect(0, upscaleNode, 0); - - switch (hrMethod) { - case "Latent (nearest-exact)": - hrMethod = "nearest-exact"; - break; - } - setWidgetValue(upscaleNode, "upscale_method", hrMethod, true); - } else { - const decode = LiteGraph.createNode("VAEDecodeTiled"); - graph.add(decode); - samplerNode.connect(0, decode, 0); - vaeLoaderNode.connect(0, decode, 1); - - const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader"); - graph.add(upscaleLoaderNode); - setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true); - - const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel"); - graph.add(modelUpscaleNode); - decode.connect(0, modelUpscaleNode, 1); - upscaleLoaderNode.connect(0, modelUpscaleNode, 0); - - upscaleNode = LiteGraph.createNode("ImageScale"); - graph.add(upscaleNode); - modelUpscaleNode.connect(0, upscaleNode, 0); - - const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled")); - graph.add(vaeEncodeNode); - upscaleNode.connect(0, vaeEncodeNode, 0); - vaeLoaderNode.connect(0, vaeEncodeNode, 1); - } - - setWidgetValue(upscaleNode, "width", ceil64(uw)); - setWidgetValue(upscaleNode, "height", ceil64(uh)); - - hrSamplerNode = LiteGraph.createNode("KSampler"); - graph.add(hrSamplerNode); - ckptNode.connect(0, hrSamplerNode, 0); - positiveNode.connect(0, hrSamplerNode, 1); - negativeNode.connect(0, hrSamplerNode, 2); - latentNode.connect(0, hrSamplerNode, 3); - hrSamplerNode.connect(0, vaeNode, 0); - } - }, - steps(v) { - setWidgetValue(samplerNode, "steps", +v); - }, - seed(v) { - setWidgetValue(samplerNode, "seed", +v); - }, - }; - - for (const opt in opts) { - if (opt in handlers) { - handlers[opt](popOpt(opt)); - } - } - - if (hrSamplerNode) { - setWidgetValue(hrSamplerNode, "steps", hrSteps? +hrSteps : getWidget(samplerNode, "steps").value); - setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value); - setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value); - setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value); - setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1")); - } - - let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 }); - positive = n.text; - n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel); - negative = n.text; - - setWidgetValue(positiveNode, "text", replaceEmbeddings(positive)); - setWidgetValue(negativeNode, "text", replaceEmbeddings(negative)); - - graph.arrange(); - - for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) { - delete opts[opt]; - } - - console.warn("Unhandled parameters:", opts); - } - } -} diff --git a/web/scripts/ui.js b/web/scripts/ui.js deleted file mode 100644 index 05258e378..000000000 --- a/web/scripts/ui.js +++ /dev/null @@ -1,664 +0,0 @@ -import { api } from "./api.js"; -import { ComfyDialog as _ComfyDialog } from "./ui/dialog.js"; -import { toggleSwitch } from "./ui/toggleSwitch.js"; -import { ComfySettingsDialog } from "./ui/settings.js"; - -export const ComfyDialog = _ComfyDialog; - -/** - * @template { string | (keyof HTMLElementTagNameMap) } K - * @typedef { K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement } ElementType - */ - -/** - * @template { string | (keyof HTMLElementTagNameMap) } K - * @param { K } tag HTML Element Tag and optional classes e.g. div.class1.class2 - * @param { string | Element | Element[] | ({ - * parent?: Element, - * $?: (el: ElementType) => void, - * dataset?: DOMStringMap, - * style?: Partial, - * for?: string - * } & Omit>, "style">) | undefined } [propsOrChildren] - * @param { string | Element | Element[] | undefined } [children] - * @returns { ElementType } - */ -export function $el(tag, propsOrChildren, children) { - const split = tag.split("."); - const element = document.createElement(split.shift()); - if (split.length > 0) { - element.classList.add(...split); - } - - if (propsOrChildren) { - if (typeof propsOrChildren === "string") { - propsOrChildren = { textContent: propsOrChildren }; - } else if (propsOrChildren instanceof Element) { - propsOrChildren = [propsOrChildren]; - } - if (Array.isArray(propsOrChildren)) { - element.append(...propsOrChildren); - } else { - const {parent, $: cb, dataset, style} = propsOrChildren; - delete propsOrChildren.parent; - delete propsOrChildren.$; - delete propsOrChildren.dataset; - delete propsOrChildren.style; - - if (Object.hasOwn(propsOrChildren, "for")) { - element.setAttribute("for", propsOrChildren.for) - } - - if (style) { - Object.assign(element.style, style); - } - - if (dataset) { - Object.assign(element.dataset, dataset); - } - - Object.assign(element, propsOrChildren); - if (children) { - element.append(...(children instanceof Array ? children.filter(Boolean) : [children])); - } - - if (parent) { - parent.append(element); - } - - if (cb) { - cb(element); - } - } - } - return element; -} - -function dragElement(dragEl, settings) { - var posDiffX = 0, - posDiffY = 0, - posStartX = 0, - posStartY = 0, - newPosX = 0, - newPosY = 0; - if (dragEl.getElementsByClassName("drag-handle")[0]) { - // if present, the handle is where you move the DIV from: - dragEl.getElementsByClassName("drag-handle")[0].onmousedown = dragMouseDown; - } else { - // otherwise, move the DIV from anywhere inside the DIV: - dragEl.onmousedown = dragMouseDown; - } - - // When the element resizes (e.g. view queue) ensure it is still in the windows bounds - const resizeObserver = new ResizeObserver(() => { - ensureInBounds(); - }).observe(dragEl); - - function ensureInBounds() { - try { - newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft)); - newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop)); - - positionElement(); - } - catch(exception){ - // robust - } - } - - function positionElement() { - if(dragEl.style.display === "none") return; - - const halfWidth = document.body.clientWidth / 2; - const anchorRight = newPosX + dragEl.clientWidth / 2 > halfWidth; - - // set the element's new position: - if (anchorRight) { - dragEl.style.left = "unset"; - dragEl.style.right = document.body.clientWidth - newPosX - dragEl.clientWidth + "px"; - } else { - dragEl.style.left = newPosX + "px"; - dragEl.style.right = "unset"; - } - - dragEl.style.top = newPosY + "px"; - dragEl.style.bottom = "unset"; - - if (savePos) { - localStorage.setItem( - "Comfy.MenuPosition", - JSON.stringify({ - x: dragEl.offsetLeft, - y: dragEl.offsetTop, - }) - ); - } - } - - function restorePos() { - let pos = localStorage.getItem("Comfy.MenuPosition"); - if (pos) { - pos = JSON.parse(pos); - newPosX = pos.x; - newPosY = pos.y; - positionElement(); - ensureInBounds(); - } - } - - let savePos = undefined; - settings.addSetting({ - id: "Comfy.MenuPosition", - name: "Save menu position", - type: "boolean", - defaultValue: savePos, - onChange(value) { - if (savePos === undefined && value) { - restorePos(); - } - savePos = value; - }, - }); - - function dragMouseDown(e) { - e = e || window.event; - e.preventDefault(); - // get the mouse cursor position at startup: - posStartX = e.clientX; - posStartY = e.clientY; - document.onmouseup = closeDragElement; - // call a function whenever the cursor moves: - document.onmousemove = elementDrag; - } - - function elementDrag(e) { - e = e || window.event; - e.preventDefault(); - - dragEl.classList.add("comfy-menu-manual-pos"); - - // calculate the new cursor position: - posDiffX = e.clientX - posStartX; - posDiffY = e.clientY - posStartY; - posStartX = e.clientX; - posStartY = e.clientY; - - newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft + posDiffX)); - newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop + posDiffY)); - - positionElement(); - } - - window.addEventListener("resize", () => { - ensureInBounds(); - }); - - function closeDragElement() { - // stop moving when mouse button is released: - document.onmouseup = null; - document.onmousemove = null; - } - - return restorePos; -} - -class ComfyList { - #type; - #text; - #reverse; - - constructor(text, type, reverse) { - this.#text = text; - this.#type = type || text.toLowerCase(); - this.#reverse = reverse || false; - this.element = $el("div.comfy-list"); - this.element.style.display = "none"; - } - - get visible() { - return this.element.style.display !== "none"; - } - - async load() { - const items = await api.getItems(this.#type); - this.element.replaceChildren( - ...Object.keys(items).flatMap((section) => [ - $el("h4", { - textContent: section, - }), - $el("div.comfy-list-items", [ - ...(this.#reverse ? items[section].reverse() : items[section]).map((item) => { - // Allow items to specify a custom remove action (e.g. for interrupt current prompt) - const removeAction = item.remove || { - name: "Delete", - cb: () => api.deleteItem(this.#type, item.prompt[1]), - }; - return $el("div", {textContent: item.prompt[0] + ": "}, [ - $el("button", { - textContent: "Load", - onclick: async () => { - await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow, true, false); - if (item.outputs) { - app.nodeOutputs = {}; - for (const [key, value] of Object.entries(item.outputs)) { - const realKey = item?.meta?.[key]?.display_node ?? key; - app.nodeOutputs[realKey] = value; - } - } - }, - }), - $el("button", { - textContent: removeAction.name, - onclick: async () => { - await removeAction.cb(); - await this.update(); - }, - }), - ]); - }), - ]), - ]), - $el("div.comfy-list-actions", [ - $el("button", { - textContent: "Clear " + this.#text, - onclick: async () => { - await api.clearItems(this.#type); - await this.load(); - }, - }), - $el("button", {textContent: "Refresh", onclick: () => this.load()}), - ]) - ); - } - - async update() { - if (this.visible) { - await this.load(); - } - } - - async show() { - this.element.style.display = "block"; - this.button.textContent = "Close"; - - await this.load(); - } - - hide() { - this.element.style.display = "none"; - this.button.textContent = "View " + this.#text; - } - - toggle() { - if (this.visible) { - this.hide(); - return false; - } else { - this.show(); - return true; - } - } -} - -export class ComfyUI { - constructor(app) { - this.app = app; - this.dialog = new ComfyDialog(); - this.settings = new ComfySettingsDialog(app); - - this.batchCount = 1; - this.lastQueueSize = 0; - this.queue = new ComfyList("Queue"); - this.history = new ComfyList("History", "history", true); - - api.addEventListener("status", () => { - this.queue.update(); - this.history.update(); - }); - - const confirmClear = this.settings.addSetting({ - id: "Comfy.ConfirmClear", - name: "Require confirmation when clearing workflow", - type: "boolean", - defaultValue: true, - }); - - const promptFilename = this.settings.addSetting({ - id: "Comfy.PromptFilename", - name: "Prompt for filename when saving workflow", - type: "boolean", - defaultValue: true, - }); - - /** - * file format for preview - * - * format;quality - * - * ex) - * webp;50 -> webp, quality 50 - * jpeg;80 -> rgb, jpeg, quality 80 - * - * @type {string} - */ - const previewImage = this.settings.addSetting({ - id: "Comfy.PreviewFormat", - name: "When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.", - type: "text", - defaultValue: "", - }); - - this.settings.addSetting({ - id: "Comfy.DisableSliders", - name: "Disable sliders.", - type: "boolean", - defaultValue: false, - }); - - this.settings.addSetting({ - id: "Comfy.DisableFloatRounding", - name: "Disable rounding floats (requires page reload).", - type: "boolean", - defaultValue: false, - }); - - this.settings.addSetting({ - id: "Comfy.FloatRoundingPrecision", - name: "Decimal places [0 = auto] (requires page reload).", - type: "slider", - attrs: { - min: 0, - max: 6, - step: 1, - }, - defaultValue: 0, - }); - - const fileInput = $el("input", { - id: "comfy-file-input", - type: "file", - accept: ".json,image/png,.latent,.safetensors,image/webp,audio/flac", - style: {display: "none"}, - parent: document.body, - onchange: () => { - app.handleFile(fileInput.files[0]); - }, - }); - - this.loadFile = () => fileInput.click(); - - const autoQueueModeEl = toggleSwitch( - "autoQueueMode", - [ - { text: "instant", tooltip: "A new prompt will be queued as soon as the queue reaches 0" }, - { text: "change", tooltip: "A new prompt will be queued when the queue is at 0 and the graph is/has changed" }, - ], - { - onChange: (value) => { - this.autoQueueMode = value.item.value; - }, - } - ); - autoQueueModeEl.style.display = "none"; - - api.addEventListener("graphChanged", () => { - if (this.autoQueueMode === "change" && this.autoQueueEnabled === true) { - if (this.lastQueueSize === 0) { - this.graphHasChanged = false; - app.queuePrompt(0, this.batchCount); - } else { - this.graphHasChanged = true; - } - } - }); - - this.menuHamburger = $el( - "div.comfy-menu-hamburger", - { - parent: document.body, - onclick: () => { - this.menuContainer.style.display = "block"; - this.menuHamburger.style.display = "none"; - }, - }, - [$el("div"), $el("div"), $el("div")] - ); - - this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [ - $el("div.drag-handle.comfy-menu-header", { - style: { - overflow: "hidden", - position: "relative", - width: "100%", - cursor: "default" - } - }, [ - $el("span.drag-handle"), - $el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }), - $el("div.comfy-menu-actions", [ - $el("button.comfy-settings-btn", { - textContent: "⚙️", - onclick: () => this.settings.show(), - }), - $el("button.comfy-close-menu-btn", { - textContent: "\u00d7", - onclick: () => { - this.menuContainer.style.display = "none"; - this.menuHamburger.style.display = "flex"; - }, - }), - ]), - ]), - $el("button.comfy-queue-btn", { - id: "queue-button", - textContent: "Queue Prompt", - onclick: () => app.queuePrompt(0, this.batchCount), - }), - $el("div", {}, [ - $el("label", {innerHTML: "Extra options"}, [ - $el("input", { - type: "checkbox", - onchange: (i) => { - document.getElementById("extraOptions").style.display = i.srcElement.checked ? "block" : "none"; - this.batchCount = i.srcElement.checked ? document.getElementById("batchCountInputRange").value : 1; - document.getElementById("autoQueueCheckbox").checked = false; - this.autoQueueEnabled = false; - }, - }), - ]), - ]), - $el("div", {id: "extraOptions", style: {width: "100%", display: "none"}}, [ - $el("div",[ - - $el("label", {innerHTML: "Batch count"}), - $el("input", { - id: "batchCountInputNumber", - type: "number", - value: this.batchCount, - min: "1", - style: {width: "35%", "margin-left": "0.4em"}, - oninput: (i) => { - this.batchCount = i.target.value; - document.getElementById("batchCountInputRange").value = this.batchCount; - }, - }), - $el("input", { - id: "batchCountInputRange", - type: "range", - min: "1", - max: "100", - value: this.batchCount, - oninput: (i) => { - this.batchCount = i.srcElement.value; - document.getElementById("batchCountInputNumber").value = i.srcElement.value; - }, - }), - ]), - $el("div",[ - $el("label",{ - for:"autoQueueCheckbox", - innerHTML: "Auto Queue" - }), - $el("input", { - id: "autoQueueCheckbox", - type: "checkbox", - checked: false, - title: "Automatically queue prompt when the queue size hits 0", - onchange: (e) => { - this.autoQueueEnabled = e.target.checked; - autoQueueModeEl.style.display = this.autoQueueEnabled ? "" : "none"; - } - }), - autoQueueModeEl - ]) - ]), - $el("div.comfy-menu-btns", [ - $el("button", { - id: "queue-front-button", - textContent: "Queue Front", - onclick: () => app.queuePrompt(-1, this.batchCount) - }), - $el("button", { - $: (b) => (this.queue.button = b), - id: "comfy-view-queue-button", - textContent: "View Queue", - onclick: () => { - this.history.hide(); - this.queue.toggle(); - }, - }), - $el("button", { - $: (b) => (this.history.button = b), - id: "comfy-view-history-button", - textContent: "View History", - onclick: () => { - this.queue.hide(); - this.history.toggle(); - }, - }), - ]), - this.queue.element, - this.history.element, - $el("button", { - id: "comfy-save-button", - textContent: "Save", - onclick: () => { - let filename = "workflow.json"; - if (promptFilename.value) { - filename = prompt("Save workflow as:", filename); - if (!filename) return; - if (!filename.toLowerCase().endsWith(".json")) { - filename += ".json"; - } - } - app.graphToPrompt().then(p=>{ - const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: filename, - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }); - }, - }), - $el("button", { - id: "comfy-dev-save-api-button", - textContent: "Save (API Format)", - style: {width: "100%", display: "none"}, - onclick: () => { - let filename = "workflow_api.json"; - if (promptFilename.value) { - filename = prompt("Save workflow (API) as:", filename); - if (!filename) return; - if (!filename.toLowerCase().endsWith(".json")) { - filename += ".json"; - } - } - app.graphToPrompt().then(p=>{ - const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string - const blob = new Blob([json], {type: "application/json"}); - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: filename, - style: {display: "none"}, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); - }); - }, - }), - $el("button", {id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click()}), - $el("button", { - id: "comfy-refresh-button", - textContent: "Refresh", - onclick: () => app.refreshComboInNodes() - }), - $el("button", {id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace()}), - $el("button", { - id: "comfy-clear-button", textContent: "Clear", onclick: () => { - if (!confirmClear.value || confirm("Clear workflow?")) { - app.clean(); - app.graph.clear(); - app.resetView(); - } - } - }), - $el("button", { - id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => { - if (!confirmClear.value || confirm("Load default workflow?")) { - app.resetView(); - await app.loadGraphData() - } - } - }), - $el("button", { - id: "comfy-reset-view-button", textContent: "Reset View", onclick: async () => { - app.resetView(); - } - }), - ]); - - const devMode = this.settings.addSetting({ - id: "Comfy.DevMode", - name: "Enable Dev mode Options", - type: "boolean", - defaultValue: false, - onChange: function(value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none"}, - }); - - this.restoreMenuPosition = dragElement(this.menuContainer, this.settings); - - this.setStatus({exec_info: {queue_remaining: "X"}}); - } - - setStatus(status) { - this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR"); - if (status) { - if ( - this.lastQueueSize != 0 && - status.exec_info.queue_remaining == 0 && - this.autoQueueEnabled && - (this.autoQueueMode === "instant" || this.graphHasChanged) && - !app.lastExecutionError - ) { - app.queuePrompt(0, this.batchCount); - status.exec_info.queue_remaining += this.batchCount; - this.graphHasChanged = false; - } - this.lastQueueSize = status.exec_info.queue_remaining; - } - } -} diff --git a/web/scripts/ui/components/asyncDialog.js b/web/scripts/ui/components/asyncDialog.js deleted file mode 100644 index 434ce4b30..000000000 --- a/web/scripts/ui/components/asyncDialog.js +++ /dev/null @@ -1,64 +0,0 @@ -import { ComfyDialog } from "../dialog.js"; -import { $el } from "../../ui.js"; - -export class ComfyAsyncDialog extends ComfyDialog { - #resolve; - - constructor(actions) { - super( - "dialog.comfy-dialog.comfyui-dialog", - actions?.map((opt) => { - if (typeof opt === "string") { - opt = { text: opt }; - } - return $el("button.comfyui-button", { - type: "button", - textContent: opt.text, - onclick: () => this.close(opt.value ?? opt.text), - }); - }) - ); - } - - show(html) { - this.element.addEventListener("close", () => { - this.close(); - }); - - super.show(html); - - return new Promise((resolve) => { - this.#resolve = resolve; - }); - } - - showModal(html) { - this.element.addEventListener("close", () => { - this.close(); - }); - - super.show(html); - this.element.showModal(); - - return new Promise((resolve) => { - this.#resolve = resolve; - }); - } - - close(result = null) { - this.#resolve(result); - this.element.close(); - super.close(); - } - - static async prompt({ title = null, message, actions }) { - const dialog = new ComfyAsyncDialog(actions); - const content = [$el("span", message)]; - if (title) { - content.unshift($el("h3", title)); - } - const res = await dialog.showModal(content); - dialog.element.remove(); - return res; - } -} diff --git a/web/scripts/ui/components/button.js b/web/scripts/ui/components/button.js deleted file mode 100644 index 25e5aeebd..000000000 --- a/web/scripts/ui/components/button.js +++ /dev/null @@ -1,163 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { applyClasses, toggleElement } from "../utils.js"; -import { prop } from "../../utils.js"; - -/** - * @typedef {{ - * icon?: string; - * overIcon?: string; - * iconSize?: number; - * content?: string | HTMLElement; - * tooltip?: string; - * enabled?: boolean; - * action?: (e: Event, btn: ComfyButton) => void, - * classList?: import("../utils.js").ClassList, - * visibilitySetting?: { id: string, showValue: any }, - * app?: import("../../app.js").ComfyApp - * }} ComfyButtonProps - */ -export class ComfyButton { - #over = 0; - #popupOpen = false; - isOver = false; - iconElement = $el("i.mdi"); - contentElement = $el("span"); - /** - * @type {import("./popup.js").ComfyPopup} - */ - popup; - - /** - * @param {ComfyButtonProps} opts - */ - constructor({ - icon, - overIcon, - iconSize, - content, - tooltip, - action, - classList = "comfyui-button", - visibilitySetting, - app, - enabled = true, - }) { - this.element = $el("button", { - onmouseenter: () => { - this.isOver = true; - if(this.overIcon) { - this.updateIcon(); - } - }, - onmouseleave: () => { - this.isOver = false; - if(this.overIcon) { - this.updateIcon(); - } - } - - }, [this.iconElement, this.contentElement]); - - this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon })); - this.overIcon = prop(this, "overIcon", overIcon, () => { - if(this.isOver) { - this.updateIcon(); - } - }); - this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon); - this.content = prop( - this, - "content", - content, - toggleElement(this.contentElement, { - onShow: (el, v) => { - if (typeof v === "string") { - el.textContent = v; - } else { - el.replaceChildren(v); - } - }, - }) - ); - - this.tooltip = prop(this, "tooltip", tooltip, (v) => { - if (v) { - this.element.title = v; - } else { - this.element.removeAttribute("title"); - } - }); - this.classList = prop(this, "classList", classList, this.updateClasses); - this.hidden = prop(this, "hidden", false, this.updateClasses); - this.enabled = prop(this, "enabled", enabled, () => { - this.updateClasses(); - this.element.disabled = !this.enabled; - }); - this.action = prop(this, "action", action); - this.element.addEventListener("click", (e) => { - if (this.popup) { - // we are either a touch device or triggered by click not hover - if (!this.#over) { - this.popup.toggle(); - } - } - this.action?.(e, this); - }); - - if (visibilitySetting?.id) { - const settingUpdated = () => { - this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue; - }; - app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated); - settingUpdated(); - } - } - - updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`); - updateClasses = () => { - const internalClasses = []; - if (this.hidden) { - internalClasses.push("hidden"); - } - if (!this.enabled) { - internalClasses.push("disabled"); - } - if (this.popup) { - if (this.#popupOpen) { - internalClasses.push("popup-open"); - } else { - internalClasses.push("popup-closed"); - } - } - applyClasses(this.element, this.classList, ...internalClasses); - }; - - /** - * - * @param { import("./popup.js").ComfyPopup } popup - * @param { "click" | "hover" } mode - */ - withPopup(popup, mode = "click") { - this.popup = popup; - - if (mode === "hover") { - for (const el of [this.element, this.popup.element]) { - el.addEventListener("mouseenter", () => { - this.popup.open = !!++this.#over; - }); - el.addEventListener("mouseleave", () => { - this.popup.open = !!--this.#over; - }); - } - } - - popup.addEventListener("change", () => { - this.#popupOpen = popup.open; - this.updateClasses(); - }); - - return this; - } -} diff --git a/web/scripts/ui/components/buttonGroup.js b/web/scripts/ui/components/buttonGroup.js deleted file mode 100644 index 573572fd0..000000000 --- a/web/scripts/ui/components/buttonGroup.js +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { ComfyButton } from "./button.js"; -import { prop } from "../../utils.js"; - -export class ComfyButtonGroup { - element = $el("div.comfyui-button-group"); - - /** @param {Array} buttons */ - constructor(...buttons) { - this.buttons = prop(this, "buttons", buttons, () => this.update()); - } - - /** - * @param {ComfyButton} button - * @param {number} index - */ - insert(button, index) { - this.buttons.splice(index, 0, button); - this.update(); - } - - /** @param {ComfyButton} button */ - append(button) { - this.buttons.push(button); - this.update(); - } - - /** @param {ComfyButton|number} indexOrButton */ - remove(indexOrButton) { - if (typeof indexOrButton !== "number") { - indexOrButton = this.buttons.indexOf(indexOrButton); - } - if (indexOrButton > -1) { - const r = this.buttons.splice(indexOrButton, 1); - this.update(); - return r; - } - } - - update() { - this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b)); - } -} diff --git a/web/scripts/ui/components/popup.js b/web/scripts/ui/components/popup.js deleted file mode 100644 index ee59b35d9..000000000 --- a/web/scripts/ui/components/popup.js +++ /dev/null @@ -1,128 +0,0 @@ -// @ts-check - -import { prop } from "../../utils.js"; -import { $el } from "../../ui.js"; -import { applyClasses } from "../utils.js"; - -export class ComfyPopup extends EventTarget { - element = $el("div.comfyui-popup"); - - /** - * @param {{ - * target: HTMLElement, - * container?: HTMLElement, - * classList?: import("../utils.js").ClassList, - * ignoreTarget?: boolean, - * closeOnEscape?: boolean, - * position?: "absolute" | "relative", - * horizontal?: "left" | "right" - * }} param0 - * @param {...HTMLElement} children - */ - constructor( - { - target, - container = document.body, - classList = "", - ignoreTarget = true, - closeOnEscape = true, - position = "absolute", - horizontal = "left", - }, - ...children - ) { - super(); - this.target = target; - this.ignoreTarget = ignoreTarget; - this.container = container; - this.position = position; - this.closeOnEscape = closeOnEscape; - this.horizontal = horizontal; - - container.append(this.element); - - this.children = prop(this, "children", children, () => { - this.element.replaceChildren(...this.children); - this.update(); - }); - this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal)); - this.open = prop(this, "open", false, (v, o) => { - if (v === o) return; - if (v) { - this.#show(); - } else { - this.#hide(); - } - }); - } - - toggle() { - this.open = !this.open; - } - - #hide() { - this.element.classList.remove("open"); - window.removeEventListener("resize", this.update); - window.removeEventListener("click", this.#clickHandler, { capture: true }); - window.removeEventListener("keydown", this.#escHandler, { capture: true }); - - this.dispatchEvent(new CustomEvent("close")); - this.dispatchEvent(new CustomEvent("change")); - } - - #show() { - this.element.classList.add("open"); - this.update(); - - window.addEventListener("resize", this.update); - window.addEventListener("click", this.#clickHandler, { capture: true }); - if (this.closeOnEscape) { - window.addEventListener("keydown", this.#escHandler, { capture: true }); - } - - this.dispatchEvent(new CustomEvent("open")); - this.dispatchEvent(new CustomEvent("change")); - } - - #escHandler = (e) => { - if (e.key === "Escape") { - this.open = false; - e.preventDefault(); - e.stopImmediatePropagation(); - } - }; - - #clickHandler = (e) => { - /** @type {any} */ - const target = e.target; - if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) { - this.open = false; - } - }; - - update = () => { - const rect = this.target.getBoundingClientRect(); - this.element.style.setProperty("--bottom", "unset"); - if (this.position === "absolute") { - if (this.horizontal === "left") { - this.element.style.setProperty("--left", rect.left + "px"); - } else { - this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px"); - } - this.element.style.setProperty("--top", rect.bottom + "px"); - this.element.style.setProperty("--limit", rect.bottom + "px"); - } else { - this.element.style.setProperty("--left", 0 + "px"); - this.element.style.setProperty("--top", rect.height + "px"); - this.element.style.setProperty("--limit", rect.height + "px"); - } - - const thisRect = this.element.getBoundingClientRect(); - if (thisRect.height < 30) { - // Move up instead - this.element.style.setProperty("--top", "unset"); - this.element.style.setProperty("--bottom", rect.height + 5 + "px"); - this.element.style.setProperty("--limit", rect.height + 5 + "px"); - } - }; -} diff --git a/web/scripts/ui/components/splitButton.js b/web/scripts/ui/components/splitButton.js deleted file mode 100644 index 2b4e6d9f8..000000000 --- a/web/scripts/ui/components/splitButton.js +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { ComfyButton } from "./button.js"; -import { prop } from "../../utils.js"; -import { ComfyPopup } from "./popup.js"; - -export class ComfySplitButton { - /** - * @param {{ - * primary: ComfyButton, - * mode?: "hover" | "click", - * horizontal?: "left" | "right", - * position?: "relative" | "absolute" - * }} param0 - * @param {Array | Array} items - */ - constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) { - this.arrow = new ComfyButton({ - icon: "chevron-down", - }); - this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [ - $el("div.comfyui-split-primary", primary.element), - $el("div.comfyui-split-arrow", this.arrow.element), - ]); - this.popup = new ComfyPopup({ - target: this.element, - container: position === "relative" ? this.element : document.body, - classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""), - closeOnEscape: mode === "click", - position, - horizontal, - }); - - this.arrow.withPopup(this.popup, mode); - - this.items = prop(this, "items", items, () => this.update()); - } - - update() { - this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b)); - } -} diff --git a/web/scripts/ui/dialog.js b/web/scripts/ui/dialog.js deleted file mode 100644 index 803a97a2b..000000000 --- a/web/scripts/ui/dialog.js +++ /dev/null @@ -1,38 +0,0 @@ -import { $el } from "../ui.js"; - -export class ComfyDialog extends EventTarget { - #buttons; - - constructor(type = "div", buttons = null) { - super(); - this.#buttons = buttons; - this.element = $el(type + ".comfy-modal", { parent: document.body }, [ - $el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]), - ]); - } - - createButtons() { - return ( - this.#buttons ?? [ - $el("button", { - type: "button", - textContent: "Close", - onclick: () => this.close(), - }), - ] - ); - } - - close() { - this.element.style.display = "none"; - } - - show(html) { - if (typeof html === "string") { - this.textElement.innerHTML = html; - } else { - this.textElement.replaceChildren(...(html instanceof Array ? html : [html])); - } - this.element.style.display = "flex"; - } -} diff --git a/web/scripts/ui/draggableList.js b/web/scripts/ui/draggableList.js deleted file mode 100644 index d53594886..000000000 --- a/web/scripts/ui/draggableList.js +++ /dev/null @@ -1,287 +0,0 @@ -// @ts-check -/* - Original implementation: - https://github.com/TahaSh/drag-to-reorder - MIT License - - Copyright (c) 2023 Taha Shashtari - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -*/ - -import { $el } from "../ui.js"; - -$el("style", { - parent: document.head, - textContent: ` - .draggable-item { - position: relative; - will-change: transform; - user-select: none; - } - .draggable-item.is-idle { - transition: 0.25s ease transform; - } - .draggable-item.is-draggable { - z-index: 10; - } - ` -}); - -export class DraggableList extends EventTarget { - listContainer; - draggableItem; - pointerStartX; - pointerStartY; - scrollYMax; - itemsGap = 0; - items = []; - itemSelector; - handleClass = "drag-handle"; - off = []; - offDrag = []; - - constructor(element, itemSelector) { - super(); - this.listContainer = element; - this.itemSelector = itemSelector; - - if (!this.listContainer) return; - - this.off.push(this.on(this.listContainer, "mousedown", this.dragStart)); - this.off.push(this.on(this.listContainer, "touchstart", this.dragStart)); - this.off.push(this.on(document, "mouseup", this.dragEnd)); - this.off.push(this.on(document, "touchend", this.dragEnd)); - } - - getAllItems() { - if (!this.items?.length) { - this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector)); - this.items.forEach((element) => { - element.classList.add("is-idle"); - }); - } - return this.items; - } - - getIdleItems() { - return this.getAllItems().filter((item) => item.classList.contains("is-idle")); - } - - isItemAbove(item) { - return item.hasAttribute("data-is-above"); - } - - isItemToggled(item) { - return item.hasAttribute("data-is-toggled"); - } - - on(source, event, listener, options) { - listener = listener.bind(this); - source.addEventListener(event, listener, options); - return () => source.removeEventListener(event, listener); - } - - dragStart(e) { - if (e.target.classList.contains(this.handleClass)) { - this.draggableItem = e.target.closest(this.itemSelector); - } - - if (!this.draggableItem) return; - - this.pointerStartX = e.clientX || e.touches[0].clientX; - this.pointerStartY = e.clientY || e.touches[0].clientY; - this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight; - - this.setItemsGap(); - this.initDraggableItem(); - this.initItemsState(); - - this.offDrag.push(this.on(document, "mousemove", this.drag)); - this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false })); - - this.dispatchEvent( - new CustomEvent("dragstart", { - detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) }, - }) - ); - } - - setItemsGap() { - if (this.getIdleItems().length <= 1) { - this.itemsGap = 0; - return; - } - - const item1 = this.getIdleItems()[0]; - const item2 = this.getIdleItems()[1]; - - const item1Rect = item1.getBoundingClientRect(); - const item2Rect = item2.getBoundingClientRect(); - - this.itemsGap = Math.abs(item1Rect.bottom - item2Rect.top); - } - - initItemsState() { - this.getIdleItems().forEach((item, i) => { - if (this.getAllItems().indexOf(this.draggableItem) > i) { - item.dataset.isAbove = ""; - } - }); - } - - initDraggableItem() { - this.draggableItem.classList.remove("is-idle"); - this.draggableItem.classList.add("is-draggable"); - } - - drag(e) { - if (!this.draggableItem) return; - - e.preventDefault(); - - const clientX = e.clientX || e.touches[0].clientX; - const clientY = e.clientY || e.touches[0].clientY; - - const listRect = this.listContainer.getBoundingClientRect(); - - if (clientY > listRect.bottom) { - if (this.listContainer.scrollTop < this.scrollYMax) { - this.listContainer.scrollBy(0, 10); - this.pointerStartY -= 10; - } - } else if (clientY < listRect.top && this.listContainer.scrollTop > 0) { - this.pointerStartY += 10; - this.listContainer.scrollBy(0, -10); - } - - const pointerOffsetX = clientX - this.pointerStartX; - const pointerOffsetY = clientY - this.pointerStartY; - - this.updateIdleItemsStateAndPosition(); - this.draggableItem.style.transform = `translate(${pointerOffsetX}px, ${pointerOffsetY}px)`; - } - - updateIdleItemsStateAndPosition() { - const draggableItemRect = this.draggableItem.getBoundingClientRect(); - const draggableItemY = draggableItemRect.top + draggableItemRect.height / 2; - - // Update state - this.getIdleItems().forEach((item) => { - const itemRect = item.getBoundingClientRect(); - const itemY = itemRect.top + itemRect.height / 2; - if (this.isItemAbove(item)) { - if (draggableItemY <= itemY) { - item.dataset.isToggled = ""; - } else { - delete item.dataset.isToggled; - } - } else { - if (draggableItemY >= itemY) { - item.dataset.isToggled = ""; - } else { - delete item.dataset.isToggled; - } - } - }); - - // Update position - this.getIdleItems().forEach((item) => { - if (this.isItemToggled(item)) { - const direction = this.isItemAbove(item) ? 1 : -1; - item.style.transform = `translateY(${direction * (draggableItemRect.height + this.itemsGap)}px)`; - } else { - item.style.transform = ""; - } - }); - } - - dragEnd() { - if (!this.draggableItem) return; - - this.applyNewItemsOrder(); - this.cleanup(); - } - - applyNewItemsOrder() { - const reorderedItems = []; - - let oldPosition = -1; - this.getAllItems().forEach((item, index) => { - if (item === this.draggableItem) { - oldPosition = index; - return; - } - if (!this.isItemToggled(item)) { - reorderedItems[index] = item; - return; - } - const newIndex = this.isItemAbove(item) ? index + 1 : index - 1; - reorderedItems[newIndex] = item; - }); - - for (let index = 0; index < this.getAllItems().length; index++) { - const item = reorderedItems[index]; - if (typeof item === "undefined") { - reorderedItems[index] = this.draggableItem; - } - } - - reorderedItems.forEach((item) => { - this.listContainer.appendChild(item); - }); - - this.items = reorderedItems; - - this.dispatchEvent( - new CustomEvent("dragend", { - detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) }, - }) - ); - } - - cleanup() { - this.itemsGap = 0; - this.items = []; - this.unsetDraggableItem(); - this.unsetItemState(); - - this.offDrag.forEach((f) => f()); - this.offDrag = []; - } - - unsetDraggableItem() { - this.draggableItem.style = null; - this.draggableItem.classList.remove("is-draggable"); - this.draggableItem.classList.add("is-idle"); - this.draggableItem = null; - } - - unsetItemState() { - this.getIdleItems().forEach((item, i) => { - delete item.dataset.isAbove; - delete item.dataset.isToggled; - item.style.transform = ""; - }); - } - - dispose() { - this.off.forEach((f) => f()); - } -} diff --git a/web/scripts/ui/imagePreview.js b/web/scripts/ui/imagePreview.js deleted file mode 100644 index 2a7f66b8f..000000000 --- a/web/scripts/ui/imagePreview.js +++ /dev/null @@ -1,97 +0,0 @@ -import { $el } from "../ui.js"; - -export function calculateImageGrid(imgs, dw, dh) { - let best = 0; - let w = imgs[0].naturalWidth; - let h = imgs[0].naturalHeight; - const numImages = imgs.length; - - let cellWidth, cellHeight, cols, rows, shiftX; - // compact style - for (let c = 1; c <= numImages; c++) { - const r = Math.ceil(numImages / c); - const cW = dw / c; - const cH = dh / r; - const scaleX = cW / w; - const scaleY = cH / h; - - const scale = Math.min(scaleX, scaleY, 1); - const imageW = w * scale; - const imageH = h * scale; - const area = imageW * imageH * numImages; - - if (area > best) { - best = area; - cellWidth = imageW; - cellHeight = imageH; - cols = c; - rows = r; - shiftX = c * ((cW - imageW) / 2); - } - } - - return { cellWidth, cellHeight, cols, rows, shiftX }; -} - -export function createImageHost(node) { - const el = $el("div.comfy-img-preview"); - let currentImgs; - let first = true; - - function updateSize() { - let w = null; - let h = null; - - if (currentImgs) { - let elH = el.clientHeight; - if (first) { - first = false; - // On first run, if we are small then grow a bit - if (elH < 190) { - elH = 190; - } - el.style.setProperty("--comfy-widget-min-height", elH); - } else { - el.style.setProperty("--comfy-widget-min-height", null); - } - - const nw = node.size[0]; - ({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH)); - w += "px"; - h += "px"; - - el.style.setProperty("--comfy-img-preview-width", w); - el.style.setProperty("--comfy-img-preview-height", h); - } - } - return { - el, - updateImages(imgs) { - if (imgs !== currentImgs) { - if (currentImgs == null) { - requestAnimationFrame(() => { - updateSize(); - }); - } - el.replaceChildren(...imgs); - currentImgs = imgs; - node.onResize(node.size); - node.graph.setDirtyCanvas(true, true); - } - }, - getHeight() { - updateSize(); - }, - onDraw() { - // Element from point uses a hittest find elements so we need to toggle pointer events - el.style.pointerEvents = "all"; - const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]); - el.style.pointerEvents = "none"; - - if(!over) return; - // Set the overIndex so Open Image etc work - const idx = currentImgs.indexOf(over); - node.overIndex = idx; - }, - }; -} diff --git a/web/scripts/ui/menu/index.js b/web/scripts/ui/menu/index.js deleted file mode 100644 index 1e00b3d22..000000000 --- a/web/scripts/ui/menu/index.js +++ /dev/null @@ -1,302 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { downloadBlob } from "../../utils.js"; -import { ComfyButton } from "../components/button.js"; -import { ComfyButtonGroup } from "../components/buttonGroup.js"; -import { ComfySplitButton } from "../components/splitButton.js"; -import { ComfyViewHistoryButton } from "./viewHistory.js"; -import { ComfyQueueButton } from "./queueButton.js"; -import { ComfyWorkflowsMenu } from "./workflows.js"; -import { ComfyViewQueueButton } from "./viewQueue.js"; -import { getInteruptButton } from "./interruptButton.js"; - -const collapseOnMobile = (t) => { - (t.element ?? t).classList.add("comfyui-menu-mobile-collapse"); - return t; -}; -const showOnMobile = (t) => { - (t.element ?? t).classList.add("lt-lg-show"); - return t; -}; - -export class ComfyAppMenu { - #sizeBreak = "lg"; - #lastSizeBreaks = { - lg: null, - md: null, - sm: null, - xs: null, - }; - #sizeBreaks = Object.keys(this.#lastSizeBreaks); - #cachedInnerSize = null; - #cacheTimeout = null; - - /** - * @param { import("../../app.js").ComfyApp } app - */ - constructor(app) { - this.app = app; - - this.workflows = new ComfyWorkflowsMenu(app); - const getSaveButton = (t) => - new ComfyButton({ - icon: "content-save", - tooltip: "Save the current workflow", - action: () => app.workflowManager.activeWorkflow.save(), - content: t, - }); - - this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI"); - this.saveButton = new ComfySplitButton( - { - primary: getSaveButton(), - mode: "hover", - position: "absolute", - }, - getSaveButton("Save"), - new ComfyButton({ - icon: "content-save-edit", - content: "Save As", - tooltip: "Save the current graph as a new workflow", - action: () => app.workflowManager.activeWorkflow.save(true), - }), - new ComfyButton({ - icon: "download", - content: "Export", - tooltip: "Export the current workflow as JSON", - action: () => this.exportWorkflow("workflow", "workflow"), - }), - new ComfyButton({ - icon: "api", - content: "Export (API Format)", - tooltip: "Export the current workflow as JSON for use with the ComfyUI API", - action: () => this.exportWorkflow("workflow_api", "output"), - visibilitySetting: { id: "Comfy.DevMode", showValue: true }, - app, - }) - ); - this.actionsGroup = new ComfyButtonGroup( - new ComfyButton({ - icon: "refresh", - content: "Refresh", - tooltip: "Refresh widgets in nodes to find new models or files", - action: () => app.refreshComboInNodes(), - }), - new ComfyButton({ - icon: "clipboard-edit-outline", - content: "Clipspace", - tooltip: "Open Clipspace window", - action: () => app["openClipspace"](), - }), - new ComfyButton({ - icon: "fit-to-page-outline", - content: "Reset View", - tooltip: "Reset the canvas view", - action: () => app.resetView(), - }), - new ComfyButton({ - icon: "cancel", - content: "Clear", - tooltip: "Clears current workflow", - action: () => { - if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) { - app.clean(); - app.graph.clear(); - } - }, - }) - ); - this.settingsGroup = new ComfyButtonGroup( - new ComfyButton({ - icon: "cog", - content: "Settings", - tooltip: "Open settings", - action: () => { - app.ui.settings.show(); - }, - }) - ); - this.viewGroup = new ComfyButtonGroup( - new ComfyViewHistoryButton(app).element, - new ComfyViewQueueButton(app).element, - getInteruptButton("nlg-hide").element - ); - this.mobileMenuButton = new ComfyButton({ - icon: "menu", - action: (_, btn) => { - btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu"; - window.dispatchEvent(new Event("resize")); - }, - classList: "comfyui-button comfyui-menu-button", - }); - - this.element = $el("nav.comfyui-menu.lg", { style: { display: "none" } }, [ - this.logo, - this.workflows.element, - this.saveButton.element, - collapseOnMobile(this.actionsGroup).element, - $el("section.comfyui-menu-push"), - collapseOnMobile(this.settingsGroup).element, - collapseOnMobile(this.viewGroup).element, - - getInteruptButton("lt-lg-show").element, - new ComfyQueueButton(app).element, - showOnMobile(this.mobileMenuButton).element, - ]); - - let resizeHandler; - this.menuPositionSetting = app.ui.settings.addSetting({ - id: "Comfy.UseNewMenu", - defaultValue: "Disabled", - name: "[Beta] Use new menu and workflow management. Note: On small screens the menu will always be at the top.", - type: "combo", - options: ["Disabled", "Top", "Bottom"], - onChange: async (v) => { - if (v && v !== "Disabled") { - if (!resizeHandler) { - resizeHandler = () => { - this.calculateSizeBreak(); - }; - window.addEventListener("resize", resizeHandler); - } - this.updatePosition(v); - } else { - if (resizeHandler) { - window.removeEventListener("resize", resizeHandler); - resizeHandler = null; - } - document.body.style.removeProperty("display"); - app.ui.menuContainer.style.removeProperty("display"); - this.element.style.display = "none"; - app.ui.restoreMenuPosition(); - } - window.dispatchEvent(new Event("resize")); - }, - }); - } - - updatePosition(v) { - document.body.style.display = "grid"; - this.app.ui.menuContainer.style.display = "none"; - this.element.style.removeProperty("display"); - this.position = v; - if (v === "Bottom") { - this.app.bodyBottom.append(this.element); - } else { - this.app.bodyTop.prepend(this.element); - } - this.calculateSizeBreak(); - } - - updateSizeBreak(idx, prevIdx, direction) { - const newSize = this.#sizeBreaks[idx]; - if (newSize === this.#sizeBreak) return; - this.#cachedInnerSize = null; - clearTimeout(this.#cacheTimeout); - - this.#sizeBreak = this.#sizeBreaks[idx]; - for (let i = 0; i < this.#sizeBreaks.length; i++) { - const sz = this.#sizeBreaks[i]; - if (sz === this.#sizeBreak) { - this.element.classList.add(sz); - } else { - this.element.classList.remove(sz); - } - if (i < idx) { - this.element.classList.add("lt-" + sz); - } else { - this.element.classList.remove("lt-" + sz); - } - } - - if (idx) { - // We're on a small screen, force the menu at the top - if (this.position !== "Top") { - this.updatePosition("Top"); - } - } else if (this.position != this.menuPositionSetting.value) { - // Restore user position - this.updatePosition(this.menuPositionSetting.value); - } - - // Allow multiple updates, but prevent bouncing - if (!direction) { - direction = prevIdx - idx; - } else if (direction != prevIdx - idx) { - return; - } - this.calculateSizeBreak(direction); - } - - calculateSizeBreak(direction = 0) { - let idx = this.#sizeBreaks.indexOf(this.#sizeBreak); - const currIdx = idx; - const innerSize = this.calculateInnerSize(idx); - if (window.innerWidth >= this.#lastSizeBreaks[this.#sizeBreaks[idx - 1]]) { - if (idx > 0) { - idx--; - } - } else if (innerSize > this.element.clientWidth) { - this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize); - // We need to shrink - if (idx < this.#sizeBreaks.length - 1) { - idx++; - } - } - - this.updateSizeBreak(idx, currIdx, direction); - } - - calculateInnerSize(idx) { - // Cache the inner size to prevent too much calculation when resizing the window - clearTimeout(this.#cacheTimeout); - if (this.#cachedInnerSize) { - // Extend cache time - this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); - } else { - let innerSize = 0; - let count = 1; - for (const c of this.element.children) { - if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push - if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items - innerSize += c.clientWidth; - count++; - } - innerSize += 8 * count; - this.#cachedInnerSize = innerSize; - this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); - } - return this.#cachedInnerSize; - } - - /** - * @param {string} defaultName - */ - getFilename(defaultName) { - if (this.app.ui.settings.getSettingValue("Comfy.PromptFilename", true)) { - defaultName = prompt("Save workflow as:", defaultName); - if (!defaultName) return; - if (!defaultName.toLowerCase().endsWith(".json")) { - defaultName += ".json"; - } - } - return defaultName; - } - - /** - * @param {string} [filename] - * @param { "workflow" | "output" } [promptProperty] - */ - async exportWorkflow(filename, promptProperty) { - if (this.app.workflowManager.activeWorkflow?.path) { - filename = this.app.workflowManager.activeWorkflow.name; - } - const p = await this.app.graphToPrompt(); - const json = JSON.stringify(p[promptProperty], null, 2); - const blob = new Blob([json], { type: "application/json" }); - const file = this.getFilename(filename); - if (!file) return; - downloadBlob(file, blob); - } -} diff --git a/web/scripts/ui/menu/interruptButton.js b/web/scripts/ui/menu/interruptButton.js deleted file mode 100644 index 4db3328db..000000000 --- a/web/scripts/ui/menu/interruptButton.js +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-check - -import { api } from "../../api.js"; -import { ComfyButton } from "../components/button.js"; - -export function getInteruptButton(visibility) { - const btn = new ComfyButton({ - icon: "close", - tooltip: "Cancel current generation", - enabled: false, - action: () => { - api.interrupt(); - }, - classList: ["comfyui-button", "comfyui-interrupt-button", visibility], - }); - - api.addEventListener("status", ({ detail }) => { - const sz = detail?.exec_info?.queue_remaining; - btn.enabled = sz > 0; - }); - - return btn; -} diff --git a/web/scripts/ui/menu/menu.css b/web/scripts/ui/menu/menu.css deleted file mode 100644 index 1d8b9ad0a..000000000 --- a/web/scripts/ui/menu/menu.css +++ /dev/null @@ -1,710 +0,0 @@ -.relative { - position: relative; -} -.hidden { - display: none !important; -} -.mdi.rotate270::before { - transform: rotate(270deg); -} - -/* Generic */ -.comfyui-button { - display: flex; - align-items: center; - gap: 0.5em; - cursor: pointer; - border: none; - border-radius: 4px; - padding: 4px 8px; - box-sizing: border-box; - margin: 0; - transition: box-shadow 0.1s; -} - -.comfyui-button:active { - box-shadow: inset 1px 1px 10px rgba(0, 0, 0, 0.5); -} -.comfyui-button:disabled { - opacity: 0.5; - cursor: not-allowed; -} -.primary .comfyui-button, -.primary.comfyui-button { - background-color: var(--primary-bg) !important; - color: var(--primary-fg) !important; -} - -.primary .comfyui-button:not(:disabled):hover, -.primary.comfyui-button:not(:disabled):hover { - background-color: var(--primary-hover-bg) !important; - color: var(--primary-hover-fg) !important; -} - -/* Popup */ -.comfyui-popup { - position: absolute; - left: var(--left); - right: var(--right); - top: var(--top); - bottom: var(--bottom); - z-index: 2000; - max-height: calc(100vh - var(--limit) - 10px); - box-shadow: 3px 3px 5px 0px rgba(0, 0, 0, 0.3); -} - -.comfyui-popup:not(.open) { - display: none; -} - -.comfyui-popup.right.open { - border-top-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - overflow: hidden; -} -/* Split button */ -.comfyui-split-button { - position: relative; - display: flex; -} - -.comfyui-split-primary { - flex: auto; -} - -.comfyui-split-primary .comfyui-button { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: 1px solid var(--comfy-menu-bg); - width: 100%; -} - -.comfyui-split-arrow .comfyui-button { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - padding-left: 2px; - padding-right: 2px; -} - -.comfyui-split-button-popup { - white-space: nowrap; - background-color: var(--content-bg); - color: var(--content-fg); - display: flex; - flex-direction: column; - overflow: auto; -} - -.comfyui-split-button-popup.hover { - z-index: 2001; -} -.comfyui-split-button-popup > .comfyui-button { - border: none; - background-color: transparent; - color: var(--fg-color); - padding: 8px 12px 8px 8px; -} - -.comfyui-split-button-popup > .comfyui-button:not(:disabled):hover { - background-color: var(--comfy-input-bg); -} - -/* Button group */ -.comfyui-button-group { - display: flex; - border-radius: 4px; - overflow: hidden; -} - -.comfyui-button-group > .comfyui-button, -.comfyui-button-group > .comfyui-button-wrapper > .comfyui-button { - padding: 4px 10px; - border-radius: 0; -} - -/* Menu */ -.comfyui-menu { - width: 100vw; - background: var(--comfy-menu-bg); - color: var(--fg-color); - font-family: Arial, Helvetica, sans-serif; - font-size: 0.8em; - display: flex; - padding: 4px 8px; - align-items: center; - gap: 8px; - box-sizing: border-box; - z-index: 1000; - order: 0; - grid-column: 1/-1; - overflow: auto; - max-height: 90vh; -} - -.comfyui-menu>* { - flex-shrink: 0; -} -.comfyui-menu .mdi::before { - font-size: 18px; -} - -.comfyui-menu .comfyui-button { - background: var(--comfy-input-bg); - color: var(--fg-color); - white-space: nowrap; -} - -.comfyui-menu .comfyui-button:not(:disabled):hover { - background: var(--border-color); - color: var(--content-fg); -} - -.comfyui-menu .comfyui-split-button-popup > .comfyui-button { - border-radius: 0; - background-color: transparent; -} - -.comfyui-menu .comfyui-split-button-popup > .comfyui-button:not(:disabled):hover { - background-color: var(--comfy-input-bg); -} - -.comfyui-menu .comfyui-split-button-popup.left { - border-top-right-radius: 4px; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; -} - -.comfyui-menu .comfyui-button.popup-open { - background-color: var(--content-bg); - color: var(--content-fg); -} - -.comfyui-menu-push { - margin-left: -0.8em; - flex: auto; -} - -.comfyui-logo { - font-size: 1.2em; - margin: 0; - user-select: none; - cursor: default; -} - -/* Workflows */ -.comfyui-workflows-button { - flex-direction: row-reverse; - max-width: 200px; - position: relative; - z-index: 0; -} - -.comfyui-workflows-button.popup-open { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} -.comfyui-workflows-button.unsaved { - font-style: italic; -} -.comfyui-workflows-button-progress { - position: absolute; - top: 0; - left: 0; - background-color: green; - height: 100%; - border-radius: 4px; - z-index: -1; -} - -.comfyui-workflows-button > span { - flex: auto; - text-align: left; - overflow: hidden; -} -.comfyui-workflows-button-inner { - display: flex; - align-items: center; - gap: 7px; - width: 150px; -} -.comfyui-workflows-label { - overflow: hidden; - text-overflow: ellipsis; - direction: rtl; - flex: auto; - position: relative; -} - -.comfyui-workflows-button.unsaved .comfyui-workflows-label { - padding-left: 8px; -} - -.comfyui-workflows-button.unsaved .comfyui-workflows-label:after { - content: "*"; - position: absolute; - top: 0; - left: 0; -} -.comfyui-workflows-button-inner .mdi-graph::before { - transform: rotate(-90deg); -} - -.comfyui-workflows-popup { - font-family: Arial, Helvetica, sans-serif; - font-size: 0.8em; - padding: 10px; - overflow: auto; - background-color: var(--content-bg); - color: var(--content-fg); - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - z-index: 400; -} - -.comfyui-workflows-panel { - min-height: 150px; -} - -.comfyui-workflows-panel .lds-ring { - transform: translate(-50%); - position: absolute; - left: 50%; - top: 75px; -} - -.comfyui-workflows-panel h3 { - margin: 10px 0 10px 0; - font-size: 11px; - opacity: 0.8; -} - -.comfyui-workflows-panel section header { - display: flex; - justify-content: space-between; - align-items: center; -} -.comfy-ui-workflows-search .mdi { - position: relative; - top: 2px; - pointer-events: none; -} -.comfy-ui-workflows-search input { - background-color: var(--comfy-input-bg); - color: var(--input-text); - border: none; - border-radius: 4px; - padding: 4px 10px; - margin-left: -24px; - text-indent: 18px; -} -.comfy-ui-workflows-search input:placeholder-shown { - width: 10px; -} -.comfy-ui-workflows-search input:placeholder-shown:focus { - width: auto; -} -.comfyui-workflows-actions { - display: flex; - gap: 10px; - margin-bottom: 10px; -} - -.comfyui-workflows-actions .comfyui-button { - background: var(--comfy-input-bg); - color: var(--input-text); -} - -.comfyui-workflows-actions .comfyui-button:not(:disabled):hover { - background: var(--primary-bg); - color: var(--primary-fg); -} - -.comfyui-workflows-favorites, -.comfyui-workflows-open { - border-bottom: 1px solid var(--comfy-input-bg); - padding-bottom: 5px; - margin-bottom: 5px; -} - -.comfyui-workflows-open .active { - font-weight: bold; - color: var(--primary-fg); -} - -.comfyui-workflows-favorites:empty { - display: none; -} - -.comfyui-workflows-tree { - padding: 0; - margin: 0; -} - -.comfyui-workflows-tree:empty::after { - content: "No saved workflows"; - display: block; - text-align: center; -} -.comfyui-workflows-tree > ul { - padding: 0; -} - -.comfyui-workflows-tree > ul ul { - margin: 0; - padding: 0 0 0 25px; -} - -.comfyui-workflows-tree:not(.filtered) .closed > ul { - display: none; -} - -.comfyui-workflows-tree li, -.comfyui-workflows-tree-file { - --item-height: 32px; - list-style-type: none; - height: var(--item-height); - display: flex; - align-items: center; - gap: 5px; - cursor: pointer; - user-select: none; -} - -.comfyui-workflows-tree-file.active::before, -.comfyui-workflows-tree li:hover::before, -.comfyui-workflows-tree-file:hover::before { - content: ""; - position: absolute; - width: 100%; - left: 0; - height: var(--item-height); - background-color: var(--content-hover-bg); - color: var(--content-hover-fg); - z-index: -1; -} - -.comfyui-workflows-tree-file.active::before { - background-color: var(--primary-bg); - color: var(--primary-fg); -} - -.comfyui-workflows-tree-file.running:not(:hover)::before { - content: ""; - position: absolute; - width: var(--progress, 0); - left: 0; - height: var(--item-height); - background-color: green; - z-index: -1; -} - -.comfyui-workflows-tree-file.unsaved span { - font-style: italic; -} - -.comfyui-workflows-tree-file span { - flex: auto; -} - -.comfyui-workflows-tree-file span + .comfyui-workflows-file-action { - margin-left: 10px; -} - -.comfyui-workflows-tree-file .comfyui-workflows-file-action { - background-color: transparent; - color: var(--fg-color); - padding: 2px 4px; -} - -.comfyui-workflows-tree-file.active .comfyui-workflows-file-action { - color: var(--primary-fg); -} - -.lg ~ .comfyui-workflows-popup .comfyui-workflows-tree-file:not(:hover) .comfyui-workflows-file-action { - opacity: 0; -} - -.comfyui-workflows-tree-file .comfyui-workflows-file-action:hover { - background-color: var(--primary-bg); - color: var(--primary-fg); -} - -.comfyui-workflows-tree-file .comfyui-workflows-file-action-primary { - background-color: transparent; - color: var(--fg-color); - padding: 2px 4px; - margin: 0 -4px; -} - -.comfyui-workflows-file-action-favorite .mdi-star { - color: orange; -} - -/* View List */ -.comfyui-view-list-popup { - padding: 10px; - background-color: var(--content-bg); - color: var(--content-fg); - min-width: 170px; - min-height: 435px; - display: flex; - flex-direction: column; - align-items: center; - box-sizing: border-box; -} -.comfyui-view-list-popup h3 { - margin: 0 0 5px 0; -} -.comfyui-view-list-items { - width: 100%; - background: var(--comfy-menu-bg); - border-radius: 5px; - display: flex; - justify-content: center; - flex: auto; - align-items: center; - flex-direction: column; -} -.comfyui-view-list-items section { - max-height: 400px; - overflow: auto; - width: 100%; - display: grid; - grid-template-columns: auto auto auto; - align-items: center; - justify-content: center; - gap: 5px; - padding: 5px 0; -} -.comfyui-view-list-items section + section { - border-top: 1px solid var(--border-color); - margin-top: 10px; - padding-top: 5px; -} -.comfyui-view-list-items section h5 { - grid-column: 1 / 4; - text-align: center; - margin: 5px; -} -.comfyui-view-list-items span { - text-align: center; - padding: 0 2px; -} -.comfyui-view-list-popup header { - margin-bottom: 10px; - display: flex; - gap: 5px; -} -.comfyui-view-list-popup header .comfyui-button { - border: 1px solid transparent; -} -.comfyui-view-list-popup header .comfyui-button:not(:disabled):hover { - border: 1px solid var(--comfy-menu-bg); -} -/* Queue button */ -.comfyui-queue-button .comfyui-split-primary .comfyui-button { - padding-right: 12px; -} -.comfyui-queue-count { - margin-left: 5px; - border-radius: 10px; - background-color: rgb(8, 80, 153); - padding: 2px 4px; - font-size: 10px; - min-width: 1em; - display: inline-block; -} -/* Queue options*/ -.comfyui-queue-options { - padding: 10px; - font-family: Arial, Helvetica, sans-serif; - font-size: 12px; - display: flex; - gap: 10px; -} - -.comfyui-queue-batch { - display: flex; - flex-direction: column; - border-right: 1px solid var(--comfy-menu-bg); - padding-right: 10px; - gap: 5px; -} - -.comfyui-queue-batch input { - width: 145px; -} - -.comfyui-queue-batch .comfyui-queue-batch-value { - width: 70px; -} - -.comfyui-queue-mode { - display: flex; - flex-direction: column; -} - -.comfyui-queue-mode span { - font-weight: bold; - margin-bottom: 2px; -} - -.comfyui-queue-mode label { - display: flex; - flex-direction: row-reverse; - justify-content: start; - gap: 5px; - padding: 2px 0; -} - -.comfyui-queue-mode label input { - padding: 0; - margin: 0; -} - -/** Send to workflow widget selection dialog */ -.comfy-widget-selection-dialog { - border: none; -} - -.comfy-widget-selection-dialog div { - color: var(--fg-color); - font-family: Arial, Helvetica, sans-serif; -} - -.comfy-widget-selection-dialog h2 { - margin-top: 0; -} - -.comfy-widget-selection-dialog section { - width: fit-content; - display: flex; - flex-direction: column; -} - -.comfy-widget-selection-item { - display: flex; - gap: 10px; - align-items: center; -} - -.comfy-widget-selection-item span { - margin-right: auto; -} - -.comfy-widget-selection-item span::before { - content: '#' attr(data-id); - opacity: 0.5; - margin-right: 5px; -} - -.comfy-modal .comfy-widget-selection-item button { - font-size: 1em; -} - -/***** Responsive *****/ -.lg.comfyui-menu .lt-lg-show { - display: none !important; -} -.comfyui-menu:not(.lg) .nlg-hide { - display: none !important; -} -/** Large screen */ -.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-button span, -.lg.comfyui-menu>.comfyui-menu-mobile-collapse.comfyui-button span { - display: none; -} -.lg.comfyui-menu>.comfyui-menu-mobile-collapse .comfyui-popup .comfyui-button span { - display: unset; -} - -/** Non large screen */ -.lt-lg.comfyui-menu { - flex-wrap: wrap; -} - -.lt-lg.comfyui-menu > *:not(.comfyui-menu-mobile-collapse) { - order: 1; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse { - order: 9999; - width: 100%; -} - -.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse { - order: -1; -} - -.comfyui-body-bottom .lt-lg.comfyui-menu > .comfyui-menu-button { - top: unset; - bottom: 4px; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button-group { - flex-wrap: wrap; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button, -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse.comfyui-button { - padding: 10px; -} -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button, -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-button-wrapper { - width: 100%; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-popup { - position: static; - background-color: var(--comfy-input-bg); - max-width: unset; - max-height: 50vh; - overflow: auto; -} - -.lt-lg.comfyui-menu:not(.expanded) > .comfyui-menu-mobile-collapse { - display: none; -} - -.lt-lg .comfyui-queue-button { - margin-right: 44px; -} - -.lt-lg .comfyui-menu-button { - position: absolute; - top: 4px; - right: 8px; -} - -.lt-lg.comfyui-menu > .comfyui-menu-mobile-collapse .comfyui-view-list-popup { - border-radius: 0; -} - -.lt-lg.comfyui-menu .comfyui-workflows-popup { - width: 100vw; -} - -/** Small */ -.lt-md .comfyui-workflows-button-inner { - width: unset !important; -} -.lt-md .comfyui-workflows-label { - display: none; -} - -/** Extra small */ -.lt-sm .comfyui-queue-button { - margin-right: 0; - width: 100%; -} -.lt-sm .comfyui-queue-button .comfyui-button { - justify-content: center; -} -.lt-sm .comfyui-interrupt-button { - margin-right: 45px; -} -.comfyui-body-bottom .lt-sm.comfyui-menu > .comfyui-menu-button{ - bottom: 41px; -} \ No newline at end of file diff --git a/web/scripts/ui/menu/queueButton.js b/web/scripts/ui/menu/queueButton.js deleted file mode 100644 index 608f4cc9b..000000000 --- a/web/scripts/ui/menu/queueButton.js +++ /dev/null @@ -1,93 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { $el } from "../../ui.js"; -import { api } from "../../api.js"; -import { ComfySplitButton } from "../components/splitButton.js"; -import { ComfyQueueOptions } from "./queueOptions.js"; -import { prop } from "../../utils.js"; - -export class ComfyQueueButton { - element = $el("div.comfyui-queue-button"); - #internalQueueSize = 0; - - queuePrompt = async (e) => { - this.#internalQueueSize += this.queueOptions.batchCount; - // Hold shift to queue front, event is undefined when auto-queue is enabled - await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount); - }; - - constructor(app) { - this.app = app; - this.queueSizeElement = $el("span.comfyui-queue-count", { - textContent: "?", - }); - - const queue = new ComfyButton({ - content: $el("div", [ - $el("span", { - textContent: "Queue", - }), - this.queueSizeElement, - ]), - icon: "play", - classList: "comfyui-button", - action: this.queuePrompt, - }); - - this.queueOptions = new ComfyQueueOptions(app); - - const btn = new ComfySplitButton( - { - primary: queue, - mode: "click", - position: "absolute", - horizontal: "right", - }, - this.queueOptions.element - ); - btn.element.classList.add("primary"); - this.element.append(btn.element); - - this.autoQueueMode = prop(this, "autoQueueMode", "", () => { - switch (this.autoQueueMode) { - case "instant": - queue.icon = "infinity"; - break; - case "change": - queue.icon = "auto-mode"; - break; - default: - queue.icon = "play"; - break; - } - }); - - this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"])); - - api.addEventListener("graphChanged", () => { - if (this.autoQueueMode === "change") { - if (this.#internalQueueSize) { - this.graphHasChanged = true; - } else { - this.graphHasChanged = false; - this.queuePrompt(); - } - } - }); - - api.addEventListener("status", ({ detail }) => { - this.#internalQueueSize = detail?.exec_info?.queue_remaining; - if (this.#internalQueueSize != null) { - this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + ""; - this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`; - if (!this.#internalQueueSize && !app.lastExecutionError) { - if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) { - this.graphHasChanged = false; - this.queuePrompt(); - } - } - } - }); - } -} diff --git a/web/scripts/ui/menu/queueOptions.js b/web/scripts/ui/menu/queueOptions.js deleted file mode 100644 index f3d34e74e..000000000 --- a/web/scripts/ui/menu/queueOptions.js +++ /dev/null @@ -1,77 +0,0 @@ -// @ts-check - -import { $el } from "../../ui.js"; -import { prop } from "../../utils.js"; - -export class ComfyQueueOptions extends EventTarget { - element = $el("div.comfyui-queue-options"); - - constructor(app) { - super(); - this.app = app; - - this.batchCountInput = $el("input", { - className: "comfyui-queue-batch-value", - type: "number", - min: "1", - value: "1", - oninput: () => (this.batchCount = +this.batchCountInput.value), - }); - - this.batchCountRange = $el("input", { - type: "range", - min: "1", - max: "100", - value: "1", - oninput: () => (this.batchCount = +this.batchCountRange.value), - }); - - this.element.append( - $el("div.comfyui-queue-batch", [ - $el( - "label", - { - textContent: "Batch count: ", - }, - this.batchCountInput - ), - this.batchCountRange, - ]) - ); - - const createOption = (text, value, checked = false) => - $el( - "label", - { textContent: text }, - $el("input", { - type: "radio", - name: "AutoQueueMode", - checked, - value, - oninput: (e) => (this.autoQueueMode = e.target["value"]), - }) - ); - - this.autoQueueEl = $el("div.comfyui-queue-mode", [ - $el("span", "Auto Queue:"), - createOption("Disabled", "", true), - createOption("Instant", "instant"), - createOption("On Change", "change"), - ]); - - this.element.append(this.autoQueueEl); - - this.batchCount = prop(this, "batchCount", 1, () => { - this.batchCountInput.value = this.batchCount + ""; - this.batchCountRange.value = this.batchCount + ""; - }); - - this.autoQueueMode = prop(this, "autoQueueMode", "Disabled", () => { - this.dispatchEvent( - new CustomEvent("autoQueueMode", { - detail: this.autoQueueMode, - }) - ); - }); - } -} diff --git a/web/scripts/ui/menu/viewHistory.js b/web/scripts/ui/menu/viewHistory.js deleted file mode 100644 index de6b343dc..000000000 --- a/web/scripts/ui/menu/viewHistory.js +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { ComfyViewList, ComfyViewListButton } from "./viewList.js"; - -export class ComfyViewHistoryButton extends ComfyViewListButton { - constructor(app) { - super(app, { - button: new ComfyButton({ - content: "View History", - icon: "history", - tooltip: "View history", - classList: "comfyui-button comfyui-history-button", - }), - list: ComfyViewHistoryList, - mode: "History", - }); - } -} - -export class ComfyViewHistoryList extends ComfyViewList { - async loadItems() { - const items = await super.loadItems(); - items["History"].reverse(); - return items; - } -} diff --git a/web/scripts/ui/menu/viewList.js b/web/scripts/ui/menu/viewList.js deleted file mode 100644 index 693ca335c..000000000 --- a/web/scripts/ui/menu/viewList.js +++ /dev/null @@ -1,203 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { $el } from "../../ui.js"; -import { api } from "../../api.js"; -import { ComfyPopup } from "../components/popup.js"; - -export class ComfyViewListButton { - get open() { - return this.popup.open; - } - - set open(open) { - this.popup.open = open; - } - - constructor(app, { button, list, mode }) { - this.app = app; - this.button = button; - this.element = $el("div.comfyui-button-wrapper", this.button.element); - this.popup = new ComfyPopup({ - target: this.element, - container: this.element, - horizontal: "right", - }); - this.list = new (list ?? ComfyViewList)(app, mode, this.popup); - this.popup.children = [this.list.element]; - this.popup.addEventListener("open", () => { - this.list.update(); - }); - this.popup.addEventListener("close", () => { - this.list.close(); - }); - this.button.withPopup(this.popup); - - api.addEventListener("status", () => { - if (this.popup.open) { - this.popup.update(); - } - }); - } -} - -export class ComfyViewList { - popup; - - constructor(app, mode, popup) { - this.app = app; - this.mode = mode; - this.popup = popup; - this.type = mode.toLowerCase(); - - this.items = $el(`div.comfyui-${this.type}-items.comfyui-view-list-items`); - this.clear = new ComfyButton({ - icon: "cancel", - content: "Clear", - action: async () => { - this.showSpinner(false); - await api.clearItems(this.type); - await this.update(); - }, - }); - - this.refresh = new ComfyButton({ - icon: "refresh", - content: "Refresh", - action: async () => { - await this.update(false); - }, - }); - - this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [ - $el("h3", mode), - $el("header", [this.clear.element, this.refresh.element]), - this.items, - ]); - - api.addEventListener("status", () => { - if (this.popup.open) { - this.update(); - } - }); - } - - async close() { - this.items.replaceChildren(); - } - - async update(resize = true) { - this.showSpinner(resize); - const res = await this.loadItems(); - let any = false; - - const names = Object.keys(res); - const sections = names - .map((section) => { - const items = res[section]; - if (items?.length) { - any = true; - } else { - return; - } - - const rows = []; - if (names.length > 1) { - rows.push($el("h5", section)); - } - rows.push(...items.flatMap((item) => this.createRow(item, section))); - return $el("section", rows); - }) - .filter(Boolean); - - if (any) { - this.items.replaceChildren(...sections); - } else { - this.items.replaceChildren($el("h5", "None")); - } - - this.popup.update(); - this.clear.enabled = this.refresh.enabled = true; - this.element.style.removeProperty("height"); - } - - showSpinner(resize = true) { - // if (!this.spinner) { - // this.spinner = createSpinner(); - // } - // if (!resize) { - // this.element.style.height = this.element.clientHeight + "px"; - // } - // this.clear.enabled = this.refresh.enabled = false; - // this.items.replaceChildren( - // $el( - // "div", - // { - // style: { - // fontSize: "18px", - // }, - // }, - // this.spinner - // ) - // ); - // this.popup.update(); - } - - async loadItems() { - return await api.getItems(this.type); - } - - getRow(item, section) { - return { - text: item.prompt[0] + "", - actions: [ - { - text: "Load", - action: async () => { - try { - await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); - if (item.outputs) { - this.app.nodeOutputs = item.outputs; - } - } catch (error) { - alert("Error loading workflow: " + error.message); - console.error(error); - } - }, - }, - { - text: "Delete", - action: async () => { - try { - await api.deleteItem(this.type, item.prompt[1]); - this.update(); - } catch (error) {} - }, - }, - ], - }; - } - - createRow = (item, section) => { - const row = this.getRow(item, section); - return [ - $el("span", row.text), - ...row.actions.map( - (a) => - new ComfyButton({ - content: a.text, - action: async (e, btn) => { - btn.enabled = false; - try { - await a.action(); - } catch (error) { - throw error; - } finally { - btn.enabled = true; - } - }, - }).element - ), - ]; - }; -} diff --git a/web/scripts/ui/menu/viewQueue.js b/web/scripts/ui/menu/viewQueue.js deleted file mode 100644 index 97d012986..000000000 --- a/web/scripts/ui/menu/viewQueue.js +++ /dev/null @@ -1,55 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { ComfyViewList, ComfyViewListButton } from "./viewList.js"; -import { api } from "../../api.js"; - -export class ComfyViewQueueButton extends ComfyViewListButton { - constructor(app) { - super(app, { - button: new ComfyButton({ - content: "View Queue", - icon: "format-list-numbered", - tooltip: "View queue", - classList: "comfyui-button comfyui-queue-button", - }), - list: ComfyViewQueueList, - mode: "Queue", - }); - } -} - -export class ComfyViewQueueList extends ComfyViewList { - getRow = (item, section) => { - if (section !== "Running") { - return super.getRow(item, section); - } - return { - text: item.prompt[0] + "", - actions: [ - { - text: "Load", - action: async () => { - try { - await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); - if (item.outputs) { - this.app.nodeOutputs = item.outputs; - } - } catch (error) { - alert("Error loading workflow: " + error.message); - console.error(error); - } - }, - }, - { - text: "Cancel", - action: async () => { - try { - await api.interrupt(); - } catch (error) {} - }, - }, - ], - }; - } -} diff --git a/web/scripts/ui/menu/workflows.js b/web/scripts/ui/menu/workflows.js deleted file mode 100644 index 3b904fb4b..000000000 --- a/web/scripts/ui/menu/workflows.js +++ /dev/null @@ -1,770 +0,0 @@ -// @ts-check - -import { ComfyButton } from "../components/button.js"; -import { prop, getStorageValue, setStorageValue } from "../../utils.js"; -import { $el } from "../../ui.js"; -import { api } from "../../api.js"; -import { ComfyPopup } from "../components/popup.js"; -import { createSpinner } from "../spinner.js"; -import { ComfyWorkflow, trimJsonExt } from "../../workflows.js"; -import { ComfyAsyncDialog } from "../components/asyncDialog.js"; - -export class ComfyWorkflowsMenu { - #first = true; - element = $el("div.comfyui-workflows"); - - get open() { - return this.popup.open; - } - - set open(open) { - this.popup.open = open; - } - - /** - * @param {import("../../app.js").ComfyApp} app - */ - constructor(app) { - this.app = app; - this.#bindEvents(); - - const classList = { - "comfyui-workflows-button": true, - "comfyui-button": true, - unsaved: getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true", - running: false, - }; - this.buttonProgress = $el("div.comfyui-workflows-button-progress"); - this.workflowLabel = $el("span.comfyui-workflows-label", ""); - this.button = new ComfyButton({ - content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]), - icon: "chevron-down", - classList, - }); - - this.element.append(this.button.element); - - this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" }); - this.content = new ComfyWorkflowsContent(app, this.popup); - this.popup.children = [this.content.element]; - this.popup.addEventListener("change", () => { - this.button.icon = "chevron-" + (this.popup.open ? "up" : "down"); - }); - this.button.withPopup(this.popup); - - this.unsaved = prop(this, "unsaved", classList.unsaved, (v) => { - classList.unsaved = v; - this.button.classList = classList; - setStorageValue("Comfy.PreviousWorkflowUnsaved", v); - }); - } - - #updateProgress = () => { - const prompt = this.app.workflowManager.activePrompt; - let percent = 0; - if (this.app.workflowManager.activeWorkflow === prompt?.workflow) { - const total = Object.values(prompt.nodes); - const done = total.filter(Boolean); - percent = (done.length / total.length) * 100; - } - this.buttonProgress.style.width = percent + "%"; - }; - - #updateActive = () => { - const active = this.app.workflowManager.activeWorkflow; - this.button.tooltip = active.path; - this.workflowLabel.textContent = active.name; - this.unsaved = active.unsaved; - - if (this.#first) { - this.#first = false; - this.content.load(); - } - - this.#updateProgress(); - }; - - #bindEvents() { - this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive); - this.app.workflowManager.addEventListener("rename", this.#updateActive); - this.app.workflowManager.addEventListener("delete", this.#updateActive); - - this.app.workflowManager.addEventListener("save", () => { - this.unsaved = this.app.workflowManager.activeWorkflow.unsaved; - }); - - this.app.workflowManager.addEventListener("execute", (e) => { - this.#updateProgress(); - }); - - api.addEventListener("graphChanged", () => { - this.unsaved = true; - }); - } - - #getMenuOptions(callback) { - const menu = []; - const directories = new Map(); - for (const workflow of this.app.workflowManager.workflows || []) { - const path = workflow.pathParts; - if (!path) continue; - let parent = menu; - let currentPath = ""; - for (let i = 0; i < path.length - 1; i++) { - currentPath += "/" + path[i]; - let newParent = directories.get(currentPath); - if (!newParent) { - newParent = { - title: path[i], - has_submenu: true, - submenu: { - options: [], - }, - }; - parent.push(newParent); - newParent = newParent.submenu.options; - directories.set(currentPath, newParent); - } - parent = newParent; - } - parent.push({ - title: trimJsonExt(path[path.length - 1]), - callback: () => callback(workflow), - }); - } - return menu; - } - - #getFavoriteMenuOptions(callback) { - const menu = []; - for (const workflow of this.app.workflowManager.workflows || []) { - if (workflow.isFavorite) { - menu.push({ - title: "⭐ " + workflow.name, - callback: () => callback(workflow), - }); - } - } - return menu; - } - - /** - * @param {import("../../app.js").ComfyApp} app - */ - registerExtension(app) { - const self = this; - app.registerExtension({ - name: "Comfy.Workflows", - async beforeRegisterNodeDef(nodeType) { - function getImageWidget(node) { - const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional }; - for (const input in inputs) { - if (inputs[input][0] === "IMAGEUPLOAD") { - const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image")); - if (imageWidget) return imageWidget; - } - } - } - - function setWidgetImage(node, widget, img) { - const url = new URL(img.src); - const filename = url.searchParams.get("filename"); - const subfolder = url.searchParams.get("subfolder"); - const type = url.searchParams.get("type"); - const imageId = `${subfolder ? subfolder + "/" : ""}${filename} [${type}]`; - widget.value = imageId; - node.imgs = [img]; - app.graph.setDirtyCanvas(true, true); - } - - /** - * @param {HTMLImageElement} img - * @param {ComfyWorkflow} workflow - */ - async function sendToWorkflow(img, workflow) { - const openWorkflow = app.workflowManager.openWorkflows.find((w) => w.path === workflow.path); - if (openWorkflow) { - workflow = openWorkflow; - } - - await workflow.load(); - let options = []; - const nodes = app.graph.computeExecutionOrder(false); - for (const node of nodes) { - const widget = getImageWidget(node); - if (widget == null) continue; - - if (node.title?.toLowerCase().includes("input")) { - options = [{ widget, node }]; - break; - } else { - options.push({ widget, node }); - } - } - - if (!options.length) { - alert("No image nodes have been found in this workflow!"); - return; - } else if (options.length > 1) { - const dialog = new WidgetSelectionDialog(options); - const res = await dialog.show(app); - if (!res) return; - options = [res]; - } - - setWidgetImage(options[0].node, options[0].widget, img); - } - - const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"]; - nodeType.prototype["getExtraMenuOptions"] = function (_, options) { - const r = getExtraMenuOptions?.apply?.(this, arguments); - - const setting = app.ui.settings.getSettingValue("Comfy.UseNewMenu", false); - if (setting && setting != "Disabled") { - const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this); - let img; - if (t.imageIndex != null) { - // An image is selected so select that - img = t.imgs?.[t.imageIndex]; - } else if (t.overIndex != null) { - // No image is selected but one is hovered - img = t.img?.s[t.overIndex]; - } - - if (img) { - let pos = options.findIndex((o) => o.content === "Save Image"); - if (pos === -1) { - pos = 0; - } else { - pos++; - } - - options.splice(pos, 0, { - content: "Send to workflow", - has_submenu: true, - submenu: { - options: [ - { - callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow), - title: "[Current workflow]", - }, - ...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)), - null, - ...self.#getMenuOptions(sendToWorkflow.bind(null, img)), - ], - }, - }); - } - } - - return r; - }; - }, - }); - } -} - -export class ComfyWorkflowsContent { - element = $el("div.comfyui-workflows-panel"); - treeState = {}; - treeFiles = {}; - /** @type { Map } */ - openFiles = new Map(); - /** @type {WorkflowElement} */ - activeElement = null; - - /** - * @param {import("../../app.js").ComfyApp} app - * @param {ComfyPopup} popup - */ - constructor(app, popup) { - this.app = app; - this.popup = popup; - this.actions = $el("div.comfyui-workflows-actions", [ - new ComfyButton({ - content: "Default", - icon: "file-code", - iconSize: 18, - classList: "comfyui-button primary", - tooltip: "Load default workflow", - action: () => { - popup.open = false; - app.loadGraphData(); - app.resetView(); - }, - }).element, - new ComfyButton({ - content: "Browse", - icon: "folder", - iconSize: 18, - tooltip: "Browse for an image or exported workflow", - action: () => { - popup.open = false; - app.ui.loadFile(); - }, - }).element, - new ComfyButton({ - content: "Blank", - icon: "plus-thick", - iconSize: 18, - tooltip: "Create a new blank workflow", - action: () => { - app.workflowManager.setWorkflow(null); - app.clean(); - app.graph.clear(); - app.workflowManager.activeWorkflow.track(); - popup.open = false; - }, - }).element, - ]); - - this.spinner = createSpinner(); - this.element.replaceChildren(this.actions, this.spinner); - - this.popup.addEventListener("open", () => this.load()); - this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner)); - - this.app.workflowManager.addEventListener("favorite", (e) => { - const workflow = e["detail"]; - const button = this.treeFiles[workflow.path]?.primary; - if (!button) return; // Can happen when a workflow is renamed - button.icon = this.#getFavoriteIcon(workflow); - button.overIcon = this.#getFavoriteOverIcon(workflow); - this.updateFavorites(); - }); - - for (const e of ["save", "open", "close", "changeWorkflow"]) { - // TODO: dont be lazy and just update the specific element - app.workflowManager.addEventListener(e, () => this.updateOpen()); - } - this.app.workflowManager.addEventListener("rename", () => this.load()); - this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive()); - } - - async load() { - await this.app.workflowManager.loadWorkflows(); - this.updateTree(); - this.updateFavorites(); - this.updateOpen(); - this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement); - } - - updateOpen() { - const current = this.openElement; - this.openFiles.clear(); - - this.openElement = $el("div.comfyui-workflows-open", [ - $el("h3", "Open"), - ...this.app.workflowManager.openWorkflows.map((w) => { - const wrapper = new WorkflowElement(this, w, { - primary: { element: $el("i.mdi.mdi-18px.mdi-progress-pencil") }, - buttons: [ - this.#getRenameButton(w), - new ComfyButton({ - icon: "close", - iconSize: 18, - classList: "comfyui-button comfyui-workflows-file-action", - tooltip: "Close workflow", - action: (e) => { - e.stopImmediatePropagation(); - this.app.workflowManager.closeWorkflow(w); - }, - }), - ], - }); - if (w.unsaved) { - wrapper.element.classList.add("unsaved"); - } - if(w === this.app.workflowManager.activeWorkflow) { - wrapper.element.classList.add("active"); - } - - this.openFiles.set(w, wrapper); - return wrapper.element; - }), - ]); - - this.#updateActive(); - current?.replaceWith(this.openElement); - } - - updateFavorites() { - const current = this.favoritesElement; - const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)]; - - this.favoritesElement = $el("div.comfyui-workflows-favorites", [ - $el("h3", "Favorites"), - ...favorites - .map((w) => { - return this.#getWorkflowElement(w).element; - }) - .filter(Boolean), - ]); - - current?.replaceWith(this.favoritesElement); - } - - filterTree() { - if (!this.filterText) { - this.treeRoot.classList.remove("filtered"); - // Unfilter whole tree - for (const item of Object.values(this.treeFiles)) { - item.element.parentElement.style.removeProperty("display"); - this.showTreeParents(item.element.parentElement); - } - return; - } - this.treeRoot.classList.add("filtered"); - const searchTerms = this.filterText.toLocaleLowerCase().split(" "); - for (const item of Object.values(this.treeFiles)) { - const parts = item.workflow.pathParts; - let termIndex = 0; - let valid = false; - for (const part of parts) { - let currentIndex = 0; - do { - currentIndex = part.indexOf(searchTerms[termIndex], currentIndex); - if (currentIndex > -1) currentIndex += searchTerms[termIndex].length; - } while (currentIndex !== -1 && ++termIndex < searchTerms.length); - - if (termIndex >= searchTerms.length) { - valid = true; - break; - } - } - if (valid) { - item.element.parentElement.style.removeProperty("display"); - this.showTreeParents(item.element.parentElement); - } else { - item.element.parentElement.style.display = "none"; - this.hideTreeParents(item.element.parentElement); - } - } - } - - hideTreeParents(element) { - // Hide all parents if no children are visible - if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { - for (let i = 1; i < element.parentElement.children.length; i++) { - const c = element.parentElement.children[i]; - if (c.style.display !== "none") { - return; - } - } - element.parentElement.style.display = "none"; - this.hideTreeParents(element.parentElement); - } - } - - showTreeParents(element) { - if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) { - element.parentElement.style.removeProperty("display"); - this.showTreeParents(element.parentElement); - } - } - - updateTree() { - const current = this.treeElement; - const nodes = {}; - let typingTimeout; - - this.treeFiles = {}; - this.treeRoot = $el("ul.comfyui-workflows-tree"); - this.treeElement = $el("section", [ - $el("header", [ - $el("h3", "Browse"), - $el("div.comfy-ui-workflows-search", [ - $el("i.mdi.mdi-18px.mdi-magnify"), - $el("input", { - placeholder: "Search", - value: this.filterText ?? "", - oninput: (e) => { - this.filterText = e.target["value"]?.trim(); - clearTimeout(typingTimeout); - typingTimeout = setTimeout(() => this.filterTree(), 250); - }, - }), - ]), - ]), - this.treeRoot, - ]); - - for (const workflow of this.app.workflowManager.workflows) { - if (!workflow.pathParts) continue; - - let currentPath = ""; - let currentRoot = this.treeRoot; - - for (let i = 0; i < workflow.pathParts.length; i++) { - currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i]; - const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot); - - nodes[currentPath] = parentNode; - currentRoot = parentNode; - } - } - - current?.replaceWith(this.treeElement); - this.filterTree(); - } - - #expandNode(el, workflow, thisPath, i) { - const expanded = !el.classList.toggle("closed"); - if (expanded) { - let c = ""; - for (let j = 0; j <= i; j++) { - c += (c ? "\\" : "") + workflow.pathParts[j]; - this.treeState[c] = true; - } - } else { - let c = thisPath; - for (let j = i + 1; j < workflow.pathParts.length; j++) { - c += (c ? "\\" : "") + workflow.pathParts[j]; - delete this.treeState[c]; - } - delete this.treeState[thisPath]; - } - } - - #updateActive() { - this.#removeActive(); - - const active = this.app.workflowManager.activePrompt; - if (!active?.workflow) return; - - const open = this.openFiles.get(active.workflow); - if (!open) return; - - this.activeElement = open; - - const total = Object.values(active.nodes); - const done = total.filter(Boolean); - const percent = done.length / total.length; - open.element.classList.add("running"); - open.element.style.setProperty("--progress", percent * 100 + "%"); - open.primary.element.classList.remove("mdi-progress-pencil"); - open.primary.element.classList.add("mdi-play"); - } - - #removeActive() { - if (!this.activeElement) return; - this.activeElement.element.classList.remove("running"); - this.activeElement.element.style.removeProperty("--progress"); - this.activeElement.primary.element.classList.add("mdi-progress-pencil"); - this.activeElement.primary.element.classList.remove("mdi-play"); - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteIcon(workflow) { - return workflow.isFavorite ? "star" : "file-outline"; - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteOverIcon(workflow) { - return workflow.isFavorite ? "star-off" : "star-outline"; - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteTooltip(workflow) { - return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites"; - } - - /** @param {ComfyWorkflow} workflow */ - #getFavoriteButton(workflow, primary) { - return new ComfyButton({ - icon: this.#getFavoriteIcon(workflow), - overIcon: this.#getFavoriteOverIcon(workflow), - iconSize: 18, - classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""), - tooltip: this.#getFavoriteTooltip(workflow), - action: (e) => { - e.stopImmediatePropagation(); - workflow.favorite(!workflow.isFavorite); - }, - }); - } - - /** @param {ComfyWorkflow} workflow */ - #getDeleteButton(workflow) { - const deleteButton = new ComfyButton({ - icon: "delete", - tooltip: "Delete this workflow", - classList: "comfyui-button comfyui-workflows-file-action", - iconSize: 18, - action: async (e, btn) => { - e.stopImmediatePropagation(); - - if (btn.icon === "delete-empty") { - btn.enabled = false; - await workflow.delete(); - await this.load(); - } else { - btn.icon = "delete-empty"; - btn.element.style.background = "red"; - } - }, - }); - deleteButton.element.addEventListener("mouseleave", () => { - deleteButton.icon = "delete"; - deleteButton.element.style.removeProperty("background"); - }); - return deleteButton; - } - - /** @param {ComfyWorkflow} workflow */ - #getInsertButton(workflow) { - return new ComfyButton({ - icon: "file-move-outline", - iconSize: 18, - tooltip: "Insert this workflow into the current workflow", - classList: "comfyui-button comfyui-workflows-file-action", - action: (e) => { - if (!this.app.shiftDown) { - this.popup.open = false; - } - e.stopImmediatePropagation(); - if (!this.app.shiftDown) { - this.popup.open = false; - } - workflow.insert(); - }, - }); - } - - /** @param {ComfyWorkflow} workflow */ - #getRenameButton(workflow) { - return new ComfyButton({ - icon: "pencil", - tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.", - classList: "comfyui-button comfyui-workflows-file-action", - iconSize: 18, - enabled: !!workflow.path, - action: async (e) => { - e.stopImmediatePropagation(); - const newName = prompt("Enter new name", workflow.path); - if (newName) { - await workflow.rename(newName); - } - }, - }); - } - - /** @param {ComfyWorkflow} workflow */ - #getWorkflowElement(workflow) { - return new WorkflowElement(this, workflow, { - primary: this.#getFavoriteButton(workflow, true), - buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)], - }); - } - - /** @param {ComfyWorkflow} workflow */ - #createLeafNode(workflow) { - const fileNode = this.#getWorkflowElement(workflow); - this.treeFiles[workflow.path] = fileNode; - return fileNode; - } - - #createNode(currentPath, workflow, i, currentRoot) { - const part = workflow.pathParts[i]; - - const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), { - $: (el) => { - el.onclick = (e) => { - this.#expandNode(el, workflow, currentPath, i); - e.stopImmediatePropagation(); - }; - }, - }); - currentRoot.append(parentNode); - - // Create a node for the current part and an inner UL for its children if it isnt a leaf node - const leaf = i === workflow.pathParts.length - 1; - let nodeElement; - if (leaf) { - nodeElement = this.#createLeafNode(workflow).element; - } else { - nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]); - } - parentNode.append(nodeElement); - return parentNode; - } -} - -class WorkflowElement { - /** - * @param { ComfyWorkflowsContent } parent - * @param { ComfyWorkflow } workflow - */ - constructor(parent, workflow, { tagName = "li", primary, buttons }) { - this.parent = parent; - this.workflow = workflow; - this.primary = primary; - this.buttons = buttons; - - this.element = $el( - tagName + ".comfyui-workflows-tree-file", - { - onclick: () => { - workflow.load(); - this.parent.popup.open = false; - }, - title: this.workflow.path, - }, - [this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)] - ); - } -} - -class WidgetSelectionDialog extends ComfyAsyncDialog { - #options; - - /** - * @param {Array<{widget: {name: string}, node: {pos: [number, number], title: string, id: string, type: string}}>} options - */ - constructor(options) { - super(); - this.#options = options; - } - - show(app) { - this.element.classList.add("comfy-widget-selection-dialog"); - return super.show( - $el("div", [ - $el("h2", "Select image target"), - $el( - "p", - "This workflow has multiple image loader nodes, you can rename a node to include 'input' in the title for it to be automatically selected, or select one below." - ), - $el( - "section", - this.#options.map((opt) => { - return $el("div.comfy-widget-selection-item", [ - $el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`), - $el( - "button.comfyui-button", - { - onclick: () => { - app.canvas.ds.offset[0] = -opt.node.pos[0] + 50; - app.canvas.ds.offset[1] = -opt.node.pos[1] + 50; - app.canvas.selectNode(opt.node); - app.graph.setDirtyCanvas(true, true); - }, - }, - "Show" - ), - $el( - "button.comfyui-button.primary", - { - onclick: () => { - this.close(opt); - }, - }, - "Select" - ), - ]); - }) - ), - ]) - ); - } -} \ No newline at end of file diff --git a/web/scripts/ui/settings.js b/web/scripts/ui/settings.js deleted file mode 100644 index 819e4e7d6..000000000 --- a/web/scripts/ui/settings.js +++ /dev/null @@ -1,333 +0,0 @@ -import { $el } from "../ui.js"; -import { api } from "../api.js"; -import { ComfyDialog } from "./dialog.js"; - -export class ComfySettingsDialog extends ComfyDialog { - constructor(app) { - super(); - this.app = app; - this.settingsValues = {}; - this.settingsLookup = {}; - this.element = $el( - "dialog", - { - id: "comfy-settings-dialog", - parent: document.body, - }, - [ - $el("table.comfy-modal-content.comfy-table", [ - $el( - "caption", - { textContent: "Settings" }, - $el("button.comfy-btn", { - type: "button", - textContent: "\u00d7", - onclick: () => { - this.element.close(); - }, - }) - ), - $el("tbody", { $: (tbody) => (this.textElement = tbody) }), - $el("button", { - type: "button", - textContent: "Close", - style: { - cursor: "pointer", - }, - onclick: () => { - this.element.close(); - }, - }), - ]), - ] - ); - } - - get settings() { - return Object.values(this.settingsLookup); - } - - #dispatchChange(id, value, oldValue) { - this.dispatchEvent( - new CustomEvent(id + ".change", { - detail: { - value, - oldValue - }, - }) - ); - } - - async load() { - if (this.app.storageLocation === "browser") { - this.settingsValues = localStorage; - } else { - this.settingsValues = await api.getSettings(); - } - - // Trigger onChange for any settings added before load - for (const id in this.settingsLookup) { - const value = this.settingsValues[this.getId(id)]; - this.settingsLookup[id].onChange?.(value); - this.#dispatchChange(id, value); - } - } - - getId(id) { - if (this.app.storageLocation === "browser") { - id = "Comfy.Settings." + id; - } - return id; - } - - getSettingValue(id, defaultValue) { - let value = this.settingsValues[this.getId(id)]; - if(value != null) { - if(this.app.storageLocation === "browser") { - try { - value = JSON.parse(value); - } catch (error) { - } - } - } - return value ?? defaultValue; - } - - async setSettingValueAsync(id, value) { - const json = JSON.stringify(value); - localStorage["Comfy.Settings." + id] = json; // backwards compatibility for extensions keep setting in storage - - let oldValue = this.getSettingValue(id, undefined); - this.settingsValues[this.getId(id)] = value; - - if (id in this.settingsLookup) { - this.settingsLookup[id].onChange?.(value, oldValue); - } - this.#dispatchChange(id, value, oldValue); - - await api.storeSetting(id, value); - } - - setSettingValue(id, value) { - this.setSettingValueAsync(id, value).catch((err) => { - alert(`Error saving setting '${id}'`); - console.error(err); - }); - } - - addSetting({ id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined }) { - if (!id) { - throw new Error("Settings must have an ID"); - } - - if (id in this.settingsLookup) { - throw new Error(`Setting ${id} of type ${type} must have a unique ID.`); - } - - let skipOnChange = false; - let value = this.getSettingValue(id); - if (value == null) { - if (this.app.isNewUserSession) { - // Check if we have a localStorage value but not a setting value and we are a new user - const localValue = localStorage["Comfy.Settings." + id]; - if (localValue) { - value = JSON.parse(localValue); - this.setSettingValue(id, value); // Store on the server - } - } - if (value == null) { - value = defaultValue; - } - } - - // Trigger initial setting of value - if (!skipOnChange) { - onChange?.(value, undefined); - } - - this.settingsLookup[id] = { - id, - onChange, - name, - render: () => { - if (type === "hidden") return; - - const setter = (v) => { - if (onChange) { - onChange(v, value); - } - - this.setSettingValue(id, v); - value = v; - }; - value = this.getSettingValue(id, defaultValue); - - let element; - const htmlID = id.replaceAll(".", "-"); - - const labelCell = $el("td", [ - $el("label", { - for: htmlID, - classList: [tooltip !== "" ? "comfy-tooltip-indicator" : ""], - textContent: name, - }), - ]); - - if (typeof type === "function") { - element = type(name, setter, value, attrs); - } else { - switch (type) { - case "boolean": - element = $el("tr", [ - labelCell, - $el("td", [ - $el("input", { - id: htmlID, - type: "checkbox", - checked: value, - onchange: (event) => { - const isChecked = event.target.checked; - if (onChange !== undefined) { - onChange(isChecked); - } - this.setSettingValue(id, isChecked); - }, - }), - ]), - ]); - break; - case "number": - element = $el("tr", [ - labelCell, - $el("td", [ - $el("input", { - type, - value, - id: htmlID, - oninput: (e) => { - setter(e.target.value); - }, - ...attrs, - }), - ]), - ]); - break; - case "slider": - element = $el("tr", [ - labelCell, - $el("td", [ - $el( - "div", - { - style: { - display: "grid", - gridAutoFlow: "column", - }, - }, - [ - $el("input", { - ...attrs, - value, - type: "range", - oninput: (e) => { - setter(e.target.value); - e.target.nextElementSibling.value = e.target.value; - }, - }), - $el("input", { - ...attrs, - value, - id: htmlID, - type: "number", - style: { maxWidth: "4rem" }, - oninput: (e) => { - setter(e.target.value); - e.target.previousElementSibling.value = e.target.value; - }, - }), - ] - ), - ]), - ]); - break; - case "combo": - element = $el("tr", [ - labelCell, - $el("td", [ - $el( - "select", - { - oninput: (e) => { - setter(e.target.value); - }, - }, - (typeof options === "function" ? options(value) : options || []).map((opt) => { - if (typeof opt === "string") { - opt = { text: opt }; - } - const v = opt.value ?? opt.text; - return $el("option", { - value: v, - textContent: opt.text, - selected: value + "" === v + "", - }); - }) - ), - ]), - ]); - break; - case "text": - default: - if (type !== "text") { - console.warn(`Unsupported setting type '${type}, defaulting to text`); - } - - element = $el("tr", [ - labelCell, - $el("td", [ - $el("input", { - value, - id: htmlID, - oninput: (e) => { - setter(e.target.value); - }, - ...attrs, - }), - ]), - ]); - break; - } - } - if (tooltip) { - element.title = tooltip; - } - - return element; - }, - }; - - const self = this; - return { - get value() { - return self.getSettingValue(id, defaultValue); - }, - set value(v) { - self.setSettingValue(id, v); - }, - }; - } - - show() { - this.textElement.replaceChildren( - $el( - "tr", - { - style: { display: "none" }, - }, - [$el("th"), $el("th", { style: { width: "33%" } })] - ), - ...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean) - ); - this.element.showModal(); - } -} diff --git a/web/scripts/ui/spinner.css b/web/scripts/ui/spinner.css deleted file mode 100644 index 56da6072e..000000000 --- a/web/scripts/ui/spinner.css +++ /dev/null @@ -1,34 +0,0 @@ -.lds-ring { - display: inline-block; - position: relative; - width: 1em; - height: 1em; -} -.lds-ring div { - box-sizing: border-box; - display: block; - position: absolute; - width: 100%; - height: 100%; - border: 0.15em solid #fff; - border-radius: 50%; - animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; - border-color: #fff transparent transparent transparent; -} -.lds-ring div:nth-child(1) { - animation-delay: -0.45s; -} -.lds-ring div:nth-child(2) { - animation-delay: -0.3s; -} -.lds-ring div:nth-child(3) { - animation-delay: -0.15s; -} -@keyframes lds-ring { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/web/scripts/ui/spinner.js b/web/scripts/ui/spinner.js deleted file mode 100644 index d049786f6..000000000 --- a/web/scripts/ui/spinner.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addStylesheet } from "../utils.js"; - -addStylesheet(import.meta.url); - -export function createSpinner() { - const div = document.createElement("div"); - div.innerHTML = `
`; - return div.firstElementChild; -} diff --git a/web/scripts/ui/toggleSwitch.js b/web/scripts/ui/toggleSwitch.js deleted file mode 100644 index 59597ef90..000000000 --- a/web/scripts/ui/toggleSwitch.js +++ /dev/null @@ -1,60 +0,0 @@ -import { $el } from "../ui.js"; - -/** - * @typedef { { text: string, value?: string, tooltip?: string } } ToggleSwitchItem - */ -/** - * Creates a toggle switch element - * @param { string } name - * @param { Array void } [opts.onChange] - */ -export function toggleSwitch(name, items, { onChange } = {}) { - let selectedIndex; - let elements; - - function updateSelected(index) { - if (selectedIndex != null) { - elements[selectedIndex].classList.remove("comfy-toggle-selected"); - } - onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] }); - selectedIndex = index; - elements[selectedIndex].classList.add("comfy-toggle-selected"); - } - - elements = items.map((item, i) => { - if (typeof item === "string") item = { text: item }; - if (!item.value) item.value = item.text; - - const toggle = $el( - "label", - { - textContent: item.text, - title: item.tooltip ?? "", - }, - $el("input", { - name, - type: "radio", - value: item.value ?? item.text, - checked: item.selected, - onchange: () => { - updateSelected(i); - }, - }) - ); - if (item.selected) { - updateSelected(i); - } - return toggle; - }); - - const container = $el("div.comfy-toggle-switch", elements); - - if (selectedIndex == null) { - elements[0].children[0].checked = true; - updateSelected(0); - } - - return container; -} diff --git a/web/scripts/ui/userSelection.css b/web/scripts/ui/userSelection.css deleted file mode 100644 index 35c9d6614..000000000 --- a/web/scripts/ui/userSelection.css +++ /dev/null @@ -1,135 +0,0 @@ -.comfy-user-selection { - width: 100vw; - height: 100vh; - position: absolute; - top: 0; - left: 0; - z-index: 999; - display: flex; - align-items: center; - justify-content: center; - font-family: sans-serif; - background: linear-gradient(var(--tr-even-bg-color), var(--tr-odd-bg-color)); -} - -.comfy-user-selection-inner { - background: var(--comfy-menu-bg); - margin-top: -30vh; - padding: 20px 40px; - border-radius: 10px; - min-width: 365px; - position: relative; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); -} - -.comfy-user-selection-inner form { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -} - -.comfy-user-selection-inner h1 { - margin: 10px 0 30px 0; - font-weight: normal; -} - -.comfy-user-selection-inner label { - display: flex; - flex-direction: column; - width: 100%; -} - -.comfy-user-selection input, -.comfy-user-selection select { - background-color: var(--comfy-input-bg); - color: var(--input-text); - border: 0; - border-radius: 5px; - padding: 5px; - margin-top: 10px; -} - -.comfy-user-selection input::placeholder { - color: var(--descrip-text); - opacity: 1; -} - -.comfy-user-existing { - width: 100%; -} - -.no-users .comfy-user-existing { - display: none; -} - -.comfy-user-selection-inner .or-separator { - margin: 10px 0; - padding: 10px; - display: block; - text-align: center; - width: 100%; - color: var(--descrip-text); -} - -.comfy-user-selection-inner .or-separator { - overflow: hidden; - text-align: center; - margin-left: -10px; -} - -.comfy-user-selection-inner .or-separator::before, -.comfy-user-selection-inner .or-separator::after { - content: ""; - background-color: var(--border-color); - position: relative; - height: 1px; - vertical-align: middle; - display: inline-block; - width: calc(50% - 20px); - top: -1px; -} - -.comfy-user-selection-inner .or-separator::before { - right: 10px; - margin-left: -50%; -} - -.comfy-user-selection-inner .or-separator::after { - left: 10px; - margin-right: -50%; -} - -.comfy-user-selection-inner section { - width: 100%; - padding: 10px; - margin: -10px; - transition: background-color 0.2s; -} - -.comfy-user-selection-inner section.selected { - background: var(--border-color); - border-radius: 5px; -} - -.comfy-user-selection-inner footer { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 20px; -} - -.comfy-user-selection-inner .comfy-user-error { - color: var(--error-text); - margin-bottom: 10px; -} - -.comfy-user-button-next { - font-size: 16px; - padding: 6px 10px; - width: 100px; - display: flex; - gap: 5px; - align-items: center; - justify-content: center; -} \ No newline at end of file diff --git a/web/scripts/ui/userSelection.js b/web/scripts/ui/userSelection.js deleted file mode 100644 index f9f1ca807..000000000 --- a/web/scripts/ui/userSelection.js +++ /dev/null @@ -1,114 +0,0 @@ -import { api } from "../api.js"; -import { $el } from "../ui.js"; -import { addStylesheet } from "../utils.js"; -import { createSpinner } from "./spinner.js"; - -export class UserSelectionScreen { - async show(users, user) { - // This will rarely be hit so move the loading to on demand - await addStylesheet(import.meta.url); - const userSelection = document.getElementById("comfy-user-selection"); - userSelection.style.display = ""; - return new Promise((resolve) => { - const input = userSelection.getElementsByTagName("input")[0]; - const select = userSelection.getElementsByTagName("select")[0]; - const inputSection = input.closest("section"); - const selectSection = select.closest("section"); - const form = userSelection.getElementsByTagName("form")[0]; - const error = userSelection.getElementsByClassName("comfy-user-error")[0]; - const button = userSelection.getElementsByClassName("comfy-user-button-next")[0]; - - let inputActive = null; - input.addEventListener("focus", () => { - inputSection.classList.add("selected"); - selectSection.classList.remove("selected"); - inputActive = true; - }); - select.addEventListener("focus", () => { - inputSection.classList.remove("selected"); - selectSection.classList.add("selected"); - inputActive = false; - select.style.color = ""; - }); - select.addEventListener("blur", () => { - if (!select.value) { - select.style.color = "var(--descrip-text)"; - } - }); - - form.addEventListener("submit", async (e) => { - e.preventDefault(); - if (inputActive == null) { - error.textContent = "Please enter a username or select an existing user."; - } else if (inputActive) { - const username = input.value.trim(); - if (!username) { - error.textContent = "Please enter a username."; - return; - } - - // Create new user - input.disabled = select.disabled = input.readonly = select.readonly = true; - const spinner = createSpinner(); - button.prepend(spinner); - try { - const resp = await api.createUser(username); - if (resp.status >= 300) { - let message = "Error creating user: " + resp.status + " " + resp.statusText; - try { - const res = await resp.json(); - if(res.error) { - message = res.error; - } - } catch (error) { - } - throw new Error(message); - } - - resolve({ username, userId: await resp.json(), created: true }); - } catch (err) { - spinner.remove(); - error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred."; - input.disabled = select.disabled = input.readonly = select.readonly = false; - return; - } - } else if (!select.value) { - error.textContent = "Please select an existing user."; - return; - } else { - resolve({ username: users[select.value], userId: select.value, created: false }); - } - }); - - if (user) { - const name = localStorage["Comfy.userName"]; - if (name) { - input.value = name; - } - } - if (input.value) { - // Focus the input, do this separately as sometimes browsers like to fill in the value - input.focus(); - } - - const userIds = Object.keys(users ?? {}); - if (userIds.length) { - for (const u of userIds) { - $el("option", { textContent: users[u], value: u, parent: select }); - } - select.style.color = "var(--descrip-text)"; - - if (select.value) { - // Focus the select, do this separately as sometimes browsers like to fill in the value - select.focus(); - } - } else { - userSelection.classList.add("no-users"); - input.focus(); - } - }).then((r) => { - userSelection.remove(); - return r; - }); - } -} diff --git a/web/scripts/ui/utils.js b/web/scripts/ui/utils.js deleted file mode 100644 index e37d8b41e..000000000 --- a/web/scripts/ui/utils.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @typedef { string | string[] | Record } ClassList - */ - -/** - * @param { HTMLElement } element - * @param { ClassList } classList - * @param { string[] } requiredClasses - */ -export function applyClasses(element, classList, ...requiredClasses) { - classList ??= ""; - - let str; - if (typeof classList === "string") { - str = classList; - } else if (classList instanceof Array) { - str = classList.join(" "); - } else { - str = Object.entries(classList).reduce((p, c) => { - if (c[1]) { - p += (p.length ? " " : "") + c[0]; - } - return p; - }, ""); - } - element.className = str; - if (requiredClasses) { - element.classList.add(...requiredClasses); - } -} - -/** - * @param { HTMLElement } element - * @param { { onHide?: (el: HTMLElement) => void, onShow?: (el: HTMLElement, value) => void } } [param1] - * @returns - */ -export function toggleElement(element, { onHide, onShow } = {}) { - let placeholder; - let hidden; - return (value) => { - if (value) { - if (hidden) { - hidden = false; - placeholder.replaceWith(element); - } - onShow?.(element, value); - } else { - if (!placeholder) { - placeholder = document.createComment(""); - } - hidden = true; - element.replaceWith(placeholder); - onHide?.(element); - } - }; -} diff --git a/web/scripts/utils.js b/web/scripts/utils.js deleted file mode 100644 index cda7600f6..000000000 --- a/web/scripts/utils.js +++ /dev/null @@ -1,156 +0,0 @@ -import { $el } from "./ui.js"; -import { api } from "./api.js"; - -// Simple date formatter -const parts = { - d: (d) => d.getDate(), - M: (d) => d.getMonth() + 1, - h: (d) => d.getHours(), - m: (d) => d.getMinutes(), - s: (d) => d.getSeconds(), -}; -const format = - Object.keys(parts) - .map((k) => k + k + "?") - .join("|") + "|yyy?y?"; - -function formatDate(text, date) { - return text.replace(new RegExp(format, "g"), function (text) { - if (text === "yy") return (date.getFullYear() + "").substring(2); - if (text === "yyyy") return date.getFullYear(); - if (text[0] in parts) { - const p = parts[text[0]](date); - return (p + "").padStart(text.length, "0"); - } - return text; - }); -} - - -export function clone(obj) { - try { - if (typeof structuredClone !== "undefined") { - return structuredClone(obj); - } - } catch (error) { - // structuredClone is stricter than using JSON.parse/stringify so fallback to that - } - - return JSON.parse(JSON.stringify(obj)); -} - -export function applyTextReplacements(app, value) { - return value.replace(/%([^%]+)%/g, function (match, text) { - const split = text.split("."); - if (split.length !== 2) { - // Special handling for dates - if (split[0].startsWith("date:")) { - return formatDate(split[0].substring(5), new Date()); - } - - if (text !== "width" && text !== "height") { - // Dont warn on standard replacements - console.warn("Invalid replacement pattern", text); - } - return match; - } - - // Find node with matching S&R property name - let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]); - // If we cant, see if there is a node with that title - if (!nodes.length) { - nodes = app.graph._nodes.filter((n) => n.title === split[0]); - } - if (!nodes.length) { - console.warn("Unable to find node", split[0]); - return match; - } - - if (nodes.length > 1) { - console.warn("Multiple nodes matched", split[0], "using first match"); - } - - const node = nodes[0]; - - const widget = node.widgets?.find((w) => w.name === split[1]); - if (!widget) { - console.warn("Unable to find widget", split[1], "on node", split[0], node); - return match; - } - - return ((widget.value ?? "") + "").replaceAll(/\/|\\/g, "_"); - }); -} - -export async function addStylesheet(urlOrFile, relativeTo) { - return new Promise((res, rej) => { - let url; - if (urlOrFile.endsWith(".js")) { - url = urlOrFile.substr(0, urlOrFile.length - 2) + "css"; - } else { - url = new URL(urlOrFile, relativeTo ?? `${window.location.protocol}//${window.location.host}`).toString(); - } - $el("link", { - parent: document.head, - rel: "stylesheet", - type: "text/css", - href: url, - onload: res, - onerror: rej, - }); - }); -} - -/** - * @param { string } filename - * @param { Blob } blob - */ -export function downloadBlob(filename, blob) { - const url = URL.createObjectURL(blob); - const a = $el("a", { - href: url, - download: filename, - style: { display: "none" }, - parent: document.body, - }); - a.click(); - setTimeout(function () { - a.remove(); - window.URL.revokeObjectURL(url); - }, 0); -} - -/** - * @template T - * @param {string} name - * @param {T} [defaultValue] - * @param {(currentValue: any, previousValue: any)=>void} [onChanged] - * @returns {T} - */ -export function prop(target, name, defaultValue, onChanged) { - let currentValue; - Object.defineProperty(target, name, { - get() { - return currentValue; - }, - set(newValue) { - const prevValue = currentValue; - currentValue = newValue; - onChanged?.(currentValue, prevValue, target, name); - }, - }); - return defaultValue; -} - -export function getStorageValue(id) { - const clientId = api.clientId ?? api.initialClientId; - return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id); -} - -export function setStorageValue(id, value) { - const clientId = api.clientId ?? api.initialClientId; - if (clientId) { - sessionStorage.setItem(`${id}:${clientId}`, value); - } - localStorage.setItem(id, value); -} \ No newline at end of file diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js deleted file mode 100644 index 59b17fd13..000000000 --- a/web/scripts/widgets.js +++ /dev/null @@ -1,533 +0,0 @@ -import { api } from "./api.js" -import "./domWidget.js"; - -let controlValueRunBefore = false; -export function updateControlWidgetLabel(widget) { - let replacement = "after"; - let find = "before"; - if (controlValueRunBefore) { - [find, replacement] = [replacement, find] - } - widget.label = (widget.label ?? widget.name).replace(find, replacement); -} - -const IS_CONTROL_WIDGET = Symbol(); -const HAS_EXECUTED = Symbol(); - -function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) { - let defaultVal = inputData[1]["default"]; - let { min, max, step, round} = inputData[1]; - - if (defaultVal == undefined) defaultVal = 0; - if (min == undefined) min = 0; - if (max == undefined) max = 2048; - if (step == undefined) step = defaultStep; - // precision is the number of decimal places to show. - // by default, display the the smallest number of decimal places such that changes of size step are visible. - if (precision == undefined) { - precision = Math.max(-Math.floor(Math.log10(step)),0); - } - - if (enable_rounding && (round == undefined || round === true)) { - // by default, round the value to those decimal places shown. - round = Math.round(1000000*Math.pow(0.1,precision))/1000000; - } - - return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } }; -} - -export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values, widgetName, inputData) { - let name = inputData[1]?.control_after_generate; - if(typeof name !== "string") { - name = widgetName; - } - const widgets = addValueControlWidgets(node, targetWidget, defaultValue, { - addFilterList: false, - controlAfterGenerateName: name - }, inputData); - return widgets[0]; -} - -export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData) { - if (!defaultValue) defaultValue = "randomize"; - if (!options) options = {}; - - const getName = (defaultName, optionName) => { - let name = defaultName; - if (options[optionName]) { - name = options[optionName]; - } else if (typeof inputData?.[1]?.[defaultName] === "string") { - name = inputData?.[1]?.[defaultName]; - } else if (inputData?.[1]?.control_prefix) { - name = inputData?.[1]?.control_prefix + " " + name - } - return name; - } - - const widgets = []; - const valueControl = node.addWidget( - "combo", - getName("control_after_generate", "controlAfterGenerateName"), - defaultValue, - function () {}, - { - values: ["fixed", "increment", "decrement", "randomize"], - serialize: false, // Don't include this in prompt. - } - ); - valueControl.tooltip = "Allows the linked widget to be changed automatically, for example randomizing the noise seed."; - valueControl[IS_CONTROL_WIDGET] = true; - updateControlWidgetLabel(valueControl); - widgets.push(valueControl); - - const isCombo = targetWidget.type === "combo"; - let comboFilter; - if (isCombo) { - valueControl.options.values.push("increment-wrap"); - } - if (isCombo && options.addFilterList !== false) { - comboFilter = node.addWidget( - "string", - getName("control_filter_list", "controlFilterListName"), - "", - function () {}, - { - serialize: false, // Don't include this in prompt. - } - ); - updateControlWidgetLabel(comboFilter); - comboFilter.tooltip = "Allows for filtering the list of values when changing the value via the control generate mode. Allows for RegEx matches in the format /abc/ to only filter to values containing 'abc'." - - widgets.push(comboFilter); - } - - const applyWidgetControl = () => { - var v = valueControl.value; - - if (isCombo && v !== "fixed") { - let values = targetWidget.options.values; - const filter = comboFilter?.value; - if (filter) { - let check; - if (filter.startsWith("/") && filter.endsWith("/")) { - try { - const regex = new RegExp(filter.substring(1, filter.length - 1)); - check = (item) => regex.test(item); - } catch (error) { - console.error("Error constructing RegExp filter for node " + node.id, filter, error); - } - } - if (!check) { - const lower = filter.toLocaleLowerCase(); - check = (item) => item.toLocaleLowerCase().includes(lower); - } - values = values.filter(item => check(item)); - if (!values.length && targetWidget.options.values.length) { - console.warn("Filter for node " + node.id + " has filtered out all items", filter); - } - } - let current_index = values.indexOf(targetWidget.value); - let current_length = values.length; - - switch (v) { - case "increment": - current_index += 1; - break; - case "increment-wrap": - current_index += 1; - if ( current_index >= current_length ) { - current_index = 0; - } - break; - case "decrement": - current_index -= 1; - break; - case "randomize": - current_index = Math.floor(Math.random() * current_length); - default: - break; - } - current_index = Math.max(0, current_index); - current_index = Math.min(current_length - 1, current_index); - if (current_index >= 0) { - let value = values[current_index]; - targetWidget.value = value; - targetWidget.callback(value); - } - } else { - //number - let min = targetWidget.options.min; - let max = targetWidget.options.max; - // limit to something that javascript can handle - max = Math.min(1125899906842624, max); - min = Math.max(-1125899906842624, min); - let range = (max - min) / (targetWidget.options.step / 10); - - //adjust values based on valueControl Behaviour - switch (v) { - case "fixed": - break; - case "increment": - targetWidget.value += targetWidget.options.step / 10; - break; - case "decrement": - targetWidget.value -= targetWidget.options.step / 10; - break; - case "randomize": - targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min; - default: - break; - } - /*check if values are over or under their respective - * ranges and set them to min or max.*/ - if (targetWidget.value < min) targetWidget.value = min; - - if (targetWidget.value > max) - targetWidget.value = max; - targetWidget.callback(targetWidget.value); - } - }; - - valueControl.beforeQueued = () => { - if (controlValueRunBefore) { - // Don't run on first execution - if (valueControl[HAS_EXECUTED]) { - applyWidgetControl(); - } - } - valueControl[HAS_EXECUTED] = true; - }; - - valueControl.afterQueued = () => { - if (!controlValueRunBefore) { - applyWidgetControl(); - } - }; - - return widgets; -}; - -function seedWidget(node, inputName, inputData, app, widgetName) { - const seed = createIntWidget(node, inputName, inputData, app, true); - const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData); - - seed.widget.linkedWidgets = [seedControl]; - return seed; -} - -function createIntWidget(node, inputName, inputData, app, isSeedInput) { - const control = inputData[1]?.control_after_generate; - if (!isSeedInput && control) { - return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined); - } - - let widgetType = isSlider(inputData[1]["display"], app); - const { val, config } = getNumberDefaults(inputData, 1, 0, true); - Object.assign(config, { precision: 0 }); - return { - widget: node.addWidget( - widgetType, - inputName, - val, - function (v) { - const s = this.options.step / 10; - let sh = this.options.min % s; - if (isNaN(sh)) { - sh = 0; - } - this.value = Math.round((v - sh) / s) * s + sh; - }, - config - ), - }; -} - -function addMultilineWidget(node, name, opts, app) { - const inputEl = document.createElement("textarea"); - inputEl.className = "comfy-multiline-input"; - inputEl.value = opts.defaultVal; - inputEl.placeholder = opts.placeholder || name; - - const widget = node.addDOMWidget(name, "customtext", inputEl, { - getValue() { - return inputEl.value; - }, - setValue(v) { - inputEl.value = v; - }, - }); - widget.inputEl = inputEl; - - inputEl.addEventListener("input", () => { - widget.callback?.(widget.value); - }); - - return { minWidth: 400, minHeight: 200, widget }; -} - -function isSlider(display, app) { - if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) { - return "number" - } - - return (display==="slider") ? "slider" : "number" -} - -export function initWidgets(app) { - app.ui.settings.addSetting({ - id: "Comfy.WidgetControlMode", - name: "Widget Value Control Mode", - type: "combo", - defaultValue: "after", - options: ["before", "after"], - tooltip: "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.", - onChange(value) { - controlValueRunBefore = value === "before"; - for (const n of app.graph._nodes) { - if (!n.widgets) continue; - for (const w of n.widgets) { - if (w[IS_CONTROL_WIDGET]) { - updateControlWidgetLabel(w); - if (w.linkedWidgets) { - for (const l of w.linkedWidgets) { - updateControlWidgetLabel(l); - } - } - } - } - } - app.graph.setDirtyCanvas(true); - }, - }); -} - -export const ComfyWidgets = { - "INT:seed": seedWidget, - "INT:noise_seed": seedWidget, - FLOAT(node, inputName, inputData, app) { - let widgetType = isSlider(inputData[1]["display"], app); - let precision = app.ui.settings.getSettingValue("Comfy.FloatRoundingPrecision"); - let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding") - if (precision == 0) precision = undefined; - const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding); - return { widget: node.addWidget(widgetType, inputName, val, - function (v) { - if (config.round) { - this.value = Math.round((v + Number.EPSILON)/config.round)*config.round; - if (this.value > config.max) this.value = config.max; - if (this.value < config.min) this.value = config.min; - } else { - this.value = v; - } - }, config) }; - }, - INT(node, inputName, inputData, app) { - return createIntWidget(node, inputName, inputData, app); - }, - BOOLEAN(node, inputName, inputData) { - let defaultVal = false; - let options = {}; - if (inputData[1]) { - if (inputData[1].default) - defaultVal = inputData[1].default; - if (inputData[1].label_on) - options["on"] = inputData[1].label_on; - if (inputData[1].label_off) - options["off"] = inputData[1].label_off; - } - return { - widget: node.addWidget( - "toggle", - inputName, - defaultVal, - () => {}, - options, - ) - }; - }, - STRING(node, inputName, inputData, app) { - const defaultVal = inputData[1].default || ""; - const multiline = !!inputData[1].multiline; - - let res; - if (multiline) { - res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app); - } else { - res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) }; - } - - if(inputData[1].dynamicPrompts != undefined) - res.widget.dynamicPrompts = inputData[1].dynamicPrompts; - - return res; - }, - COMBO(node, inputName, inputData) { - const type = inputData[0]; - let defaultValue = type[0]; - if (inputData[1] && inputData[1].default) { - defaultValue = inputData[1].default; - } - const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) }; - if (inputData[1]?.control_after_generate) { - res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData); - } - return res; - }, - IMAGEUPLOAD(node, inputName, inputData, app) { - const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image")); - let uploadWidget; - - function showImage(name) { - const img = new Image(); - img.onload = () => { - node.imgs = [img]; - app.graph.setDirtyCanvas(true); - }; - let folder_separator = name.lastIndexOf("/"); - let subfolder = ""; - if (folder_separator > -1) { - subfolder = name.substring(0, folder_separator); - name = name.substring(folder_separator + 1); - } - img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`); - node.setSizeForImage?.(); - } - - var default_value = imageWidget.value; - Object.defineProperty(imageWidget, "value", { - set : function(value) { - this._real_value = value; - }, - - get : function() { - let value = ""; - if (this._real_value) { - value = this._real_value; - } else { - return default_value; - } - - if (value.filename) { - let real_value = value; - value = ""; - if (real_value.subfolder) { - value = real_value.subfolder + "/"; - } - - value += real_value.filename; - - if(real_value.type && real_value.type !== "input") - value += ` [${real_value.type}]`; - } - return value; - } - }); - - // Add our own callback to the combo widget to render an image when it changes - const cb = node.callback; - imageWidget.callback = function () { - showImage(imageWidget.value); - if (cb) { - return cb.apply(this, arguments); - } - }; - - // On load if we have a value then render the image - // The value isnt set immediately so we need to wait a moment - // No change callbacks seem to be fired on initial setting of the value - requestAnimationFrame(() => { - if (imageWidget.value) { - showImage(imageWidget.value); - } - }); - - async function uploadFile(file, updateNode, pasted = false) { - try { - // Wrap file in formdata so it includes filename - const body = new FormData(); - body.append("image", file); - 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(); - // Add the file to the dropdown list and update the widget value - let path = data.name; - if (data.subfolder) path = data.subfolder + "/" + path; - - if (!imageWidget.options.values.includes(path)) { - imageWidget.options.values.push(path); - } - - if (updateNode) { - showImage(path); - imageWidget.value = path; - } - } else { - alert(resp.status + " - " + resp.statusText); - } - } catch (error) { - alert(error); - } - } - - const fileInput = document.createElement("input"); - Object.assign(fileInput, { - type: "file", - accept: "image/jpeg,image/png,image/webp", - style: "display: none", - onchange: async () => { - if (fileInput.files.length) { - await uploadFile(fileInput.files[0], true); - } - }, - }); - document.body.append(fileInput); - - // Create the button widget for selecting the files - uploadWidget = node.addWidget("button", inputName, "image", () => { - fileInput.click(); - }); - uploadWidget.label = "choose file to upload"; - uploadWidget.serialize = false; - - // Add handler to check if an image is being dragged over our node - node.onDragOver = function (e) { - if (e.dataTransfer && e.dataTransfer.items) { - const image = [...e.dataTransfer.items].find((f) => f.kind === "file"); - return !!image; - } - - return false; - }; - - // On drop upload files - node.onDragDrop = function (e) { - console.log("onDragDrop called"); - let handled = false; - for (const file of e.dataTransfer.files) { - if (file.type.startsWith("image/")) { - uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one - handled = true; - } - } - - return handled; - }; - - node.pasteFile = function(file) { - if (file.type.startsWith("image/")) { - const is_pasted = (file.name === "image.png") && - (file.lastModified - Date.now() < 2000); - uploadFile(file, true, is_pasted); - return true; - } - return false; - } - - return { widget: uploadWidget }; - }, -}; diff --git a/web/scripts/workflows.js b/web/scripts/workflows.js deleted file mode 100644 index d38b6f5fc..000000000 --- a/web/scripts/workflows.js +++ /dev/null @@ -1,450 +0,0 @@ -// @ts-check - -import { api } from "./api.js"; -import { ChangeTracker } from "./changeTracker.js"; -import { ComfyAsyncDialog } from "./ui/components/asyncDialog.js"; -import { getStorageValue, setStorageValue } from "./utils.js"; - -function appendJsonExt(path) { - if (!path.toLowerCase().endsWith(".json")) { - path += ".json"; - } - return path; -} - -export function trimJsonExt(path) { - return path?.replace(/\.json$/, ""); -} - -export class ComfyWorkflowManager extends EventTarget { - /** @type {string | null} */ - #activePromptId = null; - #unsavedCount = 0; - #activeWorkflow; - - /** @type {Record} */ - workflowLookup = {}; - /** @type {Array} */ - workflows = []; - /** @type {Array} */ - openWorkflows = []; - /** @type {Record}>} */ - queuedPrompts = {}; - - get activeWorkflow() { - return this.#activeWorkflow ?? this.openWorkflows[0]; - } - - get activePromptId() { - return this.#activePromptId; - } - - get activePrompt() { - return this.queuedPrompts[this.#activePromptId]; - } - - /** - * @param {import("./app.js").ComfyApp} app - */ - constructor(app) { - super(); - this.app = app; - ChangeTracker.init(app); - - this.#bindExecutionEvents(); - } - - #bindExecutionEvents() { - // TODO: on reload, set active prompt based on the latest ws message - - const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt })); - let executing = null; - api.addEventListener("execution_start", (e) => { - this.#activePromptId = e.detail.prompt_id; - - // This event can fire before the event is stored, so put a placeholder - this.queuedPrompts[this.#activePromptId] ??= { nodes: {} }; - emit(); - }); - api.addEventListener("execution_cached", (e) => { - if (!this.activePrompt) return; - for (const n of e.detail.nodes) { - this.activePrompt.nodes[n] = true; - } - emit(); - }); - api.addEventListener("executed", (e) => { - if (!this.activePrompt) return; - this.activePrompt.nodes[e.detail.node] = true; - emit(); - }); - api.addEventListener("executing", (e) => { - if (!this.activePrompt) return; - - if (executing) { - // Seems sometimes nodes that are cached fire executing but not executed - this.activePrompt.nodes[executing] = true; - } - executing = e.detail; - if (!executing) { - delete this.queuedPrompts[this.#activePromptId]; - this.#activePromptId = null; - } - emit(); - }); - } - - async loadWorkflows() { - try { - let favorites; - const resp = await api.getUserData("workflows/.index.json"); - let info; - if (resp.status === 200) { - info = await resp.json(); - favorites = new Set(info?.favorites ?? []); - } else { - favorites = new Set(); - } - - const workflows = (await api.listUserData("workflows", true, true)).map((w) => { - let workflow = this.workflowLookup[w[0]]; - if (!workflow) { - workflow = new ComfyWorkflow(this, w[0], w.slice(1), favorites.has(w[0])); - this.workflowLookup[workflow.path] = workflow; - } - return workflow; - }); - - this.workflows = workflows; - } catch (error) { - alert("Error loading workflows: " + (error.message ?? error)); - this.workflows = []; - } - } - - async saveWorkflowMetadata() { - await api.storeUserData("workflows/.index.json", { - favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)], - }); - } - - /** - * @param {string | ComfyWorkflow | null} workflow - */ - setWorkflow(workflow) { - if (workflow && typeof workflow === "string") { - // Selected by path, i.e. on reload of last workflow - const found = this.workflows.find((w) => w.path === workflow); - if (found) { - workflow = found; - workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true"; - } - } - - if (!(workflow instanceof ComfyWorkflow)) { - // Still not found, either reloading a deleted workflow or blank - workflow = new ComfyWorkflow(this, workflow || "Unsaved Workflow" + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "")); - } - - const index = this.openWorkflows.indexOf(workflow); - if (index === -1) { - // Opening a new workflow - this.openWorkflows.push(workflow); - } - - this.#activeWorkflow = workflow; - - setStorageValue("Comfy.PreviousWorkflow", this.activeWorkflow.path ?? ""); - this.dispatchEvent(new CustomEvent("changeWorkflow")); - } - - storePrompt({ nodes, id }) { - this.queuedPrompts[id] ??= {}; - this.queuedPrompts[id].nodes = { - ...nodes.reduce((p, n) => { - p[n] = false; - return p; - }, {}), - ...this.queuedPrompts[id].nodes, - }; - this.queuedPrompts[id].workflow = this.activeWorkflow; - } - - /** - * @param {ComfyWorkflow} workflow - */ - async closeWorkflow(workflow, warnIfUnsaved = true) { - if (!workflow.isOpen) { - return true; - } - if (workflow.unsaved && warnIfUnsaved) { - const res = await ComfyAsyncDialog.prompt({ - title: "Save Changes?", - message: `Do you want to save changes to "${workflow.path ?? workflow.name}" before closing?`, - actions: ["Yes", "No", "Cancel"], - }); - if (res === "Yes") { - const active = this.activeWorkflow; - if (active !== workflow) { - // We need to switch to the workflow to save it - await workflow.load(); - } - - if (!(await workflow.save())) { - // Save was canceled, restore the previous workflow - if (active !== workflow) { - await active.load(); - } - return; - } - } else if (res === "Cancel") { - return; - } - } - workflow.changeTracker = null; - this.openWorkflows.splice(this.openWorkflows.indexOf(workflow), 1); - if (this.openWorkflows.length) { - this.#activeWorkflow = this.openWorkflows[0]; - await this.#activeWorkflow.load(); - } else { - // Load default - await this.app.loadGraphData(); - } - } -} - -export class ComfyWorkflow { - #name; - #path; - #pathParts; - #isFavorite = false; - /** @type {ChangeTracker | null} */ - changeTracker = null; - unsaved = false; - - get name() { - return this.#name; - } - - get path() { - return this.#path; - } - - get pathParts() { - return this.#pathParts; - } - - get isFavorite() { - return this.#isFavorite; - } - - get isOpen() { - return !!this.changeTracker; - } - - /** - * @overload - * @param {ComfyWorkflowManager} manager - * @param {string} path - */ - /** - * @overload - * @param {ComfyWorkflowManager} manager - * @param {string} path - * @param {string[]} pathParts - * @param {boolean} isFavorite - */ - /** - * @param {ComfyWorkflowManager} manager - * @param {string} path - * @param {string[]} [pathParts] - * @param {boolean} [isFavorite] - */ - constructor(manager, path, pathParts, isFavorite) { - this.manager = manager; - if (pathParts) { - this.#updatePath(path, pathParts); - this.#isFavorite = isFavorite; - } else { - this.#name = path; - this.unsaved = true; - } - } - - /** - * @param {string} path - * @param {string[]} [pathParts] - */ - #updatePath(path, pathParts) { - this.#path = path; - - if (!pathParts) { - if (!path.includes("\\")) { - pathParts = path.split("/"); - } else { - pathParts = path.split("\\"); - } - } - - this.#pathParts = pathParts; - this.#name = trimJsonExt(pathParts[pathParts.length - 1]); - } - - async getWorkflowData() { - const resp = await api.getUserData("workflows/" + this.path); - if (resp.status !== 200) { - alert(`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`); - return; - } - return await resp.json(); - } - - load = async () => { - if (this.isOpen) { - await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this); - } else { - const data = await this.getWorkflowData(); - if (!data) return; - await this.manager.app.loadGraphData(data, true, true, this); - } - }; - - async save(saveAs = false) { - if (!this.path || saveAs) { - return !!(await this.#save(null, false)); - } else { - return !!(await this.#save(this.path, true)); - } - } - - /** - * @param {boolean} value - */ - async favorite(value) { - try { - if (this.#isFavorite === value) return; - this.#isFavorite = value; - await this.manager.saveWorkflowMetadata(); - this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this })); - } catch (error) { - alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error)); - } - } - - /** - * @param {string} path - */ - async rename(path) { - path = appendJsonExt(path); - let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path); - - if (resp.status === 409) { - if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return resp; - resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path, { overwrite: true }); - } - - if (resp.status !== 200) { - alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`); - return; - } - - const isFav = this.isFavorite; - if (isFav) { - await this.favorite(false); - } - path = (await resp.json()).substring("workflows/".length); - this.#updatePath(path, null); - if (isFav) { - await this.favorite(true); - } - this.manager.dispatchEvent(new CustomEvent("rename", { detail: this })); - setStorageValue("Comfy.PreviousWorkflow", this.path ?? ""); - } - - async insert() { - const data = await this.getWorkflowData(); - if (!data) return; - - const old = localStorage.getItem("litegrapheditor_clipboard"); - const graph = new LGraph(data); - const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true }); - canvas.selectNodes(); - canvas.copyToClipboard(); - this.manager.app.canvas.pasteFromClipboard(); - localStorage.setItem("litegrapheditor_clipboard", old); - } - - async delete() { - // TODO: fix delete of current workflow - should mark workflow as unsaved and when saving use old name by default - - try { - if (this.isFavorite) { - await this.favorite(false); - } - await api.deleteUserData("workflows/" + this.path); - this.unsaved = true; - this.#path = null; - this.#pathParts = null; - this.manager.workflows.splice(this.manager.workflows.indexOf(this), 1); - this.manager.dispatchEvent(new CustomEvent("delete", { detail: this })); - } catch (error) { - alert(`Error deleting workflow: ${error.message || error}`); - } - } - - track() { - if (this.changeTracker) { - this.changeTracker.restore(); - } else { - this.changeTracker = new ChangeTracker(this); - } - } - - /** - * @param {string|null} path - * @param {boolean} overwrite - */ - async #save(path, overwrite) { - if (!path) { - path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow"); - if (!path) return; - } - - path = appendJsonExt(path); - - const p = await this.manager.app.graphToPrompt(); - const json = JSON.stringify(p.workflow, null, 2); - let resp = await api.storeUserData("workflows/" + path, json, { stringify: false, throwOnError: false, overwrite }); - if (resp.status === 409) { - if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return; - resp = await api.storeUserData("workflows/" + path, json, { stringify: false }); - } - - if (resp.status !== 200) { - alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`); - return; - } - - path = (await resp.json()).substring("workflows/".length); - - if (!this.path) { - // Saved new workflow, patch this instance - this.#updatePath(path, null); - await this.manager.loadWorkflows(); - this.unsaved = false; - this.manager.dispatchEvent(new CustomEvent("rename", { detail: this })); - setStorageValue("Comfy.PreviousWorkflow", this.path ?? ""); - } else if (path !== this.path) { - // Saved as, open the new copy - await this.manager.loadWorkflows(); - const workflow = this.manager.workflowLookup[path]; - await workflow.load(); - } else { - // Normal save - this.unsaved = false; - this.manager.dispatchEvent(new CustomEvent("save", { detail: this })); - } - - return true; - } -} diff --git a/web/style.css b/web/style.css deleted file mode 100644 index 7774bfede..000000000 --- a/web/style.css +++ /dev/null @@ -1,664 +0,0 @@ -@import url("scripts/ui/menu/menu.css"); - -:root { - --fg-color: #000; - --bg-color: #fff; - --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; - --primary-bg: #236692; - --primary-fg: #ffffff; - --primary-hover-bg: #3485bb; - --primary-hover-fg: #ffffff; - --content-bg: #e0e0e0; - --content-fg: #000; - --content-hover-bg: #adadad; - --content-hover-fg: #000; -} - -@media (prefers-color-scheme: dark) { - :root { - --fg-color: #fff; - --bg-color: #202020; - --content-bg: #4e4e4e; - --content-fg: #fff; - --content-hover-bg: #222; - --content-hover-fg: #fff; - } -} - -body { - width: 100vw; - height: 100vh; - margin: 0; - overflow: hidden; - background-color: var(--bg-color); - color: var(--fg-color); - grid-template-columns: auto 1fr auto; - grid-template-rows: auto 1fr auto; - min-height: -webkit-fill-available; - max-height: -webkit-fill-available; - min-width: -webkit-fill-available; - max-width: -webkit-fill-available; -} - -.comfyui-body-top { - order: -5; - grid-column: 1/-1; - z-index: 10; - display: flex; - flex-direction: column; -} - -.comfyui-body-left { - order: -4; - z-index: 10; - display: flex; -} - -#graph-canvas { - width: 100%; - height: 100%; - order: -3; -} - -.comfyui-body-right { - order: -2; - z-index: 10; - display: flex; -} - -.comfyui-body-bottom { - order: -1; - grid-column: 1/-1; - z-index: 10; - display: flex; - flex-direction: column; -} - -.comfy-multiline-input { - background-color: var(--comfy-input-bg); - color: var(--input-text); - overflow: hidden; - overflow-y: auto; - padding: 2px; - resize: none; - border: none; - box-sizing: border-box; - font-size: 10px; -} - -.comfy-modal { - display: none; /* Hidden by default */ - position: fixed; /* Stay in place */ - z-index: 100; /* Sit on top */ - padding: 30px 30px 10px 30px; - background-color: var(--comfy-menu-bg); /* Modal background */ - color: var(--error-text); - box-shadow: 0 0 20px #888888; - border-radius: 10px; - top: 50%; - left: 50%; - max-width: 80vw; - max-height: 80vh; - transform: translate(-50%, -50%); - overflow: hidden; - justify-content: center; - font-family: monospace; - font-size: 15px; -} - -.comfy-modal-content { - display: flex; - flex-direction: column; -} - -.comfy-modal p { - overflow: auto; - white-space: pre-line; /* This will respect line breaks */ - margin-bottom: 20px; /* Add some margin between the text and the close button*/ -} - -.comfy-modal select, -.comfy-modal input[type=button], -.comfy-modal input[type=checkbox] { - margin: 3px 3px 3px 4px; -} - -.comfy-menu-hamburger { - position: fixed; - top: 10px; - z-index: 9999; - right: 10px; - width: 30px; - display: none; - gap: 8px; - flex-direction: column; - cursor: pointer; -} -.comfy-menu-hamburger div { - height: 3px; - width: 100%; - border-radius: 20px; - background-color: white; -} - -.comfy-menu { - font-size: 15px; - position: absolute; - top: 50%; - right: 0; - text-align: center; - z-index: 999; - width: 170px; - display: flex; - flex-direction: column; - align-items: center; - color: var(--descrip-text); - background-color: var(--comfy-menu-bg); - font-family: sans-serif; - padding: 10px; - border-radius: 0 8px 8px 8px; - box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4); -} - -.comfy-menu-header { - display: flex; -} - -.comfy-menu-actions { - display: flex; - gap: 3px; - align-items: center; - height: 20px; - position: relative; - top: -1px; - font-size: 22px; -} - -.comfy-menu .comfy-menu-actions button { - background-color: rgba(0, 0, 0, 0); - padding: 0; - border: none; - cursor: pointer; - font-size: inherit; -} - -.comfy-menu .comfy-menu-actions .comfy-settings-btn { - font-size: 0.6em; -} - -button.comfy-close-menu-btn { - font-size: 1em; - line-height: 12px; - color: #ccc; - position: relative; - top: -1px; -} - -.comfy-menu-queue-size { - flex: auto; -} - -.comfy-menu button, -.comfy-modal button { - font-size: 20px; -} - -.comfy-menu-btns { - margin-bottom: 10px; - width: 100%; -} - -.comfy-menu-btns button { - font-size: 10px; - width: 50%; - color: var(--descrip-text) !important; -} - -.comfy-menu > button { - width: 100%; -} - -.comfy-btn, -.comfy-menu > button, -.comfy-menu-btns button, -.comfy-menu .comfy-list button, -.comfy-modal button { - color: var(--input-text); - background-color: var(--comfy-input-bg); - border-radius: 8px; - border-color: var(--border-color); - border-style: solid; - margin-top: 2px; -} - -.comfy-btn:hover:not(:disabled), -.comfy-menu > button:hover, -.comfy-menu-btns button:hover, -.comfy-menu .comfy-list button:hover, -.comfy-modal button:hover, -.comfy-menu-actions button:hover { - filter: brightness(1.2); - will-change: transform; - cursor: pointer; -} - -span.drag-handle { - width: 10px; - height: 20px; - display: inline-block; - overflow: hidden; - line-height: 5px; - padding: 3px 4px; - cursor: move; - vertical-align: middle; - margin-top: -.4em; - margin-left: -.2em; - font-size: 12px; - font-family: sans-serif; - letter-spacing: 2px; - color: var(--drag-text); - text-shadow: 1px 0 1px black; -} - -span.drag-handle::after { - content: '.. .. ..'; -} - -.comfy-queue-btn { - width: 100%; -} - -.comfy-list { - color: var(--descrip-text); - background-color: var(--comfy-menu-bg); - margin-bottom: 10px; - border-color: var(--border-color); - border-style: solid; -} - -.comfy-list-items { - overflow-y: scroll; - max-height: 100px; - min-height: 25px; - background-color: var(--comfy-input-bg); - padding: 5px; -} - -.comfy-list h4 { - min-width: 160px; - margin: 0; - padding: 3px; - font-weight: normal; -} - -.comfy-list-items button { - font-size: 10px; -} - -.comfy-list-actions { - margin: 5px; - display: flex; - gap: 5px; - justify-content: center; -} - -.comfy-list-actions button { - font-size: 12px; -} - -button.comfy-queue-btn { - margin: 6px 0 !important; -} - -.comfy-modal.comfy-settings, -.comfy-modal.comfy-manage-templates { - text-align: center; - font-family: sans-serif; - color: var(--descrip-text); - z-index: 99; -} - -.comfy-modal.comfy-settings input[type="range"] { - vertical-align: middle; -} - -.comfy-modal.comfy-settings input[type="range"] + input[type="number"] { - width: 3.5em; -} - -.comfy-modal input, -.comfy-modal select { - color: var(--input-text); - background-color: var(--comfy-input-bg); - border-radius: 8px; - border-color: var(--border-color); - border-style: solid; - font-size: inherit; -} - -.comfy-tooltip-indicator { - text-decoration: underline; - text-decoration-style: dashed; -} - -@media only screen and (max-height: 850px) { - .comfy-menu { - top: 0 !important; - bottom: 0 !important; - left: auto !important; - right: 0 !important; - border-radius: 0; - } - - .comfy-menu span.drag-handle { - display: none; - } - - .comfy-menu-queue-size { - flex: unset; - } - - .comfy-menu-header { - justify-content: space-between; - } - .comfy-menu-actions { - gap: 10px; - font-size: 28px; - } -} - -/* Input popup */ - -.graphdialog { - min-height: 1em; - background-color: var(--comfy-menu-bg); -} - -.graphdialog .name { - font-size: 14px; - font-family: sans-serif; - color: var(--descrip-text); -} - -.graphdialog button { - margin-top: unset; - vertical-align: unset; - height: 1.6em; - padding-right: 8px; -} - -.graphdialog input, .graphdialog textarea, .graphdialog select { - background-color: var(--comfy-input-bg); - border: 2px solid; - border-color: var(--border-color); - color: var(--input-text); - border-radius: 12px 0 0 12px; -} - -/* Dialogs */ - -dialog { - box-shadow: 0 0 20px #888888; -} - -dialog::backdrop { - background: rgba(0, 0, 0, 0.5); -} - -.comfy-dialog.comfyui-dialog.comfy-modal { - top: 0; - left: 0; - right: 0; - bottom: 0; - transform: none; -} - -.comfy-dialog.comfy-modal { - font-family: Arial, sans-serif; - border-color: var(--bg-color); - box-shadow: none; - border: 2px solid var(--border-color); -} - -.comfy-dialog .comfy-modal-content { - flex-direction: row; - flex-wrap: wrap; - gap: 10px; - color: var(--fg-color); -} - -.comfy-dialog .comfy-modal-content h3 { - margin-top: 0; -} - -.comfy-dialog .comfy-modal-content > p { - width: 100%; -} - -.comfy-dialog .comfy-modal-content > .comfyui-button { - flex: 1; - justify-content: center; -} - -#comfy-settings-dialog { - padding: 0; - width: 41rem; -} - -#comfy-settings-dialog tr > td:first-child { - text-align: right; -} - -#comfy-settings-dialog tbody button, #comfy-settings-dialog table > button { - background-color: var(--bg-color); - border: 1px var(--border-color) solid; - border-radius: 0; - color: var(--input-text); - font-size: 1rem; - padding: 0.5rem; -} - -#comfy-settings-dialog button:hover { - background-color: var(--tr-odd-bg-color); -} - -/* General CSS for tables */ - -.comfy-table { - border-collapse: collapse; - color: var(--input-text); - font-family: Arial, sans-serif; - width: 100%; -} - -.comfy-table caption { - position: sticky; - top: 0; - background-color: var(--bg-color); - color: var(--input-text); - font-size: 1rem; - font-weight: bold; - padding: 8px; - text-align: center; - border-bottom: 1px solid var(--border-color); -} - -.comfy-table caption .comfy-btn { - position: absolute; - top: -2px; - right: 0; - bottom: 0; - cursor: pointer; - border: none; - height: 100%; - border-radius: 0; - aspect-ratio: 1/1; - user-select: none; - font-size: 20px; -} - -.comfy-table caption .comfy-btn:focus { - outline: none; -} - -.comfy-table tr:nth-child(even) { - background-color: var(--tr-even-bg-color); -} - -.comfy-table tr:nth-child(odd) { - background-color: var(--tr-odd-bg-color); -} - -.comfy-table td, -.comfy-table th { - border: 1px solid var(--border-color); - padding: 8px; -} - -/* Context menu */ - -.litegraph .dialog { - z-index: 1; - font-family: Arial, sans-serif; -} - -.litegraph .litemenu-entry.has_submenu { - position: relative; - padding-right: 20px; -} - -.litemenu-entry.has_submenu::after { - content: ">"; - position: absolute; - top: 0; - right: 2px; -} - -.litegraph.litecontextmenu, -.litegraph.litecontextmenu.dark { - z-index: 9999 !important; - background-color: var(--comfy-menu-bg) !important; - filter: brightness(95%); - will-change: transform; -} - -.litegraph.litecontextmenu .litemenu-entry:hover:not(.disabled):not(.separator) { - background-color: var(--comfy-menu-bg) !important; - filter: brightness(155%); - will-change: transform; - color: var(--input-text); -} - -.litegraph.litecontextmenu .litemenu-entry.submenu, -.litegraph.litecontextmenu.dark .litemenu-entry.submenu { - background-color: var(--comfy-menu-bg) !important; - color: var(--input-text); -} - -.litegraph.litecontextmenu input { - background-color: var(--comfy-input-bg) !important; - color: var(--input-text) !important; -} - -.comfy-context-menu-filter { - box-sizing: border-box; - border: 1px solid #999; - margin: 0 0 5px 5px; - width: calc(100% - 10px); -} - -.comfy-img-preview { - pointer-events: none; - overflow: hidden; - display: flex; - flex-wrap: wrap; - align-content: flex-start; - justify-content: center; -} - -.comfy-img-preview img { - object-fit: contain; - width: var(--comfy-img-preview-width); - height: var(--comfy-img-preview-height); -} - -.comfy-missing-nodes li button { - font-size: 12px; - margin-left: 5px; -} - -/* Search box */ - -.litegraph.litesearchbox { - z-index: 9999 !important; - background-color: var(--comfy-menu-bg) !important; - overflow: hidden; - display: block; -} - -.litegraph.litesearchbox input, -.litegraph.litesearchbox select { - background-color: var(--comfy-input-bg) !important; - color: var(--input-text); -} - -.litegraph.lite-search-item { - color: var(--input-text); - background-color: var(--comfy-input-bg); - filter: brightness(80%); - will-change: transform; - padding-left: 0.2em; -} - -.litegraph.lite-search-item.generic_type { - color: var(--input-text); - filter: brightness(50%); - will-change: transform; -} - -@media only screen and (max-width: 450px) { - #comfy-settings-dialog .comfy-table tbody { - display: grid; - } - #comfy-settings-dialog .comfy-table tr { - display: grid; - } - #comfy-settings-dialog tr > td:first-child { - text-align: center; - border-bottom: none; - padding-bottom: 0; - } - #comfy-settings-dialog tr > td:not(:first-child) { - text-align: center; - border-top: none; - } -} - -audio.comfy-audio.empty-audio-widget { - display: none; -} - -.comfy-graph-tooltip { - background: var(--comfy-input-bg); - border-radius: 5px; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); - color: var(--input-text); - display: none; - font-family: sans-serif; - left: 0; - max-width: 30vw; - padding: 4px 8px; - position: absolute; - top: 0; - transform: translate(5px, calc(-100% - 5px)); - white-space: pre-wrap; - z-index: 99999; -} diff --git a/web/types/comfy.d.ts b/web/types/comfy.d.ts deleted file mode 100644 index 9a338b349..000000000 --- a/web/types/comfy.d.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { LGraphNode, IWidget } from "./litegraph"; -import { ComfyApp } from "../../scripts/app"; - -export interface ComfyExtension { - /** - * The name of the extension - */ - name: string; - /** - * Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added - * @param app The ComfyUI app instance - */ - init?(app: ComfyApp): Promise; - /** - * Allows any additonal setup, called after the application is fully set up and running - * @param app The ComfyUI app instance - */ - setup?(app: ComfyApp): Promise; - /** - * Called before nodes are registered with the graph - * @param defs The collection of node definitions, add custom ones or edit existing ones - * @param app The ComfyUI app instance - */ - addCustomNodeDefs?(defs: Record, app: ComfyApp): Promise; - /** - * Allows the extension to add custom widgets - * @param app The ComfyUI app instance - * @returns An array of {[widget name]: widget data} - */ - getCustomWidgets?( - app: ComfyApp - ): Promise< - Record { widget?: IWidget; minWidth?: number; minHeight?: number }> - >; - /** - * Allows the extension to add additional handling to the node before it is registered with LGraph - * @param nodeType The node class (not an instance) - * @param nodeData The original node object info config object - * @param app The ComfyUI app instance - */ - beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise; - /** - * Allows the extension to register additional nodes with LGraph after standard nodes are added - * @param app The ComfyUI app instance - */ - registerCustomNodes?(app: ComfyApp): Promise; - /** - * Allows the extension to modify a node that has been reloaded onto the graph. - * If you break something in the backend and want to patch workflows in the frontend - * This is the place to do this - * @param node The node that has been loaded - * @param app The ComfyUI app instance - */ - loadedGraphNode?(node: LGraphNode, app: ComfyApp); - /** - * Allows the extension to run code after the constructor of the node - * @param node The node that has been created - * @param app The ComfyUI app instance - */ - nodeCreated?(node: LGraphNode, app: ComfyApp); -} - -export type ComfyObjectInfo = { - name: string; - display_name?: string; - description?: string; - category: string; - input?: { - required?: Record; - optional?: Record; - }; - output?: string[]; - output_name: string[]; -}; - -export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any]; diff --git a/web/types/litegraph.d.ts b/web/types/litegraph.d.ts deleted file mode 100644 index 6629e779f..000000000 --- a/web/types/litegraph.d.ts +++ /dev/null @@ -1,1506 +0,0 @@ -// Type definitions for litegraph.js 0.7.0 -// Project: litegraph.js -// Definitions by: NateScarlet - -export type Vector2 = [number, number]; -export type Vector4 = [number, number, number, number]; -export type widgetTypes = - | "number" - | "slider" - | "combo" - | "text" - | "toggle" - | "button"; -export type SlotShape = - | typeof LiteGraph.BOX_SHAPE - | typeof LiteGraph.CIRCLE_SHAPE - | typeof LiteGraph.ARROW_SHAPE - | typeof LiteGraph.SQUARE_SHAPE - | number; // For custom shapes - -/** https://github.com/jagenjo/litegraph.js/tree/master/guides#node-slots */ -export interface INodeSlot { - name: string; - type: string | -1; - label?: string; - dir?: - | typeof LiteGraph.UP - | typeof LiteGraph.RIGHT - | typeof LiteGraph.DOWN - | typeof LiteGraph.LEFT; - color_on?: string; - color_off?: string; - shape?: SlotShape; - locked?: boolean; - nameLocked?: boolean; -} - -export interface INodeInputSlot extends INodeSlot { - link: LLink["id"] | null; -} -export interface INodeOutputSlot extends INodeSlot { - links: LLink["id"][] | null; -} - -export type WidgetCallback = ( - this: T, - value: T["value"], - graphCanvas: LGraphCanvas, - node: LGraphNode, - pos: Vector2, - event?: MouseEvent -) => void; - -export interface IWidget { - name: string | null; - value: TValue; - options?: TOptions; - type?: widgetTypes; - y?: number; - property?: string; - last_y?: number; - clicked?: boolean; - marker?: boolean; - callback?: WidgetCallback; - /** Called by `LGraphCanvas.drawNodeWidgets` */ - draw?( - ctx: CanvasRenderingContext2D, - node: LGraphNode, - width: number, - posY: number, - height: number - ): void; - /** - * Called by `LGraphCanvas.processNodeWidgets` - * https://github.com/jagenjo/litegraph.js/issues/76 - */ - mouse?( - event: MouseEvent, - pos: Vector2, - node: LGraphNode - ): boolean; - /** Called by `LGraphNode.computeSize` */ - computeSize?(width: number): [number, number]; -} -export interface IButtonWidget extends IWidget { - type: "button"; -} -export interface IToggleWidget - extends IWidget { - type: "toggle"; -} -export interface ISliderWidget - extends IWidget { - type: "slider"; -} -export interface INumberWidget extends IWidget { - type: "number"; -} -export interface IComboWidget - extends IWidget< - string[], - { - values: - | string[] - | ((widget: IComboWidget, node: LGraphNode) => string[]); - } - > { - type: "combo"; -} - -export interface ITextWidget extends IWidget { - type: "text"; -} - -export interface IContextMenuItem { - content: string; - callback?: ContextMenuEventListener; - /** Used as innerHTML for extra child element */ - title?: string; - disabled?: boolean; - has_submenu?: boolean; - submenu?: { - options: ContextMenuItem[]; - } & IContextMenuOptions; - className?: string; -} -export interface IContextMenuOptions { - callback?: ContextMenuEventListener; - ignore_item_callbacks?: Boolean; - event?: MouseEvent | CustomEvent; - parentMenu?: ContextMenu; - autoopen?: boolean; - title?: string; - extra?: any; -} - -export type ContextMenuItem = IContextMenuItem | null; -export type ContextMenuEventListener = ( - value: ContextMenuItem, - options: IContextMenuOptions, - event: MouseEvent, - parentMenu: ContextMenu | undefined, - node: LGraphNode -) => boolean | void; - -export const LiteGraph: { - VERSION: number; - - CANVAS_GRID_SIZE: number; - - NODE_TITLE_HEIGHT: number; - NODE_TITLE_TEXT_Y: number; - NODE_SLOT_HEIGHT: number; - NODE_WIDGET_HEIGHT: number; - NODE_WIDTH: number; - NODE_MIN_WIDTH: number; - NODE_COLLAPSED_RADIUS: number; - NODE_COLLAPSED_WIDTH: number; - NODE_TITLE_COLOR: string; - NODE_TEXT_SIZE: number; - NODE_TEXT_COLOR: string; - NODE_SUBTEXT_SIZE: number; - NODE_DEFAULT_COLOR: string; - NODE_DEFAULT_BGCOLOR: string; - NODE_DEFAULT_BOXCOLOR: string; - NODE_DEFAULT_SHAPE: string; - DEFAULT_SHADOW_COLOR: string; - DEFAULT_GROUP_FONT: number; - - LINK_COLOR: string; - EVENT_LINK_COLOR: string; - CONNECTING_LINK_COLOR: string; - - MAX_NUMBER_OF_NODES: number; //avoid infinite loops - DEFAULT_POSITION: Vector2; //default node position - VALID_SHAPES: ["default", "box", "round", "card"]; //,"circle" - - //shapes are used for nodes but also for slots - BOX_SHAPE: 1; - ROUND_SHAPE: 2; - CIRCLE_SHAPE: 3; - CARD_SHAPE: 4; - ARROW_SHAPE: 5; - SQUARE_SHAPE: 6; - - //enums - INPUT: 1; - OUTPUT: 2; - - EVENT: -1; //for outputs - ACTION: -1; //for inputs - - ALWAYS: 0; - ON_EVENT: 1; - NEVER: 2; - ON_TRIGGER: 3; - - UP: 1; - DOWN: 2; - LEFT: 3; - RIGHT: 4; - CENTER: 5; - - STRAIGHT_LINK: 0; - LINEAR_LINK: 1; - SPLINE_LINK: 2; - - NORMAL_TITLE: 0; - NO_TITLE: 1; - TRANSPARENT_TITLE: 2; - AUTOHIDE_TITLE: 3; - - node_images_path: string; - - debug: boolean; - catch_exceptions: boolean; - throw_errors: boolean; - /** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */ - allow_scripts: boolean; - /** node types by string */ - registered_node_types: Record; - /** used for dropping files in the canvas */ - node_types_by_file_extension: Record; - /** node types by class name */ - Nodes: Record; - - /** used to add extra features to the search box */ - searchbox_extras: Record< - string, - { - data: { outputs: string[][]; title: string }; - desc: string; - type: string; - } - >; - - createNode(type: string): T; - /** Register a node class so it can be listed when the user wants to create a new one */ - registerNodeType(type: string, base: { new (): LGraphNode }): void; - /** removes a node type from the system */ - unregisterNodeType(type: string): void; - /** Removes all previously registered node's types. */ - clearRegisteredTypes(): void; - /** - * Create a new node type by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function. - * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output. - * @param name node name with namespace (p.e.: 'math/sum') - * @param func - * @param param_types an array containing the type of every parameter, otherwise parameters will accept any type - * @param return_type string with the return type, otherwise it will be generic - * @param properties properties to be configurable - */ - wrapFunctionAsNode( - name: string, - func: (...args: any[]) => any, - param_types?: string[], - return_type?: string, - properties?: object - ): void; - - /** - * Adds this method to all node types, existing and to be created - * (You can add it to LGraphNode.prototype but then existing node types wont have it) - */ - addNodeMethod(name: string, func: (...args: any[]) => any): void; - - /** - * Create a node of a given type with a name. The node is not attached to any graph yet. - * @param type full name of the node class. p.e. "math/sin" - * @param name a name to distinguish from other nodes - * @param options to set options - */ - createNode( - type: string, - title: string, - options: object - ): T; - - /** - * Returns a registered node type with a given name - * @param type full name of the node class. p.e. "math/sin" - */ - getNodeType(type: string): LGraphNodeConstructor; - - /** - * Returns a list of node types matching one category - * @method getNodeTypesInCategory - * @param {String} category category name - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the node classes - */ - getNodeTypesInCategory( - category: string, - filter: string - ): LGraphNodeConstructor[]; - - /** - * Returns a list with all the node type categories - * @method getNodeTypesCategories - * @param {String} filter only nodes with ctor.filter equal can be shown - * @return {Array} array with all the names of the categories - */ - getNodeTypesCategories(filter: string): string[]; - - /** debug purposes: reloads all the js scripts that matches a wildcard */ - reloadNodes(folder_wildcard: string): void; - - getTime(): number; - LLink: typeof LLink; - LGraph: typeof LGraph; - DragAndScale: typeof DragAndScale; - compareObjects(a: object, b: object): boolean; - distance(a: Vector2, b: Vector2): number; - colorToString(c: string): string; - isInsideRectangle( - x: number, - y: number, - left: number, - top: number, - width: number, - height: number - ): boolean; - growBounding(bounding: Vector4, x: number, y: number): Vector4; - isInsideBounding(p: Vector2, bb: Vector4): boolean; - hex2num(hex: string): [number, number, number]; - num2hex(triplet: [number, number, number]): string; - ContextMenu: typeof ContextMenu; - extendClass(target: A, origin: B): A & B; - getParameterNames(func: string): string[]; -}; - -export type serializedLGraph< - TNode = ReturnType, - // https://github.com/jagenjo/litegraph.js/issues/74 - TLink = [number, number, number, number, number, string], - TGroup = ReturnType -> = { - last_node_id: LGraph["last_node_id"]; - last_link_id: LGraph["last_link_id"]; - nodes: TNode[]; - links: TLink[]; - groups: TGroup[]; - config: LGraph["config"]; - version: typeof LiteGraph.VERSION; -}; - -export declare class LGraph { - static supported_types: string[]; - static STATUS_STOPPED: 1; - static STATUS_RUNNING: 2; - - constructor(o?: object); - - filter: string; - catch_errors: boolean; - /** custom data */ - config: object; - elapsed_time: number; - fixedtime: number; - fixedtime_lapse: number; - globaltime: number; - inputs: any; - iteration: number; - last_link_id: number; - last_node_id: number; - last_update_time: number; - links: Record; - list_of_graphcanvas: LGraphCanvas[]; - outputs: any; - runningtime: number; - starttime: number; - status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED; - - private _nodes: LGraphNode[]; - private _groups: LGraphGroup[]; - private _nodes_by_id: Record; - /** nodes that are executable sorted in execution order */ - private _nodes_executable: - | (LGraphNode & { onExecute: NonNullable }[]) - | null; - /** nodes that contain onExecute */ - private _nodes_in_order: LGraphNode[]; - private _version: number; - - getSupportedTypes(): string[]; - /** Removes all nodes from this graph */ - clear(): void; - /** Attach Canvas to this graph */ - attachCanvas(graphCanvas: LGraphCanvas): void; - /** Detach Canvas to this graph */ - detachCanvas(graphCanvas: LGraphCanvas): void; - /** - * Starts running this graph every interval milliseconds. - * @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate - */ - start(interval?: number): void; - /** Stops the execution loop of the graph */ - stop(): void; - /** - * Run N steps (cycles) of the graph - * @param num number of steps to run, default is 1 - */ - runStep(num?: number, do_not_catch_errors?: boolean): void; - /** - * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than - * nodes with only inputs. - */ - updateExecutionOrder(): void; - /** This is more internal, it computes the executable nodes in order and returns it */ - computeExecutionOrder(only_onExecute: boolean, set_level: any): T; - /** - * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively. - * It doesn't include the node itself - * @return an array with all the LGraphNodes that affect this node, in order of execution - */ - getAncestors(node: LGraphNode): LGraphNode[]; - /** - * Positions every node in a more readable manner - */ - arrange(margin?: number,layout?: string): void; - /** - * Returns the amount of time the graph has been running in milliseconds - * @return number of milliseconds the graph has been running - */ - getTime(): number; - - /** - * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant - * @return number of milliseconds the graph has been running - */ - getFixedTime(): number; - - /** - * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct - * if the nodes are using graphical actions - * @return number of milliseconds it took the last cycle - */ - getElapsedTime(): number; - /** - * Sends an event to all the nodes, useful to trigger stuff - * @param eventName the name of the event (function to be called) - * @param params parameters in array format - */ - sendEventToAllNodes(eventName: string, params: any[], mode?: any): void; - - sendActionToCanvas(action: any, params: any[]): void; - /** - * Adds a new node instance to this graph - * @param node the instance of the node - */ - add(node: LGraphNode, skip_compute_order?: boolean): void; - /** - * Called when a new node is added - * @param node the instance of the node - */ - onNodeAdded(node: LGraphNode): void; - /** Removes a node from the graph */ - remove(node: LGraphNode): void; - /** Returns a node by its id. */ - getNodeById(id: number): LGraphNode | undefined; - /** - * Returns a list of nodes that matches a class - * @param classObject the class itself (not an string) - * @return a list with all the nodes of this type - */ - findNodesByClass( - classObject: LGraphNodeConstructor - ): T[]; - /** - * Returns a list of nodes that matches a type - * @param type the name of the node type - * @return a list with all the nodes of this type - */ - findNodesByType(type: string): T[]; - /** - * Returns the first node that matches a name in its title - * @param title the name of the node to search - * @return the node or null - */ - findNodeByTitle(title: string): T | null; - /** - * Returns a list of nodes that matches a name - * @param title the name of the node to search - * @return a list with all the nodes with this name - */ - findNodesByTitle(title: string): T[]; - /** - * Returns the top-most node in this position of the canvas - * @param x the x coordinate in canvas space - * @param y the y coordinate in canvas space - * @param nodes_list a list with all the nodes to search from, by default is all the nodes in the graph - * @return the node at this position or null - */ - getNodeOnPos( - x: number, - y: number, - node_list?: LGraphNode[], - margin?: number - ): T | null; - /** - * Returns the top-most group in that position - * @param x the x coordinate in canvas space - * @param y the y coordinate in canvas space - * @return the group or null - */ - getGroupOnPos(x: number, y: number): LGraphGroup | null; - - onAction(action: any, param: any): void; - trigger(action: any, param: any): void; - /** Tell this graph it has a global graph input of this type */ - addInput(name: string, type: string, value?: any): void; - /** Assign a data to the global graph input */ - setInputData(name: string, data: any): void; - /** Returns the current value of a global graph input */ - getInputData(name: string): T; - /** Changes the name of a global graph input */ - renameInput(old_name: string, name: string): false | undefined; - /** Changes the type of a global graph input */ - changeInputType(name: string, type: string): false | undefined; - /** Removes a global graph input */ - removeInput(name: string): boolean; - /** Creates a global graph output */ - addOutput(name: string, type: string, value: any): void; - /** Assign a data to the global output */ - setOutputData(name: string, value: string): void; - /** Returns the current value of a global graph output */ - getOutputData(name: string): T; - - /** Renames a global graph output */ - renameOutput(old_name: string, name: string): false | undefined; - /** Changes the type of a global graph output */ - changeOutputType(name: string, type: string): false | undefined; - /** Removes a global graph output */ - removeOutput(name: string): boolean; - triggerInput(name: string, value: any): void; - setCallback(name: string, func: (...args: any[]) => any): void; - beforeChange(info?: LGraphNode): void; - afterChange(info?: LGraphNode): void; - connectionChange(node: LGraphNode): void; - /** returns if the graph is in live mode */ - isLive(): boolean; - /** clears the triggered slot animation in all links (stop visual animation) */ - clearTriggeredSlots(): void; - /* Called when something visually changed (not the graph!) */ - change(): void; - setDirtyCanvas(fg: boolean, bg: boolean): void; - /** Destroys a link */ - removeLink(link_id: number): void; - /** Creates a Object containing all the info about this graph, it can be serialized */ - serialize(): T; - /** - * Configure a graph from a JSON string - * @param data configure a graph from a JSON string - * @returns if there was any error parsing - */ - configure(data: object, keep_old?: boolean): boolean | undefined; - load(url: string): void; -} - -export type SerializedLLink = [number, string, number, number, number, number]; -export declare class LLink { - id: number; - type: string; - origin_id: number; - origin_slot: number; - target_id: number; - target_slot: number; - constructor( - id: number, - type: string, - origin_id: number, - origin_slot: number, - target_id: number, - target_slot: number - ); - configure(o: LLink | SerializedLLink): void; - serialize(): SerializedLLink; -} - -export type SerializedLGraphNode = { - id: T["id"]; - type: T["type"]; - pos: T["pos"]; - size: T["size"]; - flags: T["flags"]; - mode: T["mode"]; - inputs: T["inputs"]; - outputs: T["outputs"]; - title: T["title"]; - properties: T["properties"]; - widgets_values?: IWidget["value"][]; -}; - -/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */ -export declare class LGraphNode { - static title_color: string; - static title: string; - static type: null | string; - static widgets_up: boolean; - constructor(title?: string); - - title: string; - type: null | string; - size: Vector2; - graph: null | LGraph; - graph_version: number; - pos: Vector2; - is_selected: boolean; - mouseOver: boolean; - - id: number; - - //inputs available: array of inputs - inputs: INodeInputSlot[]; - outputs: INodeOutputSlot[]; - connections: any[]; - - //local data - properties: Record; - properties_info: any[]; - - flags: Partial<{ - collapsed: boolean - }>; - - color: string; - bgcolor: string; - boxcolor: string; - shape: - | typeof LiteGraph.BOX_SHAPE - | typeof LiteGraph.ROUND_SHAPE - | typeof LiteGraph.CIRCLE_SHAPE - | typeof LiteGraph.CARD_SHAPE - | typeof LiteGraph.ARROW_SHAPE; - - serialize_widgets: boolean; - skip_list: boolean; - - /** Used in `LGraphCanvas.onMenuNodeMode` */ - mode?: - | typeof LiteGraph.ON_EVENT - | typeof LiteGraph.ON_TRIGGER - | typeof LiteGraph.NEVER - | typeof LiteGraph.ALWAYS; - - /** If set to true widgets do not start after the slots */ - widgets_up: boolean; - /** widgets start at y distance from the top of the node */ - widgets_start_y: number; - /** if you render outside the node, it will be clipped */ - clip_area: boolean; - /** if set to false it wont be resizable with the mouse */ - resizable: boolean; - /** slots are distributed horizontally */ - horizontal: boolean; - /** if true, the node will show the bgcolor as 'red' */ - has_errors?: boolean; - - /** configure a node from an object containing the serialized info */ - configure(info: SerializedLGraphNode): void; - /** serialize the content */ - serialize(): SerializedLGraphNode; - /** Creates a clone of this node */ - clone(): this; - /** serialize and stringify */ - toString(): string; - /** get the title string */ - getTitle(): string; - /** sets the value of a property */ - setProperty(name: string, value: any): void; - /** sets the output data */ - setOutputData(slot: number, data: any): void; - /** sets the output data */ - setOutputDataType(slot: number, type: string): void; - /** - * Retrieves the input data (data traveling through the connection) from one slot - * @param slot - * @param force_update if set to true it will force the connected node of this slot to output data into this link - * @return data or if it is not connected returns undefined - */ - getInputData(slot: number, force_update?: boolean): T; - /** - * Retrieves the input data type (in case this supports multiple input types) - * @param slot - * @return datatype in string format - */ - getInputDataType(slot: number): string; - /** - * Retrieves the input data from one slot using its name instead of slot number - * @param slot_name - * @param force_update if set to true it will force the connected node of this slot to output data into this link - * @return data or if it is not connected returns null - */ - getInputDataByName(slot_name: string, force_update?: boolean): T; - /** tells you if there is a connection in one input slot */ - isInputConnected(slot: number): boolean; - /** tells you info about an input connection (which node, type, etc) */ - getInputInfo( - slot: number - ): { link: number; name: string; type: string | 0 } | null; - /** returns the node connected in the input slot */ - getInputNode(slot: number): LGraphNode | null; - /** returns the value of an input with this name, otherwise checks if there is a property with that name */ - getInputOrProperty(name: string): T; - /** tells you the last output data that went in that slot */ - getOutputData(slot: number): T | null; - /** tells you info about an output connection (which node, type, etc) */ - getOutputInfo( - slot: number - ): { name: string; type: string; links: number[] } | null; - /** tells you if there is a connection in one output slot */ - isOutputConnected(slot: number): boolean; - /** tells you if there is any connection in the output slots */ - isAnyOutputConnected(): boolean; - /** retrieves all the nodes connected to this output slot */ - getOutputNodes(slot: number): LGraphNode[]; - /** Triggers an event in this node, this will trigger any output with the same name */ - trigger(action: string, param: any): void; - /** - * Triggers an slot event in this node - * @param slot the index of the output slot - * @param param - * @param link_id in case you want to trigger and specific output link in a slot - */ - triggerSlot(slot: number, param: any, link_id?: number): void; - /** - * clears the trigger slot animation - * @param slot the index of the output slot - * @param link_id in case you want to trigger and specific output link in a slot - */ - clearTriggeredSlot(slot: number, link_id?: number): void; - /** - * add a new property to this node - * @param name - * @param default_value - * @param type string defining the output type ("vec3","number",...) - * @param extra_info this can be used to have special properties of the property (like values, etc) - */ - addProperty( - name: string, - default_value: any, - type: string, - extra_info?: object - ): T; - /** - * add a new output slot to use in this node - * @param name - * @param type string defining the output type ("vec3","number",...) - * @param extra_info this can be used to have special properties of an output (label, special color, position, etc) - */ - addOutput( - name: string, - type: string | -1, - extra_info?: Partial - ): INodeOutputSlot; - /** - * add a new output slot to use in this node - * @param array of triplets like [[name,type,extra_info],[...]] - */ - addOutputs( - array: [string, string | -1, Partial | undefined][] - ): void; - /** remove an existing output slot */ - removeOutput(slot: number): void; - /** - * add a new input slot to use in this node - * @param name - * @param type string defining the input type ("vec3","number",...), it its a generic one use 0 - * @param extra_info this can be used to have special properties of an input (label, color, position, etc) - */ - addInput( - name: string, - type: string | -1, - extra_info?: Partial - ): INodeInputSlot; - /** - * add several new input slots in this node - * @param array of triplets like [[name,type,extra_info],[...]] - */ - addInputs( - array: [string, string | -1, Partial | undefined][] - ): void; - /** remove an existing input slot */ - removeInput(slot: number): void; - /** - * add an special connection to this node (used for special kinds of graphs) - * @param name - * @param type string defining the input type ("vec3","number",...) - * @param pos position of the connection inside the node - * @param direction if is input or output - */ - addConnection( - name: string, - type: string, - pos: Vector2, - direction: string - ): { - name: string; - type: string; - pos: Vector2; - direction: string; - links: null; - }; - setValue(v: any): void; - /** computes the size of a node according to its inputs and output slots */ - computeSize(): [number, number]; - /** - * https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#node-widgets - * @return created widget - */ - addWidget( - type: T["type"], - name: string, - value: T["value"], - callback?: WidgetCallback | string, - options?: T["options"] - ): T; - - addCustomWidget(customWidget: T): T; - - /** - * returns the bounding of the object, used for rendering purposes - * @return [x, y, width, height] - */ - getBounding(): Vector4; - /** checks if a point is inside the shape of a node */ - isPointInside( - x: number, - y: number, - margin?: number, - skipTitle?: boolean - ): boolean; - /** checks if a point is inside a node slot, and returns info about which slot */ - getSlotInPosition( - x: number, - y: number - ): { - input?: INodeInputSlot; - output?: INodeOutputSlot; - slot: number; - link_pos: Vector2; - }; - /** - * returns the input slot with a given name (used for dynamic slots), -1 if not found - * @param name the name of the slot - * @return the slot (-1 if not found) - */ - findInputSlot(name: string): number; - /** - * returns the output slot with a given name (used for dynamic slots), -1 if not found - * @param name the name of the slot - * @return the slot (-1 if not found) - */ - findOutputSlot(name: string): number; - /** - * connect this node output to the input of another node - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param targetNode the target node - * @param targetSlot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger) - * @return {Object} the link_info is created, otherwise null - */ - connect( - slot: number | string, - targetNode: LGraphNode, - targetSlot: number | string - ): T | null; - /** - * disconnect one output to an specific node - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected] - * @return if it was disconnected successfully - */ - disconnectOutput(slot: number | string, targetNode?: LGraphNode): boolean; - /** - * disconnect one input - * @param slot (could be the number of the slot or the string with the name of the slot) - * @return if it was disconnected successfully - */ - disconnectInput(slot: number | string): boolean; - /** - * returns the center of a connection point in canvas coords - * @param is_input true if if a input slot, false if it is an output - * @param slot (could be the number of the slot or the string with the name of the slot) - * @param out a place to store the output, to free garbage - * @return the position - **/ - getConnectionPos( - is_input: boolean, - slot: number | string, - out?: Vector2 - ): Vector2; - /** Force align to grid */ - alignToGrid(): void; - /** Console output */ - trace(msg: string): void; - /** Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */ - setDirtyCanvas(fg: boolean, bg: boolean): void; - loadImage(url: string): void; - /** Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */ - captureInput(v: any): void; - /** Collapse the node to make it smaller on the canvas */ - collapse(force: boolean): void; - /** Forces the node to do not move or realign on Z */ - pin(v?: boolean): void; - localToScreen(x: number, y: number, graphCanvas: LGraphCanvas): Vector2; - - // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance - onDrawBackground?( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement - ): void; - onDrawForeground?( - ctx: CanvasRenderingContext2D, - canvas: HTMLCanvasElement - ): void; - - // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour - onMouseDown?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseMove?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseUp?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseEnter?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onMouseLeave?( - event: MouseEvent, - pos: Vector2, - graphCanvas: LGraphCanvas - ): void; - onKey?(event: KeyboardEvent, pos: Vector2, graphCanvas: LGraphCanvas): void; - - /** Called by `LGraphCanvas.selectNodes` */ - onSelected?(): void; - /** Called by `LGraphCanvas.deselectNode` */ - onDeselected?(): void; - /** Called by `LGraph.runStep` `LGraphNode.getInputData` */ - onExecute?(): void; - /** Called by `LGraph.serialize` */ - onSerialize?(o: SerializedLGraphNode): void; - /** Called by `LGraph.configure` */ - onConfigure?(o: SerializedLGraphNode): void; - /** - * when added to graph (warning: this is called BEFORE the node is configured when loading) - * Called by `LGraph.add` - */ - onAdded?(graph: LGraph): void; - /** - * when removed from graph - * Called by `LGraph.remove` `LGraph.clear` - */ - onRemoved?(): void; - /** - * if returns false the incoming connection will be canceled - * Called by `LGraph.connect` - * @param inputIndex target input slot number - * @param outputType type of output slot - * @param outputSlot output slot object - * @param outputNode node containing the output - * @param outputIndex index of output slot - */ - onConnectInput?( - inputIndex: number, - outputType: INodeOutputSlot["type"], - outputSlot: INodeOutputSlot, - outputNode: LGraphNode, - outputIndex: number - ): boolean; - /** - * if returns false the incoming connection will be canceled - * Called by `LGraph.connect` - * @param outputIndex target output slot number - * @param inputType type of input slot - * @param inputSlot input slot object - * @param inputNode node containing the input - * @param inputIndex index of input slot - */ - onConnectOutput?( - outputIndex: number, - inputType: INodeInputSlot["type"], - inputSlot: INodeInputSlot, - inputNode: LGraphNode, - inputIndex: number - ): boolean; - - /** - * Called just before connection (or disconnect - if input is linked). - * A convenient place to switch to another input, or create new one. - * This allow for ability to automatically add slots if needed - * @param inputIndex - * @return selected input slot index, can differ from parameter value - */ - onBeforeConnectInput?( - inputIndex: number - ): number; - - /** a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info or output_info ) */ - onConnectionsChange( - type: number, - slotIndex: number, - isConnected: boolean, - link: LLink, - ioSlot: (INodeOutputSlot | INodeInputSlot) - ): void; - - /** - * if returns false, will abort the `LGraphNode.setProperty` - * Called when a property is changed - * @param property - * @param value - * @param prevValue - */ - onPropertyChanged?(property: string, value: any, prevValue: any): void | boolean; - - /** Called by `LGraphCanvas.processContextMenu` */ - getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[]; - getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[]; -} - -export type LGraphNodeConstructor = { - new (): T; -}; - -export type SerializedLGraphGroup = { - title: LGraphGroup["title"]; - bounding: LGraphGroup["_bounding"]; - color: LGraphGroup["color"]; - font: LGraphGroup["font"]; -}; -export declare class LGraphGroup { - title: string; - private _bounding: Vector4; - color: string; - font: string; - - configure(o: SerializedLGraphGroup): void; - serialize(): SerializedLGraphGroup; - move(deltaX: number, deltaY: number, ignoreNodes?: boolean): void; - recomputeInsideNodes(): void; - isPointInside: LGraphNode["isPointInside"]; - setDirtyCanvas: LGraphNode["setDirtyCanvas"]; -} - -export declare class DragAndScale { - constructor(element?: HTMLElement, skipEvents?: boolean); - offset: [number, number]; - scale: number; - max_scale: number; - min_scale: number; - onredraw: Function | null; - enabled: boolean; - last_mouse: Vector2; - element: HTMLElement | null; - visible_area: Vector4; - bindEvents(element: HTMLElement): void; - computeVisibleArea(): void; - onMouse(e: MouseEvent): void; - toCanvasContext(ctx: CanvasRenderingContext2D): void; - convertOffsetToCanvas(pos: Vector2): Vector2; - convertCanvasToOffset(pos: Vector2): Vector2; - mouseDrag(x: number, y: number): void; - changeScale(value: number, zooming_center?: Vector2): void; - changeDeltaScale(value: number, zooming_center?: Vector2): void; - reset(): void; -} - -/** - * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required. - * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked - * - * @param canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself) - * @param graph - * @param options { skip_rendering, autoresize } - */ -export declare class LGraphCanvas { - static node_colors: Record< - string, - { - color: string; - bgcolor: string; - groupcolor: string; - } - >; - static link_type_colors: Record; - static gradients: object; - static search_limit: number; - - static getFileExtension(url: string): string; - static decodeHTML(str: string): string; - - static onMenuCollapseAll(): void; - static onMenuNodeEdit(): void; - static onShowPropertyEditor( - item: any, - options: any, - e: any, - menu: any, - node: any - ): void; - /** Create menu for `Add Group` */ - static onGroupAdd: ContextMenuEventListener; - /** Create menu for `Add Node` */ - static onMenuAdd: ContextMenuEventListener; - static showMenuNodeOptionalInputs: ContextMenuEventListener; - static showMenuNodeOptionalOutputs: ContextMenuEventListener; - static onShowMenuNodeProperties: ContextMenuEventListener; - static onResizeNode: ContextMenuEventListener; - static onMenuNodeCollapse: ContextMenuEventListener; - static onMenuNodePin: ContextMenuEventListener; - static onMenuNodeMode: ContextMenuEventListener; - static onMenuNodeColors: ContextMenuEventListener; - static onMenuNodeShapes: ContextMenuEventListener; - static onMenuNodeRemove: ContextMenuEventListener; - static onMenuNodeClone: ContextMenuEventListener; - - constructor( - canvas: HTMLCanvasElement | string, - graph?: LGraph, - options?: { - skip_render?: boolean; - autoresize?: boolean; - } - ); - - static active_canvas: HTMLCanvasElement; - - allow_dragcanvas: boolean; - allow_dragnodes: boolean; - /** allow to control widgets, buttons, collapse, etc */ - allow_interaction: boolean; - /** allows to change a connection with having to redo it again */ - allow_reconnect_links: boolean; - /** allow selecting multi nodes without pressing extra keys */ - multi_select: boolean; - /** No effect */ - allow_searchbox: boolean; - always_render_background: boolean; - autoresize?: boolean; - background_image: string; - bgcanvas: HTMLCanvasElement; - bgctx: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - canvas_mouse: Vector2; - clear_background: boolean; - connecting_node: LGraphNode | null; - connections_width: number; - ctx: CanvasRenderingContext2D; - current_node: LGraphNode | null; - default_connection_color: { - input_off: string; - input_on: string; - output_off: string; - output_on: string; - }; - default_link_color: string; - dirty_area: Vector4 | null; - dirty_bgcanvas?: boolean; - dirty_canvas?: boolean; - drag_mode: boolean; - dragging_canvas: boolean; - dragging_rectangle: Vector4 | null; - ds: DragAndScale; - /** used for transition */ - editor_alpha: number; - filter: any; - fps: number; - frame: number; - graph: LGraph; - highlighted_links: Record; - highquality_render: boolean; - inner_text_font: string; - is_rendering: boolean; - last_draw_time: number; - last_mouse: Vector2; - /** - * Possible duplicated with `last_mouse` - * https://github.com/jagenjo/litegraph.js/issues/70 - */ - last_mouse_position: Vector2; - /** Timestamp of last mouse click, defaults to 0 */ - last_mouseclick: number; - links_render_mode: - | typeof LiteGraph.STRAIGHT_LINK - | typeof LiteGraph.LINEAR_LINK - | typeof LiteGraph.SPLINE_LINK; - live_mode: boolean; - node_capturing_input: LGraphNode | null; - node_dragged: LGraphNode | null; - node_in_panel: LGraphNode | null; - node_over: LGraphNode | null; - node_title_color: string; - node_widget: [LGraphNode, IWidget] | null; - /** Called by `LGraphCanvas.drawBackCanvas` */ - onDrawBackground: - | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) - | null; - /** Called by `LGraphCanvas.drawFrontCanvas` */ - onDrawForeground: - | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void) - | null; - onDrawOverlay: ((ctx: CanvasRenderingContext2D) => void) | null; - /** Called by `LGraphCanvas.processMouseDown` */ - onMouse: ((event: MouseEvent) => boolean) | null; - /** Called by `LGraphCanvas.drawFrontCanvas` and `LGraphCanvas.drawLinkTooltip` */ - onDrawLinkTooltip: ((ctx: CanvasRenderingContext2D, link: LLink, _this: this) => void) | null; - /** Called by `LGraphCanvas.selectNodes` */ - onNodeMoved: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeSelected` */ - onNodeSelected: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.deselectNode` */ - onNodeDeselected: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeDblClicked` */ - onShowNodePanel: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.processNodeDblClicked` */ - onNodeDblClicked: ((node: LGraphNode) => void) | null; - /** Called by `LGraphCanvas.selectNodes` */ - onSelectionChange: ((nodes: Record) => void) | null; - /** Called by `LGraphCanvas.showSearchBox` */ - onSearchBox: - | (( - helper: Element, - value: string, - graphCanvas: LGraphCanvas - ) => string[]) - | null; - onSearchBoxSelection: - | ((name: string, event: MouseEvent, graphCanvas: LGraphCanvas) => void) - | null; - pause_rendering: boolean; - render_canvas_border: boolean; - render_collapsed_slots: boolean; - render_connection_arrows: boolean; - render_connections_border: boolean; - render_connections_shadows: boolean; - render_curved_connections: boolean; - render_execution_order: boolean; - render_only_selected: boolean; - render_shadows: boolean; - render_title_colored: boolean; - round_radius: number; - selected_group: null | LGraphGroup; - selected_group_resizing: boolean; - selected_nodes: Record; - show_info: boolean; - title_text_font: string; - /** set to true to render title bar with gradients */ - use_gradients: boolean; - visible_area: DragAndScale["visible_area"]; - visible_links: LLink[]; - visible_nodes: LGraphNode[]; - zoom_modify_alpha: boolean; - - /** clears all the data inside */ - clear(): void; - /** assigns a graph, you can reassign graphs to the same canvas */ - setGraph(graph: LGraph, skipClear?: boolean): void; - /** opens a graph contained inside a node in the current graph */ - openSubgraph(graph: LGraph): void; - /** closes a subgraph contained inside a node */ - closeSubgraph(): void; - /** assigns a canvas */ - setCanvas(canvas: HTMLCanvasElement, skipEvents?: boolean): void; - /** binds mouse, keyboard, touch and drag events to the canvas */ - bindEvents(): void; - /** unbinds mouse events from the canvas */ - unbindEvents(): void; - - /** - * this function allows to render the canvas using WebGL instead of Canvas2D - * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL - **/ - enableWebGL(): void; - - /** - * marks as dirty the canvas, this way it will be rendered again - * @param fg if the foreground canvas is dirty (the one containing the nodes) - * @param bg if the background canvas is dirty (the one containing the wires) - */ - setDirty(fg: boolean, bg: boolean): void; - - /** - * Used to attach the canvas in a popup - * @return the window where the canvas is attached (the DOM root node) - */ - getCanvasWindow(): Window; - /** starts rendering the content of the canvas when needed */ - startRendering(): void; - /** stops rendering the content of the canvas (to save resources) */ - stopRendering(): void; - - processMouseDown(e: MouseEvent): boolean | undefined; - processMouseMove(e: MouseEvent): boolean | undefined; - processMouseUp(e: MouseEvent): boolean | undefined; - processMouseWheel(e: MouseEvent): boolean | undefined; - - /** returns true if a position (in graph space) is on top of a node little corner box */ - isOverNodeBox(node: LGraphNode, canvasX: number, canvasY: number): boolean; - /** returns true if a position (in graph space) is on top of a node input slot */ - isOverNodeInput( - node: LGraphNode, - canvasX: number, - canvasY: number, - slotPos: Vector2 - ): boolean; - - /** process a key event */ - processKey(e: KeyboardEvent): boolean | undefined; - - copyToClipboard(): void; - pasteFromClipboard(): void; - processDrop(e: DragEvent): void; - checkDropItem(e: DragEvent): void; - processNodeDblClicked(n: LGraphNode): void; - processNodeSelected(n: LGraphNode, e: MouseEvent): void; - processNodeDeselected(node: LGraphNode): void; - - /** selects a given node (or adds it to the current selection) */ - selectNode(node: LGraphNode, add?: boolean): void; - /** selects several nodes (or adds them to the current selection) */ - selectNodes(nodes?: LGraphNode[], add?: boolean): void; - /** removes a node from the current selection */ - deselectNode(node: LGraphNode): void; - /** removes all nodes from the current selection */ - deselectAllNodes(): void; - /** deletes all nodes in the current selection from the graph */ - deleteSelectedNodes(): void; - - /** centers the camera on a given node */ - centerOnNode(node: LGraphNode): void; - /** changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom */ - setZoom(value: number, center: Vector2): void; - /** brings a node to front (above all other nodes) */ - bringToFront(node: LGraphNode): void; - /** sends a node to the back (below all other nodes) */ - sendToBack(node: LGraphNode): void; - /** checks which nodes are visible (inside the camera area) */ - computeVisibleNodes(nodes: LGraphNode[]): LGraphNode[]; - /** renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */ - draw(forceFG?: boolean, forceBG?: boolean): void; - /** draws the front canvas (the one containing all the nodes) */ - drawFrontCanvas(): void; - /** draws some useful stats in the corner of the canvas */ - renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void; - /** draws the back canvas (the one containing the background and the connections) */ - drawBackCanvas(): void; - /** draws the given node inside the canvas */ - drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void; - /** draws graphic for node's slot */ - drawSlotGraphic(ctx: CanvasRenderingContext2D, pos: number[], shape: SlotShape, horizontal: boolean): void; - /** draws the shape of the given node in the canvas */ - drawNodeShape( - node: LGraphNode, - ctx: CanvasRenderingContext2D, - size: [number, number], - fgColor: string, - bgColor: string, - selected: boolean, - mouseOver: boolean - ): void; - /** draws every connection visible in the canvas */ - drawConnections(ctx: CanvasRenderingContext2D): void; - /** - * draws a link between two points - * @param a start pos - * @param b end pos - * @param link the link object with all the link info - * @param skipBorder ignore the shadow of the link - * @param flow show flow animation (for events) - * @param color the color for the link - * @param startDir the direction enum - * @param endDir the direction enum - * @param numSublines number of sublines (useful to represent vec3 or rgb) - **/ - renderLink( - a: Vector2, - b: Vector2, - link: object, - skipBorder: boolean, - flow: boolean, - color?: string, - startDir?: number, - endDir?: number, - numSublines?: number - ): void; - - computeConnectionPoint( - a: Vector2, - b: Vector2, - t: number, - startDir?: number, - endDir?: number - ): void; - - drawExecutionOrder(ctx: CanvasRenderingContext2D): void; - /** draws the widgets stored inside a node */ - drawNodeWidgets( - node: LGraphNode, - posY: number, - ctx: CanvasRenderingContext2D, - activeWidget: object - ): void; - /** process an event on widgets */ - processNodeWidgets( - node: LGraphNode, - pos: Vector2, - event: Event, - activeWidget: object - ): void; - /** draws every group area in the background */ - drawGroups(canvas: any, ctx: CanvasRenderingContext2D): void; - adjustNodesSize(): void; - /** resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode */ - resize(width?: number, height?: number): void; - /** - * switches to live mode (node shapes are not rendered, only the content) - * this feature was designed when graphs where meant to create user interfaces - **/ - switchLiveMode(transition?: boolean): void; - onNodeSelectionChange(): void; - touchHandler(event: TouchEvent): void; - - showLinkMenu(link: LLink, e: any): false; - prompt( - title: string, - value: any, - callback: Function, - event: any - ): HTMLDivElement; - showSearchBox(event?: MouseEvent): void; - showEditPropertyValue(node: LGraphNode, property: any, options: any): void; - createDialog( - html: string, - options?: { position?: Vector2; event?: MouseEvent } - ): void; - - convertOffsetToCanvas: DragAndScale["convertOffsetToCanvas"]; - convertCanvasToOffset: DragAndScale["convertCanvasToOffset"]; - /** converts event coordinates from canvas2D to graph coordinates */ - convertEventToCanvasOffset(e: MouseEvent): Vector2; - /** adds some useful properties to a mouse event, like the position in graph coordinates */ - adjustMouseEvent(e: MouseEvent): void; - - getCanvasMenuOptions(): ContextMenuItem[]; - getNodeMenuOptions(node: LGraphNode): ContextMenuItem[]; - getGroupMenuOptions(): ContextMenuItem[]; - /** Called by `getCanvasMenuOptions`, replace default options */ - getMenuOptions?(): ContextMenuItem[]; - /** Called by `getCanvasMenuOptions`, append to default options */ - getExtraMenuOptions?(): ContextMenuItem[]; - /** Called when mouse right click */ - processContextMenu(node: LGraphNode, event: Event): void; -} - -declare class ContextMenu { - static trigger( - element: HTMLElement, - event_name: string, - params: any, - origin: any - ): void; - static isCursorOverElement(event: MouseEvent, element: HTMLElement): void; - static closeAllContextMenus(window: Window): void; - constructor(values: ContextMenuItem[], options?: IContextMenuOptions, window?: Window); - options: IContextMenuOptions; - parentMenu?: ContextMenu; - lock: boolean; - current_submenu?: ContextMenu; - addItem( - name: string, - value: ContextMenuItem, - options?: IContextMenuOptions - ): void; - close(e?: MouseEvent, ignore_parent_menu?: boolean): void; - getTopMenu(): void; - getFirstEvent(): void; -} - -declare global { - interface CanvasRenderingContext2D { - /** like rect but rounded corners */ - roundRect( - x: number, - y: number, - width: number, - height: number, - radius: number, - radiusLow: number - ): void; - } - - interface Math { - clamp(v: number, min: number, max: number): number; - } -} diff --git a/web/user.css b/web/user.css deleted file mode 100644 index 8b1af3868..000000000 --- a/web/user.css +++ /dev/null @@ -1 +0,0 @@ -/* Put custom styles here */ \ No newline at end of file