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!') + }, +})