diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6763945..48e7289 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,6 +29,28 @@ jobs:
with:
submodules: "recursive"
path: ${{ env.repo_name }}
+
+ # - name: 📝 Prepare file with paths to remove
+ # run: |
+ # find ${{ env.repo_name }} -type f -size +10M > .release_ignore
+ # find ${{ env.repo_name }} -type d -empty >> .release_ignore
+ # shell: bash
+
+ - name: 🗑️ Remove files and directories listed in .release_ignore
+ shell: bash
+ run: |
+ if [ -f "${{ env.repo_name }}/.release_ignore" ]; then
+ while IFS= read -r entry; do
+ if [ -f "${{ env.repo_name }}/$entry" ]; then
+ rm "${{ env.repo_name }}/$entry"
+ elif [ -d "${{ env.repo_name }}/$entry" ]; then
+ rm -r "${{ env.repo_name }}/$entry"
+ fi
+ done < "${{ env.repo_name }}/.release_ignore"
+ else
+ echo "No .release_ignore file found. Skipping removal of files and directories."
+ fi
+
- name: 📦 Building custom comfy nodes
shell: bash
run: |
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..bc6c48e
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "useTabs": false
+}
\ No newline at end of file
diff --git a/.release_ignore b/.release_ignore
new file mode 100644
index 0000000..31f6c67
--- /dev/null
+++ b/.release_ignore
@@ -0,0 +1,3 @@
+extern/frame_interpolation/moment.gif
+extern/frame_interpolation/photos
+extern/GFPGAN/inputs
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
index 81a6434..11a713d 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,3 +1,12 @@
+#!/usr/bin/env python3
+# -*- coding:utf-8 -*-
+###
+# File: __init__.py
+# Project: comfy_mtb
+# Author: Mel Massadian
+# Copyright (c) 2023 Mel Massadian
+#
+###
import os
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
@@ -5,6 +14,7 @@
import traceback
from .log import log, blue_text, cyan_text, get_summary, get_label
from .utils import here
+from .utils import comfy_dir
import importlib
import os
import ast
@@ -14,7 +24,7 @@
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_CLASS_MAPPINGS_DEBUG = {}
-__version__ = "0.1.0"
+__version__ = "0.1.1"
def extract_nodes_from_source(filename):
@@ -89,7 +99,7 @@ def load_nodes():
# - REGISTER WEB EXTENSIONS
-web_extensions_root = utils.comfy_dir / "web" / "extensions"
+web_extensions_root = comfy_dir / "web" / "extensions"
web_mtb = web_extensions_root / "mtb"
if web_mtb.exists():
@@ -158,12 +168,11 @@ def load_nodes():
# - ENDPOINT
from server import PromptServer
-from .log import mklog, log
+from .log import log
from aiohttp import web
from importlib import reload
import logging
-
-endlog = mklog("mtb endpoint")
+from .endpoint import endlog
@PromptServer.instance.routes.get("/mtb/status")
@@ -260,6 +269,30 @@ async def get_debug(request):
return web.json_response({"enabled": enabled})
+@PromptServer.instance.routes.get("/mtb/actions")
+async def no_route(request):
+ from . import endpoint
+
+ if "text/html" in request.headers.get("Accept", ""):
+ html_response = f"""
+
Actions has no get for now...
+ """
+ return web.Response(
+ text=endpoint.render_base_template("Actions", html_response),
+ content_type="text/html",
+ )
+ return web.json_response({"message": "actions has no get for now"})
+
+
+@PromptServer.instance.routes.post("/mtb/actions")
+async def do_action(request):
+ from . import endpoint
+
+ reload(endpoint)
+
+ return await endpoint.do_action(request)
+
+
# - WAS Dictionary
MANIFEST = {
"name": "MTB Nodes", # The title that will be displayed on Node Class menu,. and Node Class view
diff --git a/endpoint.py b/endpoint.py
index f75aab6..d2030e0 100644
--- a/endpoint.py
+++ b/endpoint.py
@@ -1,6 +1,58 @@
from .utils import here
+from aiohttp import web
+from .log import mklog
+import os
+endlog = mklog("mtb endpoint")
+#- ACTIONS
+
+def ACTIONS_getStyles(style_name=None):
+ from .nodes.conditions import StylesLoader
+
+ styles = StylesLoader.options
+ match_list = ["name"]
+ if styles:
+ filtered_styles = {
+ key: value
+ for key, value in styles.items()
+ if not key.startswith("__") and key not in match_list
+ }
+ if style_name:
+ if style_name in filtered_styles:
+ return filtered_styles[style_name]
+ else:
+ return {"error": "Style not found"}
+ return filtered_styles
+ return {"error": "No styles found"}
+
+
+async def do_action(request) -> web.Response:
+ endlog.debug("Init action request")
+ request_data = await request.json()
+ name = request_data.get("name")
+ args = request_data.get("args")
+
+ endlog.debug(f"Received action request: {name} {args}")
+
+ method_name = "ACTIONS_" + name
+ method = globals().get(method_name)
+
+ if callable(method):
+ result = method(args) if args else method()
+ endlog.debug(f"Action result: {result}")
+ return web.json_response({"result": result})
+
+ available_methods = [
+ attr[len("ACTIONS_") :] for attr in globals() if attr.startswith("ACTIONS_")
+ ]
+
+ return web.json_response(
+ {"error": "Invalid method name.", "available_methods": available_methods}
+ )
+
+
+# - HTML UTILS
def render_table(table_dict, sort=True, title=None):
table_rows = ""
table_dict = sorted(
diff --git a/examples/03-animation_builder-condition-lerp.json b/examples/03-animation_builder-condition-lerp.json
new file mode 100644
index 0000000..bd6e44a
--- /dev/null
+++ b/examples/03-animation_builder-condition-lerp.json
@@ -0,0 +1,1230 @@
+{
+ "last_node_id": 79,
+ "last_link_id": 162,
+ "nodes": [
+ {
+ "id": 59,
+ "type": "Reroute",
+ "pos": [
+ -150.35178124999982,
+ 644.4360633544919
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 124
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "VAE",
+ "links": [
+ 139
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 56,
+ "type": "Reroute",
+ "pos": [
+ -1580.8297949218763,
+ 644.7740239257807
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 117
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "VAE",
+ "links": [
+ 124
+ ]
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 57,
+ "type": "Reroute",
+ "pos": [
+ -673.8297949218747,
+ -185.22597607421872
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 135
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "MODEL",
+ "links": [
+ 120
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 65,
+ "type": "Reroute",
+ "pos": [
+ -1512.8297949218763,
+ -181.22597607421872
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 134
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "MODEL",
+ "links": [
+ 135
+ ]
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 16,
+ "type": "CheckpointLoaderSimple",
+ "pos": [
+ -2000.8297949218756,
+ 192.77402392578128
+ ],
+ "size": [
+ 315,
+ 98
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 134
+ ],
+ "slot_index": 0
+ },
+ {
+ "name": "CLIP",
+ "type": "CLIP",
+ "links": [
+ 116
+ ],
+ "slot_index": 1
+ },
+ {
+ "name": "VAE",
+ "type": "VAE",
+ "links": [
+ 117
+ ],
+ "slot_index": 2
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CheckpointLoaderSimple"
+ },
+ "widgets_values": [
+ "revAnimated_v122.safetensors"
+ ]
+ },
+ {
+ "id": 3,
+ "type": "KSampler",
+ "pos": [
+ -482.82979492187513,
+ -21.22597607421874
+ ],
+ "size": [
+ 315,
+ 474
+ ],
+ "flags": {},
+ "order": 19,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "link": 120
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 153
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 6
+ },
+ {
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 2
+ }
+ ],
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 138
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 5428233522,
+ "fixed",
+ 45,
+ 8,
+ "euler_ancestral",
+ "simple",
+ 1
+ ]
+ },
+ {
+ "id": 19,
+ "type": "CLIPSetLastLayer",
+ "pos": [
+ -1597,
+ 209
+ ],
+ "size": [
+ 315,
+ 58
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 116
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CLIP",
+ "type": "CLIP",
+ "links": [
+ 28,
+ 29,
+ 149
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CLIPSetLastLayer"
+ },
+ "widgets_values": [
+ -2
+ ]
+ },
+ {
+ "id": 20,
+ "type": "Styles Loader (mtb)",
+ "pos": [
+ -1615,
+ 404
+ ],
+ "size": [
+ 315,
+ 78
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 1,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "positive",
+ "type": "STRING",
+ "links": [],
+ "shape": 3
+ },
+ {
+ "name": "negative",
+ "type": "STRING",
+ "links": [
+ 87
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Styles Loader (mtb)"
+ },
+ "widgets_values": [
+ "❌Mel Negatives (general)"
+ ]
+ },
+ {
+ "id": 7,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -839,
+ 335
+ ],
+ "size": [
+ 210,
+ 54
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 29
+ },
+ {
+ "name": "text",
+ "type": "STRING",
+ "link": 87,
+ "widget": {
+ "name": "text",
+ "config": [
+ "STRING",
+ {
+ "multiline": true
+ }
+ ]
+ },
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 6
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ "worst quality, hands, embedding:EasyNegative,"
+ ]
+ },
+ {
+ "id": 6,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -1263,
+ -42
+ ],
+ "size": [
+ 210,
+ 54
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 28
+ },
+ {
+ "name": "text",
+ "type": "STRING",
+ "link": 145,
+ "widget": {
+ "name": "text",
+ "config": [
+ "STRING",
+ {
+ "multiline": true
+ }
+ ]
+ },
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 151
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ "A majestic Lion, fur in the wind, (smirk smile)"
+ ]
+ },
+ {
+ "id": 72,
+ "type": "ConditioningAverage ",
+ "pos": [
+ -988,
+ -13
+ ],
+ "size": [
+ 380.4000244140625,
+ 78
+ ],
+ "flags": {},
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "conditioning_to",
+ "type": "CONDITIONING",
+ "link": 151
+ },
+ {
+ "name": "conditioning_from",
+ "type": "CONDITIONING",
+ "link": 152,
+ "slot_index": 1
+ },
+ {
+ "name": "conditioning_to_strength",
+ "type": "FLOAT",
+ "link": 154,
+ "widget": {
+ "name": "conditioning_to_strength",
+ "config": [
+ "FLOAT",
+ {
+ "default": 1,
+ "min": 0,
+ "max": 1,
+ "step": 0.01
+ }
+ ]
+ },
+ "slot_index": 2
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 153
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ConditioningAverage "
+ },
+ "widgets_values": [
+ 1
+ ]
+ },
+ {
+ "id": 66,
+ "type": "VAEDecodeTiled",
+ "pos": [
+ 197,
+ -20
+ ],
+ "size": [
+ 210,
+ 46
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 20,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "samples",
+ "type": "LATENT",
+ "link": 138
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "link": 139,
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 161
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "VAEDecodeTiled"
+ }
+ },
+ {
+ "id": 78,
+ "type": "SaveImage",
+ "pos": [
+ 566.1508954260959,
+ 5.399725291354571
+ ],
+ "size": [
+ 315,
+ 270
+ ],
+ "flags": {},
+ "order": 21,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 161
+ }
+ ],
+ "properties": {},
+ "widgets_values": [
+ "mtb_demo-conditional_blend"
+ ]
+ },
+ {
+ "id": 5,
+ "type": "EmptyLatentImage",
+ "pos": [
+ -969,
+ 484
+ ],
+ "size": [
+ 315,
+ 106
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 2
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "EmptyLatentImage"
+ },
+ "widgets_values": [
+ 768,
+ 512,
+ 1
+ ]
+ },
+ {
+ "id": 75,
+ "type": "PrimitiveNode",
+ "pos": [
+ -1626,
+ 1228
+ ],
+ "size": [
+ 210,
+ 82
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 156,
+ 159
+ ],
+ "slot_index": 0,
+ "widget": {
+ "name": "total_frames",
+ "config": [
+ "INT",
+ {
+ "default": 100,
+ "min": 0
+ }
+ ]
+ }
+ }
+ ],
+ "properties": {},
+ "widgets_values": [
+ 24,
+ "fixed"
+ ]
+ },
+ {
+ "id": 74,
+ "type": "Get Batch From History (mtb)",
+ "pos": [
+ -594,
+ 914
+ ],
+ "size": [
+ 315,
+ 102
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "enable",
+ "type": "BOOL",
+ "link": 155
+ },
+ {
+ "name": "count",
+ "type": "INT",
+ "link": 159,
+ "widget": {
+ "name": "count",
+ "config": [
+ "INT",
+ {
+ "default": 1,
+ "min": 0
+ }
+ ]
+ },
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "i",
+ "type": "IMAGE",
+ "links": [
+ 160,
+ 162
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Get Batch From History (mtb)"
+ },
+ "widgets_values": [
+ 24,
+ 0,
+ false
+ ]
+ },
+ {
+ "id": 77,
+ "type": "Export To Prores (mtb)",
+ "pos": [
+ -57,
+ 911
+ ],
+ "size": [
+ 315,
+ 82
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 160,
+ "slot_index": 0
+ }
+ ],
+ "outputs": [
+ {
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Export To Prores (mtb)"
+ },
+ "widgets_values": [
+ 12,
+ "export"
+ ]
+ },
+ {
+ "id": 70,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -1246,
+ 72
+ ],
+ "size": [
+ 210,
+ 54
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 149,
+ "slot_index": 0
+ },
+ {
+ "name": "text",
+ "type": "STRING",
+ "link": 150,
+ "widget": {
+ "name": "text",
+ "config": [
+ "STRING",
+ {
+ "multiline": true
+ }
+ ]
+ },
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 152
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ "A majestic Lion, fur in the wind, (smirk smile)"
+ ]
+ },
+ {
+ "id": 68,
+ "type": "Text box",
+ "pos": [
+ -1871,
+ 9
+ ],
+ "size": [
+ 217.2119140625,
+ 122.18632507324219
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 141,
+ 150
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Text box"
+ },
+ "widgets_values": [
+ "A cinematic shot of a blue car, high speed, blood trail"
+ ]
+ },
+ {
+ "id": 69,
+ "type": "String Replace (mtb)",
+ "pos": [
+ -1566,
+ -58
+ ],
+ "size": [
+ 210,
+ 82
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "string",
+ "type": "STRING",
+ "link": 141
+ }
+ ],
+ "outputs": [
+ {
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 145
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "String Replace (mtb)"
+ },
+ "widgets_values": [
+ "blue",
+ "yellow"
+ ]
+ },
+ {
+ "id": 73,
+ "type": "Animation Builder (mtb)",
+ "pos": [
+ -1260,
+ 850
+ ],
+ "size": [
+ 211.60000610351562,
+ 306
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "total_frames",
+ "type": "INT",
+ "link": 156,
+ "widget": {
+ "name": "total_frames",
+ "config": [
+ "INT",
+ {
+ "default": 100,
+ "min": 0
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "frame",
+ "type": "INT",
+ "links": null,
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "0-1 (scaled)",
+ "type": "FLOAT",
+ "links": [
+ 154
+ ],
+ "shape": 3,
+ "slot_index": 1
+ },
+ {
+ "name": "count",
+ "type": "INT",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "loop_ended",
+ "type": "BOOL",
+ "links": [
+ 155
+ ],
+ "shape": 3,
+ "slot_index": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Animation Builder (mtb)"
+ },
+ "widgets_values": [
+ 24,
+ 1,
+ 1,
+ 24,
+ 1,
+ "raw: 24\nframe: 0",
+ "Done 😎!",
+ "reset",
+ "queue"
+ ]
+ },
+ {
+ "id": 79,
+ "type": "Save Gif (mtb)",
+ "pos": [
+ -33,
+ 1041
+ ],
+ "size": [
+ 210,
+ 338.00006103515625
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 162
+ },
+ {
+ "name": "pingpong",
+ "type": "BOOL",
+ "link": null
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Save Gif (mtb)"
+ },
+ "widgets_values": [
+ 12,
+ 1,
+ true
+ ]
+ }
+ ],
+ "links": [
+ [
+ 2,
+ 5,
+ 0,
+ 3,
+ 3,
+ "LATENT"
+ ],
+ [
+ 6,
+ 7,
+ 0,
+ 3,
+ 2,
+ "CONDITIONING"
+ ],
+ [
+ 28,
+ 19,
+ 0,
+ 6,
+ 0,
+ "CLIP"
+ ],
+ [
+ 29,
+ 19,
+ 0,
+ 7,
+ 0,
+ "CLIP"
+ ],
+ [
+ 87,
+ 20,
+ 1,
+ 7,
+ 1,
+ "STRING"
+ ],
+ [
+ 116,
+ 16,
+ 1,
+ 19,
+ 0,
+ "CLIP"
+ ],
+ [
+ 117,
+ 16,
+ 2,
+ 56,
+ 0,
+ "*"
+ ],
+ [
+ 120,
+ 57,
+ 0,
+ 3,
+ 0,
+ "MODEL"
+ ],
+ [
+ 124,
+ 56,
+ 0,
+ 59,
+ 0,
+ "*"
+ ],
+ [
+ 134,
+ 16,
+ 0,
+ 65,
+ 0,
+ "*"
+ ],
+ [
+ 135,
+ 65,
+ 0,
+ 57,
+ 0,
+ "*"
+ ],
+ [
+ 138,
+ 3,
+ 0,
+ 66,
+ 0,
+ "LATENT"
+ ],
+ [
+ 139,
+ 59,
+ 0,
+ 66,
+ 1,
+ "VAE"
+ ],
+ [
+ 141,
+ 68,
+ 0,
+ 69,
+ 0,
+ "STRING"
+ ],
+ [
+ 145,
+ 69,
+ 0,
+ 6,
+ 1,
+ "STRING"
+ ],
+ [
+ 149,
+ 19,
+ 0,
+ 70,
+ 0,
+ "CLIP"
+ ],
+ [
+ 150,
+ 68,
+ 0,
+ 70,
+ 1,
+ "STRING"
+ ],
+ [
+ 151,
+ 6,
+ 0,
+ 72,
+ 0,
+ "CONDITIONING"
+ ],
+ [
+ 152,
+ 70,
+ 0,
+ 72,
+ 1,
+ "CONDITIONING"
+ ],
+ [
+ 153,
+ 72,
+ 0,
+ 3,
+ 1,
+ "CONDITIONING"
+ ],
+ [
+ 154,
+ 73,
+ 1,
+ 72,
+ 2,
+ "FLOAT"
+ ],
+ [
+ 155,
+ 73,
+ 3,
+ 74,
+ 0,
+ "BOOL"
+ ],
+ [
+ 156,
+ 75,
+ 0,
+ 73,
+ 0,
+ "INT"
+ ],
+ [
+ 159,
+ 75,
+ 0,
+ 74,
+ 1,
+ "INT"
+ ],
+ [
+ 160,
+ 74,
+ 0,
+ 77,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 161,
+ 66,
+ 0,
+ 78,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 162,
+ 74,
+ 0,
+ 79,
+ 0,
+ "IMAGE"
+ ]
+ ],
+ "groups": [
+ {
+ "title": "Txt2Img",
+ "bounding": [
+ -2061,
+ -234,
+ 1932,
+ 973
+ ],
+ "color": "#a1309b",
+ "locked": false
+ },
+ {
+ "title": "Save Intermediate Image",
+ "bounding": [
+ 150,
+ -116,
+ 303,
+ 213
+ ],
+ "color": "#3f789e",
+ "locked": false
+ }
+ ],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/examples/04-animation_builder-deforum.json b/examples/04-animation_builder-deforum.json
new file mode 100644
index 0000000..0b66feb
--- /dev/null
+++ b/examples/04-animation_builder-deforum.json
@@ -0,0 +1,1190 @@
+{
+ "last_node_id": 39,
+ "last_link_id": 69,
+ "nodes": [
+ {
+ "id": 11,
+ "type": "Get Batch From History (mtb)",
+ "pos": [
+ -828,
+ 522
+ ],
+ "size": [
+ 235.1999969482422,
+ 118
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "passthrough_image",
+ "type": "IMAGE",
+ "link": 10
+ },
+ {
+ "name": "enable",
+ "type": "BOOL",
+ "link": 9,
+ "widget": {
+ "name": "enable",
+ "config": [
+ "BOOL",
+ {
+ "default": true
+ }
+ ]
+ },
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "i",
+ "type": "IMAGE",
+ "links": [
+ 26
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Get Batch From History (mtb)"
+ },
+ "widgets_values": [
+ false,
+ 1,
+ 0
+ ],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 24,
+ "type": "Note",
+ "pos": [
+ -827,
+ 406
+ ],
+ "size": [
+ 233.25148010253906,
+ 82.53218841552734
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "properties": {
+ "text": ""
+ },
+ "widgets_values": [
+ "On first frame we get the init image, on all subsequent ones the feedback from the previous queue item"
+ ],
+ "color": "#223",
+ "bgcolor": "#335",
+ "shape": 1
+ },
+ {
+ "id": 12,
+ "type": "Int To Bool (mtb)",
+ "pos": [
+ -1057,
+ 583
+ ],
+ "size": [
+ 210,
+ 36.36605696244669
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "int",
+ "type": "INT",
+ "link": 34,
+ "widget": {
+ "name": "int",
+ "config": [
+ "INT",
+ {
+ "default": 0
+ }
+ ]
+ },
+ "slot_index": 0
+ }
+ ],
+ "outputs": [
+ {
+ "name": "BOOL",
+ "type": "BOOL",
+ "links": [
+ 9
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Int To Bool (mtb)"
+ },
+ "widgets_values": [
+ 29
+ ],
+ "color": "#222",
+ "bgcolor": "#000"
+ },
+ {
+ "id": 10,
+ "type": "LoadImage",
+ "pos": [
+ -1409,
+ 524
+ ],
+ "size": [
+ 315,
+ 314
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 10
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "example.png",
+ "image"
+ ],
+ "color": "#432",
+ "bgcolor": "#653",
+ "shape": 1
+ },
+ {
+ "id": 18,
+ "type": "Get Batch From History (mtb)",
+ "pos": [
+ -960,
+ 1257
+ ],
+ "size": [
+ 235.1999969482422,
+ 118
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "passthrough_image",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "name": "enable",
+ "type": "BOOL",
+ "link": 31,
+ "widget": {
+ "name": "enable",
+ "config": [
+ "BOOL",
+ {
+ "default": true
+ }
+ ]
+ },
+ "slot_index": 1
+ },
+ {
+ "name": "count",
+ "type": "INT",
+ "link": 44,
+ "widget": {
+ "name": "count",
+ "config": [
+ "INT",
+ {
+ "default": 1,
+ "min": 0
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "i",
+ "type": "IMAGE",
+ "links": [
+ 17,
+ 18
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Get Batch From History (mtb)"
+ },
+ "widgets_values": [
+ true,
+ 30,
+ 0
+ ]
+ },
+ {
+ "id": 20,
+ "type": "Export To Prores (mtb)",
+ "pos": [
+ -612,
+ 1818
+ ],
+ "size": [
+ 292.4239807128906,
+ 93.884033203125
+ ],
+ "flags": {},
+ "order": 13,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 17
+ }
+ ],
+ "outputs": [
+ {
+ "name": "VIDEO",
+ "type": "VIDEO",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Export To Prores (mtb)"
+ },
+ "widgets_values": [
+ 12,
+ "export"
+ ]
+ },
+ {
+ "id": 35,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -118,
+ 331
+ ],
+ "size": [
+ 210,
+ 54
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 60
+ },
+ {
+ "name": "text",
+ "type": "STRING",
+ "link": 66,
+ "widget": {
+ "name": "text",
+ "config": [
+ "STRING",
+ {
+ "multiline": true
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 54
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ ""
+ ]
+ },
+ {
+ "id": 9,
+ "type": "CheckpointLoaderSimple",
+ "pos": [
+ -558,
+ 114
+ ],
+ "size": [
+ 301.23302978515596,
+ 98
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "MODEL",
+ "type": "MODEL",
+ "links": [
+ 62
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "CLIP",
+ "type": "CLIP",
+ "links": [
+ 59,
+ 60
+ ],
+ "shape": 3,
+ "slot_index": 1
+ },
+ {
+ "name": "VAE",
+ "type": "VAE",
+ "links": [
+ 58,
+ 69
+ ],
+ "shape": 3,
+ "slot_index": 2
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CheckpointLoaderSimple"
+ },
+ "widgets_values": [
+ "revAnimated_v122.safetensors"
+ ]
+ },
+ {
+ "id": 37,
+ "type": "VAEEncode",
+ "pos": [
+ -125,
+ 236
+ ],
+ "size": [
+ 210,
+ 46
+ ],
+ "flags": {},
+ "order": 16,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "pixels",
+ "type": "IMAGE",
+ "link": 57
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "link": 58,
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 55
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "VAEEncode"
+ }
+ },
+ {
+ "id": 34,
+ "type": "CLIPTextEncode",
+ "pos": [
+ -124,
+ 84
+ ],
+ "size": [
+ 210,
+ 96
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "clip",
+ "type": "CLIP",
+ "link": 59
+ }
+ ],
+ "outputs": [
+ {
+ "name": "CONDITIONING",
+ "type": "CONDITIONING",
+ "links": [
+ 53
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "CLIPTextEncode"
+ },
+ "widgets_values": [
+ "A blonde doll with a pink dress sleeping, draping, wrinkles"
+ ]
+ },
+ {
+ "id": 33,
+ "type": "Text box",
+ "pos": [
+ -497,
+ 349
+ ],
+ "size": [
+ 294,
+ 95.12843764123829
+ ],
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "STRING",
+ "type": "STRING",
+ "links": [
+ 66
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "title": "❌Mel Negatives (general) (Negative)",
+ "properties": {
+ "Node name for S&R": "Text box"
+ },
+ "widgets_values": [
+ "embedding:EasyNegative, embedding:EasyNegativeV2, watermark, text, deformed"
+ ]
+ },
+ {
+ "id": 39,
+ "type": "Reroute",
+ "pos": [
+ -156,
+ 908
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 67
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "INT",
+ "links": [
+ 68
+ ]
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 14,
+ "type": "Transform Image (mtb)",
+ "pos": [
+ -527,
+ 520
+ ],
+ "size": [
+ 315,
+ 178
+ ],
+ "flags": {},
+ "order": 15,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 26
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 57
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Transform Image (mtb)"
+ },
+ "widgets_values": [
+ 0,
+ 5,
+ 1.03,
+ 1,
+ 0,
+ "reflect"
+ ],
+ "color": "#223",
+ "bgcolor": "#335"
+ },
+ {
+ "id": 36,
+ "type": "KSampler",
+ "pos": [
+ 223,
+ 262
+ ],
+ "size": [
+ 315,
+ 442
+ ],
+ "flags": {},
+ "order": 17,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "MODEL",
+ "link": 62,
+ "slot_index": 0
+ },
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": 53
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": 54
+ },
+ {
+ "name": "latent_image",
+ "type": "LATENT",
+ "link": 55
+ },
+ {
+ "name": "denoise",
+ "type": "FLOAT",
+ "link": 63,
+ "widget": {
+ "name": "denoise",
+ "config": [
+ "FLOAT",
+ {
+ "default": 1,
+ "min": 0,
+ "max": 1,
+ "step": 0.01
+ }
+ ]
+ },
+ "slot_index": 4
+ },
+ {
+ "name": "seed",
+ "type": "INT",
+ "link": 68,
+ "widget": {
+ "name": "seed",
+ "config": [
+ "INT",
+ {
+ "default": 0,
+ "min": 0,
+ "max": 18446744073709552000
+ }
+ ]
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "LATENT",
+ "type": "LATENT",
+ "links": [
+ 56
+ ],
+ "shape": 3,
+ "slot_index": 0,
+ "color": "#FF9CF9"
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "KSampler"
+ },
+ "widgets_values": [
+ 61876307854624,
+ "randomize",
+ 15,
+ 8,
+ "euler_ancestral",
+ "normal",
+ 0.6
+ ]
+ },
+ {
+ "id": 15,
+ "type": "SaveImage",
+ "pos": [
+ 782,
+ 259
+ ],
+ "size": [
+ 330.11123461913985,
+ 378.1239961059566
+ ],
+ "flags": {},
+ "order": 19,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 65
+ }
+ ],
+ "properties": {},
+ "widgets_values": [
+ "ComfyUI"
+ ]
+ },
+ {
+ "id": 19,
+ "type": "Save Gif (mtb)",
+ "pos": [
+ -613,
+ 1256
+ ],
+ "size": [
+ 356.17758847656205,
+ 471.87858076171824
+ ],
+ "flags": {},
+ "order": 14,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 18
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Save Gif (mtb)"
+ },
+ "widgets_values": [
+ 12,
+ 1,
+ true,
+ "/view?filename=7964e7cc8b.gif&subfolder=&type=output"
+ ]
+ },
+ {
+ "id": 31,
+ "type": "PrimitiveNode",
+ "pos": [
+ -2060,
+ 1291
+ ],
+ "size": [
+ 210,
+ 82
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "INT",
+ "type": "INT",
+ "links": [
+ 43,
+ 44
+ ],
+ "widget": {
+ "name": "total_frames",
+ "config": [
+ "INT",
+ {
+ "default": 100,
+ "min": 0
+ }
+ ]
+ },
+ "slot_index": 0
+ }
+ ],
+ "title": "total_frames",
+ "properties": {},
+ "widgets_values": [
+ 30,
+ "fixed"
+ ],
+ "color": "#432",
+ "bgcolor": "#653"
+ },
+ {
+ "id": 38,
+ "type": "VAEDecode",
+ "pos": [
+ 556,
+ 260
+ ],
+ "size": [
+ 210,
+ 46
+ ],
+ "flags": {},
+ "order": 18,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "samples",
+ "type": "LATENT",
+ "link": 56
+ },
+ {
+ "name": "vae",
+ "type": "VAE",
+ "link": 69,
+ "slot_index": 1
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 65
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "VAEDecode"
+ }
+ },
+ {
+ "id": 22,
+ "type": "Fit Number (mtb)",
+ "pos": [
+ -647,
+ 882
+ ],
+ "size": [
+ 232.28509687500036,
+ 166
+ ],
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "value",
+ "type": "FLOAT",
+ "link": 27
+ }
+ ],
+ "outputs": [
+ {
+ "name": "FLOAT",
+ "type": "FLOAT",
+ "links": [
+ 63
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "title": "Fit Number (mtb) - Denoise",
+ "properties": {
+ "Node name for S&R": "Fit Number (mtb)"
+ },
+ "widgets_values": [
+ true,
+ 0,
+ 1,
+ 0.3,
+ 0.6
+ ]
+ },
+ {
+ "id": 17,
+ "type": "Animation Builder (mtb)",
+ "pos": [
+ -1309,
+ 883
+ ],
+ "size": [
+ 211.60000610351562,
+ 294
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "total_frames",
+ "type": "INT",
+ "link": 43,
+ "widget": {
+ "name": "total_frames",
+ "config": [
+ "INT",
+ {
+ "default": 100,
+ "min": 0
+ }
+ ]
+ },
+ "slot_index": 0
+ }
+ ],
+ "outputs": [
+ {
+ "name": "frame",
+ "type": "INT",
+ "links": [
+ 34
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "0-1 (scaled)",
+ "type": "FLOAT",
+ "links": [
+ 27
+ ],
+ "shape": 3,
+ "slot_index": 1
+ },
+ {
+ "name": "count",
+ "type": "INT",
+ "links": [
+ 67
+ ],
+ "shape": 3,
+ "slot_index": 2
+ },
+ {
+ "name": "loop_ended",
+ "type": "BOOL",
+ "links": [
+ 31
+ ],
+ "shape": 3,
+ "slot_index": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Animation Builder (mtb)"
+ },
+ "widgets_values": [
+ 30,
+ 1,
+ 1,
+ 0,
+ 0,
+ "Idle",
+ "Iteration: Idle",
+ "reset",
+ "queue"
+ ],
+ "color": "#232",
+ "bgcolor": "#353",
+ "shape": 1
+ }
+ ],
+ "links": [
+ [
+ 9,
+ 12,
+ 0,
+ 11,
+ 1,
+ "BOOL"
+ ],
+ [
+ 10,
+ 10,
+ 0,
+ 11,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 17,
+ 18,
+ 0,
+ 20,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 18,
+ 18,
+ 0,
+ 19,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 26,
+ 11,
+ 0,
+ 14,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 27,
+ 17,
+ 1,
+ 22,
+ 0,
+ "FLOAT"
+ ],
+ [
+ 31,
+ 17,
+ 3,
+ 18,
+ 1,
+ "BOOL"
+ ],
+ [
+ 34,
+ 17,
+ 0,
+ 12,
+ 0,
+ "INT"
+ ],
+ [
+ 43,
+ 31,
+ 0,
+ 17,
+ 0,
+ "INT"
+ ],
+ [
+ 44,
+ 31,
+ 0,
+ 18,
+ 2,
+ "INT"
+ ],
+ [
+ 53,
+ 34,
+ 0,
+ 36,
+ 1,
+ "CONDITIONING"
+ ],
+ [
+ 54,
+ 35,
+ 0,
+ 36,
+ 2,
+ "CONDITIONING"
+ ],
+ [
+ 55,
+ 37,
+ 0,
+ 36,
+ 3,
+ "LATENT"
+ ],
+ [
+ 56,
+ 36,
+ 0,
+ 38,
+ 0,
+ "LATENT"
+ ],
+ [
+ 57,
+ 14,
+ 0,
+ 37,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 58,
+ 9,
+ 2,
+ 37,
+ 1,
+ "VAE"
+ ],
+ [
+ 59,
+ 9,
+ 1,
+ 34,
+ 0,
+ "CLIP"
+ ],
+ [
+ 60,
+ 9,
+ 1,
+ 35,
+ 0,
+ "CLIP"
+ ],
+ [
+ 62,
+ 9,
+ 0,
+ 36,
+ 0,
+ "MODEL"
+ ],
+ [
+ 63,
+ 22,
+ 0,
+ 36,
+ 4,
+ "FLOAT"
+ ],
+ [
+ 65,
+ 38,
+ 0,
+ 15,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 66,
+ 33,
+ 0,
+ 35,
+ 1,
+ "STRING"
+ ],
+ [
+ 67,
+ 17,
+ 2,
+ 39,
+ 0,
+ "*"
+ ],
+ [
+ 68,
+ 39,
+ 0,
+ 36,
+ 5,
+ "INT"
+ ],
+ [
+ 69,
+ 9,
+ 2,
+ 38,
+ 1,
+ "VAE"
+ ]
+ ],
+ "groups": [
+ {
+ "title": "Video Output",
+ "bounding": [
+ -702,
+ 1161,
+ 516,
+ 773
+ ],
+ "color": "#3f789e",
+ "locked": false
+ },
+ {
+ "title": "START THE QUEUE BY CLICKLING HERE 👆",
+ "bounding": [
+ -1611,
+ 1196,
+ 521,
+ 80
+ ],
+ "color": "#8A8",
+ "locked": false
+ }
+ ],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/nodes/__init__.py b/nodes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/comfy_shared.js b/web/comfy_shared.js
index f6a2232..57850ac 100644
--- a/web/comfy_shared.js
+++ b/web/comfy_shared.js
@@ -1,282 +1,318 @@
-import { app } from "/scripts/app.js";
+/**
+ * File: comfy_shared.js
+ * Project: comfy_mtb
+ * Author: Mel Massadian
+ *
+ * Copyright (c) 2023 Mel Massadian
+ *
+ */
-export const log = (...args) => {
- if (window.MTB_DEBUG) {
- console.debug(...args);
- }
+import { app } from '/scripts/app.js'
+export const log = (...args) => {
+ if (window.MTB?.DEBUG) {
+ console.debug(...args)
+ }
}
//- WIDGET UTILS
-export const CONVERTED_TYPE = "converted-widget";
-
-export function offsetDOMWidget(widget, ctx, node, widgetWidth, widgetY, height) {
- 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 + widgetY);
-
- const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
- Object.assign(widget.inputEl.style, {
- transformOrigin: "0 0",
- transform: scale,
- left: `${transform.a + transform.e}px`,
- top: `${transform.d + transform.f}px`,
- width: `${widgetWidth - (margin * 2)}px`,
- // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`,
- height: `${(height || widget.parent?.inputHeight || 32) - (margin * 2)}px`,
-
- position: "absolute",
- background: (!node.color) ? '' : node.color,
- color: (!node.color) ? '' : 'white',
- zIndex: app.graph._nodes.indexOf(node),
- })
+export const CONVERTED_TYPE = 'converted-widget'
+
+export function offsetDOMWidget(
+ widget,
+ ctx,
+ node,
+ widgetWidth,
+ widgetY,
+ height
+) {
+ 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 + widgetY)
+
+ const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
+ Object.assign(widget.inputEl.style, {
+ transformOrigin: '0 0',
+ transform: scale,
+ left: `${transform.a + transform.e}px`,
+ top: `${transform.d + transform.f}px`,
+ width: `${widgetWidth - margin * 2}px`,
+ // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`,
+ height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`,
+
+ position: 'absolute',
+ background: !node.color ? '' : node.color,
+ color: !node.color ? '' : 'white',
+ zIndex: app.graph._nodes.indexOf(node),
+ })
}
/**
* Extracts the type and link type from a widget config object.
- * @param {*} config
- * @returns
+ * @param {*} config
+ * @returns
*/
export function getWidgetType(config) {
- // Special handling for COMBO so we restrict links based on the entries
- let type = config[0];
- let linkType = type;
- if (type instanceof Array) {
- type = "COMBO";
- linkType = linkType.join(",");
- }
- return { type, linkType };
+ // Special handling for COMBO so we restrict links based on the entries
+ let type = config[0]
+ let linkType = type
+ if (type instanceof Array) {
+ type = 'COMBO'
+ linkType = linkType.join(',')
+ }
+ return { type, linkType }
}
-export const dynamic_connection = (node, index, connected, connectionPrefix = "input_", connectionType = "PSDLAYER") => {
-
- // remove all non connected inputs
- if (!connected && node.inputs.length > 1) {
- log(`Removing input ${index} (${node.inputs[index].name})`)
- if (node.widgets) {
- const w = node.widgets.find((w) => w.name === node.inputs[index].name);
- if (w) {
- w.onRemove?.();
- node.widgets.length = node.widgets.length - 1
- }
- }
- node.removeInput(index)
-
- // make inputs sequential again
- for (let i = 0; i < node.inputs.length; i++) {
- node.inputs[i].label = `${connectionPrefix}${i + 1}`
- }
+export const dynamic_connection = (
+ node,
+ index,
+ connected,
+ connectionPrefix = 'input_',
+ connectionType = 'PSDLAYER'
+) => {
+ // remove all non connected inputs
+ if (!connected && node.inputs.length > 1) {
+ log(`Removing input ${index} (${node.inputs[index].name})`)
+ if (node.widgets) {
+ const w = node.widgets.find((w) => w.name === node.inputs[index].name)
+ if (w) {
+ w.onRemoved?.()
+ node.widgets.length = node.widgets.length - 1
+ }
}
+ node.removeInput(index)
- // add an extra input
- if (node.inputs[node.inputs.length - 1].link != undefined) {
- log(`Adding input ${node.inputs.length + 1} (${connectionPrefix}${node.inputs.length + 1})`)
-
- node.addInput(`${connectionPrefix}${node.inputs.length + 1}`, connectionType)
+ // make inputs sequential again
+ for (let i = 0; i < node.inputs.length; i++) {
+ node.inputs[i].label = `${connectionPrefix}${i + 1}`
}
-
+ }
+
+ // add an extra input
+ if (node.inputs[node.inputs.length - 1].link != undefined) {
+ log(
+ `Adding input ${node.inputs.length + 1} (${connectionPrefix}${
+ node.inputs.length + 1
+ })`
+ )
+
+ node.addInput(
+ `${connectionPrefix}${node.inputs.length + 1}`,
+ connectionType
+ )
+ }
}
-
/**
* Appends a callback to the extra menu options of a given node type.
- * @param {*} nodeType
- * @param {*} cb
+ * @param {*} nodeType
+ * @param {*} cb
*/
export function addMenuHandler(nodeType, cb) {
- const getOpts = nodeType.prototype.getExtraMenuOptions;
- nodeType.prototype.getExtraMenuOptions = function () {
- const r = getOpts.apply(this, arguments);
- cb.apply(this, arguments);
- return r;
- };
+ const getOpts = nodeType.prototype.getExtraMenuOptions
+ nodeType.prototype.getExtraMenuOptions = function () {
+ const r = getOpts.apply(this, arguments)
+ cb.apply(this, arguments)
+ return r
+ }
}
-export function hideWidget(node, widget, suffix = "") {
- widget.origType = widget.type;
- widget.hidden = true
- 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
- const { link } = node.inputs.find((i) => i.widget?.name === widget.name);
- if (link == null) {
- 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);
- }
+export function hideWidget(node, widget, suffix = '') {
+ widget.origType = widget.type
+ widget.hidden = true
+ 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
+ const { link } = node.inputs.find((i) => i.widget?.name === widget.name)
+ if (link == null) {
+ 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)
+ }
+ }
}
export 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);
- }
+ 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)
}
+ }
}
export function convertToWidget(node, widget) {
- showWidget(widget);
- const sz = node.size;
- node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
+ 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;
- }
+ 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])]);
+ // Restore original size but grow if needed
+ node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
}
-
export function convertToInput(node, widget, config) {
- hideWidget(node, widget);
+ hideWidget(node, widget)
- const { linkType } = getWidgetType(config);
+ const { linkType } = getWidgetType(config)
- // Add input and store widget config for creating on primitive node
- const sz = node.size;
- node.addInput(widget.name, linkType, {
- widget: { name: widget.name, config },
- });
+ // Add input and store widget config for creating on primitive node
+ const sz = node.size
+ node.addInput(widget.name, linkType, {
+ widget: { name: widget.name, config },
+ })
- for (const widget of node.widgets) {
- widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
- }
+ 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])]);
+ // Restore original size but grow if needed
+ node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
}
-export function hideWidgetForGood(node, widget, suffix = "") {
- 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
- // const w = node.inputs?.find((i) => i.widget?.name === widget.name);
- // if (w?.link == null) {
- // 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) {
- hideWidgetForGood(node, w, ":" + widget.name);
- }
+export function hideWidgetForGood(node, widget, suffix = '') {
+ 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
+ // const w = node.inputs?.find((i) => i.widget?.name === widget.name);
+ // if (w?.link == null) {
+ // 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) {
+ hideWidgetForGood(node, w, ':' + widget.name)
}
+ }
}
export function fixWidgets(node) {
- if (node.inputs) {
- for (const input of node.inputs) {
- log(input)
- if (input.widget || node.widgets) {
- // if (newTypes.includes(input.type)) {
- const matching_widget = node.widgets.find((w) => w.name === input.name);
- if (matching_widget) {
-
-
- // if (matching_widget.hidden) {
- // log(`Already hidden skipping ${matching_widget.name}`)
- // continue
- // }
- const w = node.widgets.find((w) => w.name === matching_widget.name);
- if (w && w.type != CONVERTED_TYPE) {
- log(w)
- log(`hidding ${w.name}(${w.type}) from ${node.type}`)
- log(node)
- hideWidget(node, w);
- } else {
- log(`converting to widget ${w}`)
-
- convertToWidget(node, input)
- }
- }
- }
+ if (node.inputs) {
+ for (const input of node.inputs) {
+ log(input)
+ if (input.widget || node.widgets) {
+ // if (newTypes.includes(input.type)) {
+ const matching_widget = node.widgets.find((w) => w.name === input.name)
+ if (matching_widget) {
+ // if (matching_widget.hidden) {
+ // log(`Already hidden skipping ${matching_widget.name}`)
+ // continue
+ // }
+ const w = node.widgets.find((w) => w.name === matching_widget.name)
+ if (w && w.type != CONVERTED_TYPE) {
+ log(w)
+ log(`hidding ${w.name}(${w.type}) from ${node.type}`)
+ log(node)
+ hideWidget(node, w)
+ } else {
+ log(`converting to widget ${w}`)
+
+ convertToWidget(node, input)
+ }
}
+ }
}
+ }
}
export function inner_value_change(widget, value, event = undefined) {
- if (widget.type == "number" || widget.type == "BBOX") {
- value = Number(value);
- } else if (widget.type == "BOOL") {
- value = Boolean(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, app.canvas, node, pos, event);
- }
+ if (widget.type == 'number' || widget.type == 'BBOX') {
+ value = Number(value)
+ } else if (widget.type == 'BOOL') {
+ value = Boolean(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, app.canvas, node, pos, event)
+ }
}
//- COLOR UTILS
export function isColorBright(rgb, threshold = 240) {
- const brightess = getBrightness(rgb)
- return brightess > threshold
+ const brightess = getBrightness(rgb)
+ return brightess > threshold
}
function getBrightness(rgbObj) {
- return Math.round(((parseInt(rgbObj[0]) * 299) + (parseInt(rgbObj[1]) * 587) + (parseInt(rgbObj[2]) * 114)) / 1000)
+ return Math.round(
+ (parseInt(rgbObj[0]) * 299 +
+ parseInt(rgbObj[1]) * 587 +
+ parseInt(rgbObj[2]) * 114) /
+ 1000
+ )
}
//- HTML / CSS UTILS
export function defineClass(className, classStyles) {
- const styleSheets = document.styleSheets;
-
- // Helper function to check if the class exists in a style sheet
- function classExistsInStyleSheet(styleSheet) {
- const rules = styleSheet.rules || styleSheet.cssRules;
- for (const rule of rules) {
- if (rule.selectorText === `.${className}`) {
- return true;
- }
- }
- return false;
+ const styleSheets = document.styleSheets
+
+ // Helper function to check if the class exists in a style sheet
+ function classExistsInStyleSheet(styleSheet) {
+ const rules = styleSheet.rules || styleSheet.cssRules
+ for (const rule of rules) {
+ if (rule.selectorText === `.${className}`) {
+ return true
+ }
}
-
- // Check if the class is already defined in any of the style sheets
- let classExists = false;
- for (const styleSheet of styleSheets) {
- if (classExistsInStyleSheet(styleSheet)) {
- classExists = true;
- break;
- }
+ return false
+ }
+
+ // Check if the class is already defined in any of the style sheets
+ let classExists = false
+ for (const styleSheet of styleSheets) {
+ if (classExistsInStyleSheet(styleSheet)) {
+ classExists = true
+ break
}
-
- // If the class doesn't exist, add the new class definition to the first style sheet
- if (!classExists) {
- if (styleSheets[0].insertRule) {
- styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0);
- } else if (styleSheets[0].addRule) {
- styleSheets[0].addRule(`.${className}`, classStyles, 0);
- }
+ }
+
+ // If the class doesn't exist, add the new class definition to the first style sheet
+ if (!classExists) {
+ if (styleSheets[0].insertRule) {
+ styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0)
+ } else if (styleSheets[0].addRule) {
+ styleSheets[0].addRule(`.${className}`, classStyles, 0)
}
+ }
}
diff --git a/web/debug.js b/web/debug.js
index 00079e1..e29d2a3 100644
--- a/web/debug.js
+++ b/web/debug.js
@@ -1,80 +1,99 @@
-import { app } from "/scripts/app.js";
+/**
+ * File: debug.js
+ * Project: comfy_mtb
+ * Author: Mel Massadian
+ *
+ * Copyright (c) 2023 Mel Massadian
+ *
+ */
+
+import { app } from '/scripts/app.js'
import * as shared from '/extensions/mtb/comfy_shared.js'
import { log } from '/extensions/mtb/comfy_shared.js'
import { MtbWidgets } from '/extensions/mtb/mtb_widgets.js'
// TODO: respect inputs order...
-
-
app.registerExtension({
- name: "mtb.Debug",
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
- if (nodeData.name === "Debug (mtb)") {
- const onConnectionsChange = nodeType.prototype.onConnectionsChange;
- nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
- const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined;
- // TODO: remove all widgets on disconnect once computed
- shared.dynamic_connection(this, index, connected, "anything_", "*")
+ name: 'mtb.Debug',
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === 'Debug (mtb)') {
+ const onConnectionsChange = nodeType.prototype.onConnectionsChange
+ nodeType.prototype.onConnectionsChange = function (
+ type,
+ index,
+ connected,
+ link_info
+ ) {
+ const r = onConnectionsChange
+ ? onConnectionsChange.apply(this, arguments)
+ : undefined
+ // TODO: remove all widgets on disconnect once computed
+ shared.dynamic_connection(this, index, connected, 'anything_', '*')
- //- infer type
- if (link_info) {
- const fromNode = this.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id);
- const type = fromNode.outputs[link_info.origin_slot].type;
- this.inputs[index].type = type;
- // this.inputs[index].label = type.toLowerCase()
- }
- //- restore dynamic input
- if (!connected) {
- this.inputs[index].type = "*";
- this.inputs[index].label = `anything_${index + 1}`
- }
- }
+ //- infer type
+ if (link_info) {
+ const fromNode = this.graph._nodes.find(
+ (otherNode) => otherNode.id == link_info.origin_id
+ )
+ const type = fromNode.outputs[link_info.origin_slot].type
+ this.inputs[index].type = type
+ // this.inputs[index].label = type.toLowerCase()
+ }
+ //- restore dynamic input
+ if (!connected) {
+ this.inputs[index].type = '*'
+ this.inputs[index].label = `anything_${index + 1}`
+ }
+ }
+
+ const onExecuted = nodeType.prototype.onExecuted
+ nodeType.prototype.onExecuted = function (message) {
+ onExecuted?.apply(this, arguments)
- const onExecuted = nodeType.prototype.onExecuted;
- nodeType.prototype.onExecuted = function (message) {
- log(message)
- onExecuted?.apply(this, arguments);
- log(message)
- if (this.widgets) {
- // const pos = this.widgets.findIndex((w) => w.name === "anything_1");
- // if (pos !== -1) {
- for (let i = 0; i < this.widgets.length; i++) {
- this.widgets[i].onRemove?.();
- }
- this.widgets.length = 0;
+ const prefix = 'anything_'
- }
- let widgetI = 1
- if (message.text) {
- for (const txt of message.text) {
- const w = this.addCustomWidget(MtbWidgets.DEBUG_STRING(txt, widgetI))
- w.parent = this;
- widgetI++;
- }
- }
- if (message.b64_images) {
- for (const img of message.b64_images) {
- const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, widgetI))
- w.parent = this;
- widgetI++;
- }
- // this.onResize?.(this.size);
- // this.resize?.(this.size)
- this.setSize(this.computeSize())
- };
+ if (this.widgets) {
+ // const pos = this.widgets.findIndex((w) => w.name === "anything_1");
+ // if (pos !== -1) {
+ for (let i = 0; i < this.widgets.length; i++) {
+ this.widgets[i].onRemoved?.()
+ }
+ this.widgets.length = 0
+ }
+ let widgetI = 1
+ if (message.text) {
+ for (const txt of message.text) {
+ const w = this.addCustomWidget(
+ MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, txt)
+ )
+ w.parent = this
+ widgetI++
+ }
+ }
+ if (message.b64_images) {
+ for (const img of message.b64_images) {
+ const w = this.addCustomWidget(
+ MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img)
+ )
+ w.parent = this
+ widgetI++
+ }
+ // this.onResize?.(this.size);
+ // this.resize?.(this.size)
+ this.setSize(this.computeSize())
+ }
- this.onRemoved = function () {
- // When removing this node we need to remove the input from the DOM
- for (let y in this.widgets) {
- if (this.widgets[y].canvas) {
- this.widgets[y].canvas.remove();
- }
- this.widgets[y].onRemove?.();
- }
- }
+ this.onRemoved = function () {
+ // When removing this node we need to remove the input from the DOM
+ for (let y in this.widgets) {
+ if (this.widgets[y].canvas) {
+ this.widgets[y].canvas.remove()
}
+ this.widgets[y].onRemoved?.()
+ }
}
+ }
}
-}
-);
+ },
+})
diff --git a/web/imageFeed.js b/web/imageFeed.js
index e003988..18347e5 100644
--- a/web/imageFeed.js
+++ b/web/imageFeed.js
@@ -1,311 +1,313 @@
-import { api } from "/scripts/api.js";
-import { app } from "/scripts/app.js";
+/**
+ * File: imageFeed.js
+ * Project: comfy_mtb
+ * Author: Mel Massadian
+ *
+ * Copyright (c) 2023 Mel Massadian
+ *
+ */
// forked from pysssss's imageFeed.js
+import { api } from '/scripts/api.js'
+import { app } from '/scripts/app.js'
+
const styles = {
- lighbox: {
- position: "fixed",
- top: 0,
- left: 0,
- width: "100vw",
- height: "100vh",
- background: "rgba(0,0,0,0.5)",
- display: "none",
- justifyContent: "center",
- alignItems: "center",
- zIndex: 999,
- },
- lightboxBtn: (extra) => ({
- position: "absolute",
- top: "50%",
- background: "none",
- border: "none",
- color: "#fff",
- zIndex: 9999999,
- fontSize: "30px",
- cursor: "pointer",
- pointerEvents: "bounding-box",
- ...extra,
- })
- ,
- img_list: {
-
- minHeight: "30px",
- maxHeight: "300px",
- width: "100vw",
- position: "absolute",
- bottom: 0,
- zIndex: 9999999,
- background: "#333",
- overflow: "auto",
- }
+ lighbox: {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ width: '100vw',
+ height: '100vh',
+ background: 'rgba(0,0,0,0.5)',
+ display: 'none',
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 999,
+ },
+ lightboxBtn: (extra) => ({
+ position: 'absolute',
+ top: '50%',
+ background: 'none',
+ border: 'none',
+ color: '#fff',
+ zIndex: 9999999,
+ fontSize: '30px',
+ cursor: 'pointer',
+ pointerEvents: 'bounding-box',
+ ...extra,
+ }),
+ img_list: {
+ minHeight: '30px',
+ maxHeight: '300px',
+ width: '100vw',
+ position: 'absolute',
+ bottom: 0,
+ zIndex: 9999999,
+ background: '#333',
+ overflow: 'auto',
+ },
}
-let currentImageIndex = 0;
-const imageUrls = [];
+let currentImageIndex = 0
+const imageUrls = []
let image_menu = null
app.registerExtension({
- name: "mtb.ImageFeed",
- setup: async () => {
- // - HTML & CSS
- //- lightbox
- const lightboxContainer = document.createElement("div");
- Object.assign(lightboxContainer.style, styles.lighbox);
-
- const lightboxImage = document.createElement("img");
- Object.assign(lightboxImage.style, {
- maxHeight: "100%",
- maxWidth: "100%",
- borderRadius: "5px",
- });
-
- // previous and next buttons
- const lightboxPrevBtn = document.createElement("button");
- const lightboxNextBtn = document.createElement("button");
-
- lightboxPrevBtn.textContent = "❮";
- lightboxNextBtn.textContent = "❯";
-
- Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: "0%" }));
- Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: "0%" }));
-
- // close button
- const lightboxCloseBtn = document.createElement("button");
- Object.assign(lightboxCloseBtn.style, styles.lightboxBtn({ right: "0", top: "0" }));
- lightboxCloseBtn.textContent = "❌";
-
- const lightboxButtons = document.createElement("div");
- Object.assign(lightboxButtons.style, {
- position: "absolute",
- top: "0%",
- right: "0%",
- // transform: "translate(50%, -50%)",
- height: "100%",
- width: "100%",
- background: "none",
- border: "none",
- color: "#fff",
- fontSize: "30px",
- cursor: "pointer",
- pointerEvents: "none",
- });
-
- lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn);
- lightboxContainer.append(lightboxButtons, lightboxImage);
-
- //- image list
- const imageListContainer = document.createElement("div");
- Object.assign(imageListContainer.style, styles.img_list);
-
-
- const createImgListBtn = (text, style) => {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.textContent = text;
- Object.assign(btn.style, {
- ...style,
- border: "none",
- color: "#fff",
- background: "none",
- height: "20px",
- cursor: "pointer",
- position: "absolute",
- top: "5px",
- fontSize: "12px",
- lineHeight: "12px",
- });
- imageListContainer.append(btn);
- return btn;
- }
- const showBtn = document.createElement("button");
- const closeBtn = createImgListBtn("❌", {
- width: "20px",
- textIndent: "-4px",
- right: "5px",
- });
- const loadButton = createImgListBtn("Load Session History", {
- right: "90px",
- });
- const clearButton = createImgListBtn("Clear", {
- right: "30px",
- });
-
-
- //- tools popup button
- showBtn.classList.add("comfy-settings-btn");
- Object.assign(showBtn.style, {
- right: "16px",
- cursor: "pointer",
- display: "none",
- });
-
- //- append to DOM
- document.body.append(imageListContainer);
-
-
- showBtn.textContent = "🖼️";
- showBtn.onclick = () => {
- imageListContainer.style.display = "block";
- showBtn.style.display = "none";
- };
- document.querySelector(".comfy-settings-btn").after(showBtn);
- document.querySelector(".comfy-settings-btn").after(lightboxContainer);
-
-
-
- // for (const { output } of history) {
- // if (output?.images) {
- // for (const src of output.images) {
- // const img = document.createElement("img");
- // const but = document.createElement("button");
-
- //- callbacks
- closeBtn.onclick = () => {
- imageListContainer.style.display = "none";
- showBtn.style.display = "unset";
- };
-
- clearButton.onclick = () => {
- imageListContainer.replaceChildren(closeBtn, clearButton, loadButton);
- }
-
- lightboxNextBtn.onclick = () => {
- currentImageIndex = (currentImageIndex + 1) % imageUrls.length;
- const imageUrl = imageUrls[currentImageIndex];
- lightboxImage.src = imageUrl;
- };
-
- // Modify the lightboxPrevBtn onclick callback
- lightboxPrevBtn.onclick = () => {
- currentImageIndex = (currentImageIndex - 1 + imageUrls.length) % imageUrls.length;
- const imageUrl = imageUrls[currentImageIndex];
- lightboxImage.src = imageUrl;
- };
-
-
- lightboxCloseBtn.onclick = () => {
- lightboxContainer.style.display = "none";
- };
- lightboxImage.onclick = lightboxNextBtn.onclick;
- /**
- * This is the function that creates the image buttons for the image list
- * They are wrapped in a button so that they can be clicked and open
- * the image in the lightbox.
- * @param {*} src
- */
- const createImageBtn = (src) => {
- console.debug(`making image ${src.filename}`);
- const img = document.createElement("img");
- const but = document.createElement("button");
-
- Object.assign(but.style, {
- height: "120px",
- width: "120px",
- });
- Object.assign(img.style, {
- width: "100%",
- height: "100%",
- objectFit: "scale-down",
- });
-
- img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${src.type}&subfolder=${encodeURIComponent(
- src.subfolder
- )}`;
-
- imageUrls.push(img.src);
-
- console.debug(img.src)
-
- but.onclick = () => {
- lightboxContainer.style.display = "flex";
- // add the same image to the lightbox
- lightboxImage.src = img.src;
- // lighboxContainer.replaceChildren(lightboxButtons, img);
-
- };
-
- // add right click menu
- but.addEventListener("contextmenu", (e) => {
- e.preventDefault();
-
- if (image_menu) {
- image_menu.remove();
- }
-
- image_menu = document.createElement("div");
- Object.assign(image_menu.style, {
- position: "absolute",
- top: `${e.clientY}px`,
- left: `${e.clientX}px`,
- background: "#333",
- color: "#fff",
- padding: "5px",
- borderRadius: "5px",
- zIndex: 999,
- });
- const load_img = document.createElement("button");
- load_img.textContent = "Load";
- load_img.onclick = () => {
- app.handleFile(img.src)
-
- }
-
- image_menu.appendChild(load_img)
- document.body.appendChild(image_menu)
- })
-
- but.append(img)
- imageListContainer.prepend(but)
- };
-
-
- loadButton.onclick = async () => {
- const all_history = await api.getHistory();
- for (const history of all_history.History) {
- if (history.outputs) {
- for (const key of Object.keys(history.outputs)) {
- console.debug(key)
- for (const im of history.outputs[key].images) {
- console.debug(im)
- createImageBtn(im)
- }
- }
- // for (const src of outputs.outputs.images) {
- // console.debug(src)
- // makeImage(`${src.subfolder}/${src.filename}`)
- // }
- }
- }
- }
-
- ///////-------
-
- // const all_history = await api.getHistory()
- // for (const history of all_history.History) {
- // if (history.outputs) {
- // for (const key of Object.keys(history.outputs)) {
- // for (const im of history.outputs[key].images) {
- // makeImage(im)
- // }
- // }
- // // for (const src of outputs.outputs.images) {
- // // console.debug(src)
- // // makeImage(`${src.subfolder}/${src.filename}`)
- // // }
- // }
- // }
-
- //- Hook into the API
- api.addEventListener("executed", ({ detail }) => {
- if (detail?.output?.images) {
- for (const src of detail.output.images) {
- console.debug(`Adding ${src} to image feed`)
- createImageBtn(src)
- }
- }
- })
- }
+ name: 'mtb.ImageFeed',
+ setup: async () => {
+ // - HTML & CSS
+ //- lightbox
+ const lightboxContainer = document.createElement('div')
+ Object.assign(lightboxContainer.style, styles.lighbox)
+
+ const lightboxImage = document.createElement('img')
+ Object.assign(lightboxImage.style, {
+ maxHeight: '100%',
+ maxWidth: '100%',
+ borderRadius: '5px',
+ })
+
+ // previous and next buttons
+ const lightboxPrevBtn = document.createElement('button')
+ const lightboxNextBtn = document.createElement('button')
+
+ lightboxPrevBtn.textContent = '❮'
+ lightboxNextBtn.textContent = '❯'
+
+ Object.assign(lightboxPrevBtn.style, styles.lightboxBtn({ left: '0%' }))
+ Object.assign(lightboxNextBtn.style, styles.lightboxBtn({ right: '0%' }))
+
+ // close button
+ const lightboxCloseBtn = document.createElement('button')
+ Object.assign(
+ lightboxCloseBtn.style,
+ styles.lightboxBtn({ right: '0', top: '0' })
+ )
+ lightboxCloseBtn.textContent = '❌'
+
+ const lightboxButtons = document.createElement('div')
+ Object.assign(lightboxButtons.style, {
+ position: 'absolute',
+ top: '0%',
+ right: '0%',
+ // transform: "translate(50%, -50%)",
+ height: '100%',
+ width: '100%',
+ background: 'none',
+ border: 'none',
+ color: '#fff',
+ fontSize: '30px',
+ cursor: 'pointer',
+ pointerEvents: 'none',
+ })
+
+ lightboxButtons.append(lightboxPrevBtn, lightboxNextBtn, lightboxCloseBtn)
+ lightboxContainer.append(lightboxButtons, lightboxImage)
+
+ //- image list
+ const imageListContainer = document.createElement('div')
+ Object.assign(imageListContainer.style, styles.img_list)
+
+ const createImgListBtn = (text, style) => {
+ const btn = document.createElement('button')
+ btn.type = 'button'
+ btn.textContent = text
+ Object.assign(btn.style, {
+ ...style,
+ border: 'none',
+ color: '#fff',
+ background: 'none',
+ height: '20px',
+ cursor: 'pointer',
+ position: 'absolute',
+ top: '5px',
+ fontSize: '12px',
+ lineHeight: '12px',
+ })
+ imageListContainer.append(btn)
+ return btn
+ }
+ const showBtn = document.createElement('button')
+ const closeBtn = createImgListBtn('❌', {
+ width: '20px',
+ textIndent: '-4px',
+ right: '5px',
+ })
+ const loadButton = createImgListBtn('Load Session History', {
+ right: '90px',
+ })
+ const clearButton = createImgListBtn('Clear', {
+ right: '30px',
+ })
+
+ //- tools popup button
+ showBtn.classList.add('comfy-settings-btn')
+ Object.assign(showBtn.style, {
+ right: '16px',
+ cursor: 'pointer',
+ display: 'none',
+ })
+
+ //- append to DOM
+ document.body.append(imageListContainer)
+
+ showBtn.textContent = '🖼️'
+ showBtn.onclick = () => {
+ imageListContainer.style.display = 'block'
+ showBtn.style.display = 'none'
+ }
+ document.querySelector('.comfy-settings-btn').after(showBtn)
+ document.querySelector('.comfy-settings-btn').after(lightboxContainer)
+
+ // for (const { output } of history) {
+ // if (output?.images) {
+ // for (const src of output.images) {
+ // const img = document.createElement("img");
+ // const but = document.createElement("button");
+
+ //- callbacks
+ closeBtn.onclick = () => {
+ imageListContainer.style.display = 'none'
+ showBtn.style.display = 'unset'
+ }
+
+ clearButton.onclick = () => {
+ imageListContainer.replaceChildren(closeBtn, clearButton, loadButton)
+ }
+
+ lightboxNextBtn.onclick = () => {
+ currentImageIndex = (currentImageIndex + 1) % imageUrls.length
+ const imageUrl = imageUrls[currentImageIndex]
+ lightboxImage.src = imageUrl
+ }
+
+ // Modify the lightboxPrevBtn onclick callback
+ lightboxPrevBtn.onclick = () => {
+ currentImageIndex =
+ (currentImageIndex - 1 + imageUrls.length) % imageUrls.length
+ const imageUrl = imageUrls[currentImageIndex]
+ lightboxImage.src = imageUrl
+ }
+
+ lightboxCloseBtn.onclick = () => {
+ lightboxContainer.style.display = 'none'
+ }
+ lightboxImage.onclick = lightboxNextBtn.onclick
+ /**
+ * This is the function that creates the image buttons for the image list
+ * They are wrapped in a button so that they can be clicked and open
+ * the image in the lightbox.
+ * @param {*} src
+ */
+ const createImageBtn = (src) => {
+ console.debug(`making image ${src.filename}`)
+ const img = document.createElement('img')
+ const but = document.createElement('button')
+
+ Object.assign(but.style, {
+ height: '120px',
+ width: '120px',
+ })
+ Object.assign(img.style, {
+ width: '100%',
+ height: '100%',
+ objectFit: 'scale-down',
+ })
+
+ img.src = `/view?filename=${encodeURIComponent(src.filename)}&type=${
+ src.type
+ }&subfolder=${encodeURIComponent(src.subfolder)}`
+
+ imageUrls.push(img.src)
+
+ console.debug(img.src)
+
+ but.onclick = () => {
+ lightboxContainer.style.display = 'flex'
+ // add the same image to the lightbox
+ lightboxImage.src = img.src
+ // lighboxContainer.replaceChildren(lightboxButtons, img);
+ }
+
+ // add right click menu
+ but.addEventListener('contextmenu', (e) => {
+ e.preventDefault()
+
+ if (image_menu) {
+ image_menu.remove()
+ }
+
+ image_menu = document.createElement('div')
+ Object.assign(image_menu.style, {
+ position: 'absolute',
+ top: `${e.clientY}px`,
+ left: `${e.clientX}px`,
+ background: '#333',
+ color: '#fff',
+ padding: '5px',
+ borderRadius: '5px',
+ zIndex: 999,
+ })
+ const load_img = document.createElement('button')
+ load_img.textContent = 'Load'
+ load_img.onclick = () => {
+ app.handleFile(img.src)
+ }
+
+ image_menu.appendChild(load_img)
+ document.body.appendChild(image_menu)
+ })
+
+ but.append(img)
+ imageListContainer.prepend(but)
+ }
+
+ loadButton.onclick = async () => {
+ const all_history = await api.getHistory()
+ for (const history of all_history.History) {
+ if (history.outputs) {
+ for (const key of Object.keys(history.outputs)) {
+ console.debug(key)
+ for (const im of history.outputs[key].images) {
+ console.debug(im)
+ createImageBtn(im)
+ }
+ }
+ // for (const src of outputs.outputs.images) {
+ // console.debug(src)
+ // makeImage(`${src.subfolder}/${src.filename}`)
+ // }
+ }
+ }
+ }
+
+ ///////-------
+
+ // const all_history = await api.getHistory()
+ // for (const history of all_history.History) {
+ // if (history.outputs) {
+ // for (const key of Object.keys(history.outputs)) {
+ // for (const im of history.outputs[key].images) {
+ // makeImage(im)
+ // }
+ // }
+ // // for (const src of outputs.outputs.images) {
+ // // console.debug(src)
+ // // makeImage(`${src.subfolder}/${src.filename}`)
+ // // }
+ // }
+ // }
+
+ //- Hook into the API
+ api.addEventListener('executed', ({ detail }) => {
+ if (detail?.output?.images) {
+ for (const src of detail.output.images) {
+ console.debug(`Adding ${src} to image feed`)
+ createImageBtn(src)
+ }
+ }
+ })
+ },
})
diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js
index 3261636..4ea2136 100644
--- a/web/mtb_widgets.js
+++ b/web/mtb_widgets.js
@@ -1,1081 +1,959 @@
-import { app } from "/scripts/app.js";
+/**
+ * File: mtb_widgets.js
+ * Project: comfy_mtb
+ * Author: Mel Massadian
+ *
+ * Copyright (c) 2023 Mel Massadian
+ *
+ */
+
+import { app } from '/scripts/app.js'
import parseCss from '/extensions/mtb/extern/parse-css.js'
import * as shared from '/extensions/mtb/comfy_shared.js'
+import { log } from '/extensions/mtb/comfy_shared.js'
+import { api } from '/scripts/api.js'
-import { api } from "/scripts/api.js";
-
-import { ComfyWidgets } from "/scripts/widgets.js";
-
-const newTypes = ["BOOL", "COLOR", "BBOX"]
-
+const newTypes = ['BOOL', 'COLOR', 'BBOX']
export const MtbWidgets = {
- BBOX: (key, val) => {
- /** @type {import("./types/litegraph").IWidget} */
- const widget = {
- name: key,
- type: "BBOX",
- // options: val,
- y: 0,
- value: val?.default || [0, 0, 0, 0],
- options: {},
-
- draw: function (ctx,
- node,
- widget_width,
- widgetY,
- height) {
- const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5;
-
- const show_text = true;
- const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR;
- const background_color = LiteGraph.WIDGET_BGCOLOR;
- const text_color = LiteGraph.WIDGET_TEXT_COLOR;
- const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
- const H = LiteGraph.NODE_WIDGET_HEIGHT;
-
- var margin = 15;
- var numWidgets = 4; // Number of stacked widgets
-
- if (hide) return;
-
- for (let i = 0; i < numWidgets; i++) {
- let currentY = widgetY + i * (H + margin); // Adjust Y position for each widget
-
- ctx.textAlign = "left";
- ctx.strokeStyle = outline_color;
- ctx.fillStyle = background_color;
- ctx.beginPath();
- if (show_text)
- ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]);
- else
- ctx.rect(margin, currentY, widget_width - margin * 2, H);
- ctx.fill();
- if (show_text) {
- if (!this.disabled)
- ctx.stroke();
- ctx.fillStyle = text_color;
- if (!this.disabled) {
- ctx.beginPath();
- ctx.moveTo(margin + 16, currentY + 5);
- ctx.lineTo(margin + 6, currentY + H * 0.5);
- ctx.lineTo(margin + 16, currentY + H - 5);
- ctx.fill();
- ctx.beginPath();
- ctx.moveTo(widget_width - margin - 16, currentY + 5);
- ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5);
- ctx.lineTo(widget_width - margin - 16, currentY + H - 5);
- ctx.fill();
- }
- ctx.fillStyle = secondary_text_color;
- ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7);
- ctx.fillStyle = text_color;
- ctx.textAlign = "right";
-
- ctx.fillText(
- Number(this.value).toFixed(
- this.options?.precision !== undefined
- ? this.options.precision
- : 3
- ),
- widget_width - margin * 2 - 20,
- currentY + H * 0.7
- );
- }
- }
- },
- mouse: function (event, pos, node) {
- var old_value = this.value;
- var x = pos[0] - node.pos[0];
- var y = pos[1] - node.pos[1];
- var width = node.size[0];
- var H = LiteGraph.NODE_WIDGET_HEIGHT;
- var margin = 5;
- var numWidgets = 4; // Number of stacked widgets
-
- for (let i = 0; i < numWidgets; i++) {
- let currentY = y + i * (H + margin); // Adjust Y position for each widget
-
-
- if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") {
- if (event.deltaX)
- this.value += event.deltaX * 0.1 * (this.options?.step || 1);
- if (this.options.min != null && this.value < this.options.min) {
- this.value = this.options.min;
- }
- if (this.options.max != null && this.value > this.options.max) {
- this.value = this.options.max;
- }
- } else if (event.type == LiteGraph.pointerevents_method + "down") {
- var values = this.options?.values;
- if (values && values.constructor === Function) {
- values = this.options.values(w, node);
- }
- var values_list = null;
-
- var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
- if (this.type == "BBOX") {
- this.value += delta * 0.1 * (this.options.step || 1);
- if (this.options.min != null && this.value < this.options.min) {
- this.value = this.options.min;
- }
- if (this.options.max != null && this.value > this.options.max) {
- this.value = this.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(this.value)) + delta;
- else
- index = values_list.indexOf(this.value) + delta;
- if (index >= values_list.length) {
- index = values_list.length - 1;
- }
- if (index < 0) {
- index = 0;
- }
- if (values.constructor === Array)
- this.value = values[index];
- else
- this.value = index;
- }
- } //end mousedown
- else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") {
- var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
- if (event.click_time < 200 && delta == 0) {
- this.prompt("Value", this.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);
- shared.inner_value_change(this, this.value, event);
- }.bind(w),
- event);
- }
- }
-
- if (old_value != this.value)
- setTimeout(
- function () {
- shared.inner_value_change(this, this.value, event);
- }.bind(this),
- 20
- );
-
- app.canvas.setDirty(true);
- }
-
- },
- computeSize: function (width) {
- return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4];
- },
- // onDrawBackground: function (ctx) {
- // if (!this.flags.collapsed) return;
- // this.inputEl.style.display = "block";
- // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px";
- // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px";
- // },
- // onInputChange: function (e) {
- // const property = e.target.dataset.property;
- // const bbox = this.getInputData(0);
- // if (!bbox) return;
- // bbox[property] = parseFloat(e.target.value);
- // this.setOutputData(0, bbox);
- // }
- }
-
- widget.desc = "Represents a Bounding Box with x, y, width, and height.";
- return widget
-
- },
- BOOL: (key, val, compute = false) => {
- /** @type {import("/types/litegraph").IWidget} */
- const widget = {
- name: key,
- type: "BOOL",
- options: { default: false },
- y: 0,
- value: val || false,
- draw: function (ctx,
- node,
- widget_width,
- widgetY,
- height) {
- const hide = this.type !== "BOOL" && app.canvas.ds.scale > 0.5
- if (hide) {
- return
- }
- const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR;
- const background_color = LiteGraph.WIDGET_BGCOLOR;
- const text_color = LiteGraph.WIDGET_TEXT_COLOR;
- const H = LiteGraph.NODE_WIDGET_HEIGHT;
- const arrowSize = 8;
-
- var margin = 15;
- if (hide) return;
-
- var currentY = widgetY;
-
- ctx.textAlign = "left";
- ctx.strokeStyle = outline_color;
- ctx.fillStyle = background_color;
- ctx.beginPath();
- // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]);
- ctx.rect(margin, currentY, H, H); // Draw checkbox square
-
- ctx.fill();
- ctx.stroke();
-
- ctx.fillStyle = text_color;
- // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7);
- ctx.fillText(this.label || this.name, H + margin * 2, currentY + H * 0.7);
-
-
- // Draw arrow if the value is true
- // Draw checkmark if the value is true
- if (this.value) {
- ctx.fillStyle = text_color;
- ctx.beginPath();
- ctx.moveTo(margin + H * 0.15, currentY + H * 0.5);
- ctx.lineTo(margin + H * 0.4, currentY + H * 0.8);
- ctx.lineTo(margin + H * 0.85, currentY + H * 0.2);
- ctx.stroke();
- }
-
- },
- get value() {
-
- return this.inputEl.value === "true";
- },
- set value(x) {
- this.inputEl.value = x;
- },
- computeSize: function (width) {
- return [width, 32];
- },
- mouse: function (event, pos, node) {
- // var x = pos[0] - node.pos[0];
- // var y = pos[1] - node.pos[1];
- // var width = node.size[0];
- // var H = LiteGraph.NODE_WIDGET_HEIGHT;
- // var margin = 15;
-
- // if (event.type == LiteGraph.pointerevents_method + "down") {
- // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) {
- // this.value = !this.value; // Toggle checkbox value
- // shared.inner_value_change(this, this.value, event);
- // app.canvas.setDirty(true);
- // }
- // }
- if (event.type === "pointerdown") {
- // get widgets of type type : "COLOR"
- const widgets = node.widgets.filter(w => w.type === "BOOL");
-
- for (const w of widgets) {
- // color picker
- const rect = [w.last_y, w.last_y + 32];
- if (pos[1] > rect[0] && pos[1] < rect[1]) {
- // picker.style.position = "absolute";
- // picker.style.left = ( pos[0]) + "px";
- // picker.style.top = ( pos[1]) + "px";
-
- // place at screen center
- // picker.style.position = "absolute";
- // picker.style.left = (window.innerWidth / 2) + "px";
- // picker.style.top = (window.innerHeight / 2) + "px";
- // picker.style.transform = "translate(-50%, -50%)";
- // picker.style.zIndex = 1000;
- console.log("Clicked a BOOL", this.value)
-
- this.value = this.value ? "false" : "true"
-
- }
- }
- }
+ BBOX: (key, val) => {
+ /** @type {import("./types/litegraph").IWidget} */
+ const widget = {
+ name: key,
+ type: 'BBOX',
+ // options: val,
+ y: 0,
+ value: val?.default || [0, 0, 0, 0],
+ options: {},
+
+ draw: function (ctx, node, widget_width, widgetY, height) {
+ const hide = this.type !== 'BBOX' && app.canvas.ds.scale > 0.5
+
+ const show_text = true
+ const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR
+ const background_color = LiteGraph.WIDGET_BGCOLOR
+ const text_color = LiteGraph.WIDGET_TEXT_COLOR
+ const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR
+ const H = LiteGraph.NODE_WIDGET_HEIGHT
+
+ let margin = 15
+ let numWidgets = 4 // Number of stacked widgets
+
+ if (hide) return
+
+ for (let i = 0; i < numWidgets; i++) {
+ let currentY = widgetY + i * (H + margin) // Adjust Y position for each widget
+
+ ctx.textAlign = 'left'
+ ctx.strokeStyle = outline_color
+ ctx.fillStyle = background_color
+ ctx.beginPath()
+ if (show_text)
+ ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [
+ H * 0.5,
+ ])
+ else ctx.rect(margin, currentY, widget_width - margin * 2, H)
+ ctx.fill()
+ if (show_text) {
+ if (!this.disabled) ctx.stroke()
+ ctx.fillStyle = text_color
+ if (!this.disabled) {
+ ctx.beginPath()
+ ctx.moveTo(margin + 16, currentY + 5)
+ ctx.lineTo(margin + 6, currentY + H * 0.5)
+ ctx.lineTo(margin + 16, currentY + H - 5)
+ ctx.fill()
+ ctx.beginPath()
+ ctx.moveTo(widget_width - margin - 16, currentY + 5)
+ ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5)
+ ctx.lineTo(widget_width - margin - 16, currentY + H - 5)
+ ctx.fill()
}
+ ctx.fillStyle = secondary_text_color
+ ctx.fillText(
+ this.label || this.name,
+ margin * 2 + 5,
+ currentY + H * 0.7
+ )
+ ctx.fillStyle = text_color
+ ctx.textAlign = 'right'
+
+ ctx.fillText(
+ Number(this.value).toFixed(
+ this.options?.precision !== undefined
+ ? this.options.precision
+ : 3
+ ),
+ widget_width - margin * 2 - 20,
+ currentY + H * 0.7
+ )
+ }
}
-
- // create a checkbox
- widget.inputEl = document.createElement("input")
- widget.inputEl.type = "checkbox"
- widget.inputEl.value = false
- document.body.appendChild(widget.inputEl);
- return widget
-
- },
- COLOR: (key, val, compute = false) => {
- /** @type {import("/types/litegraph").IWidget} */
- const widget = {}
- widget.y = 0
- widget.name = key;
- widget.type = "COLOR";
- widget.options = { default: "#ff0000" };
- widget.value = val || "#ff0000";
- widget.draw = function (ctx,
- node,
- widgetWidth,
- widgetY,
- height) {
- const hide = this.type !== "COLOR" && app.canvas.ds.scale > 0.5
- if (hide) {
- return
+ },
+ mouse: function (event, pos, node) {
+ let old_value = this.value
+ let x = pos[0] - node.pos[0]
+ let y = pos[1] - node.pos[1]
+ let width = node.size[0]
+ let H = LiteGraph.NODE_WIDGET_HEIGHT
+ let margin = 5
+ let numWidgets = 4 // Number of stacked widgets
+
+ for (let i = 0; i < numWidgets; i++) {
+ let currentY = y + i * (H + margin) // Adjust Y position for each widget
+
+ if (
+ event.type == LiteGraph.pointerevents_method + 'move' &&
+ this.type == 'BBOX'
+ ) {
+ if (event.deltaX)
+ this.value += event.deltaX * 0.1 * (this.options?.step || 1)
+ if (this.options.min != null && this.value < this.options.min) {
+ this.value = this.options.min
}
-
- const border = 3;
- // draw a rect with a border and a fill color
- ctx.fillStyle = "#000";
- ctx.fillRect(0, widgetY, widgetWidth, height);
- ctx.fillStyle = this.value;
- ctx.fillRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2);
- // write the input name
- // choose the fill based on the luminoisty of this.value color
- const color = parseCss(this.value.default || this.value)
- if (!color) {
- return
+ if (this.options.max != null && this.value > this.options.max) {
+ this.value = this.options.max
}
- ctx.fillStyle = shared.isColorBright(color.values, 125) ? "#000" : "#fff";
-
-
- ctx.font = "14px Arial";
- ctx.textAlign = "center";
- ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14);
-
-
-
- // ctx.strokeStyle = "#fff";
- // ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2);
-
+ } else if (event.type == LiteGraph.pointerevents_method + 'down') {
+ let values = this.options?.values
+ if (values && values.constructor === Function) {
+ values = this.options.values(w, node)
+ }
+ let values_list = null
+
+ let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0
+ if (this.type == 'BBOX') {
+ this.value += delta * 0.1 * (this.options.step || 1)
+ if (this.options.min != null && this.value < this.options.min) {
+ this.value = this.options.min
+ }
+ if (this.options.max != null && this.value > this.options.max) {
+ this.value = this.options.max
+ }
+ } else if (delta) {
+ //clicked in arrow, used for combos
+ let index = -1
+ this.last_mouseclick = 0 //avoids dobl click event
+ if (values.constructor === Object)
+ index = values_list.indexOf(String(this.value)) + delta
+ else index = values_list.indexOf(this.value) + delta
+ if (index >= values_list.length) {
+ index = values_list.length - 1
+ }
+ if (index < 0) {
+ index = 0
+ }
+ if (values.constructor === Array) this.value = values[index]
+ else this.value = index
+ }
+ } //end mousedown
+ else if (
+ event.type == LiteGraph.pointerevents_method + 'up' &&
+ this.type == 'BBOX'
+ ) {
+ let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0
+ if (event.click_time < 200 && delta == 0) {
+ this.prompt(
+ 'Value',
+ this.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)
+ shared.inner_value_change(this, this.value, event)
+ }.bind(w),
+ event
+ )
+ }
+ }
- // ctx.fillStyle = "#000";
- // ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2);
- // ctx.fillStyle = this.value;
- // ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height);
+ if (old_value != this.value)
+ setTimeout(
+ function () {
+ shared.inner_value_change(this, this.value, event)
+ }.bind(this),
+ 20
+ )
+ app.canvas.setDirty(true)
}
- widget.mouse = function (e, pos, node) {
- if (e.type === "pointerdown") {
- // get widgets of type type : "COLOR"
- const widgets = node.widgets.filter(w => w.type === "COLOR");
-
- for (const w of widgets) {
- // color picker
- const rect = [w.last_y, w.last_y + 32];
- if (pos[1] > rect[0] && pos[1] < rect[1]) {
- console.log("color picker", node)
- const picker = document.createElement("input");
- picker.type = "color";
- picker.value = this.value;
- // picker.style.position = "absolute";
- // picker.style.left = ( pos[0]) + "px";
- // picker.style.top = ( pos[1]) + "px";
-
- // place at screen center
- picker.style.position = "absolute";
- picker.style.left = "999999px"//(window.innerWidth / 2) + "px";
- picker.style.top = "999999px" //(window.innerHeight / 2) + "px";
- // picker.style.transform = "translate(-50%, -50%)";
- // picker.style.zIndex = 1000;
-
-
-
- document.body.appendChild(picker);
-
- picker.addEventListener("change", () => {
- this.value = picker.value;
- node.graph._version++;
- node.setDirtyCanvas(true, true);
- picker.remove();
- });
-
- picker.click()
-
- }
- }
- }
- }
- widget.computeSize = function (width) {
- return [width, 32];
- }
+ },
+ computeSize: function (width) {
+ return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4]
+ },
+ // onDrawBackground: function (ctx) {
+ // if (!this.flags.collapsed) return;
+ // this.inputEl.style.display = "block";
+ // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px";
+ // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px";
+ // },
+ // onInputChange: function (e) {
+ // const property = e.target.dataset.property;
+ // const bbox = this.getInputData(0);
+ // if (!bbox) return;
+ // bbox[property] = parseFloat(e.target.value);
+ // this.setOutputData(0, bbox);
+ // }
+ }
- return widget;
- },
-
- DEBUG_IMG: (val, index) => {
- const w = {
- name: `anything_${index}`,
- type: "image",
- value: val,
- draw: function (ctx,
- node,
- widgetWidth,
- widgetY,
- height) {
- const [cw, ch] = this.computeSize(widgetWidth)
- shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
- },
- computeSize: function (width) {
- const ratio = this.inputRatio || 1;
- if (width) {
- return [width, width / ratio + 4]
- }
- return [128, 128]
- },
- onRemove: function () {
- if (this.inputEl) {
- this.inputEl.remove();
- }
- }
+ widget.desc = 'Represents a Bounding Box with x, y, width, and height.'
+ return widget
+ },
+ BOOL: (key, val, compute = false) => {
+ /** @type {import("/types/litegraph").IWidget} */
+ const widget = {
+ name: key,
+ type: 'BOOL',
+ options: { default: false },
+ y: 0,
+
+ draw: function (ctx, node, widget_width, widgetY, height) {
+ const hide = this.type !== 'BOOL' && app.canvas.ds.scale > 0.5
+ if (hide) {
+ return
}
-
- w.inputEl = document.createElement("img");
- w.inputEl.src = w.value;
- w.inputEl.onload = function () {
- w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight;
+ const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR
+ const background_color = LiteGraph.WIDGET_BGCOLOR
+ const text_color = LiteGraph.WIDGET_TEXT_COLOR
+ const H = LiteGraph.NODE_WIDGET_HEIGHT
+ // const arrowSize = 8
+
+ let margin = 15
+ if (hide) return
+
+ let currentY = widgetY
+
+ ctx.textAlign = 'left'
+ ctx.strokeStyle = outline_color
+ ctx.fillStyle = background_color
+ ctx.beginPath()
+ // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]);
+ ctx.rect(margin, currentY, H, H) // Draw checkbox square
+
+ ctx.fill()
+ ctx.stroke()
+
+ ctx.fillStyle = text_color
+ // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7);
+ ctx.fillText(
+ this.label || this.name,
+ H + margin * 2,
+ currentY + H * 0.7
+ )
+
+ // Draw arrow if the value is true
+ // Draw checkmark if the value is true
+ if (this.value) {
+ ctx.fillStyle = text_color
+ ctx.beginPath()
+ ctx.moveTo(margin + H * 0.15, currentY + H * 0.5)
+ ctx.lineTo(margin + H * 0.4, currentY + H * 0.8)
+ ctx.lineTo(margin + H * 0.85, currentY + H * 0.2)
+ ctx.stroke()
}
- document.body.appendChild(w.inputEl);
- return w
- },
- DEBUG_STRING: (val, index) => {
- const w = {
- name: `anything_${index}`,
- type: "debug_text",
- val: val,
- draw: function (ctx,
- node,
- widgetWidth,
- widgetY,
- height) {
- // const [cw, ch] = this.computeSize(widgetWidth)
- shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height)
- },
- computeSize: function (width) {
- const value = this.inputEl.innerHTML
- if (!value) {
- return [32, 32]
- }
- if (!width) {
- log(`No width ${this.parent.size}`)
- }
-
- const fontSize = 25; // Assuming 1rem = 16px
-
- const oldFont = app.ctx.font
- app.ctx.font = `${fontSize}px Arial`;
-
- const words = value.split(" ");
- const lines = [];
- let currentLine = "";
- for (const word of words) {
- const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`;
-
- const testWidth = app.ctx.measureText(testLine).width;
-
- // log(`Testing line ${testLine}, width: ${testWidth}, width: ${width}, ratio: ${testWidth / width}`)
- if (testWidth > width) {
- lines.push(currentLine);
- currentLine = word;
- } else {
- currentLine = testLine;
- }
- }
- app.ctx.font = oldFont;
- lines.push(currentLine);
-
- // Step 3: Calculate the widget width and height
- const textHeight = lines.length * (fontSize + 2); // You can adjust the line height (2 in this case)
- const maxLineWidth = lines.reduce((maxWidth, line) => Math.max(maxWidth, app.ctx.measureText(line).width), 0);
- const widgetWidth = Math.max(width || this.width || 32, maxLineWidth);
- const widgetHeight = textHeight + 10; // Additional padding for spacing
- return [widgetWidth, widgetHeight + 4]
-
- },
- onRemove: function () {
- if (this.inputEl) {
- this.inputEl.remove();
- }
-
+ },
+ get value() {
+ return this.inputEl.value === 'true'
+ },
+ set value(x) {
+ this.inputEl.value = x
+ },
+ computeSize: function (width) {
+ return [width, 32]
+ },
+ mouse: function (event, pos, node) {
+ // let x = pos[0] - node.pos[0];
+ // let y = pos[1] - node.pos[1];
+ // let width = node.size[0];
+ // let H = LiteGraph.NODE_WIDGET_HEIGHT;
+ // let margin = 15;
+
+ // if (event.type == LiteGraph.pointerevents_method + "down") {
+ // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) {
+ // this.value = !this.value; // Toggle checkbox value
+ // shared.inner_value_change(this, this.value, event);
+ // app.canvas.setDirty(true);
+ // }
+ // }
+ if (event.type === 'pointerdown') {
+ // get widgets of type type : "COLOR"
+ const widgets = node.widgets.filter((w) => w.type === 'BOOL')
+
+ for (const w of widgets) {
+ // color picker
+ const rect = [w.last_y, w.last_y + 32]
+ if (pos[1] > rect[0] && pos[1] < rect[1]) {
+ // picker.style.position = "absolute";
+ // picker.style.left = ( pos[0]) + "px";
+ // picker.style.top = ( pos[1]) + "px";
+
+ // place at screen center
+ // picker.style.position = "absolute";
+ // picker.style.left = (window.innerWidth / 2) + "px";
+ // picker.style.top = (window.innerHeight / 2) + "px";
+ // picker.style.transform = "translate(-50%, -50%)";
+ // picker.style.zIndex = 1000;
+
+ this.value = this.value ? false : true
}
+ }
}
- w.inputEl = document.createElement("p");
- w.inputEl.style.textAlign = "center";
- w.inputEl.style.fontSize = "1.5em";
- w.inputEl.style.color = "var(--input-text)";
- w.inputEl.style.fontFamily = "monospace";
- w.inputEl.innerHTML = val
- document.body.appendChild(w.inputEl);
-
- return w
+ },
}
-}
-
+ // create a checkbox
+ widget.inputEl = document.createElement('input')
+ widget.inputEl.type = 'checkbox'
+ widget.value = val || false
-const bboxWidgetDOM = (key, val) => {
- /** @type {import("./types/litegraph").IWidget} */
- const widget = {
- name: key,
- type: "BBOX",
- // options: val,
- y: 0,
- value: val || [0, 0, 0, 0],
-
- draw: function (ctx,
- node,
- widgetWidth,
- widgetY,
- height) {
- const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5
- this.inputEl.style.display = hide ? "none" : "block";
- if (hide) return;
-
- shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height)
- },
- computeSize: function (width) {
- return [width, 32];
- },
- // onDrawBackground: function (ctx) {
- // if (!this.flags.collapsed) return;
- // this.inputEl.style.display = "block";
- // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px";
- // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px";
- // },
- onInputChange: function (e) {
- const property = e.target.dataset.property;
- const bbox = this.getInputData(0);
- if (!bbox) return;
- bbox[property] = parseFloat(e.target.value);
- this.setOutputData(0, bbox);
+ document.body.appendChild(widget.inputEl)
+ return widget
+ },
+ COLOR: (key, val, compute = false) => {
+ /** @type {import("/types/litegraph").IWidget} */
+ const widget = {}
+ widget.y = 0
+ widget.name = key
+ widget.type = 'COLOR'
+ widget.options = { default: '#ff0000' }
+ widget.value = val || '#ff0000'
+ widget.draw = function (ctx, node, widgetWidth, widgetY, height) {
+ const hide = this.type !== 'COLOR' && app.canvas.ds.scale > 0.5
+ if (hide) {
+ return
+ }
+ const border = 3
+ ctx.fillStyle = '#000'
+ ctx.fillRect(0, widgetY, widgetWidth, height)
+ ctx.fillStyle = this.value
+ ctx.fillRect(
+ border,
+ widgetY + border,
+ widgetWidth - border * 2,
+ height - border * 2
+ )
+ const color = parseCss(this.value.default || this.value)
+ if (!color) {
+ return
+ }
+ ctx.fillStyle = shared.isColorBright(color.values, 125) ? '#000' : '#fff'
+
+ ctx.font = '14px Arial'
+ ctx.textAlign = 'center'
+ ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14)
+ }
+ widget.mouse = function (e, pos, node) {
+ if (e.type === 'pointerdown') {
+ const widgets = node.widgets.filter((w) => w.type === 'COLOR')
+
+ for (const w of widgets) {
+ // color picker
+ const rect = [w.last_y, w.last_y + 32]
+ if (pos[1] > rect[0] && pos[1] < rect[1]) {
+ const picker = document.createElement('input')
+ picker.type = 'color'
+ picker.value = this.value
+
+ picker.style.position = 'absolute'
+ picker.style.left = '999999px' //(window.innerWidth / 2) + "px";
+ picker.style.top = '999999px' //(window.innerHeight / 2) + "px";
+
+ document.body.appendChild(picker)
+
+ picker.addEventListener('change', () => {
+ this.value = picker.value
+ node.graph._version++
+ node.setDirtyCanvas(true, true)
+ picker.remove()
+ })
+
+ picker.click()
+ }
}
+ }
+ }
+ widget.computeSize = function (width) {
+ return [width, 32]
}
- widget.inputEl = document.createElement("div")
- widget.parent = this
- widget.inputEl.innerHTML = `
-
-
-
-
- `;
- // set the class document wide if not present
-
- shared.defineClass("bbox-input", `background-color: var(--comfy-input-bg);
- color: var(--input-text);
- overflow: hidden;
- width:100%;
- overflow-y: auto;
- padding: 2px;
- resize: none;
- border: none;
- box-sizing: border-box;
- font-size: 10px;`)
-
-
- const bboxInputs = widget.inputEl.querySelectorAll(".bbox-input");
- bboxInputs.forEach((input) => {
- input.addEventListener("change", widget.onInputChange.bind(this));
- });
-
- widget.desc = "Represents a Bounding Box with x, y, width, and height.";
-
- document.body.appendChild(widget.inputEl);
-
-
- console.log("Bounding Box Widget DOM", widget.inputEl)
- return widget
-
-}
-/**
- * @returns {import("./types/litegraph").IWidget} widget
- */
+ return widget
+ },
+
+ DEBUG_IMG: (name, val) => {
+ const w = {
+ name,
+ type: 'image',
+ value: val,
+ draw: function (ctx, node, widgetWidth, widgetY, height) {
+ const [cw, ch] = this.computeSize(widgetWidth)
+ shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
+ },
+ computeSize: function (width) {
+ const ratio = this.inputRatio || 1
+ if (width) {
+ return [width, width / ratio + 4]
+ }
+ return [128, 128]
+ },
+ onRemoved: function () {
+ if (this.inputEl) {
+ this.inputEl.remove()
+ }
+ },
+ }
-/**
- * @returns {import("./types/litegraph").IWidget} widget
- */
+ w.inputEl = document.createElement('img')
+ w.inputEl.src = w.value
+ w.inputEl.onload = function () {
+ w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight
+ }
+ document.body.appendChild(w.inputEl)
+ return w
+ },
+ DEBUG_STRING: (name, val) => {
+ const fontSize = 16
+ const w = {
+ name,
+ type: 'debug_text',
+
+ draw: function (ctx, node, widgetWidth, widgetY, height) {
+ // const [cw, ch] = this.computeSize(widgetWidth)
+ shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height)
+ },
+ computeSize: function (width) {
+ const value = this.inputEl.innerHTML
+ if (!value) {
+ return [32, 32]
+ }
+ if (!width) {
+ log(`No width ${this.parent.size}`)
+ }
+ const oldFont = app.ctx.font
+ app.ctx.font = `${fontSize}px monospace`
+ const words = value.split(' ')
+ const lines = []
+ let currentLine = ''
+ for (const word of words) {
+ const testLine =
+ currentLine.length === 0 ? word : `${currentLine} ${word}`
-// VIDEO: (node, inputName, inputData, app) => {
-// console.log("video")
-// const videoWidget = {
-// name: "VideoWidget",
-// description: "Video Player Widget",
-// value: inputData,
-// properties: {},
-// widget: null,
-
-// init: function () {
-// this.widget = document.createElement("video");
-// this.widget.width = 200;
-// this.widget.height = 120;
-// this.widget.controls = true;
-// this.widget.style.width = "100%";
-// this.widget.style.height = "100%";
-// this.widget.style.objectFit = "contain";
-// this.widget.style.backgroundColor = "black";
-// this.widget.style.pointerEvents = "none";
-// node.addWidget(inputName, videoWidget.widget, inputData);
-// },
-
-// setValue: function (value, options) {
-// if (value instanceof HTMLVideoElement) {
-// this.widget.src = value.src;
-// } else if (typeof value === "string") {
-// this.widget.src = value;
-// }
-// },
-
-// getValue: function () {
-// return this.widget.src;
-// },
-
-// append: function (parent) {
-// parent.appendChild(this.widget);
-// },
-
-// remove: function () {
-// this.widget.parentNode.removeChild(this.widget);
-// }
-// };
-// return {
-// widget: videoWidget,
-// }
-// }
+ const testWidth = app.ctx.measureText(testLine).width
+ if (testWidth > width) {
+ lines.push(currentLine)
+ currentLine = word
+ } else {
+ currentLine = testLine
+ }
+ }
+ app.ctx.font = oldFont
+ if (lines.length === 0) lines.push(currentLine)
+
+ const textHeight = (lines.length + 1) * fontSize
+
+ const maxLineWidth = lines.reduce(
+ (maxWidth, line) =>
+ Math.max(maxWidth, app.ctx.measureText(line).width),
+ 0
+ )
+ const widgetWidth = Math.max(width || this.width || 32, maxLineWidth)
+ const widgetHeight = textHeight * 1.5
+ return [widgetWidth, widgetHeight]
+ },
+ onRemoved: function () {
+ if (this.inputEl) {
+ this.inputEl.remove()
+ }
+ },
+ }
+ Object.defineProperty(w, 'value', {
+ get() {
+ return this.inputEl.innerHTML
+ },
+ set(value) {
+ this.inputEl.innerHTML = value
+ this.parent?.setSize?.(this.parent?.computeSize())
+ },
+ })
+
+ w.inputEl = document.createElement('p')
+ w.inputEl.style.textAlign = 'center'
+ w.inputEl.style.fontSize = `${fontSize}px`
+ w.inputEl.style.color = 'var(--input-text)'
+ w.inputEl.style.lineHeight = 0
+
+ w.inputEl.style.fontFamily = 'monospace'
+ w.value = val
+ document.body.appendChild(w.inputEl)
+
+ return w
+ },
+}
/**
* @returns {import("./types/comfy").ComfyExtension} extension
*/
const mtb_widgets = {
- name: "mtb.widgets",
-
- init: async () => {
- console.log("Registering mtb.widgets")
- try {
-
- const res = await api.fetchApi('/mtb/debug')
- const msg = await res.json()
- window.MTB_DEBUG = msg.enabled;
+ name: 'mtb.widgets',
+
+ init: async () => {
+ log('Registering mtb.widgets')
+ try {
+ const res = await api.fetchApi('/mtb/debug')
+ const msg = await res.json()
+ if (!window.MTB) {
+ window.MTB = {}
+ }
+ window.MTB.DEBUG = msg.enabled
+ } catch (e) {
+ console.error('Error:', error)
+ }
+ },
+
+ setup: () => {
+ app.ui.settings.addSetting({
+ id: 'mtb.Debug.enabled',
+ name: '[mtb] Enable Debug (py and js)',
+ type: 'boolean',
+ defaultValue: false,
+
+ tooltip:
+ 'This will enable debug messages in the console and in the python console respectively',
+ attrs: {
+ style: {
+ fontFamily: 'monospace',
+ },
+ },
+ async onChange(value) {
+ if (value) {
+ console.log('Enabled DEBUG mode')
}
- catch (e) {
- console.error('Error:', error);
+ if (!window.MTB) {
+ window.MTB = {}
}
- },
-
- setup: () => {
- app.ui.settings.addSetting({
- id: "mtb.Debug.enabled",
- name: "[mtb] Enable Debug (py and js)",
- type: "boolean",
- defaultValue: false,
-
- tooltip:
- "This will enable debug messages in the console and in the python console respectively",
- attrs: {
- style: {
- fontFamily: "monospace",
- },
- },
- async onChange(value) {
- if (value) {
- console.log("Enabled DEBUG mode")
- }
- window.MTB_DEBUG = value;
- await api.fetchApi('/mtb/debug', {
- method: 'POST',
- body: JSON.stringify({
- enabled: value
+ window.MTB.DEBUG = value
+ await api
+ .fetchApi('/mtb/debug', {
+ method: 'POST',
+ body: JSON.stringify({
+ enabled: value,
+ }),
+ })
+ .then((response) => {})
+ .catch((error) => {
+ console.error('Error:', error)
+ })
+ },
+ })
+ },
+
+ getCustomWidgets: function () {
+ return {
+ BOOL: (node, inputName, inputData, app) => {
+ console.debug('Registering bool')
- })
- }).then(response => { }).catch(error => {
- console.error('Error:', error);
- });
-
- },
- });
- },
-
-
- getCustomWidgets: function () {
return {
- BOOL: (node, inputName, inputData, app) => {
- console.debug("Registering bool")
-
- return {
- widget: node.addCustomWidget(MtbWidgets.BOOL(inputName, inputData[1]?.default || false)),
- minWidth: 150,
- minHeight: 30,
- };
- },
+ widget: node.addCustomWidget(
+ MtbWidgets.BOOL(inputName, inputData[1]?.default || false)
+ ),
+ minWidth: 150,
+ minHeight: 30,
+ }
+ },
- COLOR: (node, inputName, inputData, app) => {
- console.debug("Registering color")
- return {
- widget: node.addCustomWidget(MtbWidgets.COLOR(inputName, inputData[1]?.default || "#ff0000")),
- minWidth: 150,
- minHeight: 30,
- }
- },
- // BBOX: (node, inputName, inputData, app) => {
- // console.debug("Registering bbox")
- // return {
- // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])),
- // minWidth: 150,
- // minHeight: 30,
- // }
-
- // }
+ COLOR: (node, inputName, inputData, app) => {
+ console.debug('Registering color')
+ return {
+ widget: node.addCustomWidget(
+ MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000')
+ ),
+ minWidth: 150,
+ minHeight: 30,
}
- },
- /**
- * @param {import("./types/comfy").NodeType} nodeType
- * @param {import("./types/comfy").NodeDef} nodeData
- * @param {import("./types/comfy").App} app
- */
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
-
- const rinputs = nodeData.input?.required;
-
- let has_custom = false
- if (nodeData.input && nodeData.input.required) {
- for (const i of Object.keys(nodeData.input.required)) {
- const input_type = nodeData.input.required[i][0]
-
- if (newTypes.includes(input_type)) {
- has_custom = true
- break;
- }
+ },
+ // BBOX: (node, inputName, inputData, app) => {
+ // console.debug("Registering bbox")
+ // return {
+ // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])),
+ // minWidth: 150,
+ // minHeight: 30,
+ // }
+
+ // }
+ }
+ },
+ /**
+ * @param {import("./types/comfy").NodeType} nodeType
+ * @param {import("./types/comfy").NodeDef} nodeData
+ * @param {import("./types/comfy").App} app
+ */
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ // const rinputs = nodeData.input?.required
+
+ let has_custom = false
+ if (nodeData.input && nodeData.input.required) {
+ for (const i of Object.keys(nodeData.input.required)) {
+ const input_type = nodeData.input.required[i][0]
+
+ if (newTypes.includes(input_type)) {
+ has_custom = true
+ break
+ }
+ }
+ }
+ if (has_custom) {
+ //- Add widgets on node creation
+ const onNodeCreated = nodeType.prototype.onNodeCreated
+ nodeType.prototype.onNodeCreated = function () {
+ const r = onNodeCreated
+ ? onNodeCreated.apply(this, arguments)
+ : undefined
+ this.serialize_widgets = true
+ this.setSize?.(this.computeSize())
+
+ this.onRemoved = function () {
+ // When removing this node we need to remove the input from the DOM
+ for (const w of this.widgets) {
+ if (w.canvas) {
+ w.canvas.remove()
}
+ w.onRemoved?.()
+ }
}
- if (has_custom) {
-
- //- Add widgets on node creation
- const onNodeCreated = nodeType.prototype.onNodeCreated;
- nodeType.prototype.onNodeCreated = function () {
- const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
- this.serialize_widgets = true;
- for (const [key, input] of Object.entries(rinputs)) {
- switch (input[0]) {
- case "COLOR":
- // const colW = colorWidget(key, input[1])
- // this.addCustomWidget(colW)
- // const associated_input = this.inputs.findIndex((i) => i.widget?.name === key);
- // if (associated_input !== -1) {
- // this.inputs[associated_input].widget = colW
- // }
-
-
-
- break;
- case "BOOL":
- // const widg = boolWidget(key, input[1])
- // this.addCustomWidget(widg)
- // this.addWidget("toggle", key, false, function (value, widget, node) {
- // console.log(value)
-
- // })
- //this.removeInput(this.inputs.findIndex((i) => i.widget?.name === key));
-
- break;
- case "BBOX":
- // const bboxW = bboxWidget(key, input[1])
- // this.addCustomWidget(bboxW)
- break;
- default:
- break
- }
-
-
- // }
- }
-
- this.setSize?.(this.computeSize())
-
- this.onRemoved = function () {
- // When removing this node we need to remove the input from the DOM
- for (let y in this.widgets) {
- if (this.widgets[y].canvas) {
- this.widgets[y].canvas.remove();
- }
- }
- };
+ return r
+ }
+
+ //- Extra menus
+ const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
+ 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.type === shared.CONVERTED_TYPE) {
+ //- This is already handled by widgetinputs.js
+ // toWidget.push({
+ // content: `Convert ${w.name} to widget`,
+ // callback: () => shared.convertToWidget(this, w),
+ // });
+ } else if (newTypes.includes(w.type)) {
+ const config = nodeData?.input?.required[w.name] ||
+ nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}]
+
+ toInput.push({
+ content: `Convert ${w.name} to input`,
+ callback: () => shared.convertToInput(this, w, config),
+ })
}
+ }
+ if (toInput.length) {
+ options.push(...toInput, null)
+ }
+
+ if (toWidget.length) {
+ options.push(...toWidget, null)
+ }
+ }
- //- Extra menus
- const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
- 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.type === shared.CONVERTED_TYPE) {
- //- This is already handled by widgetinputs.js
- // toWidget.push({
- // content: `Convert ${w.name} to widget`,
- // callback: () => shared.convertToWidget(this, w),
- // });
- } else if (newTypes.includes(w.type)) {
- const config = nodeData?.input?.required[w.name] || nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}];
-
- toInput.push({
- content: `Convert ${w.name} to input`,
- callback: () => shared.convertToInput(this, w, config),
- });
- }
- }
- if (toInput.length) {
- options.push(...toInput, null);
- }
-
- if (toWidget.length) {
- options.push(...toWidget, null);
- }
- }
-
- return r;
- };
+ return r
+ }
+ }
+ //- Extending Python Nodes
+ switch (nodeData.name) {
+ case 'Psd Save (mtb)': {
+ const onConnectionsChange = nodeType.prototype.onConnectionsChange
+ nodeType.prototype.onConnectionsChange = function (
+ type,
+ index,
+ connected,
+ link_info
+ ) {
+ const r = onConnectionsChange
+ ? onConnectionsChange.apply(this, arguments)
+ : undefined
+ shared.dynamic_connection(this, index, connected)
+ return r
}
-
- //- Extending Python Nodes
- switch (nodeData.name) {
- case "Psd Save (mtb)": {
- // const onConnectionsChange = nodeType.prototype.onConnectionsChange;
- nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
- // const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined;
- shared.dynamic_connection(this, index, connected)
- }
- break
+ break
+ }
+ case 'Save Gif (mtb)': {
+ const onExecuted = nodeType.prototype.onExecuted
+ nodeType.prototype.onExecuted = function (message) {
+ const prefix = 'anything_'
+ const r = onExecuted ? onExecuted.apply(this, message) : undefined
+
+ if (this.widgets) {
+ const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`)
+ if (pos !== -1) {
+ for (let i = pos; i < this.widgets.length; i++) {
+ this.widgets[i].onRemoved?.()
+ }
+ this.widgets.length = pos
}
- case "Save Gif (mtb)": {
- const onExecuted = nodeType.prototype.onExecuted;
- nodeType.prototype.onExecuted = function (message) {
- const r = onExecuted ? onExecuted.apply(this, message) : undefined;
- console.log(message)
- if (this.widgets) {
- const pos = this.widgets.findIndex((w) => w.name === "anything_0");
- if (pos !== -1) {
- for (let i = pos; i < this.widgets.length; i++) {
- console.log(this.widgets[i])
- console.log(this)
- this.widgets[i].onRemove?.();
-
- }
- this.widgets.length = pos
-
-
- }
-
- let imgURLs = []
- if (message && message.gif) {
- imgURLs = imgURLs.concat(message.gif.map(params => {
- return api.apiURL("/view?" + new URLSearchParams(params).toString());
- }))
- console.log(imgURLs)
- for (const img of imgURLs) {
- const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, 0))
- w.parent = this;
- }
- }
- this.setSize?.(this.computeSize())
- return r
-
- }
-
- const onRemoved = nodeType.prototype.onRemoved;
- nodeType.prototype.onRemoved = function (message) {
- const r = onRemoved ? onRemoved.apply(this, message) : undefined;
- if (!this.widgets) return r
- for (const w of this.widgets) {
- if (w.canvas) {
- w.canvas.remove();
- }
- w.onRemove?.()
- w.onRemoved?.()
- }
- return r
-
- }
- }
- break
+ let imgURLs = []
+ if (message && message.gif) {
+ imgURLs = imgURLs.concat(
+ message.gif.map((params) => {
+ return api.apiURL(
+ '/view?' + new URLSearchParams(params).toString()
+ )
+ })
+ )
+
+ let i = 0
+ for (const img of imgURLs) {
+ const w = this.addCustomWidget(
+ MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img)
+ )
+ w.parent = this
+ i++
+ }
}
- case "Animation Builder (mtb)": {
- // console.log(nodeType.prototype)
-
-
- const onNodeCreated = nodeType.prototype.onNodeCreated;
- nodeType.prototype.onNodeCreated = function () {
- const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
-
- this.changeMode(LiteGraph.ALWAYS)
- // api.addEventListener("executed", ({ detail }) => {
-
- // console.log("executed", detail)
- // console.log(this)
-
- // })
- const raw_iteration = this.widgets.find((w) => w.name === "raw_iteration");
- const raw_loop = this.widgets.find((w) => w.name === "raw_loop");
-
-
- const total_frames = this.widgets.find((w) => w.name === "total_frames");
- const loop_count = this.widgets.find((w) => w.name === "loop_count");
-
- shared.hideWidgetForGood(this, raw_iteration);
- shared.hideWidgetForGood(this, raw_loop);
-
- raw_iteration._value = 0
- // Object.defineProperty(raw_iteration, "value", {
- // get() {
- // return this._value
- // },
- // set(value) {
- // this._value = value;
- // },
- // });
-
- const value_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget;
- value_preview.inputEl.readOnly = true;
- value_preview.inputEl.disabled = true;
-
-
- // value_preview.inputEl.style.opacity = 0.6;
- value_preview.inputEl.style.textAlign = "center";
- value_preview.inputEl.style.fontSize = "2.5em";
- value_preview.inputEl.style.backgroundColor = "black";
-
- value_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent");
- value_preview.inputEl.style.setProperty("background", "red", "important");
- // remove the comfy-multiline-input class
-
- // disallow user selection
- value_preview.inputEl.style.userSelect = "none";
-
- const loop_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget;
- loop_preview.inputEl.readOnly = true;
- loop_preview.inputEl.disabled = true;
-
-
- // loop_preview.inputEl.style.opacity = 0.6;
- loop_preview.inputEl.style.textAlign = "center";
- loop_preview.inputEl.style.fontSize = "1.5em";
- loop_preview.inputEl.style.backgroundColor = "black";
-
- loop_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent");
- loop_preview.inputEl.style.setProperty("background", "red", "important");
- // remove the comfy-multiline-input class
-
- // disallow user selection
- loop_preview.inputEl.style.userSelect = "none";
-
- const onReset = () => {
- raw_iteration.value = 0;
- raw_loop.value = 0;
-
- value_preview.value = 0;
- loop_preview.value = 0;
-
- app.canvas.setDirty(true);
- }
-
- const reset_button = this.addWidget("button", `Reset`, "reset", onReset);
-
- const run_button = this.addWidget("button", `Queue`, "queue", () => {
- onReset() // this could maybe be a setting or checkbox
- app.queuePrompt(0, total_frames.value * loop_count.value)
-
- });
-
-
-
- raw_iteration.afterQueued = function () {
- this.value++;
- raw_loop.value = Math.floor(this.value / total_frames.value);
- value_preview.value = `raw: ${this.value}
-frame: ${this.value % total_frames.value}`;
- if (raw_loop.value + 1 > loop_count.value) {
- loop_preview.value = `Done 😎!`
- return
- }
-
- loop_preview.value = `current loop: ${raw_loop.value + 1}/${loop_count.value}`
-
- }
-
- return r
-
- }
- const onExecuted = nodeType.prototype.onExecuted;
- nodeType.prototype.onExecuted = function (data) {
- onExecuted?.apply(this, data)
- if (this.widgets) {
- const pos = this.widgets.findIndex((w) => w.name === "preview");
- if (pos !== -1) {
- for (let i = pos; i < this.widgets.length; i++) {
- this.widgets[i].onRemove?.();
- }
- this.widgets.length = pos;
- }
- }
-
- const w = ComfyWidgets["STRING"](this, "preview", ["STRING", { multiline: true }], app).widget;
- w.inputEl.readOnly = true;
- w.inputEl.style.opacity = 0.6;
- w.value = data.total_frames;
-
- // this.onResize?.(this.size);
- this.setSize?.(this.computeSize())
-
- }
- // const onAfterExecuteNode = nodeType.prototype.onAfterExecuteNode;
- // nodeType.prototype.onAfterExecuteNode = function () {
- // onAfterExecuteNode?.apply(this)
- // console.log("after", this)
-
- // }
- console.debug(`Registered ${nodeType.name} node extra events`)
- break
-
+ this.setSize?.(this.computeSize())
+ return r
+ }
+
+ const onRemoved = nodeType.prototype.onRemoved
+ nodeType.prototype.onRemoved = function (message) {
+ const r = onRemoved ? onRemoved.apply(this, message) : undefined
+ if (!this.widgets) return r
+ for (const w of this.widgets) {
+ if (w.canvas) {
+ w.canvas.remove()
+ }
+ w.onRemoved?.()
}
- default: {
- break
+ return r
+ }
+ }
+ break
+ }
+ case 'Animation Builder (mtb)': {
+ const onNodeCreated = nodeType.prototype.onNodeCreated
+ nodeType.prototype.onNodeCreated = function () {
+ const r = onNodeCreated
+ ? onNodeCreated.apply(this, arguments)
+ : undefined
+
+ this.changeMode(LiteGraph.ALWAYS)
+
+ const raw_iteration = this.widgets.find(
+ (w) => w.name === 'raw_iteration'
+ )
+ const raw_loop = this.widgets.find((w) => w.name === 'raw_loop')
+
+ const total_frames = this.widgets.find(
+ (w) => w.name === 'total_frames'
+ )
+ const loop_count = this.widgets.find((w) => w.name === 'loop_count')
+
+ shared.hideWidgetForGood(this, raw_iteration)
+ shared.hideWidgetForGood(this, raw_loop)
+
+ raw_iteration._value = 0
+
+ const value_preview = this.addCustomWidget(
+ MtbWidgets['DEBUG_STRING']('value_preview', 'Idle')
+ )
+ value_preview.parent = this
+
+ const loop_preview = this.addCustomWidget(
+ MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle')
+ )
+ loop_preview.parent = this
+
+ const onReset = () => {
+ raw_iteration.value = 0
+ raw_loop.value = 0
+
+ value_preview.value = 'Idle'
+ loop_preview.value = 'Iteration: Idle'
+
+ app.canvas.setDirty(true)
+ }
+
+ const reset_button = this.addWidget(
+ 'button',
+ `Reset`,
+ 'reset',
+ onReset
+ )
+
+ const run_button = this.addWidget('button', `Queue`, 'queue', () => {
+ onReset() // this could maybe be a setting or checkbox
+ app.queuePrompt(0, total_frames.value * loop_count.value)
+ window.MTB?.notify?.(
+ `Started a queue of ${total_frames.value} frames (for ${
+ loop_count.value
+ } loop, so ${total_frames.value * loop_count.value})`,
+ 5000
+ )
+ })
+
+ this.onRemoved = () => {
+ for (const w of this.widgets) {
+ if (w.canvas) {
+ w.canvas.remove()
+ }
+ w.onRemoved?.()
}
+ app.canvas.setDirty(true)
+ }
+
+ raw_iteration.afterQueued = function () {
+ this.value++
+ raw_loop.value = Math.floor(this.value / total_frames.value)
+
+ value_preview.value = `frame: ${
+ raw_iteration.value % total_frames.value
+ } / ${total_frames.value - 1}`
+
+ if (raw_loop.value + 1 > loop_count.value) {
+ loop_preview.value = 'Done 😎!'
+ } else {
+ loop_preview.value = `current loop: ${raw_loop.value + 1}/${
+ loop_count.value
+ }`
+ }
+ }
+ return r
}
- // const onNodeCreated = nodeType.prototype.onNodeCreated;
-
- // nodeType.prototype.onNodeCreated = function () {
- // const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
-
- // }
-
-
-
-
- // console.log(nodeData.output)
- // if (nodeData.output.includes("VIDEO") && nodeData.output_node) {
- // console.log(`Found video output for ${nodeType}`)
- // console.log(nodeData)
-
- // }
-
- // if (nodeData.name === "Psd Save (mtb)") {
- // console.log(`Found psd node`)
- // console.log(nodeData)
-
- // }
-
+ break
+ }
+ case 'Text Encore Frames (mtb)': {
+ const onConnectionsChange = nodeType.prototype.onConnectionsChange
+ nodeType.prototype.onConnectionsChange = function (
+ type,
+ index,
+ connected,
+ link_info
+ ) {
+ const r = onConnectionsChange
+ ? onConnectionsChange.apply(this, arguments)
+ : undefined
+
+ shared.dynamic_connection(this, index, connected)
+ return r
+ }
+ break
+ }
+ case 'Styles Loader (mtb)': {
+ const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
+ nodeType.prototype.getExtraMenuOptions = function (_, options) {
+ const r = origGetExtraMenuOptions
+ ? origGetExtraMenuOptions.apply(this, arguments)
+ : undefined
+
+ const getStyle = async (node) => {
+ try {
+ const getStyles = await api.fetchApi('/mtb/actions', {
+ method: 'POST',
+ body: JSON.stringify({
+ name: 'getStyles',
+ args:
+ node.widgets && node.widgets[0].value
+ ? node.widgets[0].value
+ : '',
+ }),
+ })
+
+ const output = await getStyles.json()
+ return output?.result
+ } catch (e) {
+ console.error(e)
+ }
+ }
+ const extracters = [
+ {
+ content: 'Extract Positive to Text node',
+ callback: async () => {
+ const style = await getStyle(this)
+ if (style && style.length >= 1) {
+ if (style[0]) {
+ window.MTB?.notify?.(
+ `Extracted positive from ${this.widgets[0].value}`
+ )
+ const tn = LiteGraph.createNode('Text box')
+ app.graph.add(tn)
+ tn.title = `${this.widgets[0].value} (Positive)`
+ tn.widgets[0].value = style[0]
+ } else {
+ window.MTB?.notify?.(
+ `No positive to extract for ${this.widgets[0].value}`
+ )
+ }
+ }
+ },
+ },
+ {
+ content: 'Extract Negative to Text node',
+ callback: async () => {
+ const style = await getStyle(this)
+ if (style && style.length >= 2) {
+ if (style[1]) {
+ window.MTB?.notify?.(
+ `Extracted negative from ${this.widgets[0].value}`
+ )
+ const tn = LiteGraph.createNode('Text box')
+ app.graph.add(tn)
+ tn.title = `${this.widgets[0].value} (Negative)`
+ tn.widgets[0].value = style[1]
+ } else {
+ window.MTB.notify(
+ `No negative to extract for ${this.widgets[0].value}`
+ )
+ }
+ }
+ },
+ },
+ ]
+ options.push(...extracters)
+ }
+ break
+ }
+ case 'Save Tensors (mtb)': {
+ const onDrawBackground = nodeType.prototype.onDrawBackground
+ nodeType.prototype.onDrawBackground = function (ctx, canvas) {
+ const r = onDrawBackground
+ ? onDrawBackground.apply(this, arguments)
+ : undefined
+ // // draw a circle on the top right of the node, with text inside
+ // ctx.fillStyle = "#fff";
+ // ctx.beginPath();
+ // ctx.arc(this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5, this.node_width * 0.5, 0, Math.PI * 2);
+ // ctx.fill();
+
+ // ctx.fillStyle = "#000";
+ // ctx.textAlign = "center";
+ // ctx.font = "bold 12px Arial";
+ // ctx.fillText("Save Tensors", this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5);
+
+ return r
+ }
+ break
+ }
+ default: {
+ break
+ }
}
-};
-
+ },
+}
-app.registerExtension(mtb_widgets);
\ No newline at end of file
+app.registerExtension(mtb_widgets)
diff --git a/web/notify.js b/web/notify.js
new file mode 100644
index 0000000..fd9e2a4
--- /dev/null
+++ b/web/notify.js
@@ -0,0 +1,115 @@
+/**
+ * File: notify.js
+ * Project: comfy_mtb
+ * Author: Mel Massadian
+ *
+ * Copyright (c) 2023 Mel Massadian
+ *
+ */
+
+import { app } from '/scripts/app.js'
+
+const log = (...args) => {
+ if (window.MTB?.TRACE) {
+ console.debug(...args)
+ }
+}
+
+let transition_time = 300
+
+const containerStyle = `
+position: fixed;
+top: 20px;
+left: 20px;
+font-family: monospace;
+z-index: 99999;
+height: 0;
+overflow: hidden;
+transition: height ${transition_time}ms ease-in-out;
+
+`
+
+const toastStyle = `
+ background-color: #333;
+ color: #fff;
+ padding: 10px;
+ border-radius: 5px;
+ opacity: 0;
+ overflow:hidden;
+ height:20px;
+ transition-property: opacity, height, padding;
+ transition-duration: ${transition_time}ms;
+ `
+
+function notify(message, timeout = 3000) {
+ log('Creating toast')
+ const container = document.getElementById('mtb-notify-container')
+ const toast = document.createElement('div')
+ toast.style.cssText = toastStyle
+ toast.innerText = message
+ container.appendChild(toast)
+
+ toast.addEventListener('transitionend', (e) => {
+ // Only on out
+ if (
+ e.target === toast &&
+ e.propertyName === 'height' &&
+ e.elapsedTime > transition_time / 1000 - Number.EPSILON
+ ) {
+ log('Transition out')
+ const totalHeight = Array.from(container.children).reduce(
+ (acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts
+ 0
+ )
+ container.style.height = `${totalHeight}px`
+
+ // If there are no toasts left, set the container's height to 0
+ if (container.children.length === 0) {
+ container.style.height = '0'
+ }
+
+ setTimeout(() => {
+ container.removeChild(toast)
+ log('Removed toast from DOM')
+ }, transition_time)
+ } else {
+ log('Transition')
+ }
+ })
+
+ // Fading in the toast
+ toast.style.opacity = '1'
+
+ // Update container's height to fit new toast
+ const totalHeight = Array.from(container.children).reduce(
+ (acc, child) => acc + child.offsetHeight + 10, // Add spacing of 10px between toasts
+ 0
+ )
+ container.style.height = `${totalHeight}px`
+
+ // remove the toast after the specified timeout
+ setTimeout(() => {
+ // trigger the transitions
+ toast.style.opacity = '0'
+ toast.style.height = '0'
+ toast.style.paddingTop = '0'
+ toast.style.paddingBottom = '0'
+ }, timeout - transition_time)
+}
+
+app.registerExtension({
+ name: 'mtb.Notify',
+ setup() {
+ if (!window.MTB) {
+ window.MTB = {}
+ }
+
+ const container = document.createElement('div')
+ container.id = 'mtb-notify-container'
+ container.style.cssText = containerStyle
+
+ document.body.appendChild(container)
+ window.MTB.notify = notify
+ // window.MTB.notify('Hello world!')
+ },
+})