Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b35b5d8a17 | ||
|
|
6d5fd74333 | ||
|
|
b705a177d3 | ||
|
|
00fbad37c5 | ||
|
|
6cbe294c1b |
@@ -14,4 +14,3 @@ node.zip
|
||||
# I store the gh-pages worktrees (src & build) there
|
||||
.worktrees
|
||||
comfy.lock
|
||||
web/dist
|
||||
|
||||
+6
-8
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"plugins": ["prettier-plugin-svelte"]
|
||||
}
|
||||
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
# MTB Nodes
|
||||
|
||||
> [!CAUTION]
|
||||
> A lot of recent changes to comfy broke many things in mtb (colors, dynamic inputs and probably more)
|
||||
> [`dev/0.6.0`](https://github.com/melMass/comfy_mtb/tree/dev/0.6.0) partially address these. My time is limited lately so it might take time to finish and merge
|
||||
> [!NOTE]
|
||||
> master/main is outdated for now to keep backward compatibility, the next version is being worked on in
|
||||
> [`dev/0.6.0`](https://github.com/melMass/comfy_mtb/tree/dev/0.6.0)
|
||||
|
||||
|
||||
[](https://github.com/melMass/comfy_mtb/actions/workflows/test_embedded.yml)
|
||||
|
||||
+59
-23
@@ -7,7 +7,7 @@
|
||||
#
|
||||
###
|
||||
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.5.4"
|
||||
|
||||
import os
|
||||
|
||||
@@ -144,41 +144,81 @@ Please manually remove it from disk ({web_mtb}) and restart the server."""
|
||||
|
||||
|
||||
# - GATHER WIKI PAGES
|
||||
from .docs import assign_descriptions, load_wiki_docs
|
||||
def wiki_to_classname(s: str):
|
||||
wiki_name = s.replace("nodes-", "", 1)
|
||||
return "MTB_" + "".join(
|
||||
[part.capitalize() for part in wiki_name.split("-")]
|
||||
)
|
||||
|
||||
|
||||
def classname_to_wiki(s: str):
|
||||
classname = s.replace("MTB_", "")
|
||||
parts: list[str] = []
|
||||
start = 0
|
||||
for i in range(1, len(classname)):
|
||||
if classname[i].isupper():
|
||||
parts.append(classname[start:i].lower())
|
||||
start = i
|
||||
parts.append(classname[start:].lower())
|
||||
return "nodes-" + "-".join(parts)
|
||||
|
||||
|
||||
wiki = here / "wiki"
|
||||
node_docs = load_wiki_docs(wiki)
|
||||
node_docs = {}
|
||||
if wiki.exists() and wiki.is_dir():
|
||||
node_docs = {
|
||||
wiki_to_classname(x.stem): x.read_text(encoding="utf-8")
|
||||
for x in (wiki / "nodes").glob("*.md")
|
||||
}
|
||||
|
||||
|
||||
# - REGISTER NODES
|
||||
MTB_EXPORT = os.environ.get("MTB_EXPORT")
|
||||
|
||||
nodes, failed = load_nodes()
|
||||
assign_descriptions(nodes, node_docs, wiki, log, export=bool(MTB_EXPORT))
|
||||
|
||||
for node_class in nodes:
|
||||
class_name: str = node_class.__name__
|
||||
linked_doc = node_docs.get(class_name)
|
||||
|
||||
if not hasattr(node_class, "DESCRIPTION"):
|
||||
if linked_doc:
|
||||
log.debug(f"Found linked doc for {class_name}, using it")
|
||||
node_class.DESCRIPTION = linked_doc
|
||||
elif node_class.__doc__:
|
||||
log.debug(f"Using __doc__ as description for {class_name}")
|
||||
node_class.DESCRIPTION = node_class.__doc__
|
||||
if MTB_EXPORT:
|
||||
wiki_name = classname_to_wiki(class_name)
|
||||
_ = (wiki / "nodes" / (wiki_name + ".md")).write_text(
|
||||
node_class.__doc__, encoding="utf-8"
|
||||
)
|
||||
|
||||
else:
|
||||
log.debug(
|
||||
f"None of the methods could retrieve documentation for {class_name}"
|
||||
)
|
||||
|
||||
node_label = f"{get_label(class_name)} (mtb)"
|
||||
NODE_CLASS_MAPPINGS[node_label] = node_class
|
||||
NODE_DISPLAY_NAME_MAPPINGS[class_name] = node_label
|
||||
NODE_CLASS_MAPPINGS_DEBUG[node_label] = node_class.__doc__
|
||||
|
||||
# TODO: I removed this, I find it more convenient to write without spaces
|
||||
# but it breaks every of my workflows
|
||||
# TODO (cont): and until I find a way to automate the conversion
|
||||
# I'll leave it like this
|
||||
# TODO: I removed this, I find it more convenient to write without spaces
|
||||
# but it breaks every of my workflows
|
||||
# TODO (cont): and until I find a way to automate the conversion
|
||||
# I'll leave it like this
|
||||
|
||||
if MTB_EXPORT:
|
||||
with open(here / "node_list.json", "w") as f:
|
||||
_ = f.write(
|
||||
json.dumps(
|
||||
{
|
||||
k: NODE_CLASS_MAPPINGS_DEBUG[k]
|
||||
for k in sorted(NODE_CLASS_MAPPINGS_DEBUG.keys())
|
||||
},
|
||||
indent=4,
|
||||
if os.environ.get("MTB_EXPORT"):
|
||||
with open(here / "node_list.json", "w") as f:
|
||||
_ = f.write(
|
||||
json.dumps(
|
||||
{
|
||||
k: NODE_CLASS_MAPPINGS_DEBUG[k]
|
||||
for k in sorted(NODE_CLASS_MAPPINGS_DEBUG.keys())
|
||||
},
|
||||
indent=4,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
log.debug(
|
||||
"Loaded the following nodes:\n\t"
|
||||
@@ -225,10 +265,6 @@ def register_routes():
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .repl import setup_custom_web_routes
|
||||
|
||||
setup_custom_web_routes(PromptServer.instance.app)
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
from cachetools import TTLCache
|
||||
|
||||
|
||||
+3
-5
@@ -1,17 +1,15 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": { "source": { "useSortedKeys": "on", "organizeImports": "on" } }
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noConsole": "error"
|
||||
"noConsole": { "level": "warn", "options": { "allow": ["log"] } }
|
||||
},
|
||||
"style": {
|
||||
"noParameterAssign": "off",
|
||||
"noShoutyConstants": "warn",
|
||||
"useNamingConvention": "off",
|
||||
"useAsConstAssertion": "error",
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
###
|
||||
# File: docs.py
|
||||
# Project: comfy_mtb
|
||||
# Author: Mel Massadian
|
||||
# Copyright (c) 2023-2025 Mel Massadian
|
||||
#
|
||||
###
|
||||
"""Documentation handling utilities for MTB nodes."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def strip_html_tags(text: str) -> str:
|
||||
"""Strip HTML tags from description text, converting images to markdown.
|
||||
|
||||
- Removes <details>...</details> blocks entirely (they contain JSON workflows)
|
||||
- Converts <img src="URL"> to 
|
||||
- Removes all other HTML tags while keeping their content
|
||||
"""
|
||||
# Remove <details>...</details> blocks entirely (contain JSON workflows)
|
||||
text = re.sub(r"<details>.*?</details>", "", text, flags=re.DOTALL)
|
||||
# Convert <img src="URL"> to 
|
||||
text = re.sub(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*/?\s*>', r"", text)
|
||||
# Remove remaining HTML tags, keep content
|
||||
text = re.sub(r"<[^>]+>", "", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def wiki_to_classname(s: str) -> str:
|
||||
"""Convert wiki filename to class name.
|
||||
|
||||
Example: "nodes-animation-builder" -> "MTB_AnimationBuilder"
|
||||
"""
|
||||
wiki_name = s.replace("nodes-", "", 1)
|
||||
return "MTB_" + "".join([part.capitalize() for part in wiki_name.split("-")])
|
||||
|
||||
|
||||
def classname_to_wiki(s: str) -> str:
|
||||
"""Convert class name to wiki filename.
|
||||
|
||||
Example: "MTB_AnimationBuilder" -> "nodes-animation-builder"
|
||||
"""
|
||||
classname = s.replace("MTB_", "")
|
||||
parts: list[str] = []
|
||||
start = 0
|
||||
for i in range(1, len(classname)):
|
||||
if classname[i].isupper():
|
||||
parts.append(classname[start:i].lower())
|
||||
start = i
|
||||
parts.append(classname[start:].lower())
|
||||
return "nodes-" + "-".join(parts)
|
||||
|
||||
|
||||
def load_wiki_docs(wiki_path: Path) -> dict[str, str]:
|
||||
"""Load wiki documentation files as a dict of classname -> content."""
|
||||
if not wiki_path.exists() or not wiki_path.is_dir():
|
||||
return {}
|
||||
nodes_path = wiki_path / "nodes"
|
||||
if not nodes_path.exists():
|
||||
return {}
|
||||
return {
|
||||
wiki_to_classname(x.stem): x.read_text(encoding="utf-8")
|
||||
for x in nodes_path.glob("*.md")
|
||||
}
|
||||
|
||||
|
||||
def assign_descriptions(
|
||||
nodes: list,
|
||||
node_docs: dict[str, str],
|
||||
wiki_path: Path,
|
||||
log,
|
||||
export: bool = False,
|
||||
) -> None:
|
||||
"""Assign DESCRIPTION to nodes from wiki docs or docstrings.
|
||||
|
||||
Priority:
|
||||
1. Existing DESCRIPTION attribute (not modified)
|
||||
2. Wiki doc file
|
||||
3. __doc__ docstring
|
||||
"""
|
||||
for node_class in nodes:
|
||||
class_name = node_class.__name__
|
||||
linked_doc = node_docs.get(class_name)
|
||||
|
||||
if not hasattr(node_class, "DESCRIPTION"):
|
||||
if linked_doc:
|
||||
log.debug(f"Found linked doc for {class_name}, using it")
|
||||
node_class.DESCRIPTION = linked_doc
|
||||
elif node_class.__doc__:
|
||||
log.debug(f"Using __doc__ as description for {class_name}")
|
||||
node_class.DESCRIPTION = node_class.__doc__
|
||||
if export:
|
||||
wiki_name = classname_to_wiki(class_name)
|
||||
(wiki_path / "nodes" / f"{wiki_name}.md").write_text(
|
||||
node_class.__doc__, encoding="utf-8"
|
||||
)
|
||||
else:
|
||||
log.debug(
|
||||
f"None of the methods could retrieve documentation for {class_name}"
|
||||
)
|
||||
@@ -1,175 +1,85 @@
|
||||
# NOTE: This file is only use for development you can ignore it
|
||||
|
||||
use log.nu
|
||||
use nssm.nu *
|
||||
use nutils.nu [ make-id upsert-all fwd-slash backup-file ]
|
||||
use os.nu [ link ]
|
||||
use private/log.nu
|
||||
|
||||
# --- utilities ---
|
||||
def get_root [ --clean] {
|
||||
if $clean {
|
||||
$env.COMFY.ROOTS.clean
|
||||
} else {
|
||||
$env.COMFY.ROOTS.main
|
||||
}
|
||||
def get_root [--clean] {
|
||||
if $clean {
|
||||
$env.COMFY_CLEAN_ROOT
|
||||
} else {
|
||||
$env.COMFY_ROOT
|
||||
}
|
||||
}
|
||||
|
||||
def --env path-add [pth] {
|
||||
$env.PATH = ($env.PATH | append ($pth | path expand))
|
||||
export def "comfy build-web" [] {
|
||||
cd $env.COMFY_MTB
|
||||
cd web_source
|
||||
npm run build
|
||||
cp dist/*.js ../web/dist
|
||||
}
|
||||
|
||||
export def "comfy dev-web" [] {
|
||||
cd $env.COMFY_MTB
|
||||
cd web_source
|
||||
npm run dev
|
||||
}
|
||||
|
||||
export def "daily run" [] {
|
||||
let res = (comfy update --rebase)
|
||||
comfy update --clean
|
||||
comfy update_extensions
|
||||
|
||||
daily commit $res.from $res.to
|
||||
}
|
||||
|
||||
def short-date [] {
|
||||
format date "%Y-%m-%d"
|
||||
}
|
||||
|
||||
export def spawn-for [timeout: duration task: closure] {
|
||||
let input = $in
|
||||
let parent_id = job id
|
||||
let task_id = job spawn {
|
||||
$input | do $task | job send --tag (job id) $parent_id
|
||||
}
|
||||
try {
|
||||
job recv --tag $task_id --timeout $timeout
|
||||
} catch {
|
||||
job kill $task_id
|
||||
error make {
|
||||
msg: "Task timed out."
|
||||
label: {
|
||||
text: "timed out"
|
||||
span: (metadata $task).span
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- exports --
|
||||
export def "comfy profile" [timeout = 60sec] {
|
||||
let to_match = "To see the GUI go to"
|
||||
|
||||
pyinstrument -r html main.py ...($env.COMFY.ARGS)
|
||||
| tee -e {
|
||||
each {
|
||||
let stde = $in
|
||||
print -ne $stde
|
||||
if $to_match in $stde {
|
||||
print $"(ansi gb)Profiling Done!(ansi reset)"
|
||||
let process = (ps -l | where name =~ python | where command =~ pyinstrument | last)
|
||||
kill -f $process.pid
|
||||
}
|
||||
}
|
||||
}
|
||||
| complete
|
||||
| get stdout
|
||||
| save $"profiled_(date now | format date '%s').html"
|
||||
}
|
||||
|
||||
export def "comfy profile-plus" [] {
|
||||
|
||||
let timestamp = (date now | format date "%s")
|
||||
let log_name = $"cprofile_run_($timestamp)"
|
||||
let profiled = (python -m cProfile main.py --port 3000 --preview-method auto | tee -e { print -ne } | complete)
|
||||
|
||||
let out = (
|
||||
$profiled.stdout
|
||||
| lines
|
||||
# skip summary
|
||||
| skip 4
|
||||
| str join "\n"
|
||||
)
|
||||
# save result
|
||||
$out | save $"raw_($log_name).txt"
|
||||
|
||||
# process
|
||||
$out
|
||||
| from ssv
|
||||
| upsert-all { into float } tottime percall cumtime
|
||||
| save $"($log_name).nuon"
|
||||
}
|
||||
|
||||
export def restart-server [] {
|
||||
nssm restart -c comfy
|
||||
}
|
||||
|
||||
# build the web components of mtb
|
||||
export def "comfy build-web" [] {
|
||||
cd $env.COMFY.ROOTS.mtb
|
||||
if ("./web/dist" | path exists) {
|
||||
rm -rt ./web/dist
|
||||
}
|
||||
|
||||
cd web_source
|
||||
^$env.NPM_BINARY run build
|
||||
cp -r dist ../web/dist
|
||||
}
|
||||
|
||||
# start the dev server for web components
|
||||
export def "comfy dev-web" [] {
|
||||
cd $env.COMFY.ROOTS.mtb
|
||||
cd web_source
|
||||
^$env.NPM_BINARY run dev
|
||||
}
|
||||
|
||||
# daily check / update
|
||||
export def "daily run" [] {
|
||||
let res = (comfy update --rebase)
|
||||
comfy update --clean
|
||||
comfy update_extensions
|
||||
|
||||
daily commit $res.from_commit $res.to_commit
|
||||
}
|
||||
|
||||
# was daily run today?
|
||||
export def "daily was-run" [] {
|
||||
|
||||
let daily = ($env.COMFY.ROOTS.mtb | path join daily.nuon)
|
||||
let daily = ($env.COMFY_MTB | path join daily.nuon)
|
||||
|
||||
if ($daily | path exists) {
|
||||
let last = (open $daily | sort-by date | get date | last | short-date)
|
||||
let last = (open $daily | sort-by date | get date | last | short-date)
|
||||
let today = (date now | short-date)
|
||||
return ($last == $today)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export def "daily commit" [from_commit: string to_commit: string] {
|
||||
let daily = ($env.COMFY.ROOTS.mtb | path join daily.nuon)
|
||||
let commit = [{date: (date now) from_commit: $from_commit to_commit: $to_commit}]
|
||||
export def "daily commit" [from:string, to:string] {
|
||||
let daily = ($env.COMFY_MTB | path join daily.nuon)
|
||||
let commit = [{date: (date now) from:$from to:$to}]
|
||||
|
||||
let dailies = (
|
||||
if ($daily | path exists) {
|
||||
open $daily | append $commit
|
||||
} else {
|
||||
let dailies = (if ($daily | path exists) {
|
||||
open $daily | append $commit
|
||||
} else {
|
||||
$commit
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
$dailies | save -f $daily
|
||||
log success "Commited daily check"
|
||||
}
|
||||
|
||||
# start the comfy server
|
||||
export def "comfy start" [
|
||||
--clean
|
||||
--old-ui
|
||||
--listen
|
||||
--skip-daily (-s)
|
||||
] {
|
||||
if not (daily was-run) and not $skip_daily {
|
||||
log info "Running daily checks"
|
||||
daily run
|
||||
}
|
||||
let root = (get_root --clean=$clean)
|
||||
cd $root
|
||||
export def "comfy start" [--clean,--old-ui, --listen, --skip-daily(-s)] {
|
||||
if (not (daily was-run)) and not $skip_daily {
|
||||
log info "Running daily checks"
|
||||
daily run
|
||||
}
|
||||
let root = get_root --clean=($clean)
|
||||
cd $root
|
||||
|
||||
log info "Running Server"
|
||||
log info "Running Server"
|
||||
|
||||
MTB_DEBUG=true python main.py --port 3000 ...(if $old_ui { ["--front-end-version" "Comfy-Org/ComfyUI_legacy_frontend@latest"] } else { [--front-end-version Comfy-Org/ComfyUI_frontend@latest] }) --preview-method auto ...(if $listen { ["--listen"] } else { [] })
|
||||
MTB_DEBUG=true python main.py --port 3000 ...(if $old_ui { ["--front-end-version", "Comfy-Org/ComfyUI_legacy_frontend@latest"]} else {[ --front-end-version Comfy-Org/ComfyUI_frontend@latest]}) --preview-method auto ...(if $listen {["--listen"]} else {[]})
|
||||
}
|
||||
|
||||
# update comfy itself and merge master in current branch
|
||||
export def "comfy update" [
|
||||
--clean # comfy clean instance
|
||||
--rebase # Rebase instead of merge
|
||||
--clean # ??
|
||||
--rebase # Rebase instead of merge
|
||||
] {
|
||||
let root = get_root --clean=$clean
|
||||
|
||||
@@ -184,28 +94,21 @@ export def "comfy update" [
|
||||
log info "Backing up and removing models symlinks"
|
||||
|
||||
# preparing root for pull
|
||||
let pyproject = if not $clean {
|
||||
log info "Backing up the pyproject.toml..."
|
||||
let proj = (backup-file --root pyproject.toml)
|
||||
|
||||
log info "Restoring the original pyproject"
|
||||
if not $clean {
|
||||
git checkout pyproject.toml
|
||||
cd $models
|
||||
# find and store all symlinks
|
||||
log info "Checking for links in models..."
|
||||
let links = (
|
||||
ls -la | where not ($it.target | is-empty) | select name target | sort-by name
|
||||
)
|
||||
log info $"Found links: ($links)"
|
||||
let links = (ls -la |
|
||||
where not ($it.target | is-empty) |
|
||||
select name target |
|
||||
sort-by name)
|
||||
|
||||
|
||||
if not ($links | is-empty) {
|
||||
log info "Backing up the symlinks..."
|
||||
backup-file --root links.nuon
|
||||
$links | save -f links.nuon
|
||||
# remove them
|
||||
open links.nuon | each {|p| rm $p.name }
|
||||
}
|
||||
$proj
|
||||
} else {
|
||||
# just remove symlinks
|
||||
rm $models
|
||||
@@ -238,6 +141,7 @@ export def "comfy update" [
|
||||
if $rebase {
|
||||
log info "Rebasing changes"
|
||||
git rebase master
|
||||
|
||||
} else {
|
||||
log info "Merging changes"
|
||||
git merge master
|
||||
@@ -248,10 +152,9 @@ export def "comfy update" [
|
||||
|
||||
if not $clean {
|
||||
rm pyproject.toml
|
||||
log info "Using our own pyproject..."
|
||||
cp $pyproject pyproject.toml
|
||||
cp pyproject-mel.toml pyproject.toml
|
||||
cd $models
|
||||
log info "Relinking models..."
|
||||
|
||||
# resymlink them
|
||||
open links.nuon | each {|p| link -a $p.target $p.name }
|
||||
} else {
|
||||
@@ -264,82 +167,72 @@ export def "comfy update" [
|
||||
|
||||
log success $"Update successful \(($commit_count) new commits\)"
|
||||
|
||||
return {from_commit: $current_commit to_commit: $new_commit}
|
||||
return {from:$current_commit to:$new_commit}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export def "comfy toggle_extensions" [
|
||||
--clean
|
||||
] {
|
||||
let root = get_root --clean=$clean
|
||||
cd $root
|
||||
cd custom_nodes
|
||||
let exts = (ls | where type in ["dir" "symlink"] | get name)
|
||||
let choices = ($exts | input list -m "choose extension to toggle")
|
||||
if ($choices | is-empty) {
|
||||
return
|
||||
}
|
||||
export def "comfy toggle_extensions" [--clean] {
|
||||
let root = get_root --clean=($clean)
|
||||
cd $root
|
||||
cd custom_nodes
|
||||
let exts = (ls | where type in ["dir","symlink"] | get name)
|
||||
let choices = ($exts | input list -m "choose extension to toggle")
|
||||
if ($choices | is-empty) {
|
||||
return
|
||||
}
|
||||
|
||||
log info "Choices" $choices
|
||||
log info "Choices" $choices
|
||||
|
||||
let filtered = $choices | wrap name | upsert enabled {|p| not ($p.name | str ends-with ".disabled") }
|
||||
let filtered = $choices | wrap name | upsert enabled {|p| not ($p.name | str ends-with ".disabled")}
|
||||
|
||||
log info "Filtered" $filtered
|
||||
$filtered | each {|f|
|
||||
let new_name = ($f.name | str replace ".disabled" "")
|
||||
log info "Filtered" $filtered
|
||||
$filtered | each {|f|
|
||||
let new_name = ($f.name | str replace ".disabled" "")
|
||||
|
||||
let new_name = if $f.enabled {
|
||||
$"($new_name).disabled"
|
||||
} else {
|
||||
$new_name
|
||||
let new_name = if $f.enabled {
|
||||
$"($new_name).disabled"
|
||||
} else {
|
||||
$new_name
|
||||
}
|
||||
log info $"Moving ($f.name) to ($new_name)"
|
||||
mv $f.name $new_name
|
||||
}
|
||||
log info $"Moving ($f.name) to ($new_name)"
|
||||
mv $f.name $new_name
|
||||
}
|
||||
}
|
||||
|
||||
# git pull all extensions
|
||||
export def "comfy update_extensions" [ --clean] {
|
||||
let root = get_root --clean=$clean
|
||||
cd $root
|
||||
cd custom_nodes
|
||||
git multipull . -s -q
|
||||
export def "comfy update_extensions" [--clean] {
|
||||
let root = get_root --clean=($clean)
|
||||
cd $root
|
||||
cd custom_nodes
|
||||
git multipull . -s -q
|
||||
}
|
||||
|
||||
# manual set version of mtb
|
||||
export def "comfy-mtb set-version" [version: string] {
|
||||
# let pyproject = open pyproject.toml
|
||||
# let current_version = $pyproject.project.version
|
||||
# $pyproject | upsert project.version $version | save -f pyproject.toml
|
||||
# taplo format pyproject.toml
|
||||
sd "(__version__ = )\"(.*)\"" $"${1}\"($version)\"" __init__.py
|
||||
sd "(version = )(.*)" $"${1}\"($version)\"" pyproject.toml
|
||||
# log info $"⬆️ Bump version: ($current_version) → ($version)"
|
||||
def --env path-add [pth] {
|
||||
$env.PATH = ($env.PATH | append ($pth | path expand))
|
||||
|
||||
}
|
||||
|
||||
# -- env
|
||||
|
||||
export-env {
|
||||
$env.PYTHONUTF8 = 1
|
||||
$env.COMFY = {
|
||||
base_url : "https://mel-pc.tail3c8eb.ts.net"
|
||||
ARGS: [--port 3000 --preview-method auto]
|
||||
ROOTS: {
|
||||
mtb: ("." | path expand | fwd-slash)
|
||||
main: ("../.." | path expand | fwd-slash)
|
||||
clean: ($env.COMFY_ROOT | path dirname | path join ComfyClean | fwd-slash)
|
||||
}
|
||||
}
|
||||
$env.COMFY_MTB = ("." | path expand)
|
||||
# $env.CUDA_ROOT = 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.1\'
|
||||
|
||||
$env.NPM_BINARY = "bun"
|
||||
$env.CUDA_HOME = $env.CUDA_ROOT
|
||||
#
|
||||
path-add 'C:/Portable/TensorRT-8.6.0.12/lib'
|
||||
#
|
||||
if $nu.os-info.family == 'windows' {
|
||||
path-add "G:/BIN/TensorRT-10.7.0.23/lib"
|
||||
path-add "G:/BIN/cudnn-windows-x86_64-9.6.0.74_cuda12-archive/bin"
|
||||
}
|
||||
#
|
||||
path-add ($env.CUDA_ROOT | path join bin)
|
||||
|
||||
overlay use "../../.venv/Scripts/activate.nu"
|
||||
$env.COMFY_ROOT = ("../.." | path expand)
|
||||
$env.COMFY_CLEAN_ROOT = ($env.COMFY_ROOT | path dirname | path join ComfyClean)
|
||||
|
||||
path-add 'C:/Portable/TensorRT-8.6.0.12/lib'
|
||||
|
||||
if $nu.os-info.family == 'windows' {
|
||||
path-add 'G:\BIN\TensorRT-10.7.0.23\lib'
|
||||
path-add 'G:\BIN\cudnn-windows-x86_64-9.6.0.74_cuda12-archive\bin'
|
||||
}
|
||||
|
||||
path-add ($env.CUDA_ROOT | path join bin)
|
||||
overlay use ../../.venv/Scripts/activate.nu
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"use_repl": false
|
||||
}
|
||||
@@ -55,7 +55,7 @@ def mklog(name: str, level: int = base_log_level):
|
||||
|
||||
|
||||
# - The main app logger
|
||||
log = mklog("comfy-mtb", base_log_level)
|
||||
log = mklog(__package__, base_log_level)
|
||||
|
||||
|
||||
def log_user(arg: str):
|
||||
|
||||
+11
-19
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
import torch
|
||||
@@ -278,14 +277,14 @@ class MTB_AudioToText(MtbAudio):
|
||||
f"Processing chunk {chunk_offset:.1f}s - {chunk_end / sample_rate:.1f}s"
|
||||
)
|
||||
|
||||
max_length = model.config.max_length or 448
|
||||
max_length = getattr(model.config, "max_length", None) or 448
|
||||
attention_mask = torch.ones((1, max_length))
|
||||
|
||||
input_features = processor(
|
||||
chunk_waveform,
|
||||
sampling_rate=sample_rate,
|
||||
return_tensors="pt",
|
||||
).input_features.to(device)
|
||||
).input_features.to(device=device, dtype=model.dtype)
|
||||
|
||||
with torch.no_grad():
|
||||
predicted_ids = model.generate(
|
||||
@@ -486,24 +485,17 @@ class MTB_AudioCut(MtbAudio):
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"length": (
|
||||
("INT"),
|
||||
("FLOAT"),
|
||||
{
|
||||
"default": 1000,
|
||||
"min": 100,
|
||||
"max": sys.maxsize,
|
||||
"default": 1000.0,
|
||||
"min": 0.0,
|
||||
"max": 999999.0,
|
||||
"step": 1,
|
||||
"tooltip": "Length in milliseconds",
|
||||
},
|
||||
),
|
||||
"offset": (
|
||||
("INT"),
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": sys.maxsize,
|
||||
"step": 1,
|
||||
"tooltip": "Offset in milliseconds",
|
||||
},
|
||||
("FLOAT"),
|
||||
{"default": 0.0, "min": 0.0, "max": 999999.0, "step": 1},
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -513,11 +505,11 @@ class MTB_AudioCut(MtbAudio):
|
||||
CATEGORY = "mtb/audio"
|
||||
FUNCTION = "cut"
|
||||
|
||||
def cut(self, audio: AudioTensor, length: int, offset: int):
|
||||
def cut(self, audio: AudioTensor, length: float, offset: float):
|
||||
sample_rate = audio["sample_rate"]
|
||||
start_idx: int = int(float(offset) * float(sample_rate) / 1000)
|
||||
start_idx = int(offset * sample_rate / 1000)
|
||||
end_idx = min(
|
||||
start_idx + int(float(length) * float(sample_rate) / 1000),
|
||||
start_idx + int(length * sample_rate / 1000),
|
||||
audio["waveform"].shape[-1],
|
||||
)
|
||||
cut_waveform = audio["waveform"][:, :, start_idx:end_idx]
|
||||
|
||||
+26
-11
@@ -200,7 +200,7 @@ class MTB_ImageBatchToSublist:
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "INT")
|
||||
RETURN_NAMES = ("image_list", "mask_list", "item_count")
|
||||
|
||||
OUTPUT_IS_LIST = (True, True, False)
|
||||
OUTPUT_IS_LIST = (True, True)
|
||||
FUNCTION = "split_batch"
|
||||
CATEGORY = "batch_processing"
|
||||
|
||||
@@ -215,26 +215,41 @@ class MTB_ImageBatchToSublist:
|
||||
"You must either pass mask or image, none received"
|
||||
)
|
||||
|
||||
image_count = image.size(0) if image is not None else 0
|
||||
mask_count = mask.size(0) if mask is not None else 0
|
||||
image_count = 0
|
||||
if image is not None:
|
||||
image_count = image.size(0)
|
||||
|
||||
mask_count = 0
|
||||
if mask is not None:
|
||||
mask_count = mask.size(0)
|
||||
|
||||
if image_count > 0 and mask_count > 0 and mask_count != image_count:
|
||||
raise ValueError(
|
||||
f"When providing image and mask, batch size must match (got {mask_count} mask and {image_count} images)"
|
||||
f"When providing image and mask, batch size must match (got {mask.size(0)} mask and {image.size(0)} images)"
|
||||
)
|
||||
|
||||
batch_size = max(image_count, mask_count)
|
||||
if batch_size == 0:
|
||||
return ([], [], [0])
|
||||
|
||||
num_full_batches = batch_size // sub_batch_size
|
||||
im_batches = []
|
||||
mask_batches = []
|
||||
|
||||
for i in range(0, batch_size, sub_batch_size):
|
||||
if image is not None:
|
||||
im_batches.append(image[i : i + sub_batch_size])
|
||||
if mask is not None:
|
||||
mask_batches.append(mask[i : i + sub_batch_size])
|
||||
for i in range(num_full_batches):
|
||||
start_idx = i * sub_batch_size
|
||||
end_idx = start_idx + sub_batch_size
|
||||
if image_count > 0:
|
||||
im_batches.append(image[start_idx:end_idx, ...])
|
||||
|
||||
if mask_count > 0:
|
||||
mask_batches.append(mask[start_idx:end_idx, ...])
|
||||
|
||||
if batch_size % sub_batch_size != 0:
|
||||
remaining_start = num_full_batches * sub_batch_size
|
||||
if image_count > 0:
|
||||
im_batches.append(image[remaining_start:, ...])
|
||||
|
||||
if mask_count > 0:
|
||||
mask_batches.append(mask[remaining_start:, ...])
|
||||
|
||||
return (im_batches, mask_batches, len(im_batches))
|
||||
|
||||
|
||||
+179
-605
@@ -1,70 +1,33 @@
|
||||
import base64
|
||||
import io
|
||||
import textwrap
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, Literal, Protocol, TypedDict, runtime_checkable
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import folder_paths
|
||||
import torch
|
||||
from rich import inspect
|
||||
from rich.console import Console
|
||||
|
||||
from ..log import log
|
||||
from ..utils import LazyProxyTensor, get_torch_tensor_info, tensor2pil
|
||||
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
plt.style.use("dark_background")
|
||||
MATPLOTLIB_AVAILABLE = True
|
||||
except ImportError:
|
||||
MATPLOTLIB_AVAILABLE = False
|
||||
from ..utils import tensor2pil
|
||||
|
||||
|
||||
# region Decorator
|
||||
def metadata(**meta_kwargs: Any) -> Callable[[Any], Any]:
|
||||
"""Add metadata to method (`__meta__` dict)."""
|
||||
|
||||
def decorator(func: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
wrapper.__meta__ = meta_kwargs
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# endregion
|
||||
class UIResult(TypedDict):
|
||||
kind: Literal["text", "b64_images"]
|
||||
data: str
|
||||
|
||||
|
||||
def indent_results(results: list[UIResult], by: str = " "):
|
||||
for res in results:
|
||||
if res["kind"] == "text":
|
||||
log.debug(f"Indenting: {res['data']}")
|
||||
res["data"] = textwrap.indent(res["data"], by)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
ProcessorResult = list[UIResult]
|
||||
|
||||
|
||||
def _get_detailed_type_info(obj) -> str:
|
||||
type_info: list[str] = []
|
||||
def get_detailed_type_info(obj):
|
||||
type_info = []
|
||||
|
||||
type_name = type(obj).__name__
|
||||
type_info.append(f"Type: {type_name}")
|
||||
|
||||
if isinstance(obj, torch.Tensor):
|
||||
return get_torch_tensor_info(obj)
|
||||
|
||||
elif isinstance(obj, list | tuple):
|
||||
type_info.extend(
|
||||
[
|
||||
f"Shape: {obj.shape}",
|
||||
f"Dtype: {obj.dtype}",
|
||||
f"Device: {obj.device}",
|
||||
f"Requires grad: {obj.requires_grad}",
|
||||
f"Stride: {obj.stride()}",
|
||||
f"Contiguous: {obj.is_contiguous()}",
|
||||
]
|
||||
)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
type_info.extend(
|
||||
[
|
||||
f"Length: {len(obj)}",
|
||||
@@ -84,184 +47,122 @@ def _get_detailed_type_info(obj) -> str:
|
||||
attributes = [attr for attr in dir(obj) if not attr.startswith("_")]
|
||||
type_info.append(f"Attributes: {attributes}")
|
||||
|
||||
return "\n".join(type_info)
|
||||
|
||||
|
||||
def _apply_rich_results(processed, mode="none", title=""):
|
||||
processing_text = False
|
||||
acc = ""
|
||||
reshaped: list[UIResult] = []
|
||||
for i in range(len(processed)):
|
||||
if processed[i]["kind"] == "text":
|
||||
if not processing_text:
|
||||
processing_text = True
|
||||
acc += processed[i]["data"] + "\n"
|
||||
if len(processed) == (i + 1):
|
||||
reshaped.append(
|
||||
UIResult(
|
||||
kind="text", data=_apply_rich(acc, mode, title=title)
|
||||
)
|
||||
)
|
||||
else:
|
||||
if processing_text:
|
||||
processing_text = False
|
||||
reshaped.append(
|
||||
UIResult(
|
||||
kind="text", data=_apply_rich(acc, mode, title=title)
|
||||
)
|
||||
)
|
||||
acc = ""
|
||||
reshaped.append(processed[i])
|
||||
|
||||
return reshaped
|
||||
# for item in processed:
|
||||
return type_info
|
||||
|
||||
|
||||
# region processors
|
||||
def _apply_rich(
|
||||
formatted: str | list[str], rich_mode: str | None = None, *, title=""
|
||||
) -> str:
|
||||
if rich_mode is None:
|
||||
return (
|
||||
formatted if isinstance(formatted, str) else "\n".join(formatted)
|
||||
def process_tensor(tensor: torch.Tensor, as_type=False):
|
||||
log.debug(f"Tensor: {tensor.shape}")
|
||||
|
||||
if as_type:
|
||||
return {
|
||||
"text": [f"Tensor of shape {tensor.shape} of type {tensor.dtype}"]
|
||||
}
|
||||
|
||||
is_mask = len(tensor.shape) == 3
|
||||
|
||||
if is_mask:
|
||||
tensor = tensor.unsqueeze(-1).repeat(1, 1, 1, 3)
|
||||
|
||||
image = tensor2pil(tensor)
|
||||
b64_imgs = []
|
||||
for im in image:
|
||||
if is_mask:
|
||||
im = im.convert("L")
|
||||
|
||||
buffered = io.BytesIO()
|
||||
im.save(buffered, format="PNG")
|
||||
b64_imgs.append(
|
||||
"data:image/png;base64,"
|
||||
+ base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
)
|
||||
|
||||
from rich.console import Console
|
||||
return {"b64_images": b64_imgs}
|
||||
|
||||
console = Console(record=True)
|
||||
|
||||
if isinstance(formatted, list):
|
||||
for line in formatted:
|
||||
console.print(line)
|
||||
def process_list(anything, as_type=False):
|
||||
text = []
|
||||
if not anything:
|
||||
return {"text": []}
|
||||
|
||||
if as_type:
|
||||
type_info = get_detailed_type_info(anything)
|
||||
type_info.extend(get_detailed_type_info(anything[0]))
|
||||
return {"text": type_info}
|
||||
|
||||
first_element = anything[0]
|
||||
if (
|
||||
isinstance(first_element, list)
|
||||
and first_element
|
||||
and isinstance(first_element[0], torch.Tensor)
|
||||
):
|
||||
text.append(
|
||||
"List of List of Tensors: "
|
||||
f"{first_element[0].shape} (x{len(anything)})"
|
||||
)
|
||||
|
||||
elif isinstance(first_element, torch.Tensor):
|
||||
text.append(
|
||||
f"List of Tensors: {first_element.shape} (x{len(anything)})"
|
||||
)
|
||||
else:
|
||||
console.print(formatted)
|
||||
text.append(f"Array ({len(anything)}): {anything}")
|
||||
|
||||
CSV_CODE_FORMAT = """
|
||||
<svg class="rich-terminal" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
return {"text": text}
|
||||
|
||||
@font-face {{
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}}
|
||||
@font-face {{
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}}
|
||||
|
||||
.{unique_id}-matrix {{
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: {char_height}px;
|
||||
line-height: {line_height}px;
|
||||
font-variant-east-asian: full-width;
|
||||
}}
|
||||
def process_dict(anything, as_type=False):
|
||||
text = []
|
||||
if as_type:
|
||||
return {"text": get_detailed_type_info(anything)}
|
||||
|
||||
.{unique_id}-title {{
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}}
|
||||
if "samples" in anything:
|
||||
is_empty = (
|
||||
"(empty)" if torch.count_nonzero(anything["samples"]) == 0 else ""
|
||||
)
|
||||
text.append(f"Latent Samples: {anything['samples'].shape} {is_empty}")
|
||||
|
||||
{styles}
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="{unique_id}-clip-terminal">
|
||||
<rect x="0" y="0" width="{terminal_width}" height="{terminal_height}" />
|
||||
</clipPath>
|
||||
{lines}
|
||||
</defs>
|
||||
|
||||
{chrome}
|
||||
<g clip-path="url(#{unique_id}-clip-terminal)">
|
||||
{backgrounds}
|
||||
<g class="{unique_id}-matrix">
|
||||
{matrix}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
if rich_mode == "svg-window":
|
||||
return console.export_svg(title=title, code_format=CSV_CODE_FORMAT)
|
||||
elif rich_mode == "svg":
|
||||
return console.export_svg(
|
||||
title=title,
|
||||
code_format=CSV_CODE_FORMAT.replace("{chrome}", ""),
|
||||
elif "waveform" in anything:
|
||||
is_empty = (
|
||||
"(empty) " if torch.count_nonzero(anything["samples"]) == 0 else ""
|
||||
)
|
||||
|
||||
elif rich_mode == "html":
|
||||
CONSOLE_HTML_FORMAT = textwrap.dedent("""
|
||||
<div style="color:{foreground};">
|
||||
<code style="font-family:inherit">{code}</code>
|
||||
</div>
|
||||
""").strip()
|
||||
|
||||
import rich.terminal_theme
|
||||
|
||||
return console.export_html(
|
||||
inline_styles=True,
|
||||
code_format=CONSOLE_HTML_FORMAT,
|
||||
theme=rich.terminal_theme.MONOKAI,
|
||||
text.append(
|
||||
f"Audio Samples: {anything['waveform'].shape}{is_empty} | sample rate {anything['sample_rate']}"
|
||||
)
|
||||
|
||||
log.error(f"Unknown rich mode: {rich_mode}")
|
||||
return formatted if isinstance(formatted, str) else "\n".join(formatted)
|
||||
else:
|
||||
log.debug(f"Unhandled dict: {anything.keys()}")
|
||||
text.append(json.dumps(anything, indent=2))
|
||||
|
||||
return {"text": text}
|
||||
|
||||
|
||||
def process_bool(anything, as_type=False):
|
||||
return {"text": ["True" if anything else "False"]}
|
||||
|
||||
|
||||
def process_text(anything, as_type=False):
|
||||
if as_type:
|
||||
return {"text": get_detailed_type_info(anything)}
|
||||
|
||||
return {"text": [str(anything)]}
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region conditions
|
||||
|
||||
|
||||
# those are pretty dumb there is now probably a better way..
|
||||
def is_condition(item):
|
||||
return (
|
||||
isinstance(item, list)
|
||||
and all(isinstance(i, list) for i in item)
|
||||
and isinstance(item[0][0], torch.Tensor)
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
RICH_MODE = Literal["none", "html", "svg", "svg-window"]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Processor(Protocol):
|
||||
"""Generic protocol for processor functions."""
|
||||
|
||||
def __call__(
|
||||
self, item: Any, *, as_type: bool = False, deep: bool = False
|
||||
) -> ProcessorResult: ...
|
||||
|
||||
|
||||
class MTB_Debug:
|
||||
"""A debug node."""
|
||||
"""Experimental node to debug any Comfy values.
|
||||
|
||||
support for more types and widgets is planned.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {"output_to_console": ("BOOLEAN", {"default": False})},
|
||||
"optional": {
|
||||
"as_detailed_types": ("BOOLEAN", {"default": False}),
|
||||
"deep_inspect": ("BOOLEAN", {"default": False}),
|
||||
"rich_mode": (
|
||||
("none", "html", "svg", "svg-window"),
|
||||
{"default": "none"},
|
||||
),
|
||||
},
|
||||
"optional": {"as_detailed_types": ("BOOLEAN", {"default": False})},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
@@ -269,426 +170,99 @@ class MTB_Debug:
|
||||
CATEGORY = "mtb/debug"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
_processors: dict[type, Processor]
|
||||
|
||||
def __init__(self):
|
||||
self._condition_processors = {is_condition: self._process_condition}
|
||||
self._class_name_processors = {
|
||||
"CLIP": self._process_clip,
|
||||
"VAE": self._process_vae,
|
||||
}
|
||||
self._processors = {
|
||||
torch.nn.Module: self._process_module,
|
||||
torch.Tensor: self._process_tensor,
|
||||
LazyProxyTensor: self._process_repr,
|
||||
list: self._process_container,
|
||||
tuple: self._process_container,
|
||||
dict: self._process_dict,
|
||||
bool: self._process_bool,
|
||||
str: self._process_primitive,
|
||||
int: self._process_primitive,
|
||||
float: self._process_primitive,
|
||||
type(None): self._process_primitive,
|
||||
}
|
||||
|
||||
# - Dispatchers ------------------------------------------------------------
|
||||
def _dispatch_processor(
|
||||
self, item: Any, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
"""Find and calls the appropriate processor for the given item."""
|
||||
# first conditions
|
||||
for c, process in self._condition_processors.items():
|
||||
if c(item):
|
||||
return process(item, as_type=as_type, deep=deep)
|
||||
|
||||
# named class
|
||||
class_name = type(item).__name__
|
||||
if class_name in self._class_name_processors:
|
||||
return self._class_name_processors[class_name](
|
||||
item, as_type=as_type, deep=deep
|
||||
)
|
||||
|
||||
# type based or unknown
|
||||
processor = self._processors.get(type(item), self._process_unknown)
|
||||
res = processor(item, as_type=as_type, deep=deep)
|
||||
|
||||
return res
|
||||
|
||||
def do_debug(
|
||||
self,
|
||||
**kwargs,
|
||||
self, output_to_console: bool, as_detailed_types: bool, **kwargs
|
||||
):
|
||||
output = {"ui": {"items": []}}
|
||||
|
||||
settings = {k: kwargs.pop(k) for k in self.INPUT_TYPES()["optional"]}
|
||||
output_to_console = kwargs.pop("output_to_console")
|
||||
as_type = settings.get("as_detailed_types", False)
|
||||
deep = settings.get("deep_inspect", False)
|
||||
rich_mode = settings.get("rich_mode", "none")
|
||||
if output_to_console:
|
||||
for k, v in kwargs.items():
|
||||
log.info(f"{k}: {v}")
|
||||
|
||||
for input_name, item in kwargs.items():
|
||||
processed = self._dispatch_processor(
|
||||
item, as_type=as_type, deep=deep
|
||||
)
|
||||
if processed is None:
|
||||
continue
|
||||
for input_name, anything in kwargs.items():
|
||||
processor = processors.get(type(anything), process_text)
|
||||
|
||||
if rich_mode != "none":
|
||||
title = f"{input_name} ({type(item).__name__})"
|
||||
processed = _apply_rich_results(processed, rich_mode, title)
|
||||
processed = processor(anything, as_detailed_types)
|
||||
|
||||
if output_to_console:
|
||||
log.info(f"- Input '{input_name}':")
|
||||
for p in processed:
|
||||
if p["kind"] == "text":
|
||||
log.info(f" {p['data']}")
|
||||
if p["kind"] == "b64_image":
|
||||
log.info(f" (contains {len(p['data'])} images)")
|
||||
item = {
|
||||
"input": input_name,
|
||||
**processed,
|
||||
}
|
||||
output["ui"]["items"].append(item)
|
||||
|
||||
output["ui"]["items"].append(
|
||||
{"input": input_name, "items": processed}
|
||||
)
|
||||
return output
|
||||
|
||||
def _process_unknown(
|
||||
self, item: Any, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
console = Console(
|
||||
record=True,
|
||||
width=120,
|
||||
)
|
||||
|
||||
console.print(f"Generic {type(item).__name__}", emoji=True)
|
||||
if as_type:
|
||||
inspect(item, console=console, all=deep, methods=deep, docs=deep)
|
||||
else:
|
||||
console.print(item, emoji=True)
|
||||
class MTB_SaveTensors:
|
||||
"""Save torch tensors (image, mask or latent) to disk.
|
||||
|
||||
text_output = console.export_text(clear=True)
|
||||
useful to debug things outside comfy.
|
||||
"""
|
||||
|
||||
return [UIResult(kind="text", data=text_output.strip())]
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "mtb/debug"
|
||||
|
||||
def _process_repr(
|
||||
self, item: Any, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
return [{"kind": "text", "data": item.__repr__()}]
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"filename_prefix": ("STRING", {"default": "ComfyPickle"}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"latent": ("LATENT",),
|
||||
},
|
||||
}
|
||||
|
||||
def _process_primitive(
|
||||
self, item: Any, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
if as_type:
|
||||
return self._process_unknown(item, as_type=as_type, deep=deep)
|
||||
FUNCTION = "save"
|
||||
OUTPUT_NODE = True
|
||||
RETURN_TYPES = ()
|
||||
CATEGORY = "mtb/debug"
|
||||
|
||||
return [UIResult(kind="text", data=str(item))]
|
||||
def save(
|
||||
self,
|
||||
filename_prefix,
|
||||
image: torch.Tensor | None = None,
|
||||
mask: torch.Tensor | None = None,
|
||||
latent: torch.Tensor | None = None,
|
||||
):
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
full_output_folder = Path(full_output_folder)
|
||||
if image is not None:
|
||||
image_file = f"{filename}_image_{counter:05}.pt"
|
||||
torch.save(image, full_output_folder / image_file)
|
||||
# np.save(full_output_folder/ image_file, image.cpu().numpy())
|
||||
|
||||
def _process_bool(
|
||||
self, item: bool, *, as_type=False, deep=False
|
||||
) -> ProcessorResult: # noqa: FBT001
|
||||
return [{"kind": "text", "data": "True" if item else "False"}]
|
||||
if mask is not None:
|
||||
mask_file = f"{filename}_mask_{counter:05}.pt"
|
||||
torch.save(mask, full_output_folder / mask_file)
|
||||
# np.save(full_output_folder/ mask_file, mask.cpu().numpy())
|
||||
|
||||
def _process_clip(
|
||||
self, item: Any, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
try:
|
||||
clip_model = getattr(item, "cond_stage_model", None)
|
||||
tokenizer = getattr(item, "tokenizer", None)
|
||||
if latent is not None:
|
||||
# for latent we must use pickle
|
||||
latent_file = f"{filename}_latent_{counter:05}.pt"
|
||||
torch.save(latent, full_output_folder / latent_file)
|
||||
# pickle.dump(latent, open(full_output_folder/ latent_file, "wb"))
|
||||
|
||||
text = [UIResult(kind="text", data="CLIP")]
|
||||
if clip_model:
|
||||
text.append(UIResult(kind="text", data="CLIP Model:"))
|
||||
model_summary = self._process_module(
|
||||
clip_model, as_type=as_type
|
||||
)
|
||||
if model_summary:
|
||||
text.extend(indent_results(model_summary, " "))
|
||||
else:
|
||||
text.append(
|
||||
UIResult(
|
||||
kind="text",
|
||||
data="[error] failed to get informations about clip model",
|
||||
)
|
||||
)
|
||||
# np.save(full_output_folder / latent_file,
|
||||
# latent[""].cpu().numpy())
|
||||
|
||||
if tokenizer:
|
||||
text.append(UIResult(kind="text", data="Tokenizer:"))
|
||||
vocab_size = getattr(tokenizer, "vocab_size", "N/A")
|
||||
text.append(
|
||||
UIResult(
|
||||
kind="text",
|
||||
data=f" Class: {type(tokenizer).__name__}\n Vocab Size: {vocab_size}",
|
||||
)
|
||||
)
|
||||
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Failed to process CLIP object: {e}")
|
||||
return self._process_unknown(item, as_type=as_type, deep=deep)
|
||||
|
||||
def _process_condition(
|
||||
self, item: Any, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
count = len(item)
|
||||
result = [UIResult(kind="text", data=f"Conditions: {count}")]
|
||||
|
||||
for cond in item:
|
||||
result.extend(self._preview_conditioning_tensor(cond[0]))
|
||||
|
||||
return result
|
||||
|
||||
def _process_vae(
|
||||
self, item: Any, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
try:
|
||||
vae_model = getattr(
|
||||
item, "first_stage_model", getattr(item, "vae", item)
|
||||
)
|
||||
text = [
|
||||
UIResult(kind="text", data="VAE"),
|
||||
UIResult(kind="text", data="Internal Model:"),
|
||||
]
|
||||
|
||||
model_summary = self._process_module(
|
||||
vae_model, as_type=as_type, deep=deep
|
||||
)
|
||||
text.extend(indent_results(model_summary, " "))
|
||||
|
||||
return text
|
||||
except Exception as e:
|
||||
log.error(f"Failed to process VAE object: {e}")
|
||||
return self._process_unknown(item, as_type=as_type, deep=deep)
|
||||
|
||||
def _process_module(
|
||||
self, item: torch.nn.Module, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
if as_type and deep:
|
||||
return self._process_unknown(item, as_type=as_type, deep=deep)
|
||||
|
||||
total_params = sum(p.numel() for p in item.parameters())
|
||||
trainable_params = sum(
|
||||
p.numel() for p in item.parameters() if p.requires_grad
|
||||
)
|
||||
try:
|
||||
device = next(item.parameters()).device
|
||||
except StopIteration:
|
||||
device = "cpu (no parameters)"
|
||||
|
||||
train_percent = (
|
||||
f"{trainable_params / total_params:.2%}"
|
||||
if total_params > 0
|
||||
else "0.00%"
|
||||
)
|
||||
|
||||
text = [
|
||||
f"Model: {type(item).__name__} on {device}",
|
||||
textwrap.dedent(f"""
|
||||
- Parameters: {total_params:,}
|
||||
- Trainable: {trainable_params:,} ({train_percent})
|
||||
""").strip(),
|
||||
]
|
||||
return [{"kind": "text", "data": d} for d in text]
|
||||
|
||||
def _process_tensor(
|
||||
self, item: torch.Tensor, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
is_latent = item.ndim == 4 and item.shape[1] == 4
|
||||
is_image = (
|
||||
not is_latent and item.ndim == 4 and item.shape[3] in [1, 3, 4]
|
||||
)
|
||||
is_conditioning = item.ndim == 3 and item.shape[2] in [
|
||||
768,
|
||||
1024,
|
||||
1152,
|
||||
1280,
|
||||
2048,
|
||||
4096,
|
||||
]
|
||||
is_mask = (item.ndim == 2) or (item.ndim == 3 and not is_conditioning)
|
||||
|
||||
if as_type:
|
||||
type_name = "Unknown Tensor"
|
||||
if is_latent:
|
||||
type_name = "Latent Tensor"
|
||||
elif is_image:
|
||||
type_name = "Image Tensor"
|
||||
elif is_conditioning:
|
||||
type_name = "CLIP Conditioning Tensor"
|
||||
elif is_mask:
|
||||
type_name = "Mask Tensor"
|
||||
return [
|
||||
{
|
||||
"kind": "text",
|
||||
"data": get_torch_tensor_info(item, name=type_name),
|
||||
}
|
||||
]
|
||||
|
||||
if is_image or is_mask:
|
||||
return self._render_image_tensor(item)
|
||||
if is_latent:
|
||||
return self._preview_latent_tensor(item)
|
||||
if is_conditioning:
|
||||
return self._preview_conditioning_tensor(item)
|
||||
return self._process_unknown(item, as_type=as_type, deep=deep)
|
||||
|
||||
def _visualize_tensor_heatmap(
|
||||
self, tensor_2d: torch.Tensor, title: str
|
||||
) -> str | None:
|
||||
if not MATPLOTLIB_AVAILABLE:
|
||||
log.warning("Matplotlib not found. Skipping tensor visualization.")
|
||||
return None
|
||||
if tensor_2d.ndim != 2:
|
||||
log.warning(
|
||||
f"Cannot visualize tensor with {tensor_2d.ndim} dimensions. Requires 2."
|
||||
)
|
||||
return None
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6, 4), dpi=100)
|
||||
im = ax.imshow(tensor_2d.cpu().numpy(), cmap="viridis", aspect="auto")
|
||||
fig.colorbar(im, ax=ax)
|
||||
ax.set_title(title)
|
||||
fig.tight_layout()
|
||||
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf, format="png", bbox_inches="tight", pad_inches=0.1)
|
||||
plt.close(fig)
|
||||
buf.seek(0)
|
||||
return "data:image/png;base64," + base64.b64encode(buf.read()).decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
def _render_image_tensor(self, item: torch.Tensor) -> ProcessorResult:
|
||||
is_mask = (item.ndim == 2) or (item.ndim == 3 and item.shape[-1] != 3)
|
||||
img_tensor = (
|
||||
item.unsqueeze(0) if item.ndim == 3 and not is_mask else item
|
||||
)
|
||||
img_tensor = item.unsqueeze(0) if item.ndim == 2 else img_tensor
|
||||
|
||||
images = tensor2pil(img_tensor)
|
||||
b64_imgs = []
|
||||
for im in images:
|
||||
if is_mask:
|
||||
im = im.convert("L")
|
||||
buffered = io.BytesIO()
|
||||
im.save(buffered, format="PNG")
|
||||
b64_imgs.append(
|
||||
"data:image/png;base64,"
|
||||
+ base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
)
|
||||
return [UIResult(kind="b64_images", data=b64_imgs)]
|
||||
|
||||
def _preview_latent_tensor(self, item: torch.Tensor) -> ProcessorResult:
|
||||
is_empty = "(empty)" if torch.count_nonzero(item) == 0 else ""
|
||||
stats = [
|
||||
f"Min: {item.min():.4f}",
|
||||
f"Max: {item.max():.4f}",
|
||||
f"Mean: {item.mean():.4f}",
|
||||
]
|
||||
text = [
|
||||
get_torch_tensor_info(item, name="Latent Tensor"),
|
||||
is_empty,
|
||||
] + stats
|
||||
|
||||
result = [UIResult(kind="text", data=t) for t in text]
|
||||
vis_tensor = item[0].mean(dim=0)
|
||||
heatmap_b64 = self._visualize_tensor_heatmap(
|
||||
vis_tensor, "Latent Energy (Channel Mean)"
|
||||
)
|
||||
if heatmap_b64:
|
||||
result.append(UIResult(kind="b64_images", data=[heatmap_b64]))
|
||||
return result
|
||||
|
||||
def _preview_conditioning_tensor(
|
||||
self, item: torch.Tensor
|
||||
) -> ProcessorResult:
|
||||
_batch, tokens, embed_dim = item.shape
|
||||
text = [
|
||||
get_torch_tensor_info(item, name="CLIP Conditioning Tensor"),
|
||||
f"Token Count: {tokens}",
|
||||
f"Embedding Dim: {embed_dim}",
|
||||
]
|
||||
|
||||
result = [UIResult(kind="text", data=d) for d in text]
|
||||
heatmap_b64 = self._visualize_tensor_heatmap(
|
||||
item[0], "Token Embeddings (approx)"
|
||||
)
|
||||
if heatmap_b64:
|
||||
result.append(UIResult(kind="b64_images", data=[heatmap_b64]))
|
||||
return result
|
||||
|
||||
def _process_container(
|
||||
self, item: list | tuple, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
if not item:
|
||||
return [UIResult(kind="text", data=f"Empty {type(item).__name__}")]
|
||||
|
||||
container_type = type(item).__name__
|
||||
element_type = type(item[0]).__name__
|
||||
|
||||
all_match = all(type(i) is type(item[0]) for i in item)
|
||||
|
||||
result = [
|
||||
UIResult(
|
||||
kind="text",
|
||||
data=f"{container_type} of {len(item)} x {element_type}",
|
||||
),
|
||||
UIResult(kind="text", data=f"(mixed types: {not all_match})"),
|
||||
]
|
||||
|
||||
if not as_type or (as_type and deep):
|
||||
for i, sub_item in enumerate(item):
|
||||
res = self._dispatch_processor(
|
||||
sub_item, as_type=as_type, deep=deep
|
||||
)
|
||||
if res:
|
||||
text = res[0].get("data", "Unknown")
|
||||
res[0]["data"] = f"[{i}]: {text}"
|
||||
|
||||
result.extend(res)
|
||||
|
||||
return result
|
||||
|
||||
first_item_result = self._dispatch_processor(
|
||||
item[0], as_type=as_type, deep=deep
|
||||
)
|
||||
if not first_item_result:
|
||||
return result
|
||||
|
||||
return (
|
||||
result
|
||||
+ [UIResult(kind="text", data="Preview of first element:")]
|
||||
+ indent_results(first_item_result, " - ")
|
||||
)
|
||||
|
||||
def _process_dict(
|
||||
self, item: dict, *, as_type=False, deep=False
|
||||
) -> ProcessorResult:
|
||||
if "pooled_output" in item and isinstance(
|
||||
item["pooled_output"], torch.Tensor
|
||||
):
|
||||
return self._dispatch_processor(
|
||||
item["pooled_output"], as_type=as_type, deep=deep
|
||||
)
|
||||
|
||||
if "samples" in item and isinstance(item.get("samples"), torch.Tensor):
|
||||
return self._dispatch_processor(
|
||||
item["samples"], as_type=as_type, deep=deep
|
||||
)
|
||||
|
||||
if "waveform" in item and isinstance(
|
||||
item.get("waveform"), torch.Tensor
|
||||
):
|
||||
waveform = item["waveform"]
|
||||
is_empty = "(empty) " if torch.count_nonzero(waveform) == 0 else ""
|
||||
text = textwrap.dedent(f"""
|
||||
Audio Waveform: {waveform.shape}{is_empty}
|
||||
Sample Rate: {item.get("sample_rate", "N/A")}
|
||||
""").strip()
|
||||
return [{"kind": "text", "data": text}]
|
||||
|
||||
log.debug(
|
||||
f"Processing generic dict with rich inspector: {item.keys()}"
|
||||
)
|
||||
return self._process_unknown(item, as_type=as_type, deep=deep)
|
||||
return f"{filename_prefix}_{counter:05}"
|
||||
|
||||
|
||||
__nodes__ = [MTB_Debug]
|
||||
processors = {
|
||||
torch.Tensor: process_tensor,
|
||||
list: process_list,
|
||||
dict: process_dict,
|
||||
bool: process_bool,
|
||||
}
|
||||
|
||||
__nodes__ = [MTB_Debug, MTB_SaveTensors]
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import folder_paths
|
||||
import torch
|
||||
|
||||
|
||||
class MTB_SaveTensors:
|
||||
"""Save torch tensors (image, mask or latent) to disk.
|
||||
|
||||
useful to debug things outside comfy.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "mtb/debug"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"filename_prefix": ("STRING", {"default": "ComfyPickle"}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"latent": ("LATENT",),
|
||||
},
|
||||
}
|
||||
|
||||
FUNCTION = "save"
|
||||
OUTPUT_NODE = True
|
||||
RETURN_TYPES = ()
|
||||
CATEGORY = "mtb/debug"
|
||||
|
||||
def save(
|
||||
self,
|
||||
filename_prefix,
|
||||
image: torch.Tensor | None = None,
|
||||
mask: torch.Tensor | None = None,
|
||||
latent: torch.Tensor | None = None,
|
||||
):
|
||||
(
|
||||
full_output_folder,
|
||||
filename,
|
||||
counter,
|
||||
subfolder,
|
||||
filename_prefix,
|
||||
) = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||
full_output_folder = Path(full_output_folder)
|
||||
if image is not None:
|
||||
image_file = f"{filename}_image_{counter:05}.pt"
|
||||
torch.save(image, full_output_folder / image_file)
|
||||
# np.save(full_output_folder/ image_file, image.cpu().numpy())
|
||||
|
||||
if mask is not None:
|
||||
mask_file = f"{filename}_mask_{counter:05}.pt"
|
||||
torch.save(mask, full_output_folder / mask_file)
|
||||
# np.save(full_output_folder/ mask_file, mask.cpu().numpy())
|
||||
|
||||
if latent is not None:
|
||||
# for latent we must use pickle
|
||||
latent_file = f"{filename}_latent_{counter:05}.pt"
|
||||
torch.save(latent, full_output_folder / latent_file)
|
||||
# pickle.dump(latent, open(full_output_folder/ latent_file, "wb"))
|
||||
|
||||
# np.save(full_output_folder / latent_file,
|
||||
# latent[""].cpu().numpy())
|
||||
|
||||
return f"{filename_prefix}_{counter:05}"
|
||||
|
||||
|
||||
__nodes__ = [MTB_SaveTensors]
|
||||
+5
-19
@@ -7,13 +7,6 @@ from PIL import Image, ImageDraw, ImageFont
|
||||
from ..log import log
|
||||
from ..utils import comfy_dir, font_path, pil2tensor
|
||||
|
||||
# try:
|
||||
# from cairosvg import svg2png
|
||||
# HAS_CAIRO = True
|
||||
# except ImportError:
|
||||
# HAS_CAIRO = False
|
||||
|
||||
|
||||
# class MtbExamples:
|
||||
# """MTB Example Images"""
|
||||
|
||||
@@ -306,7 +299,7 @@ by default it fallsback to a default font.
|
||||
|
||||
def text_to_image(
|
||||
self,
|
||||
text: str | list[str],
|
||||
text: str,
|
||||
font,
|
||||
wrap,
|
||||
trim,
|
||||
@@ -348,7 +341,7 @@ by default it fallsback to a default font.
|
||||
color = (255, 255, 255, 255)
|
||||
background = (0, 0, 0, 255)
|
||||
|
||||
def render_text(text_to_render: str, alpha=None) -> Image.Image:
|
||||
def render_text(text_to_render, alpha=None):
|
||||
if trim:
|
||||
text_to_render = text_to_render.strip()
|
||||
if wrap:
|
||||
@@ -433,16 +426,9 @@ by default it fallsback to a default font.
|
||||
frame_tensors = [pil2tensor(frame) for frame in frames]
|
||||
return (torch.cat(frame_tensors, dim=0),)
|
||||
else:
|
||||
results = []
|
||||
if not isinstance(text, list):
|
||||
text = [text]
|
||||
|
||||
for t in text:
|
||||
text_img = render_text(t)
|
||||
result = Image.alpha_composite(base_img, text_img)
|
||||
results.append(result)
|
||||
|
||||
return (pil2tensor(results),)
|
||||
text_img = render_text(text)
|
||||
result = Image.alpha_composite(base_img, text_img)
|
||||
return (pil2tensor(result),)
|
||||
|
||||
|
||||
__nodes__ = [
|
||||
|
||||
+8
-151
@@ -6,7 +6,7 @@ import urllib.request
|
||||
from math import pi
|
||||
from typing import Any
|
||||
|
||||
import comfy.model_management as mm
|
||||
import comfy.model_management as model_management
|
||||
import comfy.utils
|
||||
import numpy as np
|
||||
import torch
|
||||
@@ -16,10 +16,8 @@ from PIL import Image
|
||||
from ..log import log
|
||||
from ..utils import (
|
||||
EASINGS,
|
||||
LazyProxyTensor,
|
||||
apply_easing,
|
||||
get_server_info,
|
||||
get_torch_tensor_info,
|
||||
numpy_NFOV,
|
||||
pil2tensor,
|
||||
tensor2np,
|
||||
@@ -129,9 +127,6 @@ class MTB_ApplyTextTemplate:
|
||||
"required": {
|
||||
"template": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
"optional":{
|
||||
"strip": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
@@ -139,70 +134,12 @@ class MTB_ApplyTextTemplate:
|
||||
CATEGORY = "mtb/utils"
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, *, strip:bool,template: str, **kwargs) -> tuple[str | list[str]]:
|
||||
keys = list(kwargs.keys())
|
||||
values = list(kwargs.values())
|
||||
def execute(self, *, template: str, **kwargs):
|
||||
res = f"{template}"
|
||||
for k, v in kwargs.items():
|
||||
res = res.replace(f"{{{k}}}", f"{v}")
|
||||
|
||||
has_list = any(isinstance(v, list) for v in values)
|
||||
target_length = -1
|
||||
|
||||
if has_list:
|
||||
first_list = next(x for x in values if isinstance(x, list))
|
||||
|
||||
# all_list = all(isinstance(x, list) for x in kwargs.values())
|
||||
# if not all_list:
|
||||
# raise ValueError(
|
||||
# "Text template supports either str or list[str] but not a mix of the two (yet?)"
|
||||
# )
|
||||
target_length = len(first_list)
|
||||
same_length = all(
|
||||
len(v) == target_length for v in values if isinstance(v, list)
|
||||
)
|
||||
if not same_length:
|
||||
raise ValueError(
|
||||
"Text template received multiple list[str] but their size is varying, they should match..."
|
||||
)
|
||||
|
||||
if has_list:
|
||||
results = []
|
||||
|
||||
# do a padded loop, not the most efficient but easy
|
||||
# to handle for now
|
||||
for it in range(target_length):
|
||||
res = f"{template}"
|
||||
for k, v in kwargs.items():
|
||||
if isinstance(v, list):
|
||||
res = self.apply_res(res, k, v[it])
|
||||
else:
|
||||
res = self.apply_res(res, k, v)
|
||||
if strip:
|
||||
results.append(res.strip())
|
||||
else:
|
||||
results.append(res)
|
||||
|
||||
return (results,)
|
||||
|
||||
else:
|
||||
res = f"{template}"
|
||||
for k, v in kwargs.items():
|
||||
res = self.apply_res(res, k, v)
|
||||
|
||||
if strip:
|
||||
return (res.strip(),)
|
||||
else:
|
||||
return (res,)
|
||||
|
||||
def apply_res(self, res, key, value):
|
||||
if isinstance(value, float):
|
||||
value = f"{value:.3f}"
|
||||
elif isinstance(value, torch.Tensor):
|
||||
value = get_torch_tensor_info(value)
|
||||
else:
|
||||
log.debug(
|
||||
f"Falling back to default string conversion for {key} of type {type(value).__name__}"
|
||||
)
|
||||
|
||||
return res.replace(f"{{{key}}}", f"{value}")
|
||||
return (res,)
|
||||
|
||||
|
||||
class MTB_MatchDimensions:
|
||||
@@ -260,10 +197,7 @@ class MTB_MatchDimensions:
|
||||
|
||||
|
||||
class MTB_FloatToFloats:
|
||||
"""Conversion utility for compatibility with other extensions.
|
||||
|
||||
AD, IPA, Fitz, KJ are using FLOAT to represent list of floats.
|
||||
"""
|
||||
"""Conversion utility for compatibility with other extensions (AD, IPA, Fitz are using FLOAT to represent list of floats.)"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -281,26 +215,6 @@ class MTB_FloatToFloats:
|
||||
def convert(self, float: float):
|
||||
return (float,)
|
||||
|
||||
class MTB_FloatsToFloatList:
|
||||
"""Turn a FLOATS type into a list of floats (for loops)."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"floats": ("FLOATS", { "forceInput": True }),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT",)
|
||||
RETURN_NAMES = ("float",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
|
||||
CATEGORY = "mtb/utils"
|
||||
FUNCTION = "convert"
|
||||
|
||||
def convert(self, floats: list[float]):
|
||||
return (floats,)
|
||||
|
||||
class MTB_FloatsToInts:
|
||||
"""Conversion utility for compatibility with frame interpolation."""
|
||||
@@ -429,7 +343,7 @@ class MTB_AutoPanEquilateral:
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
mm.throw_exception_if_processing_interrupted()
|
||||
model_management.throw_exception_if_processing_interrupted()
|
||||
pbar.update(1)
|
||||
|
||||
return (pil2tensor(frames),)
|
||||
@@ -1002,61 +916,6 @@ class MTB_BooleanNot:
|
||||
return (not bool_in,)
|
||||
|
||||
|
||||
class MTB_ProxyTensor:
|
||||
"""Wraps an input tensor into a LazyProxyTensor.
|
||||
|
||||
builds upon an idea by @AustinMroz
|
||||
"""
|
||||
|
||||
NODE_NAME = "ProxyTensor"
|
||||
NODE_DISPLAY_NAME = "Proxy Tensor"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"tensor": ("IMAGE",),
|
||||
"target_dtype": (
|
||||
["float32", "float16", "bfloat16"],
|
||||
{"default": "float32"},
|
||||
),
|
||||
"target_device": (
|
||||
["keep", "cpu", "gpu"],
|
||||
{
|
||||
"default": "keep",
|
||||
"tooltip": "CAUTION: This isn't compatible with most nodes for now",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("proxy_tensor",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "mtb/utils"
|
||||
|
||||
def execute(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
target_dtype: str = "float32",
|
||||
target_device: str = "keep",
|
||||
):
|
||||
torch_dtype: torch.dtype = getattr(torch, target_dtype)
|
||||
|
||||
if target_device == "gpu":
|
||||
torch_device = mm.get_torch_device()
|
||||
elif target_device == "cpu":
|
||||
torch_device = torch.device("cpu")
|
||||
else:
|
||||
torch_device = tensor.device
|
||||
|
||||
proxy = LazyProxyTensor(tensor, torch_dtype, torch_device)
|
||||
|
||||
log.info(f"Created Proxy Tensor: \n{proxy}")
|
||||
|
||||
return (proxy,)
|
||||
|
||||
|
||||
__nodes__ = [
|
||||
MTB_StringReplace,
|
||||
MTB_FitNumber,
|
||||
@@ -1070,10 +929,8 @@ __nodes__ = [
|
||||
MTB_AutoPanEquilateral,
|
||||
MTB_FloatsToFloat,
|
||||
MTB_FloatToFloats,
|
||||
MTB_FloatsToFloatList,
|
||||
MTB_FloatsToInts,
|
||||
MTB_TensorOps,
|
||||
MTB_BooleanNot,
|
||||
MTB_GetItem,
|
||||
MTB_ProxyTensor,
|
||||
]
|
||||
|
||||
@@ -683,6 +683,7 @@ class MTB_ImageCompare:
|
||||
|
||||
|
||||
import requests
|
||||
import time
|
||||
|
||||
|
||||
class MTB_LoadImageFromUrl:
|
||||
@@ -698,6 +699,14 @@ class MTB_LoadImageFromUrl:
|
||||
"default": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Example.jpg/800px-Example.jpg"
|
||||
},
|
||||
),
|
||||
"retry_count": (
|
||||
"INT",
|
||||
{"default": 3, "min": 1, "max": 20, "step": 1},
|
||||
),
|
||||
"retry_interval": (
|
||||
"FLOAT",
|
||||
{"default": 1.0, "min": 0.0, "max": 60.0, "step": 0.1},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,11 +714,27 @@ class MTB_LoadImageFromUrl:
|
||||
FUNCTION = "load"
|
||||
CATEGORY = "mtb/IO"
|
||||
|
||||
def load(self, url):
|
||||
# get the image from the url
|
||||
image = Image.open(requests.get(url, stream=True).raw)
|
||||
image = ImageOps.exif_transpose(image)
|
||||
return (pil2tensor(image),)
|
||||
def load(self, url, retry_count, retry_interval):
|
||||
# get the image from the url with retry + exponential backoff
|
||||
last_error = None
|
||||
for attempt in range(retry_count):
|
||||
try:
|
||||
response = requests.get(url, stream=True)
|
||||
response.raise_for_status()
|
||||
image = Image.open(response.raw)
|
||||
image = ImageOps.exif_transpose(image)
|
||||
return (pil2tensor(image),)
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt == retry_count - 1:
|
||||
raise
|
||||
wait_seconds = retry_interval * (2**attempt)
|
||||
if wait_seconds > 0:
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
if last_error is not None:
|
||||
raise last_error
|
||||
raise RuntimeError("Failed to load image from URL without captured exception")
|
||||
|
||||
|
||||
class MTB_Blur:
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import node_helpers
|
||||
import torch
|
||||
|
||||
from ..log import log
|
||||
|
||||
|
||||
class MTB_LatentLerp:
|
||||
"""Linear interpolation (blend) between two latent vectors"""
|
||||
@@ -23,9 +20,6 @@ class MTB_LatentLerp:
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
FUNCTION = "lerp_latent"
|
||||
|
||||
# should fix or remove
|
||||
DEPRECATED = True
|
||||
|
||||
CATEGORY = "mtb/latent"
|
||||
|
||||
def lerp_latent(self, A, B, t):
|
||||
@@ -37,52 +31,6 @@ class MTB_LatentLerp:
|
||||
return (a,)
|
||||
|
||||
|
||||
class MTB_ReferenceLatents:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"positive": ("CONDITIONING",),
|
||||
"negative": ("CONDITIONING",),
|
||||
"vae": ("VAE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CONDITIONING", "CONDITIONING")
|
||||
RETURN_NAMES = ("positive", "negative")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "mtb/latent"
|
||||
|
||||
def execute(self, positive, negative, vae, **kwargs):
|
||||
if not kwargs:
|
||||
raise ValueError("At least one image must be provided.")
|
||||
|
||||
image_refs = list(kwargs.values())
|
||||
# device = image_refs[0].device
|
||||
|
||||
for im in image_refs:
|
||||
# encode
|
||||
log.debug("Encoding reference image to latents")
|
||||
log.debug(f"Image shape: {im.shape}")
|
||||
latent = vae.encode(im)
|
||||
|
||||
if positive is not None:
|
||||
positive = node_helpers.conditioning_set_values(
|
||||
positive,
|
||||
{"reference_latents": [latent]},
|
||||
append=True,
|
||||
)
|
||||
if negative is not None:
|
||||
negative = node_helpers.conditioning_set_values(
|
||||
negative,
|
||||
{"reference_latents": [latent]},
|
||||
append=True,
|
||||
)
|
||||
|
||||
return (positive, negative)
|
||||
|
||||
|
||||
__nodes__ = [
|
||||
MTB_LatentLerp,
|
||||
MTB_ReferenceLatents,
|
||||
]
|
||||
|
||||
+4
-16
@@ -1,14 +1,6 @@
|
||||
from ..utils import hex_to_rgb
|
||||
|
||||
|
||||
# from ..utils import hex_to_rgb
|
||||
class MTB_ColorInput:
|
||||
RETURN_TYPES = ("COLOR","STRING","STRING")
|
||||
RETURN_NAMES = ("color","hex","r,g,b")
|
||||
OUTPUT_TOOLTIPS = (
|
||||
"Color in mtb format (internaly just a hex string)",
|
||||
"Hex color string",
|
||||
"RGB values as comma-separated string",
|
||||
)
|
||||
RETURN_TYPES = ("COLOR",)
|
||||
FUNCTION = "color"
|
||||
CATEGORY = "mtb/color"
|
||||
|
||||
@@ -18,12 +10,8 @@ class MTB_ColorInput:
|
||||
"required": {"color": ("MTB_COLOR", {"default": "#ffffff"})},
|
||||
}
|
||||
|
||||
def color(self, color:str):
|
||||
# convert hex to rgb
|
||||
r, g, b = hex_to_rgb(color)
|
||||
|
||||
# TODO: official COLOR will be without the #
|
||||
return (color,color,f"{r},{g},{b}")
|
||||
def color(self, color):
|
||||
return (color,)
|
||||
|
||||
|
||||
__nodes__ = [MTB_ColorInput]
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import comfy.sd
|
||||
import comfy.utils
|
||||
import folder_paths
|
||||
|
||||
|
||||
class MTB_LoraLoaderModelOnlyByPath:
|
||||
def __init__(self):
|
||||
self.loaded_lora = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
all_loras = folder_paths.get_filename_list("loras")
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"lora_name": (
|
||||
"STRING",
|
||||
{"default": next(iter(all_loras), "no lora found")},
|
||||
),
|
||||
"strength_model": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": -100.0,
|
||||
"max": 100.0,
|
||||
"step": 0.01,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
|
||||
OUTPUT_TOOLTIPS = ("The modified diffusion model.",)
|
||||
CATEGORY = "mtb/lora"
|
||||
FUNCTION = "load_lora_model_only"
|
||||
DESCRIPTION = "Exact copy of the native node using string instead of combo, useful to 'build' paths from the graph."
|
||||
|
||||
def load_lora_model_only(self, model, lora_name, strength_model):
|
||||
return (self.load_lora(model, None, lora_name, strength_model, 0)[0],)
|
||||
|
||||
def load_lora(self, model, clip, lora_name, strength_model, strength_clip):
|
||||
if strength_model == 0 and strength_clip == 0:
|
||||
return (model, clip)
|
||||
|
||||
# all_loras = folder_paths.get_filename_list("loras")
|
||||
# if lora_name not in all_loras:
|
||||
# raise ValueError(f"{lora_name} not in {folder_paths.get_filename_list('loras')}")
|
||||
|
||||
lora_path = folder_paths.get_full_path_or_raise("loras", lora_name)
|
||||
lora = None
|
||||
if self.loaded_lora is not None:
|
||||
if self.loaded_lora[0] == lora_path:
|
||||
lora = self.loaded_lora[1]
|
||||
else:
|
||||
self.loaded_lora = None
|
||||
|
||||
if lora is None:
|
||||
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
|
||||
self.loaded_lora = (lora_path, lora)
|
||||
|
||||
model_lora, clip_lora = comfy.sd.load_lora_for_models(
|
||||
model, clip, lora, strength_model, strength_clip
|
||||
)
|
||||
return (model_lora, clip_lora)
|
||||
|
||||
|
||||
__nodes__ = [MTB_LoraLoaderModelOnlyByPath]
|
||||
+1
-1
@@ -145,7 +145,7 @@ class MTB_ModelPatchSeamless:
|
||||
tilingX,
|
||||
tilingY,
|
||||
):
|
||||
hacked_model = copy.deepcopy(model)
|
||||
hacked_model = model.clone()
|
||||
self.apply_circular(
|
||||
hacked_model.model, startStep, stopStep, tilingX, tilingY
|
||||
)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
class MTB_PromptPresets:
|
||||
RETURN_TYPES = ("STRING",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "enprompt"
|
||||
CATEGORY = "mtb/prompt"
|
||||
EXPERIMENTAL = True
|
||||
|
||||
_all_prompts = {}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
from ..utils import comfy_dir
|
||||
import json
|
||||
|
||||
cls._all_prompts = {}
|
||||
prompt_dir = comfy_dir / "prompts"
|
||||
if prompt_dir.exists():
|
||||
json_files = prompt_dir.glob("*.json")
|
||||
# merge them
|
||||
for json_file in json_files:
|
||||
try:
|
||||
with open(json_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
if isinstance(item, dict) and 'name' in item and 'prompts' in item:
|
||||
name = item['name']
|
||||
prompts = item['prompts']
|
||||
if isinstance(prompts, list):
|
||||
cls._all_prompts[name] = prompts
|
||||
except Exception as e:
|
||||
print(f"Error loading prompts from {json_file}: {e}")
|
||||
return {
|
||||
"required": {
|
||||
"name": (list(cls._all_prompts.keys()) if cls._all_prompts else ["No prompts found"],),
|
||||
"size": ("INT", {"default": -1, "min": -1, "max": 100, "step": 1}),
|
||||
"random": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
}
|
||||
|
||||
def enprompt(self, name, size, random, seed=None):
|
||||
import random as rand
|
||||
|
||||
if name not in self._all_prompts:
|
||||
return ([],)
|
||||
|
||||
prompts = self._all_prompts[name].copy()
|
||||
|
||||
if random:
|
||||
if seed is not None or seed != -1:
|
||||
rand.seed(seed)
|
||||
else:
|
||||
import time
|
||||
rand.seed(int(time.time() * 1000000) % 0xffffffffffffffff)
|
||||
rand.shuffle(prompts)
|
||||
|
||||
# Return all prompts
|
||||
if size == -1:
|
||||
return (prompts,)
|
||||
else:
|
||||
clamped_size = min(size, len(prompts))
|
||||
return (prompts[:clamped_size],)
|
||||
|
||||
|
||||
__nodes__ = [MTB_PromptPresets]
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
import textwrap
|
||||
|
||||
from comfy.comfy_types import IO
|
||||
|
||||
from ..repl import SOCKET_COUNT, ComfyREPLBackend
|
||||
from ..utils import log
|
||||
|
||||
|
||||
class MTB_Repl:
|
||||
"""Write python code from within a comfy graph."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"uuid": ("STRING",),
|
||||
"code": ("CODE_EDITOR", {"lang": "python"}),
|
||||
},
|
||||
"optional": {
|
||||
f"input_{i:02d}": (IO.ANY,) for i in range(1, SOCKET_COUNT + 1)
|
||||
}
|
||||
| {
|
||||
"reset": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"label_on": "reset state at each run",
|
||||
"label_off": "keep mutated state",
|
||||
},
|
||||
),
|
||||
"propagate": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"label_on": "allow",
|
||||
"label_off": "block forward",
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
EXPERIMENTAL = True
|
||||
RETURN_TYPES = tuple(IO.ANY for _ in range(SOCKET_COUNT))
|
||||
RETURN_NAMES = tuple(f"output_{i:02d}" for i in range(1, SOCKET_COUNT + 1))
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "mtb/repl"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
# @classmethod
|
||||
# def IS_CHANGED(cls, code: str, **kwargs):
|
||||
# code_hash = hashlib.sha256(code.encode("utf-8")).hexdigest()
|
||||
# return code_hash
|
||||
|
||||
# TODO: hide uuid from the frontend
|
||||
# same for propagate, it should be managed from our run button in js
|
||||
def run(
|
||||
self, *, uuid: str, code: str, reset=False, propagate=True, **kwargs
|
||||
):
|
||||
log.debug(f"Received code from frontend:\n{code}")
|
||||
log.debug(f"Kwargs: {kwargs}")
|
||||
|
||||
repl_backend = ComfyREPLBackend()
|
||||
|
||||
console = repl_backend.get_console(uuid, reset=reset)
|
||||
|
||||
if not console:
|
||||
# TODO: handle this case automatically
|
||||
raise RuntimeError(
|
||||
textwrap.dedent(f"""
|
||||
No matching console found for {uuid}
|
||||
active console: {ComfyREPLBackend.active_consoles}
|
||||
""")
|
||||
)
|
||||
|
||||
inputs = list(kwargs.values())
|
||||
|
||||
console.locals["IS_LIVE"] = False
|
||||
|
||||
ui = repl_backend.execute_code(
|
||||
code,
|
||||
console=console,
|
||||
inputs=inputs,
|
||||
# , reset=True
|
||||
)
|
||||
if ui is None:
|
||||
ui = {"output_html": [""], "error": [""]}
|
||||
else:
|
||||
ui = {"output_html": [ui["output_html"]], "error": [ui["error"]]}
|
||||
|
||||
# process outputs
|
||||
outputs = repl_backend.get_outputs(console=console)
|
||||
|
||||
log.debug(ui)
|
||||
# return outputs
|
||||
if propagate:
|
||||
results = tuple(outputs)
|
||||
else:
|
||||
from comfy_execution.graph import ExecutionBlocker
|
||||
|
||||
results = [ExecutionBlocker(None)] * SOCKET_COUNT
|
||||
|
||||
return {"ui": ui, "result": results}
|
||||
|
||||
|
||||
__nodes__ = [MTB_Repl]
|
||||
-267
@@ -1,267 +0,0 @@
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
def lerp(v0, v1, t):
|
||||
return v0 * (1.0 - t) + v1 * t
|
||||
|
||||
|
||||
def interpolate_box(box1, box2, t):
|
||||
"""Linearly interpolate between two boxes (tuples of 4 ints)."""
|
||||
if box1 is None or box2 is None:
|
||||
return None
|
||||
x1 = lerp(box1[0], box2[0], t)
|
||||
y1 = lerp(box1[1], box2[1], t)
|
||||
x2 = lerp(box1[2], box2[2], t)
|
||||
y2 = lerp(box1[3], box2[3], t)
|
||||
return (int(x1), int(y1), int(x2), int(y2))
|
||||
|
||||
|
||||
class MTB_FaceMeshBatchToSEGSAndFill:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
bool_true_widget = (
|
||||
"BOOLEAN",
|
||||
{"default": True, "label_on": "Enabled", "label_off": "Disabled"},
|
||||
)
|
||||
bool_false_widget = (
|
||||
"BOOLEAN",
|
||||
{"default": False, "label_on": "Enabled", "label_off": "Disabled"},
|
||||
)
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"interpolation": (
|
||||
["linear", "hold_last"],
|
||||
{"default": "linear"},
|
||||
),
|
||||
"fill_shape": (
|
||||
["interpolate_contour", "bbox"],
|
||||
{"default": "interpolate_contour"},
|
||||
),
|
||||
"crop_factor": (
|
||||
"FLOAT",
|
||||
{"default": 3.0, "min": 1.0, "max": 100, "step": 0.1},
|
||||
),
|
||||
"bbox_fill": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": False,
|
||||
"label_on": "enabled",
|
||||
"label_off": "disabled",
|
||||
},
|
||||
),
|
||||
"drop_size": (
|
||||
"INT",
|
||||
{"min": 1, "max": 8192, "step": 1, "default": 1},
|
||||
),
|
||||
"dilation": (
|
||||
"INT",
|
||||
{"default": 0, "min": -512, "max": 512, "step": 1},
|
||||
),
|
||||
"face": bool_true_widget,
|
||||
"mouth": bool_false_widget,
|
||||
"left_eyebrow": bool_false_widget,
|
||||
"left_eye": bool_false_widget,
|
||||
"left_pupil": bool_false_widget,
|
||||
"right_eyebrow": bool_false_widget,
|
||||
"right_eye": bool_false_widget,
|
||||
"right_pupil": bool_false_widget,
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SEGS",)
|
||||
FUNCTION = "generate_and_fill"
|
||||
CATEGORY = "ImpactPack/Operation"
|
||||
|
||||
def generate_and_fill(
|
||||
self,
|
||||
image,
|
||||
interpolation,
|
||||
fill_shape,
|
||||
crop_factor,
|
||||
bbox_fill,
|
||||
drop_size,
|
||||
dilation,
|
||||
face,
|
||||
mouth,
|
||||
left_eyebrow,
|
||||
left_eye,
|
||||
left_pupil,
|
||||
right_eyebrow,
|
||||
right_eye,
|
||||
right_pupil,
|
||||
):
|
||||
import cv2
|
||||
import impact.core as core
|
||||
|
||||
total_frames = image.shape[0]
|
||||
logging.info(
|
||||
f"[FaceMesh Batch & Fill] Starting. Processing {total_frames} frames. Mode: {interpolation}/{fill_shape}."
|
||||
)
|
||||
|
||||
all_labels_found = set()
|
||||
structured_segs = {}
|
||||
for i in range(total_frames):
|
||||
single_frame_batch = image[i : i + 1]
|
||||
_, segs_for_this_frame = core.mediapipe_facemesh_to_segs(
|
||||
single_frame_batch,
|
||||
crop_factor,
|
||||
bbox_fill,
|
||||
50,
|
||||
drop_size,
|
||||
dilation,
|
||||
face,
|
||||
mouth,
|
||||
left_eyebrow,
|
||||
left_eye,
|
||||
left_pupil,
|
||||
right_eyebrow,
|
||||
right_eye,
|
||||
right_pupil,
|
||||
)
|
||||
for seg in segs_for_this_frame:
|
||||
if seg.label not in all_labels_found:
|
||||
all_labels_found.add(seg.label)
|
||||
structured_segs[seg.label] = [None] * total_frames
|
||||
structured_segs[seg.label][i] = seg
|
||||
|
||||
for label, timeline in structured_segs.items():
|
||||
if interpolation == "linear":
|
||||
last_valid_idx = -1
|
||||
for i in range(total_frames):
|
||||
if timeline[i] is not None:
|
||||
if i > last_valid_idx + 1 and last_valid_idx != -1:
|
||||
start_seg, end_seg = (
|
||||
timeline[last_valid_idx],
|
||||
timeline[i],
|
||||
)
|
||||
gap_size = i - last_valid_idx
|
||||
for j in range(1, gap_size):
|
||||
t = j / float(gap_size)
|
||||
new_bbox = interpolate_box(
|
||||
start_seg.bbox, end_seg.bbox, t
|
||||
)
|
||||
new_crop = interpolate_box(
|
||||
start_seg.crop_region,
|
||||
end_seg.crop_region,
|
||||
t,
|
||||
)
|
||||
|
||||
if new_crop is None:
|
||||
continue
|
||||
crop_h, crop_w = (
|
||||
new_crop[3] - new_crop[1],
|
||||
new_crop[2] - new_crop[0],
|
||||
)
|
||||
if crop_h <= 0 or crop_w <= 0:
|
||||
continue
|
||||
|
||||
new_mask = None
|
||||
if fill_shape == "interpolate_contour":
|
||||
start_mask = start_seg.cropped_mask
|
||||
end_mask = end_seg.cropped_mask
|
||||
|
||||
start_resized = cv2.resize(
|
||||
start_mask,
|
||||
(crop_w, crop_h),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
end_resized = cv2.resize(
|
||||
end_mask,
|
||||
(crop_w, crop_h),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
new_mask = lerp(
|
||||
start_resized, end_resized, t
|
||||
)
|
||||
else:
|
||||
new_mask = np.ones(
|
||||
(crop_h, crop_w), dtype=np.float32
|
||||
)
|
||||
|
||||
timeline[last_valid_idx + j] = core.SEG(
|
||||
None,
|
||||
new_mask,
|
||||
1.0,
|
||||
new_crop,
|
||||
new_bbox,
|
||||
label,
|
||||
None,
|
||||
)
|
||||
last_valid_idx = i
|
||||
|
||||
last_valid_seg = None
|
||||
for i in range(total_frames):
|
||||
if timeline[i] is not None:
|
||||
last_valid_seg = timeline[i]
|
||||
elif last_valid_seg is not None:
|
||||
timeline[i] = last_valid_seg
|
||||
|
||||
if last_valid_seg is not None:
|
||||
for i in range(total_frames - 1, -1, -1):
|
||||
if timeline[i] is not None:
|
||||
last_valid_seg = timeline[i]
|
||||
elif last_valid_seg is not None:
|
||||
timeline[i] = last_valid_seg
|
||||
|
||||
final_segs_by_frame = [[] for _ in range(total_frames)]
|
||||
for label in sorted(list(all_labels_found)):
|
||||
timeline = structured_segs[label]
|
||||
for i in range(total_frames):
|
||||
if timeline[i] is not None:
|
||||
final_segs_by_frame[i].append(timeline[i])
|
||||
|
||||
original_dims = (image.shape[1], image.shape[2])
|
||||
final_segs_object = (original_dims, final_segs_by_frame)
|
||||
logging.info(
|
||||
f"[FaceMesh Batch & Fill] Completed. Processed {len(final_segs_by_frame)} frames."
|
||||
)
|
||||
return (final_segs_object,)
|
||||
|
||||
|
||||
class MTB_SegsToCombinedMaskBatch:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"segs": ("SEGS",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "ImpactPack/Operation"
|
||||
|
||||
def doit(self, segs):
|
||||
import impact.core as core
|
||||
import impact.utils as utils
|
||||
|
||||
outputs = []
|
||||
if isinstance(segs[1], list) and len(segs[1]):
|
||||
if isinstance(segs[1][0], list):
|
||||
for seg in segs[1]:
|
||||
mask = core.segs_to_combined_mask((segs[0], seg))
|
||||
mask = utils.make_3d_mask(mask)
|
||||
outputs.append(mask)
|
||||
|
||||
return (
|
||||
torch.stack(outputs, dim=0)
|
||||
.permute(0, 2, 3, 1)
|
||||
.squeeze(-1),
|
||||
)
|
||||
|
||||
else:
|
||||
mask = core.segs_to_combined_mask(segs)
|
||||
mask = utils.make_3d_mask(mask)
|
||||
|
||||
return (mask,)
|
||||
return (torch.zeros((0, 10, 10, 1)),)
|
||||
|
||||
|
||||
__nodes__ = [MTB_FaceMeshBatchToSEGSAndFill, MTB_SegsToCombinedMaskBatch]
|
||||
+1
-1
@@ -27,7 +27,7 @@ class MTB_LoadVitMatteModel:
|
||||
def execute(self, *, kind: str, autodownload: bool):
|
||||
dest = models_dir / "vitmatte"
|
||||
dest.mkdir(exist_ok=True)
|
||||
name = "dist" if kind == "Distinctions-646" else "com"
|
||||
name = "dis" if kind == "Distinctions-646" else "com"
|
||||
|
||||
file = hf_hub_download(
|
||||
repo_id="melmass/pytorch-scripts",
|
||||
|
||||
+40
-10
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "comfy-mtb"
|
||||
version = "0.6.0"
|
||||
version = "0.5.4"
|
||||
description = "Animation oriented nodes pack for ComfyUI."
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
@@ -62,6 +62,39 @@ PublisherId = "mel"
|
||||
DisplayName = "comfy-mtb"
|
||||
Icon = "https://avatars.githubusercontent.com/u/7041726?v=4"
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "0.5.1"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
search = "{current_version}"
|
||||
replace = "{new_version}"
|
||||
regex = false
|
||||
ignore_missing_version = false
|
||||
ignore_missing_files = false
|
||||
tag = true
|
||||
sign_tags = true
|
||||
tag_name = "v{new_version}"
|
||||
tag_message = "⬆️ Bump version: {current_version} → {new_version}"
|
||||
allow_dirty = true
|
||||
commit = true
|
||||
message = "⬆️ Bump version: {current_version} → {new_version}"
|
||||
commit_args = ""
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "__init__.py"
|
||||
search = "__version__ = \"{current_version}\""
|
||||
replace = "__version__ = \"{new_version}\""
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "pyproject.toml"
|
||||
search = "version = \"{current_version}\""
|
||||
replace = "version = \"{new_version}\""
|
||||
|
||||
# [[tool.bumpversion.files]]
|
||||
# filename = "your_package/__init__.py"
|
||||
# search = "__version__ = '{current_version}'"
|
||||
# replace = "__version__ = '{new_version}'"
|
||||
|
||||
# INFO: All those remaining keys are meant for local dev
|
||||
[tool.pyright]
|
||||
include = ["."]
|
||||
@@ -70,27 +103,26 @@ exclude = [
|
||||
"**/__pycache__",
|
||||
"src/experimental",
|
||||
"src/typestubs",
|
||||
"extern/",
|
||||
]
|
||||
ignore = ["src/oldstuff"]
|
||||
defineConstant = { DEBUG = true }
|
||||
extraPaths = ["python", "../..", "../ComfyUI-Impact-Pack/modules"]
|
||||
extraPaths = ["python", "../.."]
|
||||
stubPath = "src/stubs"
|
||||
|
||||
reportMissingImports = true
|
||||
reportMissingTypeStubs = false
|
||||
reportExplicitAny = false
|
||||
typeCheckingMode = "basic"
|
||||
pythonVersion = "3.11"
|
||||
pythonPlatform = "Windows"
|
||||
|
||||
pythonVersion = "3.10"
|
||||
pythonPlatform = "Windows"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_level = "DEBUG"
|
||||
log_cli = true
|
||||
markers = [
|
||||
"wip: tests that aren't fully finished yet",
|
||||
'''heavy: marks tests as heavy (deselect with '-m "not heavy"')''',
|
||||
"heavy: marks tests as heavy (deselect with '-m \"not heavy\"')",
|
||||
|
||||
]
|
||||
filterwarnings = ["ignore::UserWarning", 'ignore::DeprecationWarning']
|
||||
|
||||
@@ -126,10 +158,8 @@ select = ["A", "B", "C", "D", "E", "F", "FBT", "I", "N", "S", "SIM", "UP", "W"]
|
||||
# D102 - undocumented-public-method (noisy)
|
||||
# D103 - undocumented-public-function (noisy)
|
||||
# D100 - undocumented-public-module (noisy)
|
||||
# D101 - undocumented-public-class (kept for DESCRIPTION definitions)
|
||||
# N801 - classname should use CapWord (forced by mtb registration (for now))
|
||||
# N802 - invalid-function-name (forced by comfy's arch)
|
||||
ignore = ["D103", "D102", "D100", "N801", "N802"]
|
||||
ignore = ["D103", "D102", "D100", "N802"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
# imported but unused
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
"**/__pycache__",
|
||||
],
|
||||
"ignore": [
|
||||
"extern"
|
||||
],
|
||||
"defineConstant": {
|
||||
"DEBUG": true
|
||||
},
|
||||
"venvPath": "../../../.venv/",
|
||||
"reportMissingImports": true,
|
||||
"reportMissingTypeStubs": false,
|
||||
"pythonVersion": "3.10",
|
||||
"pythonPlatform": "All",
|
||||
"reportOptionalMemberAccess": "none"
|
||||
}
|
||||
@@ -8,14 +8,11 @@ import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import uuid
|
||||
import warnings
|
||||
from collections.abc import Callable, Sequence
|
||||
from enum import Enum
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from types import EllipsisType
|
||||
from typing import TypeVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -53,35 +50,6 @@ def make_report():
|
||||
# endregion
|
||||
|
||||
|
||||
# region decorators
|
||||
class classproperty:
|
||||
def __init__(self, method=None):
|
||||
self.fget = method
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
if self.fget:
|
||||
return self.fget(cls)
|
||||
|
||||
def getter(self, method):
|
||||
self.fget = method
|
||||
return self
|
||||
|
||||
|
||||
def singleton(cls):
|
||||
"""Turn a class into a singleton."""
|
||||
instances = {}
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kwargs)
|
||||
return instances[cls]
|
||||
|
||||
return get_instance
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region NFOV
|
||||
class numpy_NFOV:
|
||||
def __init__(self, fov=None, height: int = 400, width: int = 800):
|
||||
@@ -493,6 +461,8 @@ def _run_command(shell_cmd, ignored_lines_start):
|
||||
print("Command executed successfully!")
|
||||
|
||||
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
@@ -556,195 +526,6 @@ PIL_FILTER_MAP = {
|
||||
|
||||
|
||||
# region TENSOR Utilities
|
||||
|
||||
|
||||
class LazyProxyTensor:
|
||||
"""Memory-efficient proxy that wrap a tensor but presents itself as a different dtype (e.g., float32).
|
||||
|
||||
It mimics a torch.Tensor's read-only attributes and methods. Data conversion
|
||||
and normalization happen lazily on access (e.g., via slicing), avoiding
|
||||
the high memory cost of a full conversion.
|
||||
|
||||
Supported source dtypes:
|
||||
- torch.uint8 (normalized from [0, 255])
|
||||
- torch.uint16 (normalized from [0, 65535])
|
||||
- All float types (passed through, assumed to be in [0, 1] range)"
|
||||
"""
|
||||
|
||||
_source_tensor: torch.Tensor
|
||||
_target_dtype: torch.dtype
|
||||
_target_element_size: int
|
||||
_scale_divisor: float
|
||||
_warned_inefficient_access: bool
|
||||
|
||||
def __init__(
|
||||
self, source_tensor, target_dtype=torch.float32, target_device=None
|
||||
):
|
||||
if not isinstance(source_tensor, torch.Tensor):
|
||||
raise ValueError("Input must be a torch.Tensor.")
|
||||
|
||||
self._source_tensor = source_tensor
|
||||
self._target_dtype = target_dtype
|
||||
self._target_device = (
|
||||
target_device
|
||||
if target_device is not None
|
||||
else source_tensor.device
|
||||
)
|
||||
|
||||
# Determine the normalization divisor based on source dtype
|
||||
# fmt: off
|
||||
if source_tensor.dtype == torch.uint8: self._scale_divisor = 255.0
|
||||
elif source_tensor.dtype == torch.uint16: self._scale_divisor = 65535.0
|
||||
elif torch.is_floating_point(source_tensor): self._scale_divisor = 1.0
|
||||
else: raise ValueError(f"Unsupported source dtype for LazyProxyTensor: {source_tensor.dtype}")
|
||||
# fmt: on
|
||||
|
||||
self._target_element_size = torch.empty(
|
||||
(), dtype=self._target_dtype
|
||||
).element_size()
|
||||
self._warned_inefficient_access = False
|
||||
|
||||
def is_contiguous(self, *args, **kwargs):
|
||||
return self._source_tensor.is_contiguous(*args, **kwargs)
|
||||
|
||||
def stride(self, *args, **kwargs):
|
||||
return self._source_tensor.stride(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._source_tensor.shape
|
||||
|
||||
@property
|
||||
def requires_grad(self):
|
||||
return False
|
||||
|
||||
def nelement(self):
|
||||
"""Return the total number of elements in the (pretend) tensor."""
|
||||
return self._source_tensor.nelement()
|
||||
|
||||
def element_size(self):
|
||||
"""Return the size in bytes of an individual (pretend) float element."""
|
||||
return self._target_element_size
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return self._target_dtype
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self._target_device
|
||||
|
||||
def __len__(self):
|
||||
return self._source_tensor.shape[0]
|
||||
|
||||
def __getitem__(self, key):
|
||||
if (
|
||||
self._source_tensor.device != self._target_device
|
||||
and not self._warned_inefficient_access
|
||||
):
|
||||
warnings.warn(
|
||||
"Inefficient access pattern detected for LazyProxyTensor. "
|
||||
"You are slicing a device-proxied tensor, which causes slow, "
|
||||
"repeated data transfers. For performance, use the .iter_chunks() method."
|
||||
)
|
||||
self._warned_inefficient_access = True
|
||||
|
||||
subset = self._source_tensor[key]
|
||||
|
||||
return (
|
||||
subset.to(self._target_device).to(self._target_dtype)
|
||||
/ self._scale_divisor
|
||||
)
|
||||
|
||||
# def __iter__(self):
|
||||
# for i in range(len(self)):
|
||||
# yield self[i]
|
||||
|
||||
def iter_chunks(self, chunk_size=16):
|
||||
for i in range(0, len(self), chunk_size):
|
||||
chunk = self._source_tensor[i : i + chunk_size]
|
||||
yield (
|
||||
chunk.to(self._target_device, non_blocking=True).to(
|
||||
self._target_dtype
|
||||
)
|
||||
/ self._scale_divisor
|
||||
)
|
||||
|
||||
def squeeze(self, dim: str | EllipsisType | None = None):
|
||||
squeezed = self._source_tensor.squeeze(dim)
|
||||
return LazyProxyTensor(squeezed, self._target_dtype)
|
||||
|
||||
def unsqueeze(self, dim: int = 0):
|
||||
unsqueezed = self._source_tensor.unsqueeze(dim)
|
||||
return LazyProxyTensor(unsqueezed, self._target_dtype)
|
||||
|
||||
def repeat(self, *sizes):
|
||||
repeated = self._source_tensor.repeat(*sizes)
|
||||
return LazyProxyTensor(repeated, self._target_dtype)
|
||||
|
||||
def _format_mem_size(self, mem_bytes):
|
||||
if mem_bytes > 1e9:
|
||||
return f"{mem_bytes / 1e9:.2f} GB"
|
||||
if mem_bytes > 1e6:
|
||||
return f"{mem_bytes / 1e6:.2f} MB"
|
||||
if mem_bytes > 1e3:
|
||||
return f"{mem_bytes / 1e3:.2f} KB"
|
||||
return f"{mem_bytes} B"
|
||||
|
||||
def __repr__(self):
|
||||
actual_info = get_torch_tensor_info(self._source_tensor, name="Source")
|
||||
target_info = get_torch_tensor_info(self, name="Target")
|
||||
|
||||
info = f"""
|
||||
{target_info}
|
||||
|
||||
{actual_info}
|
||||
"""
|
||||
return textwrap.dedent(info).strip()
|
||||
|
||||
|
||||
def get_torch_tensor_info(
|
||||
tensor: torch.Tensor | LazyProxyTensor | np.ndarray,
|
||||
*,
|
||||
name: str | None = None,
|
||||
):
|
||||
mem_str = "N/A"
|
||||
|
||||
is_tensor = isinstance(tensor, torch.Tensor | LazyProxyTensor)
|
||||
if is_tensor:
|
||||
mem_bytes = tensor.element_size() * tensor.nelement()
|
||||
else:
|
||||
mem_bytes = tensor.itemsize * tensor.size
|
||||
|
||||
if mem_bytes > 1e9:
|
||||
mem_str = f"{mem_bytes / 1e9:.2f} GB"
|
||||
elif mem_bytes > 1e6:
|
||||
mem_str = f"{mem_bytes / 1e6:.2f} MB"
|
||||
elif mem_bytes > 1e3:
|
||||
mem_str = f"{mem_bytes / 1e3:.2f} KB"
|
||||
else:
|
||||
mem_str = f"{mem_bytes} B"
|
||||
|
||||
device = "N/A"
|
||||
grad = "False"
|
||||
type_name = name or "Tensor" if is_tensor else "Numpy Array"
|
||||
|
||||
if is_tensor:
|
||||
device = tensor.device
|
||||
grad = str(tensor.requires_grad)
|
||||
|
||||
text = f"""
|
||||
{type_name}
|
||||
shape: {tensor.shape}
|
||||
dtype: {str(tensor.dtype).replace("torch.", "")}
|
||||
device: {device}
|
||||
requires grad: {grad}
|
||||
memory: {mem_str}
|
||||
"""
|
||||
|
||||
return textwrap.dedent(text).strip()
|
||||
|
||||
|
||||
def to_numpy(image: torch.Tensor) -> npt.NDArray[np.uint8]:
|
||||
"""Converts a tensor to a ndarray with proper scaling and type conversion."""
|
||||
np_array = np.clip(255.0 * image.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
@@ -755,12 +536,12 @@ def handle_batch(
|
||||
tensor: torch.Tensor,
|
||||
func: Callable[[torch.Tensor], Image.Image | npt.NDArray[np.uint8]],
|
||||
) -> list[Image.Image] | list[npt.NDArray[np.uint8]]:
|
||||
"""Handle batch processing for a given tensor and conversion function."""
|
||||
"""Handles batch processing for a given tensor and conversion function."""
|
||||
return [func(tensor[i]) for i in range(tensor.shape[0])]
|
||||
|
||||
|
||||
def tensor2pil(tensor: torch.Tensor) -> list[Image.Image]:
|
||||
"""Convert a batch of tensors to a list of PIL Images."""
|
||||
"""Converts a batch of tensors to a list of PIL Images."""
|
||||
|
||||
def single_tensor2pil(t: torch.Tensor) -> Image.Image:
|
||||
np_array = to_numpy(t)
|
||||
@@ -777,7 +558,7 @@ def tensor2pil(tensor: torch.Tensor) -> list[Image.Image]:
|
||||
|
||||
|
||||
def pil2tensor(images: Image.Image | list[Image.Image]) -> torch.Tensor:
|
||||
"""Convert a PIL Image or a list of PIL Images to a tensor."""
|
||||
"""Converts a PIL Image or a list of PIL Images to a tensor."""
|
||||
|
||||
def single_pil2tensor(image: Image.Image) -> torch.Tensor:
|
||||
np_image = np.array(image).astype(np.float32) / 255.0
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "PromptAlchemy.Bridge",
|
||||
|
||||
setup() {
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', async (event) => {
|
||||
// Optionally verify origin
|
||||
const { type, ...payload } = event.data || {};
|
||||
|
||||
switch (type) {
|
||||
case 'loadWorkflow':
|
||||
await app.loadGraphData(payload.workflow);
|
||||
this.sendToParent({ type: 'workflowLoaded' });
|
||||
break;
|
||||
|
||||
case 'getWorkflow':
|
||||
const workflow = app.graph.serialize();
|
||||
this.sendToParent({ type: 'workflow', workflow });
|
||||
break;
|
||||
|
||||
case 'queuePrompt':
|
||||
app.queuePrompt(0); // 0 = front of queue
|
||||
break;
|
||||
|
||||
case 'getPrompt':
|
||||
const prompt = await app.graphToPrompt();
|
||||
this.sendToParent({ type: 'prompt', prompt });
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Alchemy Bridge Extension Loaded");
|
||||
|
||||
// Notify parent we're ready
|
||||
this.sendToParent({ type: 'ready' });
|
||||
},
|
||||
|
||||
sendToParent(data) {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(data, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+1168
-6
File diff suppressed because it is too large
Load Diff
+86
-67
@@ -11,12 +11,7 @@
|
||||
/// <reference path="../types/typedefs.js" />
|
||||
|
||||
import { app } from '../../scripts/app.js'
|
||||
|
||||
import {
|
||||
setupDynamicConnections,
|
||||
cleanupNode,
|
||||
infoLogger,
|
||||
} from './comfy_shared.js'
|
||||
import * as shared from './comfy_shared.js'
|
||||
import * as mtb_ui from './mtb_ui.js'
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
@@ -50,25 +45,25 @@ function createDebugSection(title) {
|
||||
return section
|
||||
}
|
||||
|
||||
function createDebugContent(item) {
|
||||
function createDebugContent(content, type) {
|
||||
const wrapper = mtb_ui.makeElement('div', {
|
||||
margin: '4px 0',
|
||||
})
|
||||
|
||||
if (item.kind === 'text') {
|
||||
const text = mtb_ui.makeElement('div', {
|
||||
if (type === 'text') {
|
||||
const text = mtb_ui.makeElement('p', {
|
||||
margin: '2px 0',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
})
|
||||
text.innerHTML = item.data
|
||||
text.innerHTML = content
|
||||
wrapper.appendChild(text)
|
||||
} else if (item.kind === 'b64_images') {
|
||||
} else if (type === 'image') {
|
||||
const img = mtb_ui.makeElement('img', {
|
||||
width: '100%',
|
||||
borderRadius: '2px',
|
||||
})
|
||||
img.src = item.data
|
||||
img.src = content
|
||||
wrapper.appendChild(img)
|
||||
}
|
||||
|
||||
@@ -85,85 +80,110 @@ app.registerExtension({
|
||||
*/
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === 'Debug (mtb)') {
|
||||
const clear_widgets = (target) => {
|
||||
if (target.widgets) {
|
||||
let tgt_len = target.widgets.length
|
||||
for (let i = 0; i < target.widgets.length; i++) {
|
||||
if (
|
||||
![
|
||||
'output_to_console',
|
||||
'deep_inspect',
|
||||
'as_detailed_types',
|
||||
'rich_mode',
|
||||
].includes(target.widgets[i].name)
|
||||
) {
|
||||
target.widgets[i].onRemove?.()
|
||||
target.widgets[i].onRemoved?.()
|
||||
tgt_len -= 1
|
||||
}
|
||||
}
|
||||
target.widgets.length = tgt_len
|
||||
}
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function (...args) {
|
||||
this.options = {}
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, args) : undefined
|
||||
this.addInput('anything_1', '*')
|
||||
return r
|
||||
}
|
||||
|
||||
const original_getExtraMenuOptions =
|
||||
nodeType.prototype.getExtraMenuOptions
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
original_getExtraMenuOptions?.apply(this, arguments)
|
||||
options.push({
|
||||
content: '🐛 Clear Outputs',
|
||||
callback: async () => {
|
||||
clear_widgets(this)
|
||||
},
|
||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||||
/**
|
||||
* @param {OnConnectionsChangeParams} args
|
||||
*/
|
||||
nodeType.prototype.onConnectionsChange = function (...args) {
|
||||
const [_type, index, connected, link_info, ioSlot] = args
|
||||
const r = onConnectionsChange
|
||||
? onConnectionsChange.apply(this, args)
|
||||
: undefined
|
||||
// TODO: remove all widgets on disconnect once computed
|
||||
shared.dynamic_connection(this, index, connected, 'anything_', '*', {
|
||||
link: link_info,
|
||||
ioSlot: ioSlot,
|
||||
})
|
||||
}
|
||||
|
||||
setupDynamicConnections(nodeType, 'var', '*')
|
||||
//- infer type
|
||||
if (link_info) {
|
||||
// const fromNode = this.graph._nodes.find(
|
||||
// (otherNode) => otherNode.id === link_info.origin_id,
|
||||
// )
|
||||
// const fromNode = app.graph.getNodeById(link_info.origin_id)
|
||||
const { from } = shared.nodesFromLink(this, link_info)
|
||||
if (!from || this.inputs.length === 0) return
|
||||
const type = from.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}`
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (...args) {
|
||||
onExecuted?.apply(this, args)
|
||||
const [data, ..._rest] = args
|
||||
|
||||
clear_widgets(this)
|
||||
if (this.widgets) {
|
||||
let tgt_len = this.widgets.length
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
if (
|
||||
this.widgets[i].name !== 'output_to_console' &&
|
||||
this.widgets[i].name !== 'as_detailed_types'
|
||||
) {
|
||||
this.widgets[i].onRemove?.()
|
||||
this.widgets[i].onRemoved?.()
|
||||
tgt_len -= 1
|
||||
}
|
||||
}
|
||||
this.widgets.length = tgt_len
|
||||
}
|
||||
|
||||
const inputData = {}
|
||||
|
||||
const uiData = data.ui || data
|
||||
|
||||
const name_to_label = this.inputs.reduce((acc, input) => {
|
||||
acc[input.name] = input.label || input.name
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
|
||||
if (uiData.items) {
|
||||
uiData.items.forEach((item) => {
|
||||
const inputName = item.input
|
||||
inputData[inputName] = item.items
|
||||
if (!inputData[inputName]) {
|
||||
inputData[inputName] = { text: [], b64_images: [] }
|
||||
}
|
||||
if (item.text) {
|
||||
inputData[inputName].text.push(...item.text)
|
||||
}
|
||||
if (item.b64_images) {
|
||||
inputData[inputName].b64_images.push(...item.b64_images)
|
||||
}
|
||||
})
|
||||
}
|
||||
const mainDebugContainer = mtb_ui.makeElement('div', {
|
||||
width: '100%',
|
||||
})
|
||||
let hasContent = false
|
||||
|
||||
let widgetI = 1
|
||||
for (const [inputName, content] of Object.entries(inputData)) {
|
||||
if (!content || content?.length === 0) {
|
||||
if (content.text.length === 0 && content.b64_images.length === 0) {
|
||||
continue
|
||||
}
|
||||
hasContent = true
|
||||
|
||||
const section = createDebugSection(name_to_label[inputName])
|
||||
const section = createDebugSection(inputName)
|
||||
|
||||
for (const item of content) {
|
||||
section.appendChild(createDebugContent(item))
|
||||
if (content.text.length > 0) {
|
||||
content.text.forEach((text) => {
|
||||
section.appendChild(createDebugContent(text, 'text'))
|
||||
})
|
||||
}
|
||||
mainDebugContainer.appendChild(section)
|
||||
}
|
||||
if (hasContent) {
|
||||
this.addDOMWidget('debug_output', 'CUSTOM', mainDebugContainer, {
|
||||
hideOnZoom: false,
|
||||
})
|
||||
|
||||
if (content.b64_images.length > 0) {
|
||||
content.b64_images.forEach((img) => {
|
||||
section.appendChild(createDebugContent(img, 'image'))
|
||||
})
|
||||
}
|
||||
|
||||
this.addDOMWidget(`debug_section_${widgetI}`, 'CUSTOM', section, {})
|
||||
widgetI++
|
||||
}
|
||||
|
||||
this.onRemoved = function () {
|
||||
@@ -174,9 +194,8 @@ app.registerExtension({
|
||||
widget.onRemoved?.()
|
||||
widget.onRemove?.()
|
||||
}
|
||||
cleanupNode(this)
|
||||
shared.cleanupNode(this)
|
||||
}
|
||||
this.setDirtyCanvas(true, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/**
|
||||
* MTB API Authoring Layer
|
||||
* This file imports the compiled TypeScript bundle and registers the extension.
|
||||
*
|
||||
* The source code is in web_source/src/mtb_api/
|
||||
* Build with: cd web_source && bun run build
|
||||
*/
|
||||
|
||||
import { registerMtbApiExtension } from './dist/mtb_api.js'
|
||||
|
||||
// Register the extension with ComfyUI
|
||||
registerMtbApiExtension()
|
||||
@@ -1,640 +0,0 @@
|
||||
import { app } from '../../scripts/app.js'
|
||||
import * as shared from './comfy_shared.js'
|
||||
|
||||
import { errorLogger, infoLogger } from './comfy_shared.js'
|
||||
import * as mtb_ui from './mtb_ui.js'
|
||||
|
||||
const defaultOptions = {
|
||||
capabilities: {
|
||||
execute: false,
|
||||
},
|
||||
lint: false,
|
||||
mode: 'python',
|
||||
theme: 'dracula',
|
||||
}
|
||||
|
||||
// wrapper around ACE
|
||||
export class MtbEditor {
|
||||
setOptions(options) {
|
||||
this.options = shared.deepMerge(defaultOptions, options || {})
|
||||
if (
|
||||
this.options.capabilities.execute === false &&
|
||||
this.options.lint === true
|
||||
) {
|
||||
infoLogger('ME: Disabling lint because execute is disabled')
|
||||
this.options.lint = false
|
||||
}
|
||||
|
||||
if (this.options.lint) {
|
||||
this.debouncedLint = shared.debounce(this.lintCode.bind(this), 500)
|
||||
}
|
||||
}
|
||||
constructor(node, options) {
|
||||
infoLogger('ME: construct')
|
||||
|
||||
this.uuid = shared.makeUUID()
|
||||
this._code = ''
|
||||
|
||||
// sanitize options
|
||||
this.setOptions(options)
|
||||
this.setupUI()
|
||||
|
||||
infoLogger('ME: inputDiv', this.inputDiv)
|
||||
|
||||
if (node) {
|
||||
infoLogger('ME: Hooking up parent node', shared.safe_json(node))
|
||||
this.parentNode = node
|
||||
const editor = this
|
||||
infoLogger('ME: NODE', shared.safe_json(this.parentNode))
|
||||
|
||||
infoLogger(
|
||||
'ME: NODE KEYS',
|
||||
shared.safe_json(Object.keys(this.parentNode).sort()),
|
||||
)
|
||||
|
||||
|
||||
shared.chainCallback(this.parentNode, 'onExecuted', function (res) {
|
||||
infoLogger('Executed', res)
|
||||
const { error, output_html } = res
|
||||
if (error[0]) {
|
||||
editor.appendOutput(
|
||||
`<div style="color: #f00; font-weight: bold;">Error:</div>${output_html[0]}`,
|
||||
)
|
||||
} else {
|
||||
editor.appendOutput(output_html[0])
|
||||
}
|
||||
})
|
||||
|
||||
shared.chainCallback(this.parentNode, 'onConfigure', function (serial) {
|
||||
shared
|
||||
.infoLogger('ME Node: configure called', { node: this, serial })
|
||||
.notify()
|
||||
|
||||
// editor.setCode(serial.properties.inputCode)
|
||||
// if (serial.properties.uuid) {
|
||||
// editor.uuid = serial.properties.uuid
|
||||
// }
|
||||
|
||||
})
|
||||
shared.chainCallback(this.parentNode, 'onRemoved', function (...serial) {
|
||||
errorLogger('ME Node: removed called TODO', {
|
||||
node: this,
|
||||
serial,
|
||||
})
|
||||
})
|
||||
shared.chainCallback(this.parentNode, 'onSerialize', function (serial) {
|
||||
infoLogger('ME Node: serialize called', {
|
||||
editor,
|
||||
node: this,
|
||||
serial,
|
||||
})
|
||||
|
||||
// if (editor.aceEditor) {
|
||||
// serial.properties.inputCode = editor.aceEditor.getValue()
|
||||
// }
|
||||
|
||||
const uuid_widget = this.widgets.find((e) => e.name === 'uuid')
|
||||
if (uuid_widget) {
|
||||
if (uuid_widget.value) {
|
||||
editor.uuid = uuid_widget.value
|
||||
} else {
|
||||
uuid_widget.value = editor.uuid
|
||||
}
|
||||
}
|
||||
|
||||
// serial.properties.inputHeightRatio = this.properties.inputHeightRatio
|
||||
})
|
||||
|
||||
// shared.chainCallback(this.parentNode,"onResize", function(size) {
|
||||
// editor.container.style.width = `${size[0] - 10}px` // Account for padding
|
||||
// container.style.height = `${size[1] - 10}px`
|
||||
//
|
||||
// })
|
||||
|
||||
infoLogger('ME: callback setup on node')
|
||||
this.debouncedUpdateNodeProperty = shared.debounce(() => {
|
||||
if (this.aceEditor && this.parentNode) {
|
||||
const currentCode = this.aceEditor.getValue()
|
||||
// this.parentNode.setProperty('inputCode', currentCode)
|
||||
this._widget.value = currentCode
|
||||
infoLogger("ME: Node property 'inputCode' updated.")
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
this.loadAceEditor()
|
||||
}
|
||||
|
||||
setupUI() {
|
||||
this.container = mtb_ui.makeElement('div', {
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontFamily: 'monospace',
|
||||
height: '100%',
|
||||
padding: '5px',
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
this.inputDiv = mtb_ui.makeElement(
|
||||
'div',
|
||||
{
|
||||
// backgroundColor: '#333',
|
||||
// color: '#eee',
|
||||
// border: '1px solid #555',
|
||||
// borderRadius: '4px',
|
||||
// marginBottom: '5px',
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '100px',
|
||||
width: 'calc(100% - 10px)',
|
||||
// overflow: 'hidden',
|
||||
},
|
||||
this.container,
|
||||
)
|
||||
|
||||
if (this.options.capabilities.execute) {
|
||||
this.handleDiv = mtb_ui.makeElement(
|
||||
'div',
|
||||
{
|
||||
backgroundColor: '#666',
|
||||
borderRadius: '2px',
|
||||
cursor: 'ns-resize',
|
||||
height: '5px',
|
||||
marginBottom: '5px',
|
||||
width: '100%',
|
||||
},
|
||||
this.container,
|
||||
)
|
||||
this.handleDiv.addEventListener(
|
||||
'mousedown',
|
||||
this.startResizing.bind(this),
|
||||
)
|
||||
|
||||
const runButton = mtb_ui.makeElement(
|
||||
'button',
|
||||
{
|
||||
backgroundColor: '#555',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
marginBottom: '5px',
|
||||
padding: '8px',
|
||||
width: '100%',
|
||||
},
|
||||
this.container,
|
||||
)
|
||||
runButton.textContent = 'Run Code (Alt+Enter)'
|
||||
runButton.onclick = () => {
|
||||
// node.__repl('RUN') //node.executeCode()
|
||||
this.executeCode()
|
||||
}
|
||||
const clearButton = mtb_ui.makeElement(
|
||||
'button',
|
||||
{
|
||||
backgroundColor: '#555',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
marginBottom: '5px',
|
||||
padding: '8px',
|
||||
width: '100%',
|
||||
},
|
||||
this.container,
|
||||
)
|
||||
clearButton.textContent = 'Clear Output'
|
||||
clearButton.onclick = () => {
|
||||
this.outputArea.innerHTML = ''
|
||||
// this.properties.outputHistory = ''
|
||||
this.parentNode?.setProperty('outputHistory', '')
|
||||
}
|
||||
|
||||
this.outputArea = mtb_ui.makeElement(
|
||||
'div',
|
||||
{
|
||||
backgroundColor: '#222',
|
||||
border: '1px solid #555',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
color: '#ddd',
|
||||
flexGrow: '1',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
overflowY: 'auto',
|
||||
padding: '5px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
width: 'calc(100% - 10px)',
|
||||
},
|
||||
this.container,
|
||||
)
|
||||
|
||||
// augment the node
|
||||
// node.__repl = function (msg) {
|
||||
// console.log('Called msg on api REPl', { msg, node: this })
|
||||
// }
|
||||
// node.setProperty('inputCode', '')
|
||||
// node.title = '🐍 REPL (mtb)'
|
||||
}
|
||||
}
|
||||
get code() {
|
||||
return this._code
|
||||
}
|
||||
async lintCode() {
|
||||
if (this.options.mode !== 'python') {
|
||||
errorLogger('ME: Only python supports lint').notify()
|
||||
return
|
||||
}
|
||||
if (!this.aceEditor) {
|
||||
shared.warnLogger('ME: Ace Editor not loaded yet')
|
||||
return
|
||||
}
|
||||
const code = this.aceEditor.getValue()
|
||||
if (!code.trim()) {
|
||||
this.aceEditor.session.setAnnotations([])
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/mtb/lint', {
|
||||
body: JSON.stringify({ code: code, name: this.uuid }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
errorLogger('ME: Failed to lint', response)
|
||||
throw new Error(
|
||||
`HTTP error! status: ${response.status} ${response.statusText}`,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
// result.diagnostics should be an array of {row, column, text, type}
|
||||
this.aceEditor.session.setAnnotations(result.diagnostics)
|
||||
} catch (e) {
|
||||
errorLogger('ME: Linting Error', e)
|
||||
this.aceEditor.session.setAnnotations([
|
||||
{
|
||||
column: 0,
|
||||
row: 0,
|
||||
text: `Linting failed: ${e.message}`,
|
||||
type: 'error',
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
setupWidget(name, typeName) {
|
||||
if (this._widget) {
|
||||
throw Error('Widget already setup, this should not happen')
|
||||
}
|
||||
|
||||
this._widget = this.parentNode.addDOMWidget(
|
||||
name,
|
||||
typeName,
|
||||
this.container,
|
||||
{
|
||||
getValue: () => {
|
||||
// infoLogger(
|
||||
// 'Called get value of Code Editor',
|
||||
// node.properties,
|
||||
// editor.code,
|
||||
// )
|
||||
// return 'foobar'
|
||||
// return node.properties.inputCode
|
||||
// return this.parentNode?.properties?.inputCode // editor.code
|
||||
return this.code
|
||||
},
|
||||
hideOnZoom: false,
|
||||
setValue: (v) => {
|
||||
infoLogger('Called set value of Code Editor with:', v)
|
||||
// this.parentNode.setProperty('inputCode', v)
|
||||
this._code = v
|
||||
},
|
||||
},
|
||||
)
|
||||
return this._widget
|
||||
}
|
||||
|
||||
// --- Resizing Logic ---
|
||||
startResizing(e) {
|
||||
if (!this.inputDiv) {
|
||||
infoLogger("The input div isn't ready", this)
|
||||
errorLogger("The input div isn't ready")
|
||||
return
|
||||
}
|
||||
this.isResizing = true
|
||||
this.initialMouseY = e.clientY
|
||||
this.initialInputHeight = this.inputDiv.offsetHeight
|
||||
this.initialOutputHeight = this.outputArea.offsetHeight
|
||||
|
||||
document.addEventListener('mousemove', this.doResize.bind(this))
|
||||
document.addEventListener('mouseup', this.stopResizing.bind(this))
|
||||
document.body.style.cursor = 'ns-resize' // Change cursor globally
|
||||
}
|
||||
doResize(e) {
|
||||
if (!this.aceEditor || !this.isResizing) return
|
||||
|
||||
const deltaY = e.clientY - this.initialMouseY
|
||||
|
||||
let new_input_height = this.initialInputHeight + deltaY
|
||||
let new_output_height = this.initialOutputHeight - deltaY
|
||||
|
||||
const minInputHeight = 50 // Minimum height for Ace editor
|
||||
const minOutputHeight = 50 // Minimum height for output area
|
||||
|
||||
// Clamp heights to minimums
|
||||
if (new_input_height < minInputHeight) {
|
||||
new_input_height = minInputHeight
|
||||
new_output_height =
|
||||
this.initialInputHeight + this.initialOutputHeight - minInputHeight
|
||||
}
|
||||
if (new_output_height < minOutputHeight) {
|
||||
new_output_height = minOutputHeight
|
||||
new_input_height =
|
||||
this.initialInputHeight + this.initialOutputHeight - minOutputHeight
|
||||
}
|
||||
|
||||
this.inputDiv.style.height = `${new_input_height}px`
|
||||
this.outputArea.style.height = `${new_output_height}px`
|
||||
|
||||
// Update the stored ratio for persistence
|
||||
const totalDynamicHeight =
|
||||
this.inputDiv.offsetHeight + this.outputArea.offsetHeight
|
||||
if (totalDynamicHeight > 0) {
|
||||
this.parentNode.setProperty(
|
||||
'inputHeightRatio',
|
||||
new_input_height / totalDynamicHeight,
|
||||
)
|
||||
}
|
||||
|
||||
this.aceEditor.resize() // Important for Ace to redraw
|
||||
}
|
||||
|
||||
stopResizing() {
|
||||
this.isResizing = false
|
||||
document.removeEventListener('mousemove', this.doResize)
|
||||
document.removeEventListener('mouseup', this.stopResizing)
|
||||
document.body.style.cursor = '' // Restore default cursor
|
||||
}
|
||||
|
||||
setCode(code) {
|
||||
infoLogger('called set code')
|
||||
this._code = code || this._code
|
||||
if (!this._code) {
|
||||
return
|
||||
}
|
||||
if (this._widget) {
|
||||
this._widget.value = this._code
|
||||
}
|
||||
|
||||
if (this.reference) {
|
||||
this.reference.value = this._code
|
||||
}
|
||||
if (this.aceEditor) {
|
||||
this.aceEditor.setValue(this._code)
|
||||
}
|
||||
// if (this.parentNode) {
|
||||
// this.parentNode.setProperty('inputCode', this._code)
|
||||
// }
|
||||
// TODO: probably shouldn't be auto called depending on the call chain..
|
||||
if (this.options.lint) {
|
||||
this.debouncedLint()
|
||||
}
|
||||
}
|
||||
|
||||
loadAceEditor() {
|
||||
if (window.MTB?.ace_loaded) {
|
||||
if (this.aceEditor) {
|
||||
infoLogger('ACE is already loaded, nothing to do')
|
||||
} else {
|
||||
this.initAceEditor()
|
||||
}
|
||||
return
|
||||
}
|
||||
// NOTE: this need more testing, globals can come from other extensions...
|
||||
//
|
||||
// let NEED_PATCH = false
|
||||
if (window.ace) {
|
||||
infoLogger(
|
||||
'A global ace was found in scope, to avoid issues with it we will patch it',
|
||||
)
|
||||
// NEED_PATCH = true
|
||||
// // window._backupAce = window.ace
|
||||
// // window.ace = null
|
||||
}
|
||||
|
||||
shared
|
||||
.loadScript('/mtb_async/ace/ace.js')
|
||||
.then((m) => {
|
||||
infoLogger('ACE was loaded', m)
|
||||
// window.MTB_ACE = window.ace
|
||||
if (!window.MTB) {
|
||||
// might happen now that we don't control the lifecycle
|
||||
window.MTB = {}
|
||||
}
|
||||
|
||||
window.MTB.ace_loaded = true
|
||||
this.initAceEditor()
|
||||
// this.aceEditor.setValue(this.properties.inputCode, -1)
|
||||
// this.aceEditor.setValue(this.properties.inputCode, -1)
|
||||
this.setCode()
|
||||
})
|
||||
.catch((e) => {
|
||||
errorLogger(`Error loading ace: ${e}`)
|
||||
})
|
||||
.finally(() => {
|
||||
// if (NEED_PATCH) {
|
||||
// console.log('Patching back window object')
|
||||
// window.ace = window._backupAce
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
appendOutput(html) {
|
||||
this.outputArea.innerHTML += html
|
||||
this.outputArea.scrollTop = this.outputArea.scrollHeight
|
||||
|
||||
if (this.parentNode) {
|
||||
const currentHistory = this.parentNode.properties.outputHistory || ''
|
||||
this.parentNode.setProperty('outputHistory', currentHistory + html)
|
||||
}
|
||||
|
||||
this.outputArea.scrollTop = this.outputArea.scrollHeight
|
||||
}
|
||||
|
||||
async executeCode() {
|
||||
if (!this.aceEditor) {
|
||||
infoLogger('Editor is gone, recreating it.')
|
||||
this.initAceEditor()
|
||||
}
|
||||
|
||||
const code = this.aceEditor.getValue()
|
||||
if (!code.trim()) {
|
||||
infoLogger('No code to execute')
|
||||
return
|
||||
}
|
||||
|
||||
const inputPrompt = `<div style="color:#888; margin-top: 10px;">>>> ${code}</div>`
|
||||
|
||||
this.appendOutput(inputPrompt)
|
||||
|
||||
try {
|
||||
const response = await fetch('/mtb/execute', {
|
||||
body: JSON.stringify({ code: code, name: this.uuid, reset: true }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
infoLogger('Received from backend', result)
|
||||
const outputHtml = result.output_html || ''
|
||||
const error = result.error
|
||||
|
||||
if (error) {
|
||||
this.appendOutput(
|
||||
`<div style="color: #f00; font-weight: bold;">Error:</div>${outputHtml}`,
|
||||
)
|
||||
} else {
|
||||
this.appendOutput(outputHtml)
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = `<div style="color: #f00;">Frontend Error: ${e.message}</div>`
|
||||
|
||||
this.appendOutput(errorMessage)
|
||||
errorLogger('MTBEditor Backend Error:', e).notify()
|
||||
} finally {
|
||||
// Not clearing
|
||||
// this.inputArea.value = '' // Clear input after execution
|
||||
// this.properties.inputCode = '' // Clear persisted input
|
||||
}
|
||||
}
|
||||
|
||||
initAceEditor() {
|
||||
if (!window.MTB?.ace_loaded) {
|
||||
errorLogger('ACE editor not loaded. Cannot set up editors.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.inputDiv) {
|
||||
errorLogger('Input div not found for Ace editor initialization.')
|
||||
return
|
||||
}
|
||||
this.aceEditor = ace.edit(this.inputDiv)
|
||||
this.aceEditor.setTheme(`ace/theme/${this.options.theme || 'dracula'}`) //"ace/theme/monokai", "ace/theme/github"
|
||||
this.aceEditor.session.setMode(`ace/mode/${this.options.mode}`)
|
||||
this.aceEditor.setOptions({
|
||||
autoScrollEditorIntoView: true,
|
||||
behavioursEnabled: true,
|
||||
cursorStyle: 'ace', // "ace" | "slim" | "smooth" | "wide"
|
||||
displayIndentGuides: true,
|
||||
fixedWidthGutter: true,
|
||||
fontFamily: 'monospace',
|
||||
// enableBasicAutocompletion: true,
|
||||
// enableLiveAutocompletion: true,
|
||||
// enableSnippets: true,
|
||||
fontSize: '14px',
|
||||
hasCssTransforms: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectedWord: true,
|
||||
scrollPastEnd: 0.5,
|
||||
showPrintMargin: false,
|
||||
tabSize: 4,
|
||||
useSoftTabs: true,
|
||||
wrap: true,
|
||||
})
|
||||
|
||||
// Custom keybinding for Ctrl+Enter
|
||||
// this.aceEditor.commands.addCommand({
|
||||
// name: 'runCode',
|
||||
// bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
|
||||
// exec: () => this.executeCode(),
|
||||
// })
|
||||
|
||||
// this.addCommand = this.aceEditor.commands.addCommand
|
||||
|
||||
if (this.options.capabilities.execute) {
|
||||
this.aceEditor.commands.addCommand({
|
||||
bindKey: { mac: 'Alt-Enter', win: 'Alt-Enter' },
|
||||
exec: () => this.executeCode(),
|
||||
name: 'runCode',
|
||||
})
|
||||
}
|
||||
|
||||
// Listen for changes to trigger linting
|
||||
// for now keeping it like that but we might just not register
|
||||
// the event at all.
|
||||
this.aceEditor.session.on('change', () => {
|
||||
this.debouncedUpdateNodeProperty()
|
||||
if (this.options.lint) {
|
||||
this.debouncedLint()
|
||||
}
|
||||
})
|
||||
|
||||
infoLogger('ACE editor created', { aceEditor: this.aceEditor })
|
||||
// this.outputArea.scrollTop = this.outputArea.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// the widget
|
||||
// This is a bit hacky and backward but the simplest I could
|
||||
// find for the bidirectionality
|
||||
export const CODE_EDITOR = (node, name, inputData, _app) => {
|
||||
infoLogger('CODE EDITOR NODE NOW', {
|
||||
inputData: shared.safe_json(inputData),
|
||||
node: shared.safe_json(node),
|
||||
})
|
||||
|
||||
const [typeName, options] = inputData
|
||||
|
||||
infoLogger('CODE_EDITOR', { name, node, options })
|
||||
switch (
|
||||
options.lang //(node.type || node.title) {
|
||||
) {
|
||||
case undefined: {
|
||||
shared
|
||||
.errorLogger(
|
||||
'Using CODE_EDITOR without specifying lang is not supported anymore',
|
||||
)
|
||||
.notify()
|
||||
break
|
||||
}
|
||||
// case 'Repl (mtb)': {
|
||||
case 'python': {
|
||||
infoLogger('Handling python editor', { node })
|
||||
|
||||
const editor = new MtbEditor(node, {
|
||||
capabilities: {
|
||||
execute: true, // options.allow_exec
|
||||
},
|
||||
lang: 'python',
|
||||
})
|
||||
infoLogger('MTBEditor created', editor)
|
||||
|
||||
return editor.setupWidget(name, typeName)
|
||||
|
||||
}
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
getCustomWidgets: () => {
|
||||
return {
|
||||
CODE_EDITOR,
|
||||
}
|
||||
},
|
||||
name: 'mtb.code_editor',
|
||||
})
|
||||
+27
-200
@@ -15,13 +15,8 @@ import {
|
||||
renderSidebar,
|
||||
} from './mtb_ui.js'
|
||||
|
||||
// pagination state
|
||||
let pageOffset = 0
|
||||
let isLoadingPage = false
|
||||
let hasMorePages = true
|
||||
let pageSizeCache = undefined
|
||||
const offset = 0
|
||||
|
||||
let observer = null
|
||||
// These are "global" variables mostly meant to sync user settings.
|
||||
let currentWidth = 200
|
||||
let saltUrls =
|
||||
@@ -135,13 +130,6 @@ const getImgsFromUrls = (urls, target, options = { prepend: false }) => {
|
||||
const a = makeElement(elem)
|
||||
a.src = url
|
||||
a.width = currentWidth
|
||||
if (elem === 'img') {
|
||||
a.loading = 'lazy'
|
||||
a.decoding = 'async'
|
||||
} else {
|
||||
// video
|
||||
a.preload = 'metadata'
|
||||
}
|
||||
if (currentMode === 'input') {
|
||||
a.onclick = (_e) => {
|
||||
if (subfolder !== '') {
|
||||
@@ -170,66 +158,25 @@ const getImgsFromUrls = (urls, target, options = { prepend: false }) => {
|
||||
}
|
||||
}
|
||||
} else if (currentMode === 'output') {
|
||||
a.onclick = async (_e) => {
|
||||
const params = new URLSearchParams()
|
||||
params.set('filename', key)
|
||||
params.set('type', 'output')
|
||||
if (subfolder) params.set('subfolder', subfolder)
|
||||
params.set('workflow', 'true')
|
||||
const url = `/mtb/view?${params.toString()}`
|
||||
try {
|
||||
const res = await api.fetchApi(url)
|
||||
if (!res?.ok) throw new Error(`Request failed (${res?.status})`)
|
||||
const data = await res.json()
|
||||
const workflow = data?.workflow || data?.prompt
|
||||
if (!workflow) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'No workflow in image',
|
||||
detail: 'This file does not contain embedded workflow metadata.',
|
||||
life: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Try to import via File first; fallback to blob URL
|
||||
const jsonText = JSON.stringify(workflow, null, 2)
|
||||
const blob = new Blob([jsonText], { type: 'application/json' })
|
||||
const suggestedName = `${(key || 'workflow').replace(/\.[^.]+$/, '')}-workflow.json`
|
||||
let loaded = false
|
||||
try {
|
||||
const file = new File([blob], suggestedName, {
|
||||
type: 'application/json',
|
||||
})
|
||||
await app.handleFile(file)
|
||||
loaded = true
|
||||
} catch (_err) {
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
try {
|
||||
await app.handleFile(blobUrl)
|
||||
loaded = true
|
||||
} finally {
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
}
|
||||
|
||||
if (loaded) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Workflow loaded',
|
||||
detail: 'Imported workflow from image metadata.',
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load workflow from output image:', err)
|
||||
a.onclick = (_e) => {
|
||||
// window.MTB?.notify?.("Output import isn't supported yet...", 5000)
|
||||
if (subfolder !== '') {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Import failed',
|
||||
detail: String(err?.message || err),
|
||||
life: 6000,
|
||||
severity: 'warn',
|
||||
summary: 'Subfolder not supported',
|
||||
detail: "The LoadImage node doesn't support subfolders",
|
||||
life: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Outputs not supported',
|
||||
detail:
|
||||
'For now only inputs can be clicked to load the image on the active LoadImage node.',
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
a.autoplay = true
|
||||
@@ -267,14 +214,9 @@ const getModes = async () => {
|
||||
const inputs = await shared.runAction('getUserImageFolders')
|
||||
return inputs
|
||||
}
|
||||
const getUrls = async (subfolder, countOverride, offsetOverride) => {
|
||||
const count =
|
||||
typeof countOverride === 'number'
|
||||
? countOverride
|
||||
: (await api.getSetting('mtb.io-sidebar.count')) || 1000
|
||||
const offset = typeof offsetOverride === 'number' ? offsetOverride : pageOffset
|
||||
pageSizeCache = count
|
||||
// console.debug('Sidebar count', count, 'offset', offset)
|
||||
const getUrls = async (subfolder) => {
|
||||
const count = (await api.getSetting('mtb.io-sidebar.count')) || 1000
|
||||
console.log('Sidebar count', count)
|
||||
if (currentMode === 'video') {
|
||||
const output = await shared.runAction(
|
||||
'getUserVideos',
|
||||
@@ -299,89 +241,6 @@ const getUrls = async (subfolder, countOverride, offsetOverride) => {
|
||||
return output || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize pagination (loader, end-of-results message, sentinel, and IntersectionObserver)
|
||||
* for the provided grid container. Safely disconnects any previous observer.
|
||||
* @param {HTMLElement} imgGrid
|
||||
*/
|
||||
function initPagination(imgGrid) {
|
||||
// disconnect any existing observer to avoid duplicates
|
||||
if (observer) {
|
||||
try {
|
||||
observer.disconnect()
|
||||
} catch { }
|
||||
observer = null
|
||||
}
|
||||
|
||||
// create footer UI elements
|
||||
const loader = makeElement('div', {}, imgGrid)
|
||||
Object.assign(loader.style, {
|
||||
display: 'none',
|
||||
width: '100%',
|
||||
padding: '8px 0',
|
||||
textAlign: 'center',
|
||||
color: 'var(--mtb-text, #ccc)',
|
||||
fontSize: '12px',
|
||||
})
|
||||
loader.textContent = 'Loading…'
|
||||
|
||||
const endMsg = makeElement('div', {}, imgGrid)
|
||||
Object.assign(endMsg.style, {
|
||||
display: 'none',
|
||||
width: '100%',
|
||||
padding: '8px 0',
|
||||
textAlign: 'center',
|
||||
color: 'var(--mtb-text, #888)',
|
||||
fontSize: '12px',
|
||||
})
|
||||
endMsg.textContent = 'No more items'
|
||||
|
||||
const sentinel = makeElement('div', {}, imgGrid)
|
||||
sentinel.style.height = '1px'
|
||||
sentinel.style.width = '100%'
|
||||
sentinel.style.marginTop = '1px'
|
||||
|
||||
const loadNextPage = async () => {
|
||||
if (isLoadingPage || !hasMorePages) return
|
||||
isLoadingPage = true
|
||||
loader.style.display = 'block'
|
||||
try {
|
||||
const nextUrls = await getUrls(subfolder, undefined, pageOffset)
|
||||
const keys = Object.keys(nextUrls || {})
|
||||
if (!keys.length) {
|
||||
hasMorePages = false
|
||||
if (observer) observer.disconnect()
|
||||
loader.style.display = 'none'
|
||||
endMsg.style.display = 'block'
|
||||
return
|
||||
}
|
||||
getImgsFromUrls(nextUrls, imgGrid)
|
||||
if (pageSizeCache != null) pageOffset += pageSizeCache
|
||||
// keep footer elements and sentinel at the bottom
|
||||
imgGrid.appendChild(loader)
|
||||
imgGrid.appendChild(endMsg)
|
||||
imgGrid.appendChild(sentinel)
|
||||
} catch (e) {
|
||||
console.error('Failed to load next page:', e)
|
||||
hasMorePages = false
|
||||
if (observer) observer.disconnect()
|
||||
loader.style.display = 'none'
|
||||
endMsg.style.display = 'block'
|
||||
} finally {
|
||||
isLoadingPage = false
|
||||
if (hasMorePages) loader.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) loadNextPage()
|
||||
}
|
||||
})
|
||||
observer.observe(sentinel)
|
||||
|
||||
}
|
||||
|
||||
//NOTE: do not load if using the old ui
|
||||
if (window?.__COMFYUI_FRONTEND_VERSION__) {
|
||||
// NOTE: removed this for now since I'm not actually exposing anything a client
|
||||
@@ -568,12 +427,7 @@ if (window?.__COMFYUI_FRONTEND_VERSION__) {
|
||||
const allModes = await getModes()
|
||||
const input_modes = allModes.input.map((m) => `input - ${m}`)
|
||||
const output_modes = allModes.output.map((m) => `output - ${m}`)
|
||||
//- reset pagination state for fresh render
|
||||
pageOffset = 0
|
||||
isLoadingPage = false
|
||||
hasMorePages = true
|
||||
const urls = await getUrls(undefined, undefined, 0)
|
||||
if (pageSizeCache != null) pageOffset += pageSizeCache
|
||||
const urls = await getUrls()
|
||||
let imgs = {}
|
||||
|
||||
const cont = makeElement('div.mtb_sidebar')
|
||||
@@ -602,17 +456,10 @@ if (window?.__COMFYUI_FRONTEND_VERSION__) {
|
||||
subfolder = newSub
|
||||
if (changed) {
|
||||
imgGrid.innerHTML = ''
|
||||
//- reset pagination on mode change
|
||||
pageOffset = 0
|
||||
isLoadingPage = false
|
||||
hasMorePages = true
|
||||
const urls = await getUrls(subfolder, undefined, 0)
|
||||
if (pageSizeCache != null) pageOffset += pageSizeCache
|
||||
const urls = await getUrls(subfolder)
|
||||
if (urls) {
|
||||
imgs = getImgsFromUrls(urls, imgGrid)
|
||||
}
|
||||
// re-init pagination after content reset
|
||||
initPagination(imgGrid)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -628,17 +475,10 @@ if (window?.__COMFYUI_FRONTEND_VERSION__) {
|
||||
currentSort = newSort
|
||||
if (changed) {
|
||||
imgGrid.innerHTML = ''
|
||||
//- reset pagination on sort change
|
||||
pageOffset = 0
|
||||
isLoadingPage = false
|
||||
hasMorePages = true
|
||||
const urls = await getUrls(subfolder, undefined, 0)
|
||||
if (pageSizeCache != null) pageOffset += pageSizeCache
|
||||
const urls = await getUrls(subfolder)
|
||||
if (urls) {
|
||||
imgs = getImgsFromUrls(urls, imgGrid)
|
||||
}
|
||||
// re-init pagination after content reset
|
||||
initPagination(imgGrid)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -647,23 +487,12 @@ if (window?.__COMFYUI_FRONTEND_VERSION__) {
|
||||
imgTools.appendChild(sizeSlider)
|
||||
|
||||
imgs = getImgsFromUrls(urls, imgGrid)
|
||||
// Setup infinite pagination for the initial render
|
||||
initPagination(imgGrid)
|
||||
|
||||
let pendingWidth = null
|
||||
let rafToken = null
|
||||
sizeSlider.addEventListener('input', (e) => {
|
||||
pendingWidth = e.target.value
|
||||
if (rafToken) return
|
||||
rafToken = requestAnimationFrame(() => {
|
||||
rafToken = null
|
||||
if (pendingWidth == null) return
|
||||
currentWidth = pendingWidth
|
||||
pendingWidth = null
|
||||
for (const img of imgs) {
|
||||
img.style.width = `${currentWidth}px`
|
||||
}
|
||||
})
|
||||
currentWidth = e.target.value
|
||||
for (const img of imgs) {
|
||||
img.style.width = `${e.target.value}px`
|
||||
}
|
||||
})
|
||||
handle = renderSidebar(el, cont, [selector, imgGrid, imgTools])
|
||||
app.api.addEventListener('status', async () => {
|
||||
@@ -677,8 +506,6 @@ if (window?.__COMFYUI_FRONTEND_VERSION__) {
|
||||
handle = undefined
|
||||
app.api.removeEventListener('status')
|
||||
}
|
||||
// Attempt to disconnect any stray observers to avoid retained callbacks
|
||||
if (observer) observer.disconnect()
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/** Python REPL for the frontend (uses rich)*/
|
||||
|
||||
import { app } from '../../scripts/app.js'
|
||||
|
||||
// import * as mtb_ui from './mtb_ui.js'
|
||||
|
||||
// #endregion
|
||||
|
||||
const repl = {
|
||||
name: 'mtb.repl',
|
||||
|
||||
// async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
}
|
||||
|
||||
+48
-80
@@ -21,7 +21,7 @@ import { infoLogger } from './comfy_shared.js'
|
||||
import { NumberInputWidget } from './numberInput.js'
|
||||
|
||||
// NOTE: new widget types registered by MTB Widgets
|
||||
const newTypes = [/*'BOOL'*/ 'COLOR', 'MTB_COLOR', 'BBOX']
|
||||
const newTypes = [/*'BOOL'*/ 'COLOR','MTB_COLOR', 'BBOX']
|
||||
|
||||
const deprecated_nodes = {
|
||||
// 'Animation Builder':
|
||||
@@ -120,7 +120,7 @@ export function addVectorWidgetW(
|
||||
'number',
|
||||
`${name}_${VECTOR_AXIS[i]}`,
|
||||
value[VECTOR_AXIS[i]],
|
||||
(val) => { },
|
||||
(val) => {},
|
||||
)
|
||||
|
||||
inputs.push(input)
|
||||
@@ -448,7 +448,7 @@ export const MtbWidgets = {
|
||||
try {
|
||||
//solve the equation if possible
|
||||
v = eval(v)
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}
|
||||
this.value = Number(v)
|
||||
shared.inner_value_change(this, this.value, event)
|
||||
@@ -1060,6 +1060,7 @@ const mtb_widgets = {
|
||||
const currentChunkSize = Math.min(chunkSize, totalPrompts - i)
|
||||
|
||||
await app.queuePrompt(0, currentChunkSize)
|
||||
|
||||
}
|
||||
if (!cancelQueue) {
|
||||
window.MTB?.notify?.(
|
||||
@@ -1085,14 +1086,16 @@ const mtb_widgets = {
|
||||
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}`
|
||||
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
|
||||
}`
|
||||
loop_preview.value = `current loop: ${raw_loop.value + 1}/${
|
||||
loop_count.value
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1203,9 +1206,7 @@ const mtb_widgets = {
|
||||
|
||||
//NOTE: dynamic nodes
|
||||
case 'Apply Text Template (mtb)': {
|
||||
shared.setupDynamicConnections(nodeType, 'var', '*', {
|
||||
rename_menu: 'name',
|
||||
})
|
||||
shared.setupDynamicConnections(nodeType, 'var', '*')
|
||||
break
|
||||
}
|
||||
case 'Save Data Bundle (mtb)': {
|
||||
@@ -1229,7 +1230,6 @@ const mtb_widgets = {
|
||||
// break
|
||||
// }
|
||||
case 'Stack Images (mtb)':
|
||||
case 'Reference Latents (mtb)':
|
||||
case 'Concat Images (mtb)': {
|
||||
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
|
||||
break
|
||||
@@ -1295,32 +1295,7 @@ const mtb_widgets = {
|
||||
|
||||
break
|
||||
}
|
||||
case 'String Replace (mtb)': {
|
||||
shared.addMenuHandler(nodeType, function (_app, options) {
|
||||
/** @type {ContextMenuItem} */
|
||||
const item = {
|
||||
content: 'swap',
|
||||
title: 'Swap Old/New ⚡',
|
||||
callback: (_menuItem) => {
|
||||
const old_w = this.widgets.find((w) => w.name === 'old')
|
||||
const novel_w = this.widgets.find(
|
||||
(w) => w.name === 'new',
|
||||
)
|
||||
|
||||
const old = old_w.value
|
||||
const novel = novel_w.value
|
||||
|
||||
novel_w.value = old
|
||||
old_w.value = novel
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
options.push(item)
|
||||
return [item]
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'Batch Shape (mtb)':
|
||||
case 'Mask To Image (mtb)':
|
||||
case 'Text To Image (mtb)': {
|
||||
@@ -1363,54 +1338,47 @@ const mtb_widgets = {
|
||||
const related = new Set([this.id])
|
||||
const visited = new Set()
|
||||
if (this.outputs[0].links) {
|
||||
for (const linkId of this.outputs[0].links) {
|
||||
const { to: loopEnd } = shared.nodesFromLink(this, linkId)
|
||||
const canReachEnd = (node, visited = new Set()) => {
|
||||
if (node === loopEnd) return true
|
||||
if (visited.has(node.id)) return false
|
||||
visited.add(node.id)
|
||||
for (const output of node.outputs || []) {
|
||||
if (!output.links) continue
|
||||
for (const linkId of output.links) {
|
||||
const { to: nextNode } = shared.nodesFromLink(
|
||||
node,
|
||||
linkId,
|
||||
)
|
||||
if (!nextNode) continue
|
||||
if (canReachEnd(nextNode, visited)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const traverseNodes = (node) => {
|
||||
if (visited.has(node.id)) return
|
||||
visited.add(node.id)
|
||||
|
||||
// can reach the end
|
||||
if (node !== this && node !== loopEnd && !canReachEnd(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
related.add(node.id)
|
||||
for (const output of node.outputs || []) {
|
||||
if (!output.links) continue
|
||||
|
||||
for (const linkId of output.links) {
|
||||
const { to: nextNode } = shared.nodesFromLink(
|
||||
node,
|
||||
linkId,
|
||||
)
|
||||
if (!nextNode) continue
|
||||
|
||||
traverseNodes(nextNode)
|
||||
const initLink = this.outputs[0].links[0]
|
||||
const { to: loopEnd } = shared.nodesFromLink(this, initLink)
|
||||
const canReachEnd = (node, visited = new Set()) => {
|
||||
if (node === loopEnd) return true
|
||||
if (visited.has(node.id)) return false
|
||||
visited.add(node.id)
|
||||
for (const output of node.outputs || []) {
|
||||
if (!output.links) continue
|
||||
for (const linkId of output.links) {
|
||||
const { to: nextNode } = shared.nodesFromLink(node, linkId)
|
||||
if (!nextNode) continue
|
||||
if (canReachEnd(nextNode, visited)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseNodes(this)
|
||||
return false
|
||||
}
|
||||
const traverseNodes = (node) => {
|
||||
if (visited.has(node.id)) return
|
||||
visited.add(node.id)
|
||||
|
||||
// can reach the end
|
||||
if (node !== this && node !== loopEnd && !canReachEnd(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
related.add(node.id)
|
||||
for (const output of node.outputs || []) {
|
||||
if (!output.links) continue
|
||||
|
||||
for (const linkId of output.links) {
|
||||
const { to: nextNode } = shared.nodesFromLink(node, linkId)
|
||||
if (!nextNode) continue
|
||||
|
||||
traverseNodes(nextNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseNodes(this)
|
||||
}
|
||||
this.related_to_flow = Array.from(related)
|
||||
this.computed_flow = true
|
||||
|
||||
+5
-1
@@ -194,7 +194,9 @@ class NotePlus extends LiteGraph.LGraphNode {
|
||||
this.calculateHeight.cancel()
|
||||
}
|
||||
this.calculateHeight = shared.debounce(() => {
|
||||
this.calculated_height = shared.calculateTotalChildrenHeight(this.inner)
|
||||
this.calculated_height = shared.calculateTotalChildrenHeight(
|
||||
this.inner,
|
||||
)
|
||||
}, ms)
|
||||
}
|
||||
|
||||
@@ -408,6 +410,7 @@ class NotePlus extends LiteGraph.LGraphNode {
|
||||
this.editorsContainer,
|
||||
)
|
||||
|
||||
|
||||
//- "Dynamic" elements
|
||||
const syncUI = () => {
|
||||
// let convert_to_html =
|
||||
@@ -458,6 +461,7 @@ class NotePlus extends LiteGraph.LGraphNode {
|
||||
|
||||
syncUI()
|
||||
|
||||
|
||||
this.setupEditors()
|
||||
}
|
||||
loadAceEditor() {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,768 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "vite-project",
|
||||
"dependencies": {
|
||||
"@untemps/svelte-palette": "^4.1.0",
|
||||
"iconify-icon": "^3.0.0",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@comfyorg/comfyui-frontend-types": "^1.20.2",
|
||||
"@comfyorg/litegraph": "^0.15.15",
|
||||
"@neodrag/svelte": "^2.3.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@swc/cli": "^0.7.7",
|
||||
"@swc/core": "^1.11.31",
|
||||
"@types/node": "^22.15.30",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"sass": "^1.89.1",
|
||||
"svelte": "^5.33.14",
|
||||
"svelte-awesome-color-picker": "^4.0.2",
|
||||
"svelte-dnd-action": "^0.9.61",
|
||||
"svelte-portal": "^2.2.1",
|
||||
"syncpack": "^13.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-no-bundle": "^4.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@swc/core",
|
||||
"@parcel/watcher",
|
||||
],
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.24.5", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
||||
|
||||
"@comfyorg/comfyui-frontend-types": ["@comfyorg/comfyui-frontend-types@1.21.7", "", { "dependencies": { "@comfyorg/litegraph": "^0.15.15" }, "peerDependencies": { "vue": "^3.5.13", "zod": "^3.23.8" } }, "sha512-IqfqUYIeFremdlMo+MSZdrro2PcuI2biUO+j4OPF1jWEZboAUEiLTlo8RH5nhA/tWgZaLd2/LJ0GvfgeGRdcjA=="],
|
||||
|
||||
"@comfyorg/litegraph": ["@comfyorg/litegraph@0.15.15", "", {}, "sha512-otOKgTxNPV6gEa6PW1fHGMMF8twjnZkP0vWQhGsRISK4vN8tPfX8O9sC9Hnq3nV8axaMv4/Ff49+7mMVcFEKeA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
|
||||
|
||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@neodrag/svelte": ["@neodrag/svelte@2.3.2", "", { "peerDependencies": { "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, "sha512-1aPgnv2LqikkPEoTVgCPCqtXUXOD7OjCHLAIssTv0hkmyHLG9i697Cu+Tzco4/Q4KgOQXCDnLsa4Fvya5wXP9g=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
|
||||
|
||||
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.41.1", "", { "os": "android", "cpu": "arm" }, "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.41.1", "", { "os": "android", "cpu": "arm64" }, "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.41.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.41.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.41.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.41.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.41.1", "", { "os": "linux", "cpu": "arm" }, "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.41.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.41.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.41.1", "", { "os": "linux", "cpu": "none" }, "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.41.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.41.1", "", { "os": "linux", "cpu": "x64" }, "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.41.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.41.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.41.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="],
|
||||
|
||||
"@swc/cli": ["@swc/cli@0.7.7", "", { "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", "commander": "^8.3.0", "fast-glob": "^3.2.5", "minimatch": "^9.0.3", "piscina": "^4.3.1", "semver": "^7.3.8", "slash": "3.0.0", "source-map": "^0.7.3" }, "peerDependencies": { "@swc/core": "^1.2.66", "chokidar": "^4.0.1" }, "optionalPeers": ["chokidar"], "bin": { "swc": "bin/swc.js", "swcx": "bin/swcx.js", "spack": "bin/spack.js" } }, "sha512-j4yYm9bx3pxWofaJKX1BFwj/3ngUDynN4UIQ2Xd2h0h/7Gt7zkReBTpDN7g5S13mgAYxacaTHTOUsz18097E8w=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.11.31", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.31", "@swc/core-darwin-x64": "1.11.31", "@swc/core-linux-arm-gnueabihf": "1.11.31", "@swc/core-linux-arm64-gnu": "1.11.31", "@swc/core-linux-arm64-musl": "1.11.31", "@swc/core-linux-x64-gnu": "1.11.31", "@swc/core-linux-x64-musl": "1.11.31", "@swc/core-win32-arm64-msvc": "1.11.31", "@swc/core-win32-ia32-msvc": "1.11.31", "@swc/core-win32-x64-msvc": "1.11.31" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-mAby9aUnKRjMEA7v8cVZS9Ah4duoRBnX7X6r5qrhTxErx+68MoY1TPrVwj/66/SWN3Bl+jijqAqoB8Qx0QE34A=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.31", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.31", "", { "os": "darwin", "cpu": "x64" }, "sha512-THSGaSwT96JwXDwuXQ6yFBbn+xDMdyw7OmBpnweAWsh5DhZmQkALEm1DgdQO3+rrE99MkmzwAfclc0UmYro/OA=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.31", "", { "os": "linux", "cpu": "arm" }, "sha512-laKtQFnW7KHgE57Hx32os2SNAogcuIDxYE+3DYIOmDMqD7/1DCfJe6Rln2N9WcOw6HuDbDpyQavIwZNfSAa8vQ=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.31", "", { "os": "linux", "cpu": "arm64" }, "sha512-T+vGw9aPE1YVyRxRr1n7NAdkbgzBzrXCCJ95xAZc/0+WUwmL77Z+js0J5v1KKTRxw4FvrslNCOXzMWrSLdwPSA=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.31", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mztp5NZkyd5MrOAG+kl+QSn0lL4Uawd4CK4J7wm97Hs44N9DHGIG5nOz7Qve1KZo407Y25lTxi/PqzPKHo61zQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.31", "", { "os": "linux", "cpu": "x64" }, "sha512-DDVE0LZcXOWwOqFU1Xi7gdtiUg3FHA0vbGb3trjWCuI1ZtDZHEQYL4M3/2FjqKZtIwASrDvO96w91okZbXhvMg=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.31", "", { "os": "linux", "cpu": "x64" }, "sha512-mJA1MzPPRIfaBUHZi0xJQ4vwL09MNWDeFtxXb0r4Yzpf0v5Lue9ymumcBPmw/h6TKWms+Non4+TDquAsweuKSw=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.31", "", { "os": "win32", "cpu": "arm64" }, "sha512-RdtakUkNVAb/FFIMw3LnfNdlH1/ep6KgiPDRlmyUfd0WdIQ3OACmeBegEFNFTzi7gEuzy2Yxg4LWf4IUVk8/bg=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.31", "", { "os": "win32", "cpu": "ia32" }, "sha512-hErXdCGsg7swWdG1fossuL8542I59xV+all751mYlBoZ8kOghLSKObGQTkBbuNvc0sUKWfWg1X0iBuIhAYar+w=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.31", "", { "os": "win32", "cpu": "x64" }, "sha512-5t7SGjUBMMhF9b5j17ml/f/498kiBJNf4vZFNM421UGUEETdtjPN9jZIuQrowBkoFGJTCVL/ECM4YRtTH30u/A=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="],
|
||||
|
||||
"@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="],
|
||||
|
||||
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA=="],
|
||||
|
||||
"@untemps/dom-observer": ["@untemps/dom-observer@2.0.0", "", {}, "sha512-oZBj7j9hYSbuveNFvyPLamRfNh15KvGJC5pLGGpWVcO61EzISMESL0wudwP0UZETEu+0WfaJ16rGXtEZHsq+xw=="],
|
||||
|
||||
"@untemps/svelte-palette": ["@untemps/svelte-palette@4.1.0", "", { "dependencies": { "@untemps/svelte-use-drop-outside": "^1.6.0", "@untemps/svelte-use-tooltip": "^2.8.0", "@untemps/utils": "^3.0.0" }, "peerDependencies": { "svelte": "^4.0.0" } }, "sha512-Hm+e17xhFDETSa/xLecfN5t+nRYBNdO6xRnH+5UaUsZytNFB3uFXd1KrusLakbqPvvps94nqgJE30pc1dnY3aQ=="],
|
||||
|
||||
"@untemps/svelte-use-drop-outside": ["@untemps/svelte-use-drop-outside@1.6.0", "", { "dependencies": { "@untemps/dom-observer": "^2.0.0", "@untemps/utils": "^2.0.0", "svelte": "3.49.0" } }, "sha512-izzpTG27DwMzx7QIIkt1S+Z5JAbzNAKn568JZWJXR1ZXWx6ZAJigj8oFlSDAVltlFjeasDPXPJSmGoXALgTw/A=="],
|
||||
|
||||
"@untemps/svelte-use-tooltip": ["@untemps/svelte-use-tooltip@2.8.0", "", { "dependencies": { "@untemps/dom-observer": "^2.0.0", "@untemps/utils": "^2.4.0", "svelte": "3.49.0" } }, "sha512-rXzYyU1JzKucyjAr4J+byrDUk0eFhwC8ggiitJE/v945OJDjjA29dA2d+DTjyppqlm2ZQZzGHYKY5hrdRKdrAg=="],
|
||||
|
||||
"@untemps/utils": ["@untemps/utils@3.0.0", "", {}, "sha512-Iv7a1nvbTzk7ho8bry5iN/RYtns4dwNRuTqD49ixq0N3dOpoMIdSlhvVanbxEmRBqtV7MirCBEfScpTjDbo0ag=="],
|
||||
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.16", "", { "dependencies": { "@babel/parser": "^7.27.2", "@vue/shared": "3.5.16", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ=="],
|
||||
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.16", "", { "dependencies": { "@vue/compiler-core": "3.5.16", "@vue/shared": "3.5.16" } }, "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ=="],
|
||||
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.16", "", { "dependencies": { "@babel/parser": "^7.27.2", "@vue/compiler-core": "3.5.16", "@vue/compiler-dom": "3.5.16", "@vue/compiler-ssr": "3.5.16", "@vue/shared": "3.5.16", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.3", "source-map-js": "^1.2.1" } }, "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw=="],
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.16", "", { "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/shared": "3.5.16" } }, "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.16", "", { "dependencies": { "@vue/shared": "3.5.16" } }, "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.16", "", { "dependencies": { "@vue/reactivity": "3.5.16", "@vue/shared": "3.5.16" } }, "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ=="],
|
||||
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.16", "", { "dependencies": { "@vue/reactivity": "3.5.16", "@vue/runtime-core": "3.5.16", "@vue/shared": "3.5.16", "csstype": "^3.1.3" } }, "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww=="],
|
||||
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.16", "", { "dependencies": { "@vue/compiler-ssr": "3.5.16", "@vue/shared": "3.5.16" }, "peerDependencies": { "vue": "3.5.16" } }, "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg=="],
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.16", "", {}, "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg=="],
|
||||
|
||||
"@xhmikosr/archive-type": ["@xhmikosr/archive-type@7.0.0", "", { "dependencies": { "file-type": "^19.0.0" } }, "sha512-sIm84ZneCOJuiy3PpWR5bxkx3HaNt1pqaN+vncUBZIlPZCq8ASZH+hBVdu5H8znR7qYC6sKwx+ie2Q7qztJTxA=="],
|
||||
|
||||
"@xhmikosr/bin-check": ["@xhmikosr/bin-check@7.0.3", "", { "dependencies": { "execa": "^5.1.1", "isexe": "^2.0.0" } }, "sha512-4UnCLCs8DB+itHJVkqFp9Zjg+w/205/J2j2wNBsCEAm/BuBmtua2hhUOdAMQE47b1c7P9Xmddj0p+X1XVsfHsA=="],
|
||||
|
||||
"@xhmikosr/bin-wrapper": ["@xhmikosr/bin-wrapper@13.0.5", "", { "dependencies": { "@xhmikosr/bin-check": "^7.0.3", "@xhmikosr/downloader": "^15.0.1", "@xhmikosr/os-filter-obj": "^3.0.0", "bin-version-check": "^5.1.0" } }, "sha512-DT2SAuHDeOw0G5bs7wZbQTbf4hd8pJ14tO0i4cWhRkIJfgRdKmMfkDilpaJ8uZyPA0NVRwasCNAmMJcWA67osw=="],
|
||||
|
||||
"@xhmikosr/decompress": ["@xhmikosr/decompress@10.0.1", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.0.1", "@xhmikosr/decompress-tarbz2": "^8.0.1", "@xhmikosr/decompress-targz": "^8.0.1", "@xhmikosr/decompress-unzip": "^7.0.0", "graceful-fs": "^4.2.11", "make-dir": "^4.0.0", "strip-dirs": "^3.0.0" } }, "sha512-6uHnEEt5jv9ro0CDzqWlFgPycdE+H+kbJnwyxgZregIMLQ7unQSCNVsYG255FoqU8cP46DyggI7F7LohzEl8Ag=="],
|
||||
|
||||
"@xhmikosr/decompress-tar": ["@xhmikosr/decompress-tar@8.0.1", "", { "dependencies": { "file-type": "^19.0.0", "is-stream": "^2.0.1", "tar-stream": "^3.1.7" } }, "sha512-dpEgs0cQKJ2xpIaGSO0hrzz3Kt8TQHYdizHsgDtLorWajuHJqxzot9Hbi0huRxJuAGG2qiHSQkwyvHHQtlE+fg=="],
|
||||
|
||||
"@xhmikosr/decompress-tarbz2": ["@xhmikosr/decompress-tarbz2@8.0.2", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.0.1", "file-type": "^19.6.0", "is-stream": "^2.0.1", "seek-bzip": "^2.0.0", "unbzip2-stream": "^1.4.3" } }, "sha512-p5A2r/AVynTQSsF34Pig6olt9CvRj6J5ikIhzUd3b57pUXyFDGtmBstcw+xXza0QFUh93zJsmY3zGeNDlR2AQQ=="],
|
||||
|
||||
"@xhmikosr/decompress-targz": ["@xhmikosr/decompress-targz@8.0.1", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.0.1", "file-type": "^19.0.0", "is-stream": "^2.0.1" } }, "sha512-mvy5AIDIZjQ2IagMI/wvauEiSNHhu/g65qpdM4EVoYHUJBAmkQWqcPJa8Xzi1aKVTmOA5xLJeDk7dqSjlHq8Mg=="],
|
||||
|
||||
"@xhmikosr/decompress-unzip": ["@xhmikosr/decompress-unzip@7.0.0", "", { "dependencies": { "file-type": "^19.0.0", "get-stream": "^6.0.1", "yauzl": "^3.1.2" } }, "sha512-GQMpzIpWTsNr6UZbISawsGI0hJ4KA/mz5nFq+cEoPs12UybAqZWKbyIaZZyLbJebKl5FkLpsGBkrplJdjvUoSQ=="],
|
||||
|
||||
"@xhmikosr/downloader": ["@xhmikosr/downloader@15.0.1", "", { "dependencies": { "@xhmikosr/archive-type": "^7.0.0", "@xhmikosr/decompress": "^10.0.1", "content-disposition": "^0.5.4", "defaults": "^3.0.0", "ext-name": "^5.0.0", "file-type": "^19.0.0", "filenamify": "^6.0.0", "get-stream": "^6.0.1", "got": "^13.0.0" } }, "sha512-fiuFHf3Dt6pkX8HQrVBsK0uXtkgkVlhrZEh8b7VgoDqFf+zrgFBPyrwCqE/3nDwn3hLeNz+BsrS7q3mu13Lp1g=="],
|
||||
|
||||
"@xhmikosr/os-filter-obj": ["@xhmikosr/os-filter-obj@3.0.0", "", { "dependencies": { "arch": "^3.0.0" } }, "sha512-siPY6BD5dQ2SZPl3I0OZBHL27ZqZvLEosObsZRQ1NUB8qcxegwt0T9eKtV96JMFQpIz1elhkzqOg4c/Ri6Dp9A=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"arch": ["arch@3.0.0", "", {}, "sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bin-version": ["bin-version@6.0.0", "", { "dependencies": { "execa": "^5.0.0", "find-versions": "^5.0.0" } }, "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw=="],
|
||||
|
||||
"bin-version-check": ["bin-version-check@5.1.0", "", { "dependencies": { "bin-version": "^6.0.0", "semver": "^7.5.3", "semver-truncate": "^3.0.0" } }, "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"braces": ["braces@3.0.2", "", { "dependencies": { "fill-range": "^7.0.1" } }, "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
|
||||
"cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="],
|
||||
|
||||
"cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"chalk-template": ["chalk-template@1.1.0", "", { "dependencies": { "chalk": "^5.2.0" } }, "sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="],
|
||||
|
||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"defaults": ["defaults@3.0.0", "", {}, "sha512-RsqXDEAALjfRTro+IFNKpcPCt0/Cy2FqHSIlnomiJp9YGadpQnrtbRpSgN2+np21qHcIKiva4fiOQGjS9/qR/A=="],
|
||||
|
||||
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||
|
||||
"effect": ["effect@3.16.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-zJo5MRPEMROLjTHyrOJs0PUeEnRLy0ABwum3+HWlP8Y9LNF+NjipIRldVAcjSVQpJ7vY/OEGBxzw0USPzTfWag=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
||||
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"esrap": ["esrap@1.4.7", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-0ZxW6guTF/AeKeKi7he93lmgv7Hx7giD1tBrOeVqkqsZGQJd2/kfnL7LdIsr9FT/AtkBK9XeDTov+gxprBqdEg=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
|
||||
|
||||
"ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="],
|
||||
|
||||
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
|
||||
|
||||
"fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
|
||||
|
||||
"fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||
|
||||
"file-type": ["file-type@19.6.0", "", { "dependencies": { "get-stream": "^9.0.1", "strtok3": "^9.0.1", "token-types": "^6.0.0", "uint8array-extras": "^1.3.0" } }, "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ=="],
|
||||
|
||||
"filename-reserved-regex": ["filename-reserved-regex@3.0.0", "", {}, "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw=="],
|
||||
|
||||
"filenamify": ["filenamify@6.0.0", "", { "dependencies": { "filename-reserved-regex": "^3.0.0" } }, "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.0.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ=="],
|
||||
|
||||
"find-versions": ["find-versions@5.1.0", "", { "dependencies": { "semver-regex": "^4.0.5" } }, "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
|
||||
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="],
|
||||
|
||||
"got": ["got@13.0.0", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hosted-git-info": ["hosted-git-info@8.1.0", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="],
|
||||
|
||||
"http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="],
|
||||
|
||||
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"iconify-icon": ["iconify-icon@3.0.0", "", { "dependencies": { "@iconify/types": "^2.0.0" } }, "sha512-yPcnpkn8HUEUckrxxJBOer3jbGv3bqozHsLMLBRxhk3As1X76BgV2mS2a1HTNOIagR8nUs30H3qAd9GLe8Mnlg=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"immutable": ["immutable@5.1.2", "", {}, "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"inspect-with-kind": ["inspect-with-kind@1.0.5", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
|
||||
|
||||
"lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.5", "", { "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" } }, "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="],
|
||||
|
||||
"minimatch": ["minimatch@9.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nice-napi": ["nice-napi@1.0.2", "", { "dependencies": { "node-addon-api": "^3.0.0", "node-gyp-build": "^4.2.2" }, "os": "!win32" }, "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-gyp-build": ["node-gyp-build@4.8.1", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw=="],
|
||||
|
||||
"normalize-url": ["normalize-url@8.0.1", "", {}, "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w=="],
|
||||
|
||||
"npm-package-arg": ["npm-package-arg@12.0.2", "", { "dependencies": { "hosted-git-info": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^6.0.0" } }, "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
|
||||
|
||||
"p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="],
|
||||
|
||||
"peek-readable": ["peek-readable@5.4.2", "", {}, "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg=="],
|
||||
|
||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"piscina": ["piscina@4.4.0", "", { "optionalDependencies": { "nice-napi": "^1.0.2" } }, "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="],
|
||||
|
||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||
|
||||
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ=="],
|
||||
|
||||
"proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
|
||||
|
||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||
|
||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
|
||||
|
||||
"read-yaml-file": ["read-yaml-file@2.1.0", "", { "dependencies": { "js-yaml": "^4.0.0", "strip-bom": "^4.0.0" } }, "sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
||||
|
||||
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
|
||||
|
||||
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"sass": ["sass@1.89.1", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q=="],
|
||||
|
||||
"seek-bzip": ["seek-bzip@2.0.0", "", { "dependencies": { "commander": "^6.0.0" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg=="],
|
||||
|
||||
"semver": ["semver@7.6.2", "", { "bin": "bin/semver.js" }, "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="],
|
||||
|
||||
"semver-regex": ["semver-regex@4.0.5", "", {}, "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw=="],
|
||||
|
||||
"semver-truncate": ["semver-truncate@3.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="],
|
||||
|
||||
"sort-keys-length": ["sort-keys-length@1.0.1", "", { "dependencies": { "sort-keys": "^1.0.0" } }, "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw=="],
|
||||
|
||||
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.0", "", {}, "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg=="],
|
||||
|
||||
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
||||
|
||||
"streamx": ["streamx@2.22.1", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="],
|
||||
|
||||
"strip-dirs": ["strip-dirs@3.0.0", "", { "dependencies": { "inspect-with-kind": "^1.0.5", "is-plain-obj": "^1.1.0" } }, "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
"strtok3": ["strtok3@9.1.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.3.1" } }, "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw=="],
|
||||
|
||||
"svelte": ["svelte@5.33.14", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-kRlbhIlMTijbFmVDQFDeKXPLlX1/ovXwV0I162wRqQhRcygaqDIcu1d/Ese3H2uI+yt3uT8E7ndgDthQv5v5BA=="],
|
||||
|
||||
"svelte-awesome-color-picker": ["svelte-awesome-color-picker@4.0.2", "", { "dependencies": { "colord": "^2.9.3", "svelte-awesome-slider": "2.0.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-Ez72goMMNmw6sZhB1/BXEA8984lEkudPrdlNS+y3nHm2Lnk1w4nwy5NFyWPxTP7nFnLxhIqyV3VuJVG4PokKwg=="],
|
||||
|
||||
"svelte-awesome-slider": ["svelte-awesome-slider@2.0.0", "", { "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-YBkOdYm1Feaqsn2JkJBRs+Kc/X3Qy/3GuVmI7GmoYDjBaHkjx9uH4khTuED22z57Hg3gGWeDhp/clIjWDdLNaw=="],
|
||||
|
||||
"svelte-dnd-action": ["svelte-dnd-action@0.9.61", "", { "peerDependencies": { "svelte": ">=3.23.0 || ^5.0.0-next.0" } }, "sha512-xj0jZNdV44MAMbdGIAuBYEombmK7A7ticemOtdR5ErWw0f3IxcfcnrRjlYEK0rRjsahVsU+VdGUMwjPVyXrSrA=="],
|
||||
|
||||
"svelte-portal": ["svelte-portal@2.2.1", "", {}, "sha512-uF7is5sM4aq5iN7QF/67XLnTUvQCf2iiG/B1BHTqLwYVY1dsVmTeXZ/LeEyU6dLjApOQdbEG9lkqHzxiQtOLEQ=="],
|
||||
|
||||
"syncpack": ["syncpack@13.0.4", "", { "dependencies": { "chalk": "^5.4.1", "chalk-template": "^1.1.0", "commander": "^13.1.0", "cosmiconfig": "^9.0.0", "effect": "^3.13.7", "enquirer": "^2.4.1", "fast-check": "^3.23.2", "globby": "^14.1.0", "jsonc-parser": "^3.3.1", "minimatch": "9.0.5", "npm-package-arg": "^12.0.2", "ora": "^8.2.0", "prompts": "^2.4.2", "read-yaml-file": "^2.1.0", "semver": "^7.7.1", "tightrope": "0.2.0", "ts-toolbelt": "^9.6.0" }, "bin": { "syncpack": "dist/bin.js", "syncpack-lint": "dist/bin-lint/index.js", "syncpack-list": "dist/bin-list/index.js", "syncpack-format": "dist/bin-format/index.js", "syncpack-prompt": "dist/bin-prompt/index.js", "syncpack-update": "dist/bin-update/index.js", "syncpack-fix-mismatches": "dist/bin-fix-mismatches/index.js", "syncpack-list-mismatches": "dist/bin-list-mismatches/index.js", "syncpack-set-semver-ranges": "dist/bin-set-semver-ranges/index.js", "syncpack-lint-semver-ranges": "dist/bin-lint-semver-ranges/index.js" } }, "sha512-kJ9VlRxNCsBD5pJAE29oXeBYbPLhEySQmK4HdpsLv81I6fcDDW17xeJqMwiU3H7/woAVsbgq25DJNS8BeiN5+w=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
|
||||
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
|
||||
|
||||
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
|
||||
|
||||
"tightrope": ["tightrope@0.2.0", "", {}, "sha512-Kw36UHxJEELq2VUqdaSGR2/8cAsPgMtvX8uGVU6Jk26O66PhXec0A5ZnRYs47btbtwPDpXXF66+Fo3vimCM9aQ=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
||||
|
||||
"ts-toolbelt": ["ts-toolbelt@9.6.0", "", {}, "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
|
||||
|
||||
"unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"validate-npm-package-name": ["validate-npm-package-name@6.0.1", "", {}, "sha512-OaI//3H0J7ZkR1OqlhGA8cA+Cbk/2xFOQpJOt5+s27/ta9eZwpeervh4Mxh4w0im/kdgktowaqVNR7QOrUd7Yg=="],
|
||||
|
||||
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||
|
||||
"vite-plugin-css-injected-by-js": ["vite-plugin-css-injected-by-js@3.5.2", "", { "peerDependencies": { "vite": ">2.0.0-0" } }, "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ=="],
|
||||
|
||||
"vite-plugin-no-bundle": ["vite-plugin-no-bundle@4.0.0", "", { "dependencies": { "fast-glob": "^3.3.2", "micromatch": "^4.0.5" } }, "sha512-DXsJGXtp/QLWNFBfBqr+arxaTHEgiPCAgf9bcOPGv4n3AsFigsFj+oL95nFdMt8cRbgRDtvyTX802IZNBGg3Xg=="],
|
||||
|
||||
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
|
||||
|
||||
"vue": ["vue@3.5.16", "", { "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/compiler-sfc": "3.5.16", "@vue/runtime-dom": "3.5.16", "@vue/server-renderer": "3.5.16", "@vue/shared": "3.5.16" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"yauzl": ["yauzl@3.2.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
|
||||
|
||||
"zod": ["zod@3.25.51", "", {}, "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="],
|
||||
|
||||
"@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="],
|
||||
|
||||
"@untemps/svelte-use-drop-outside/@untemps/utils": ["@untemps/utils@2.6.0", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-3B5bb4iYkh2h86vqtY8+Mo9vdX1s/NT1cltWpS4U3/WHXFgy/4lJ9180vELJv01MbNNhjlZPtJCMtBp7Fpsb1Q=="],
|
||||
|
||||
"@untemps/svelte-use-drop-outside/svelte": ["svelte@3.49.0", "", {}, "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA=="],
|
||||
|
||||
"@untemps/svelte-use-tooltip/@untemps/utils": ["@untemps/utils@2.6.0", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-3B5bb4iYkh2h86vqtY8+Mo9vdX1s/NT1cltWpS4U3/WHXFgy/4lJ9180vELJv01MbNNhjlZPtJCMtBp7Fpsb1Q=="],
|
||||
|
||||
"@untemps/svelte-use-tooltip/svelte": ["svelte@3.49.0", "", {}, "sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA=="],
|
||||
|
||||
"@vue/compiler-core/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"@vue/compiler-sfc/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"file-type/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"globby/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
|
||||
|
||||
"is-reference/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"nice-napi/node-addon-api": ["node-addon-api@3.2.1", "", {}, "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="],
|
||||
|
||||
"npm-package-arg/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"ora/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"postcss/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"rollup/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"seek-bzip/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"syncpack/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"syncpack/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"syncpack/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"file-type/get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"globby/fast-glob/micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"globby/fast-glob/micromatch/braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"globby/fast-glob/micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"globby/fast-glob/micromatch/braces/fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>mtb web source</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "comfy-mtb-web",
|
||||
"private": true,
|
||||
"version": "0.4.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && bun run postbuild",
|
||||
"postbuild": "cp -r dist/* ../web/dist/",
|
||||
"preview": "vite preview",
|
||||
"check-deps": "bun x syncpack list"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@comfyorg/comfyui-frontend-types": "^1.20.2",
|
||||
"@comfyorg/litegraph": "^0.15.15",
|
||||
"@neodrag/svelte": "^2.3.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@swc/cli": "^0.7.7",
|
||||
"@swc/core": "^1.11.31",
|
||||
"@types/node": "^22.15.30",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"sass": "^1.89.1",
|
||||
"svelte": "^5.33.14",
|
||||
"svelte-awesome-color-picker": "^4.0.2",
|
||||
"svelte-dnd-action": "^0.9.61",
|
||||
"svelte-portal": "^2.2.1",
|
||||
"syncpack": "^13.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-no-bundle": "^4.0.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.15",
|
||||
"volta": {
|
||||
"node": "22.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@untemps/svelte-palette": "^4.1.0",
|
||||
"iconify-icon": "^3.0.0",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||
},
|
||||
"trustedDependencies": ["@parcel/watcher", "@swc/core", "svelte-preprocess"]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<script>
|
||||
import Inspector from './lib/Inspector.svelte'
|
||||
import './app.css'
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Inspector />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
:root {
|
||||
--fg-color: #fff;
|
||||
--bg-color: #202020;
|
||||
--comfy-menu-bg: #353535;
|
||||
--comfy-input-bg: #222;
|
||||
--input-text: #ddd;
|
||||
--descrip-text: #999;
|
||||
--drag-text: #ccc;
|
||||
--error-text: #ff4444;
|
||||
--border-color: #4e4e4e;
|
||||
--tr-even-bg-color: #222;
|
||||
--tr-odd-bg-color: #353535;
|
||||
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
/* background-color: #242424; */
|
||||
|
||||
background-color: #646464;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
/**/
|
||||
/* button { */
|
||||
/* border-radius: 8px; */
|
||||
/* border: 1px solid transparent; */
|
||||
/* padding: 0.6em 1.2em; */
|
||||
/* font-size: 1em; */
|
||||
/* font-weight: 500; */
|
||||
/* font-family: inherit; */
|
||||
/* background-color: #1a1a1a; */
|
||||
/* cursor: pointer; */
|
||||
/* transition: border-color 0.25s; */
|
||||
/* } */
|
||||
/* button:hover { */
|
||||
/* border-color: #646cff; */
|
||||
/* } */
|
||||
/* button:focus, */
|
||||
/* button:focus-visible { */
|
||||
/* outline: 4px auto -webkit-focus-ring-color; */
|
||||
/* } */
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/// <reference types="comfyui-frontend-types" />
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Server API utilities
|
||||
*/
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
export interface ActionResult {
|
||||
result: unknown
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a server action
|
||||
*/
|
||||
export const runAction = async (name: string, ...args: unknown[]): Promise<unknown> => {
|
||||
const req = await api.fetchApi('/mtb/actions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
args,
|
||||
}),
|
||||
})
|
||||
|
||||
const res = (await req.json()) as ActionResult
|
||||
return res.result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server info
|
||||
*/
|
||||
export const getServerInfo = async (): Promise<ServerInfo> => {
|
||||
const res = await api.fetchApi('/mtb/server-info')
|
||||
return (await res.json()) as ServerInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Set server info
|
||||
*/
|
||||
export const setServerInfo = async (opts: ServerInfo): Promise<void> => {
|
||||
await api.fetchApi('/mtb/server-info', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(opts),
|
||||
})
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Color utilities
|
||||
*/
|
||||
|
||||
type RGB = [number, number, number]
|
||||
|
||||
function getBrightness(rgb: RGB): number {
|
||||
return Math.round(
|
||||
(Number.parseInt(String(rgb[0])) * 299 +
|
||||
Number.parseInt(String(rgb[1])) * 587 +
|
||||
Number.parseInt(String(rgb[2])) * 114) /
|
||||
1000,
|
||||
)
|
||||
}
|
||||
|
||||
export function isColorBright(rgb: RGB, threshold = 240): boolean {
|
||||
const brightness = getBrightness(rgb)
|
||||
return brightness > threshold
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
/**
|
||||
* Documentation widget for nodes
|
||||
*/
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import type { MTBNode, NodeType, NodeData, DocumentationOptions, MarkdownParser } from './types'
|
||||
import { infoLogger } from './logger'
|
||||
|
||||
const create_documentation_stylesheet = (): void => {
|
||||
const tag = 'mtb-documentation-stylesheet'
|
||||
let styleTag = document.head.querySelector(`#${tag}`) as HTMLStyleElement | null
|
||||
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style')
|
||||
styleTag.type = 'text/css'
|
||||
styleTag.id = tag
|
||||
|
||||
styleTag.innerHTML = `
|
||||
.documentation-popup {
|
||||
background: var(--comfy-menu-bg);
|
||||
position: absolute;
|
||||
color: var(--fg-color);
|
||||
font: 12px monospace;
|
||||
line-height: 1.5em;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
pointer-events: "inherit";
|
||||
z-index: 5;
|
||||
overflow: hidden;
|
||||
}
|
||||
.documentation-wrapper {
|
||||
padding: 0 2em;
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--fg-color);
|
||||
border-radius: 6px;
|
||||
border: 3px solid var(--bg-color);
|
||||
}
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--fg-color) var(--bg-color);
|
||||
a {
|
||||
color: yellow;
|
||||
}
|
||||
a:visited {
|
||||
color: orange;
|
||||
}
|
||||
a:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
.documentation-popup img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.documentation-popup table {
|
||||
border-collapse: collapse;
|
||||
border: 1px var(--border-color) solid;
|
||||
}
|
||||
.documentation-popup th,
|
||||
.documentation-popup td {
|
||||
border: 1px var(--border-color) solid;
|
||||
}
|
||||
.documentation-popup th {
|
||||
background-color: var(--comfy-input-bg);
|
||||
}`
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
|
||||
let parserPromise: Promise<MarkdownParser> | undefined
|
||||
const callbackQueue: Array<(parser: MarkdownParser) => void> = []
|
||||
|
||||
function runQueuedCallbacks(): void {
|
||||
while (callbackQueue.length) {
|
||||
const cb = callbackQueue.shift()
|
||||
if (cb && window.MTB?.mdParser) {
|
||||
cb(window.MTB.mdParser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadParser(shiki: boolean): Promise<MarkdownParser> {
|
||||
if (!parserPromise) {
|
||||
parserPromise = import(
|
||||
shiki ? '/mtb_async/mtb_markdown_plus.umd.js' : '/mtb_async/mtb_markdown.umd.js'
|
||||
)
|
||||
.then(() => (shiki ? window.MTBMarkdownPlus!.getParser() : window.MTBMarkdown!.getParser()))
|
||||
.then((instance) => {
|
||||
window.MTB = window.MTB || {}
|
||||
window.MTB.mdParser = instance
|
||||
runQueuedCallbacks()
|
||||
return instance
|
||||
})
|
||||
.catch((error) => {
|
||||
// biome-ignore lint/suspicious/noConsole: error logging
|
||||
console.error('Error loading the parser:', error)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
return parserPromise
|
||||
}
|
||||
|
||||
export const ensureMarkdownParser = async (
|
||||
callback?: (parser: MarkdownParser) => void,
|
||||
): Promise<MarkdownParser> => {
|
||||
infoLogger('Ensuring md parser')
|
||||
const use_shiki = app.extensionManager?.setting?.get('mtb.noteplus.use-shiki', false) as boolean
|
||||
|
||||
if (window.MTB?.mdParser) {
|
||||
infoLogger('Markdown parser found')
|
||||
callback?.(window.MTB.mdParser)
|
||||
return window.MTB.mdParser
|
||||
}
|
||||
|
||||
if (!parserPromise) {
|
||||
infoLogger('Running promise to fetch parser')
|
||||
try {
|
||||
loadParser(use_shiki)
|
||||
} catch (error) {
|
||||
// biome-ignore lint/suspicious/noConsole: error logging
|
||||
console.error('Error loading the parser:', error)
|
||||
}
|
||||
} else {
|
||||
infoLogger('A similar promise is already running, waiting for it to finish')
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callbackQueue.push(callback)
|
||||
}
|
||||
|
||||
await parserPromise
|
||||
|
||||
return window.MTB!.mdParser!
|
||||
}
|
||||
|
||||
/**
|
||||
* Add documentation widget to the given node.
|
||||
*/
|
||||
export const addDocumentation = (
|
||||
nodeData: NodeData,
|
||||
nodeType: NodeType,
|
||||
opts: DocumentationOptions = { icon_size: 14, icon_margin: 4 },
|
||||
): void => {
|
||||
if (!nodeData.description) {
|
||||
infoLogger(`Skipping ${nodeData.name} doesn't have a description, skipping...`)
|
||||
return
|
||||
}
|
||||
|
||||
const options = opts || {}
|
||||
const iconSize = options.icon_size || 14
|
||||
const iconMargin = options.icon_margin || 4
|
||||
|
||||
let docElement: HTMLDivElement | null = null
|
||||
let wrapper: HTMLDivElement | null = null
|
||||
|
||||
const onRem = nodeType.prototype.onRemoved
|
||||
|
||||
nodeType.prototype.onRemoved = function (this: MTBNode) {
|
||||
const r = onRem ? onRem.apply(this) : undefined
|
||||
|
||||
if (docElement) {
|
||||
docElement.remove()
|
||||
docElement = null
|
||||
}
|
||||
|
||||
if (wrapper) {
|
||||
wrapper.remove()
|
||||
wrapper = null
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
const drawFg = nodeType.prototype.onDrawForeground
|
||||
|
||||
nodeType.prototype.onDrawForeground = function (
|
||||
this: MTBNode,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvas: unknown,
|
||||
) {
|
||||
const r = drawFg ? drawFg.apply(this, [ctx, canvas]) : undefined
|
||||
|
||||
if (this.flags.collapsed) return r
|
||||
|
||||
const x = this.size[0] - iconSize - iconMargin
|
||||
|
||||
// create it
|
||||
if (this.show_doc && docElement === null) {
|
||||
create_documentation_stylesheet()
|
||||
|
||||
docElement = document.createElement('div')
|
||||
docElement.classList.add('documentation-popup')
|
||||
document.body.appendChild(docElement)
|
||||
|
||||
wrapper = document.createElement('div')
|
||||
wrapper.classList.add('documentation-wrapper')
|
||||
docElement.appendChild(wrapper)
|
||||
|
||||
ensureMarkdownParser().then(() => {
|
||||
window.MTB!.mdParser!.parse(nodeData.description!).then((e) => {
|
||||
if (!wrapper) return
|
||||
wrapper.innerHTML = e
|
||||
|
||||
// resize handle
|
||||
const resizeHandle = document.createElement('div')
|
||||
resizeHandle.classList.add('doc-resize-handle')
|
||||
Object.assign(resizeHandle.style, {
|
||||
width: '0',
|
||||
height: '0',
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
cursor: 'se-resize',
|
||||
userSelect: 'none',
|
||||
borderWidth: '15px',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'transparent var(--border-color) var(--border-color) transparent',
|
||||
})
|
||||
|
||||
wrapper.appendChild(resizeHandle)
|
||||
let isResizing = false
|
||||
let startX: number
|
||||
let startY: number
|
||||
let startWidth: number
|
||||
let startHeight: number
|
||||
|
||||
resizeHandle.addEventListener(
|
||||
'mousedown',
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
isResizing = true
|
||||
startX = e.clientX
|
||||
startY = e.clientY
|
||||
startWidth = Number.parseInt(
|
||||
document.defaultView!.getComputedStyle(docElement!).width,
|
||||
10,
|
||||
)
|
||||
startHeight = Number.parseInt(
|
||||
document.defaultView!.getComputedStyle(docElement!).height,
|
||||
10,
|
||||
)
|
||||
},
|
||||
{ signal: this.docCtrl!.signal },
|
||||
)
|
||||
|
||||
document.addEventListener(
|
||||
'mousemove',
|
||||
(e) => {
|
||||
if (!isResizing || !docElement) return
|
||||
const scale = app.canvas.ds.scale
|
||||
const newWidth = startWidth + (e.clientX - startX) / scale
|
||||
const newHeight = startHeight + (e.clientY - startY) / scale
|
||||
|
||||
docElement.style.width = `${newWidth}px`
|
||||
docElement.style.height = `${newHeight}px`
|
||||
|
||||
this.docPos = {
|
||||
width: `${newWidth}px`,
|
||||
height: `${newHeight}px`,
|
||||
}
|
||||
},
|
||||
{ signal: this.docCtrl!.signal },
|
||||
)
|
||||
|
||||
document.addEventListener(
|
||||
'mouseup',
|
||||
() => {
|
||||
isResizing = false
|
||||
},
|
||||
{ signal: this.docCtrl!.signal },
|
||||
)
|
||||
})
|
||||
})
|
||||
} else if (!this.show_doc && docElement !== null) {
|
||||
docElement.remove()
|
||||
docElement = null
|
||||
}
|
||||
|
||||
// reposition
|
||||
if (this.show_doc && docElement !== null) {
|
||||
const rect = ctx.canvas.getBoundingClientRect()
|
||||
const scaleX = rect.width / ctx.canvas.width
|
||||
const scaleY = rect.height / ctx.canvas.height
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(scaleX, scaleY)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(this.size[0] * scaleX * Math.max(1.0, window.devicePixelRatio), 0)
|
||||
.translateSelf(10, -32)
|
||||
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
|
||||
Object.assign(docElement.style, {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale.toString(),
|
||||
left: `${transform.a + rect.x + transform.e}px`,
|
||||
top: `${transform.d + rect.y + transform.f}px`,
|
||||
width: this.docPos ? this.docPos.width : `${this.size[0] * 1.5}px`,
|
||||
height: this.docPos?.height,
|
||||
})
|
||||
|
||||
if (this.docPos === undefined) {
|
||||
this.docPos = {
|
||||
width: docElement.style.width,
|
||||
height: docElement.style.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(x, iconSize - 34)
|
||||
ctx.scale(iconSize / 32, iconSize / 32)
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.lineWidth = 2.4
|
||||
ctx.font = 'bold 36px monospace'
|
||||
ctx.fillText('?', 0, 24)
|
||||
ctx.restore()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
const mouseDown = nodeType.prototype.onMouseDown
|
||||
|
||||
nodeType.prototype.onMouseDown = function (
|
||||
this: MTBNode,
|
||||
event: MouseEvent,
|
||||
localPos: [number, number],
|
||||
graphCanvas: unknown,
|
||||
): boolean | void {
|
||||
const r = mouseDown ? mouseDown.apply(this, [event, localPos, graphCanvas]) : undefined
|
||||
const iconX = this.size[0] - iconSize - iconMargin
|
||||
const iconY = iconSize - 34
|
||||
|
||||
if (
|
||||
localPos[0] > iconX &&
|
||||
localPos[0] < iconX + iconSize &&
|
||||
localPos[1] > iconY &&
|
||||
localPos[1] < iconY + iconSize
|
||||
) {
|
||||
if (this.show_doc === undefined) {
|
||||
this.show_doc = true
|
||||
} else {
|
||||
this.show_doc = !this.show_doc
|
||||
}
|
||||
if (this.show_doc) {
|
||||
this.docCtrl = new AbortController()
|
||||
} else {
|
||||
this.docCtrl?.abort()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* DOM and HTML utilities
|
||||
*/
|
||||
|
||||
import { infoLogger } from './logger'
|
||||
|
||||
/**
|
||||
* Calculate total height of DOM element children
|
||||
*/
|
||||
export function calculateTotalChildrenHeight(parentElement: HTMLElement | null): number {
|
||||
let totalHeight = 0
|
||||
|
||||
if (!parentElement || !parentElement.children) {
|
||||
return 0
|
||||
}
|
||||
|
||||
for (const child of parentElement.children) {
|
||||
const style = window.getComputedStyle(child)
|
||||
|
||||
const height = Number.parseFloat(style.height)
|
||||
const marginTop = Number.parseFloat(style.marginTop)
|
||||
const marginBottom = Number.parseFloat(style.marginBottom)
|
||||
|
||||
totalHeight += height + marginTop + marginBottom
|
||||
}
|
||||
|
||||
return Math.ceil(totalHeight)
|
||||
}
|
||||
|
||||
interface LoadScriptResult {
|
||||
status: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically load a script
|
||||
*/
|
||||
export const loadScript = (
|
||||
FILE_URL: string,
|
||||
async = true,
|
||||
type = 'text/javascript',
|
||||
): Promise<LoadScriptResult> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Check if the script already exists
|
||||
let scriptEle = document.querySelector(`script[src="${FILE_URL}"]`) as HTMLScriptElement | null
|
||||
|
||||
if (scriptEle) {
|
||||
scriptEle.addEventListener('load', () => {
|
||||
resolve({ status: true })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
scriptEle = document.createElement('script')
|
||||
scriptEle.type = type
|
||||
scriptEle.async = async
|
||||
scriptEle.src = FILE_URL
|
||||
|
||||
scriptEle.addEventListener('load', () => {
|
||||
resolve({ status: true })
|
||||
})
|
||||
|
||||
scriptEle.addEventListener('error', () => {
|
||||
reject({
|
||||
status: false,
|
||||
message: `Failed to load the script ${FILE_URL}`,
|
||||
})
|
||||
})
|
||||
|
||||
document.body.appendChild(scriptEle)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
} finally {
|
||||
infoLogger(`Finally loaded script: ${FILE_URL}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
/**
|
||||
* Dynamic connections management for nodes
|
||||
*/
|
||||
|
||||
import type { INodeInputSlot, INodeOutputSlot } from '@comfyorg/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { MTBNode, NodeType, DynamicConnectionOptions, ContextMenuItem } from './types'
|
||||
import { infoLogger, errorLogger } from './logger'
|
||||
import { nodesFromLink } from './widgets'
|
||||
|
||||
type SlotCondition = (slot: INodeInputSlot | INodeOutputSlot) => boolean
|
||||
|
||||
const isDynamicInput = (input: INodeInputSlot & { _isDynamic?: boolean }): boolean => {
|
||||
return input._isDynamic === true
|
||||
}
|
||||
|
||||
const addDynamicInput = (
|
||||
node: MTBNode,
|
||||
name: string,
|
||||
kind: string,
|
||||
): INodeInputSlot & { _isDynamic?: boolean } => {
|
||||
const input = node.addInput(name, kind) as INodeInputSlot & { _isDynamic?: boolean }
|
||||
input._isDynamic = true
|
||||
|
||||
update_dynamic_properties(node)
|
||||
set_slot_colors(node, ['cyan', undefined], isDynamicInput as SlotCondition)
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
const set_slot_colors = (
|
||||
node: MTBNode,
|
||||
colors: [string | undefined, string | undefined],
|
||||
condition?: SlotCondition,
|
||||
): void => {
|
||||
const check = condition || (() => true)
|
||||
|
||||
for (const slot of node.slots || []) {
|
||||
if (check(slot)) {
|
||||
;(slot as INodeInputSlot & { color_off?: string; color_on?: string }).color_off = colors[0]
|
||||
;(slot as INodeInputSlot & { color_on?: string }).color_on = colors[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const update_dynamic_properties = (node: MTBNode): void => {
|
||||
const dyn: string[] = []
|
||||
for (const input of node.inputs) {
|
||||
if (isDynamicInput(input)) {
|
||||
dyn.push(input.name)
|
||||
}
|
||||
}
|
||||
node.setProperty('dynamic_connections', dyn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup dynamic connections for a node type
|
||||
*/
|
||||
export const setupDynamicConnections = (
|
||||
nodeType: NodeType,
|
||||
prefix: string,
|
||||
inputType: string | string[],
|
||||
opts?: Partial<DynamicConnectionOptions>,
|
||||
): void => {
|
||||
infoLogger(
|
||||
'Setting up dynamic connections for',
|
||||
(Object.getOwnPropertyDescriptor(nodeType, 'title')?.value as string) || 'unknown',
|
||||
)
|
||||
|
||||
const options: DynamicConnectionOptions = {
|
||||
separator: '_',
|
||||
start_index: 1,
|
||||
rename_menu: 'label',
|
||||
...opts,
|
||||
}
|
||||
|
||||
const is_valid_name = (_node: MTBNode, _val: string): boolean => {
|
||||
return true
|
||||
}
|
||||
|
||||
nodeType.prototype.getSlotMenuOptions = (slot): ContextMenuItem[] | undefined => {
|
||||
if (!slot.input) {
|
||||
return undefined
|
||||
}
|
||||
infoLogger('Slot Menu', { slot })
|
||||
return [
|
||||
{
|
||||
content: `Rename Input (${options.rename_menu})`,
|
||||
callback: () => {
|
||||
const dialog = app.canvas.createDialog(
|
||||
"<span class='name'>Name</span><input autofocus type='text'/><button>OK</button>",
|
||||
{},
|
||||
) as HTMLElement & { close: () => void }
|
||||
|
||||
const dialogInput = dialog.querySelector('input') as HTMLInputElement | null
|
||||
if (dialogInput) {
|
||||
if (options.rename_menu === 'label') {
|
||||
dialogInput.value = slot.input!.label || slot.input!.name || ''
|
||||
} else if (options.rename_menu === 'name') {
|
||||
dialogInput.value = slot.input!.name || ''
|
||||
}
|
||||
}
|
||||
|
||||
const inner = (): void => {
|
||||
const val = dialogInput?.value || ''
|
||||
if (!is_valid_name(slot.node, val)) {
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
app.graph.beforeChange()
|
||||
if (options.rename_menu === 'label') {
|
||||
slot.input!.label = val
|
||||
} else if (options.rename_menu === 'name') {
|
||||
slot.input!.name = val
|
||||
slot.input!.label = val
|
||||
}
|
||||
|
||||
app.graph.afterChange()
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
dialog.querySelector('button')?.addEventListener('click', inner)
|
||||
dialogInput?.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
;(dialog as HTMLElement & { is_modified?: boolean }).is_modified = true
|
||||
if (e.keyCode === 27) {
|
||||
dialog.close()
|
||||
} else if (e.keyCode === 13) {
|
||||
inner()
|
||||
} else if (
|
||||
e.keyCode !== 13 &&
|
||||
(e.target as HTMLElement)?.localName !== 'textarea'
|
||||
) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
dialogInput?.focus()
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const onConfigure = nodeType.prototype.onConfigure
|
||||
|
||||
nodeType.prototype.onConfigure = function (this: MTBNode, data: unknown) {
|
||||
const r = onConfigure ? onConfigure.apply(this, [data]) : undefined
|
||||
|
||||
if (!('dynamic_connections' in this.properties)) {
|
||||
this.setProperty('dynamic_connections', [])
|
||||
} else {
|
||||
const dynamic_connections = this.properties.dynamic_connections as string[] | string
|
||||
if (typeof dynamic_connections !== 'object') {
|
||||
return r
|
||||
}
|
||||
for (const name of dynamic_connections) {
|
||||
infoLogger(`Would dynamize: ${name}`)
|
||||
const input = this.inputs.find((i) => i.name === name)
|
||||
if (input) {
|
||||
infoLogger('Input found', { input })
|
||||
input._isDynamic = true
|
||||
}
|
||||
}
|
||||
}
|
||||
set_slot_colors(this, ['cyan', undefined], isDynamicInput as SlotCondition)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
const inputList = typeof inputType === 'object'
|
||||
|
||||
nodeType.prototype.onNodeCreated = function (this: MTBNode) {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this) : undefined
|
||||
|
||||
addDynamicInput(
|
||||
this,
|
||||
`${prefix}${options.separator}${options.start_index}`,
|
||||
inputList ? '*' : (inputType as string),
|
||||
)
|
||||
return r
|
||||
}
|
||||
|
||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||||
|
||||
nodeType.prototype.onConnectionsChange = function (this: MTBNode, ...args) {
|
||||
const [type, slotIndex, isConnected, link, ioSlot] = args
|
||||
|
||||
options.link = link
|
||||
options.ioSlot = ioSlot
|
||||
const r = onConnectionsChange
|
||||
? onConnectionsChange.apply(this, [type, slotIndex, isConnected, link, ioSlot])
|
||||
: undefined
|
||||
|
||||
options.DEBUG = {
|
||||
node: this,
|
||||
type,
|
||||
slotIndex,
|
||||
isConnected,
|
||||
link,
|
||||
ioSlot,
|
||||
}
|
||||
|
||||
dynamic_connection(
|
||||
this,
|
||||
slotIndex,
|
||||
isConnected,
|
||||
`${prefix}${options.separator}`,
|
||||
inputType,
|
||||
options,
|
||||
)
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main logic around dynamic inputs
|
||||
*/
|
||||
export const dynamic_connection = (
|
||||
node: MTBNode,
|
||||
index: number,
|
||||
connected: boolean,
|
||||
connectionPrefix = 'input_',
|
||||
connectionType: string | string[] = '*',
|
||||
opts?: Partial<DynamicConnectionOptions>,
|
||||
): void => {
|
||||
const options: DynamicConnectionOptions = {
|
||||
start_index: 1,
|
||||
...opts,
|
||||
}
|
||||
|
||||
if (node.inputs.length > 0 && !isDynamicInput(node.inputs[index])) {
|
||||
return
|
||||
}
|
||||
|
||||
const listConnection = typeof connectionType === 'object'
|
||||
const conType = listConnection ? '*' : connectionType
|
||||
const nameArray = options.nameArray || []
|
||||
|
||||
const clean_inputs = (): void => {
|
||||
if (node.id < 0) return // being duplicated
|
||||
if (node.inputs.length === 0) return
|
||||
|
||||
let w_count = node.widgets?.length || 0
|
||||
let i_count = node.inputs?.length || 0
|
||||
infoLogger(`Cleaning inputs: [BEFORE] (w: ${w_count} | inputs: ${i_count})`)
|
||||
|
||||
const to_remove: number[] = []
|
||||
for (let n = 1; n < node.inputs.length; n++) {
|
||||
const element = node.inputs[n]
|
||||
if (!element.link && isDynamicInput(element)) {
|
||||
if (node.widgets) {
|
||||
const w = node.widgets.find((w) => w.name === element.name)
|
||||
if (w) {
|
||||
w.onRemoved?.()
|
||||
node.widgets.length = node.widgets.length - 1
|
||||
}
|
||||
}
|
||||
infoLogger(`Removing input ${n}`)
|
||||
to_remove.push(n)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < to_remove.length; i++) {
|
||||
const id = to_remove[i]
|
||||
try {
|
||||
node.removeInput(id)
|
||||
i_count -= 1
|
||||
} catch (err) {
|
||||
errorLogger('Cannot remove input', err)
|
||||
}
|
||||
}
|
||||
node.inputs.length = i_count
|
||||
|
||||
w_count = node.widgets?.length || 0
|
||||
i_count = node.inputs?.length || 0
|
||||
infoLogger(`Cleaning inputs: [AFTER] (w: ${w_count} | inputs: ${i_count})`)
|
||||
|
||||
infoLogger('Cleaning inputs: making it sequential again')
|
||||
// make inputs sequential again
|
||||
let prefixed_idx = options.start_index!
|
||||
for (let i = 0; i < node.inputs.length; i++) {
|
||||
let name = ''
|
||||
if (node.inputs[i].name.startsWith(connectionPrefix)) {
|
||||
name = `${connectionPrefix}${prefixed_idx}`
|
||||
prefixed_idx += 1
|
||||
} else {
|
||||
name = node.inputs[i].name
|
||||
}
|
||||
|
||||
if (nameArray.length > 0) {
|
||||
name = i < nameArray.length ? nameArray[i] : name
|
||||
}
|
||||
|
||||
// preserve label if it exists
|
||||
;(node.inputs[i] as INodeInputSlot & { label?: string }).label =
|
||||
(node.inputs[i] as INodeInputSlot & { label?: string }).label || name
|
||||
node.inputs[i].name = name
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
if (!options.link) {
|
||||
infoLogger('Disconnecting', { options })
|
||||
clean_inputs()
|
||||
} else {
|
||||
if (!options.ioSlot?.link) {
|
||||
node.connectionTransit = true
|
||||
} else {
|
||||
node.connectionTransit = false
|
||||
clean_inputs()
|
||||
}
|
||||
infoLogger('Reconnecting', { options })
|
||||
}
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
if (options.link) {
|
||||
const { from, to, type } = nodesFromLink(node, options.link)
|
||||
if (type === 'outgoing') return
|
||||
infoLogger('Connecting', { options, from, to, type })
|
||||
} else {
|
||||
infoLogger('Connecting', { options })
|
||||
}
|
||||
|
||||
if (node.connectionTransit) {
|
||||
infoLogger('In Transit')
|
||||
node.connectionTransit = false
|
||||
}
|
||||
|
||||
clean_inputs()
|
||||
|
||||
if (node.inputs.length === 0) return
|
||||
|
||||
// add an extra input
|
||||
if (node.inputs[node.inputs.length - 1].link !== null) {
|
||||
const nextIndex = node.inputs.reduce(
|
||||
(acc, cur) => (isDynamicInput(cur) ? ++acc : acc),
|
||||
0,
|
||||
)
|
||||
|
||||
const name =
|
||||
nextIndex < nameArray.length
|
||||
? nameArray[nextIndex]
|
||||
: `${connectionPrefix}${nextIndex + options.start_index!}`
|
||||
|
||||
infoLogger(`Adding input ${nextIndex + 1} (${name})`)
|
||||
addDynamicInput(node, name, conType as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* MTB Shared Utilities
|
||||
* Main entry point - re-exports all modules
|
||||
*
|
||||
* NOTE: This bundle does NOT auto-execute anything.
|
||||
* Import the functions you need directly.
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types'
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
getConvexHull,
|
||||
makeUUID,
|
||||
debounce,
|
||||
deepMerge,
|
||||
safe_json,
|
||||
getNodes,
|
||||
type Debounced,
|
||||
} from './utils'
|
||||
|
||||
// Storage
|
||||
export { LocalStorageManager } from './storage'
|
||||
|
||||
// Logging
|
||||
export {
|
||||
infoLogger,
|
||||
warnLogger,
|
||||
errorLogger,
|
||||
successLogger,
|
||||
log,
|
||||
} from './logger'
|
||||
|
||||
// Widgets
|
||||
export {
|
||||
CONVERTED_TYPE,
|
||||
hideWidget,
|
||||
showWidget,
|
||||
convertToWidget,
|
||||
convertToInput,
|
||||
hideWidgetForGood,
|
||||
fixWidgets,
|
||||
inner_value_change,
|
||||
getNamedWidget,
|
||||
nodesFromLink,
|
||||
hasWidgets,
|
||||
cleanupNode,
|
||||
offsetDOMWidget,
|
||||
getWidgetType,
|
||||
} from './widgets'
|
||||
|
||||
// Dynamic connections
|
||||
export {
|
||||
setupDynamicConnections,
|
||||
dynamic_connection,
|
||||
} from './dynamic-connections'
|
||||
|
||||
// Colors
|
||||
export { isColorBright } from './colors'
|
||||
|
||||
// DOM utilities
|
||||
export {
|
||||
calculateTotalChildrenHeight,
|
||||
loadScript,
|
||||
} from './dom'
|
||||
|
||||
// Documentation
|
||||
export {
|
||||
ensureMarkdownParser,
|
||||
addDocumentation,
|
||||
} from './documentation'
|
||||
|
||||
// Node extensions
|
||||
export {
|
||||
chainCallback,
|
||||
addMenuHandler,
|
||||
addDeprecation,
|
||||
} from './node-extensions'
|
||||
|
||||
// Server API
|
||||
export {
|
||||
runAction,
|
||||
getServerInfo,
|
||||
setServerInfo,
|
||||
} from './api'
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Logging utilities
|
||||
*/
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
type ConsoleMethod = 'log' | 'warn' | 'error' | 'debug'
|
||||
type ToastSeverity = 'info' | 'warn' | 'error' | 'secondary'
|
||||
|
||||
const consoleMethodToSeverity = (method: ConsoleMethod): ToastSeverity => {
|
||||
switch (method) {
|
||||
case 'log':
|
||||
return 'info'
|
||||
case 'error':
|
||||
case 'warn':
|
||||
return method
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
interface LogResult {
|
||||
notify: (timeout?: number) => void
|
||||
}
|
||||
|
||||
type Logger = (message: string, ...args: unknown[]) => LogResult
|
||||
|
||||
function createLogger(emoji: string, color: string, consoleMethod: ConsoleMethod = 'log'): Logger {
|
||||
return (message: string, ...args: unknown[]): LogResult => {
|
||||
if (window.MTB?.DEBUG) {
|
||||
// biome-ignore lint/suspicious/noConsole: logger wrapper
|
||||
console[consoleMethod](`%c${emoji} ${message}`, `color: ${color};`, ...args)
|
||||
}
|
||||
return {
|
||||
notify: (timeout = 3000) => {
|
||||
app.extensionManager?.toast?.add({
|
||||
severity: consoleMethodToSeverity(consoleMethod),
|
||||
summary: 'MTB',
|
||||
detail: `${emoji} ${message}`,
|
||||
life: timeout,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const infoLogger = createLogger('ℹ️', 'yellow')
|
||||
export const warnLogger = createLogger('⚠️', 'orange', 'warn')
|
||||
export const errorLogger = createLogger('🔥', 'red', 'error')
|
||||
export const successLogger = createLogger('✅', 'green')
|
||||
|
||||
export const log = (...args: unknown[]): void => {
|
||||
if (window.MTB?.DEBUG) {
|
||||
// biome-ignore lint/suspicious/noConsole: logger wrapper
|
||||
console.debug(...args)
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Node extension utilities
|
||||
*/
|
||||
|
||||
import type { MTBNode, NodeType, ContextMenuItem } from './types'
|
||||
import { errorLogger } from './logger'
|
||||
|
||||
/**
|
||||
* Extend an object, either replacing the original property or extending it.
|
||||
*/
|
||||
export function chainCallback<T extends object, K extends keyof T>(
|
||||
object: T | undefined,
|
||||
property: K,
|
||||
callback: T[K],
|
||||
): void {
|
||||
if (object === undefined) {
|
||||
errorLogger('Could not extend undefined object', { object, property })
|
||||
return
|
||||
}
|
||||
|
||||
if (property in object) {
|
||||
const callback_orig = object[property] as ((...args: unknown[]) => unknown) | undefined
|
||||
;(object as Record<K, unknown>)[property] = function (
|
||||
this: unknown,
|
||||
...args: unknown[]
|
||||
): unknown {
|
||||
const r = callback_orig?.apply(this, args)
|
||||
const n = (callback as (...args: unknown[]) => unknown).apply(this, args)
|
||||
return r || n
|
||||
}
|
||||
} else {
|
||||
object[property] = callback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a callback to the extra menu options of a given node type.
|
||||
*/
|
||||
export function addMenuHandler(
|
||||
nodeType: NodeType,
|
||||
cb: (this: MTBNode, app: unknown, options: ContextMenuItem[]) => ContextMenuItem[],
|
||||
): void {
|
||||
const getOpts = nodeType.prototype.getExtraMenuOptions
|
||||
|
||||
nodeType.prototype.getExtraMenuOptions = function (
|
||||
this: MTBNode,
|
||||
app: unknown,
|
||||
options: ContextMenuItem[],
|
||||
): ContextMenuItem[] {
|
||||
const r = getOpts?.apply(this, [app, options]) || []
|
||||
const newItems = cb.apply(this, [app, options]) || []
|
||||
return [...r, ...newItems]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes the node title with '[DEPRECATED]' and log the deprecation reason to the console.
|
||||
*/
|
||||
export const addDeprecation = (nodeType: NodeType, reason: string): void => {
|
||||
const title = nodeType.title || 'Unknown'
|
||||
nodeType.title = `[DEPRECATED] ${title}`
|
||||
|
||||
const styles = {
|
||||
title: 'font-size:1.3em;font-weight:900;color:yellow; background: black',
|
||||
reason: 'font-size:1.2em',
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole: intentional deprecation warning
|
||||
console.log(`%c! ${title} is deprecated:%c ${reason}`, styles.title, styles.reason)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Local storage management with namespacing
|
||||
*/
|
||||
|
||||
export class LocalStorageManager {
|
||||
private namespace: string
|
||||
|
||||
constructor(namespace: string) {
|
||||
this.namespace = namespace
|
||||
}
|
||||
|
||||
private _namespacedKey(key: string): string {
|
||||
return `${this.namespace}:${key}`
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): void {
|
||||
const serializedValue = JSON.stringify(value)
|
||||
localStorage.setItem(this._namespacedKey(key), serializedValue)
|
||||
}
|
||||
|
||||
get<T>(key: string, default_val: T | null = null): T | null {
|
||||
const value = localStorage.getItem(this._namespacedKey(key))
|
||||
return value ? (JSON.parse(value) as T) : default_val
|
||||
}
|
||||
|
||||
remove(key: string): void {
|
||||
localStorage.removeItem(this._namespacedKey(key))
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
const prefix = `${this.namespace}:`
|
||||
const keysToRemove = Object.keys(localStorage).filter((k) => k.startsWith(prefix))
|
||||
for (const key of keysToRemove) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* Type definitions for comfy_shared
|
||||
*/
|
||||
|
||||
import type { LGraphNode, IWidget, LLink, INodeInputSlot, INodeOutputSlot } from '@comfyorg/litegraph'
|
||||
|
||||
// Extend window for MTB globals
|
||||
declare global {
|
||||
interface Window {
|
||||
MTB?: {
|
||||
DEBUG?: boolean
|
||||
mdParser?: MarkdownParser
|
||||
}
|
||||
MTBMarkdown?: { getParser: () => Promise<MarkdownParser> }
|
||||
MTBMarkdownPlus?: { getParser: () => Promise<MarkdownParser> }
|
||||
}
|
||||
const LiteGraph: {
|
||||
NODE_SLOT_HEIGHT: number
|
||||
NODE_TITLE_HEIGHT: number
|
||||
NODE_COLLAPSED_RADIUS: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MarkdownParser {
|
||||
parse: (content: string) => Promise<string>
|
||||
}
|
||||
|
||||
// Widget types
|
||||
export interface MTBWidget extends IWidget {
|
||||
origType?: string
|
||||
origComputeSize?: () => [number, number]
|
||||
origSerializeValue?: () => unknown
|
||||
hidden?: boolean
|
||||
linkedWidgets?: MTBWidget[]
|
||||
last_y?: number
|
||||
canvas?: HTMLCanvasElement
|
||||
inputEl?: HTMLElement
|
||||
onRemoved?: () => void
|
||||
parent?: { inputHeight?: number }
|
||||
}
|
||||
|
||||
export interface MTBNode extends LGraphNode {
|
||||
widgets?: MTBWidget[]
|
||||
inputs: (INodeInputSlot & { widget?: { name: string; config?: unknown }; _isDynamic?: boolean })[]
|
||||
outputs: INodeOutputSlot[]
|
||||
properties: Record<string, unknown>
|
||||
color?: string
|
||||
flags: { collapsed?: boolean }
|
||||
show_doc?: boolean
|
||||
docCtrl?: AbortController
|
||||
docPos?: { width: string; height: string }
|
||||
connectionTransit?: boolean
|
||||
setProperty: (name: string, value: unknown) => void
|
||||
addInput: (name: string, type: string, extra?: Record<string, unknown>) => INodeInputSlot
|
||||
removeInput: (index: number) => void
|
||||
setSize: (size: [number, number]) => void
|
||||
graph: {
|
||||
getLink: (id: number) => LLink
|
||||
getNodeById: (id: number) => MTBNode
|
||||
beforeChange: () => void
|
||||
afterChange: () => void
|
||||
}
|
||||
slots: (INodeInputSlot | INodeOutputSlot)[]
|
||||
}
|
||||
|
||||
export interface NodeType {
|
||||
title?: string
|
||||
prototype: {
|
||||
onNodeCreated?: (this: MTBNode) => void
|
||||
onConfigure?: (this: MTBNode, data: unknown) => void
|
||||
onConnectionsChange?: (this: MTBNode, ...args: OnConnectionsChangeParams) => void
|
||||
onDrawForeground?: (this: MTBNode, ctx: CanvasRenderingContext2D, canvas: unknown) => void
|
||||
onMouseDown?: (this: MTBNode, ...args: OnMouseDownParams) => boolean | void
|
||||
onRemoved?: (this: MTBNode) => void
|
||||
getExtraMenuOptions?: (this: MTBNode, app: unknown, options: ContextMenuItem[]) => ContextMenuItem[]
|
||||
getSlotMenuOptions?: (slot: SlotMenuContext) => ContextMenuItem[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface NodeData {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type OnConnectionsChangeParams = [
|
||||
type: number,
|
||||
slotIndex: number,
|
||||
isConnected: boolean,
|
||||
link: LLink | null,
|
||||
ioSlot: INodeInputSlot | INodeOutputSlot,
|
||||
]
|
||||
|
||||
export type OnMouseDownParams = [
|
||||
event: MouseEvent,
|
||||
localPos: [number, number],
|
||||
graphCanvas: unknown,
|
||||
]
|
||||
|
||||
export interface ContextMenuItem {
|
||||
content: string
|
||||
callback?: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
export interface SlotMenuContext {
|
||||
input?: INodeInputSlot & { label?: string; name: string }
|
||||
node: MTBNode
|
||||
}
|
||||
|
||||
export interface DocumentationOptions {
|
||||
icon_size?: number
|
||||
icon_margin?: number
|
||||
}
|
||||
|
||||
export interface DynamicConnectionOptions {
|
||||
separator?: string
|
||||
rename_menu?: 'label' | 'name'
|
||||
start_index?: number
|
||||
link?: LLink | null
|
||||
ioSlot?: INodeInputSlot | INodeOutputSlot
|
||||
nameArray?: string[]
|
||||
DEBUG?: unknown
|
||||
}
|
||||
|
||||
export interface LinkInfo {
|
||||
to: MTBNode
|
||||
from: MTBNode
|
||||
type: 'error' | 'incoming' | 'outgoing'
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
/**
|
||||
* Base utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Computes the convex hull of a set of points using the Monotone Chain algorithm.
|
||||
*/
|
||||
export const getConvexHull = (points: [number, number][]): [number, number][] => {
|
||||
if (points.length <= 3) {
|
||||
return points
|
||||
}
|
||||
|
||||
const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1])
|
||||
|
||||
const cross_product = (o: [number, number], a: [number, number], b: [number, number]) => {
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
||||
}
|
||||
|
||||
const lower: [number, number][] = []
|
||||
for (const p of sorted) {
|
||||
while (
|
||||
lower.length >= 2 &&
|
||||
cross_product(lower[lower.length - 2], lower[lower.length - 1], p) <= 0
|
||||
) {
|
||||
lower.pop()
|
||||
}
|
||||
lower.push(p)
|
||||
}
|
||||
|
||||
const upper: [number, number][] = []
|
||||
for (let i = sorted.length - 1; i >= 0; i--) {
|
||||
const p = sorted[i]
|
||||
while (
|
||||
upper.length >= 2 &&
|
||||
cross_product(upper[upper.length - 2], upper[upper.length - 1], p) <= 0
|
||||
) {
|
||||
upper.pop()
|
||||
}
|
||||
upper.push(p)
|
||||
}
|
||||
|
||||
return lower.slice(0, lower.length - 1).concat(upper.slice(0, upper.length - 1))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a crude UUID v4
|
||||
*/
|
||||
export function makeUUID(): string {
|
||||
let dt = new Date().getTime()
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = ((dt + Math.random() * 16) % 16) | 0
|
||||
dt = Math.floor(dt / 16)
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
return uuid
|
||||
}
|
||||
|
||||
export interface Debounced<T extends (...args: unknown[]) => void> {
|
||||
(...args: Parameters<T>): void
|
||||
cancel: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic debounce decorator
|
||||
*/
|
||||
export function debounce<T extends (...args: unknown[]) => void>(
|
||||
func: T,
|
||||
delay: number,
|
||||
): Debounced<T> {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const debounced = function (this: unknown, ...args: Parameters<T>) {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func.apply(this, args), delay)
|
||||
} as Debounced<T>
|
||||
|
||||
debounced.cancel = () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
return debounced
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects.
|
||||
*/
|
||||
export function deepMerge<T extends Record<string, unknown>>(
|
||||
target: T,
|
||||
...sources: Partial<T>[]
|
||||
): T {
|
||||
if (!sources.length) return target
|
||||
const source = sources.shift()
|
||||
|
||||
if (source) {
|
||||
for (const key in source) {
|
||||
const sourceValue = source[key]
|
||||
if (sourceValue instanceof Object && !Array.isArray(sourceValue)) {
|
||||
if (!target[key]) Object.assign(target, { [key]: {} })
|
||||
deepMerge(target[key] as Record<string, unknown>, sourceValue as Record<string, unknown>)
|
||||
} else {
|
||||
Object.assign(target, { [key]: sourceValue })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deepMerge(target, ...sources)
|
||||
}
|
||||
|
||||
type Replacer = (value: unknown) => unknown
|
||||
|
||||
/**
|
||||
* Safely serialize an object to JSON, handling circular references
|
||||
*/
|
||||
export const safe_json = (object: unknown, replacer?: Replacer): unknown => {
|
||||
const objects = new WeakMap<object, string>()
|
||||
|
||||
const derez = (value: unknown, path: string): unknown => {
|
||||
if (replacer !== undefined) {
|
||||
value = replacer(value)
|
||||
}
|
||||
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!(value instanceof Boolean) &&
|
||||
!(value instanceof Date) &&
|
||||
!(value instanceof Number) &&
|
||||
!(value instanceof RegExp) &&
|
||||
!(value instanceof String)
|
||||
) {
|
||||
const old_path = objects.get(value)
|
||||
if (old_path !== undefined) {
|
||||
return { $ref: old_path }
|
||||
}
|
||||
objects.set(value, path)
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const nu: unknown[] = []
|
||||
value.forEach((element, i) => {
|
||||
nu[i] = derez(element, `${path}[${i}]`)
|
||||
})
|
||||
return nu
|
||||
} else {
|
||||
const nu: Record<string, unknown> = {}
|
||||
Object.keys(value).forEach((name) => {
|
||||
nu[name] = derez(
|
||||
(value as Record<string, unknown>)[name],
|
||||
path + '[' + JSON.stringify(name) + ']',
|
||||
)
|
||||
})
|
||||
return nu
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
return derez(object, '$')
|
||||
}
|
||||
|
||||
// Declare app global for ComfyUI
|
||||
declare const app: {
|
||||
graph: {
|
||||
_nodes: import('./types').MTBNode[]
|
||||
computeExecutionOrder?: (onlyOnExecute: boolean) => import('./types').MTBNode[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nodes from the current graph
|
||||
* @param sorted - If true, returns nodes in execution order
|
||||
*/
|
||||
export function getNodes(sorted = false): import('./types').MTBNode[] {
|
||||
if (!app?.graph?._nodes) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (sorted) {
|
||||
// computeExecutionOrder returns nodes in dependency order
|
||||
return app.graph.computeExecutionOrder?.(false) ?? app.graph._nodes
|
||||
}
|
||||
|
||||
return app.graph._nodes
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
/**
|
||||
* Widget utilities for LiteGraph nodes
|
||||
*/
|
||||
|
||||
import type { LLink } from '@comfyorg/litegraph'
|
||||
import type { MTBNode, MTBWidget, LinkInfo } from './types'
|
||||
import { log } from './logger'
|
||||
|
||||
export const CONVERTED_TYPE = 'converted-widget'
|
||||
|
||||
export function hideWidget(node: MTBNode, widget: MTBWidget, suffix = ''): void {
|
||||
widget.origType = widget.type as string
|
||||
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 input = node.inputs?.find((i) => i.widget?.name === widget.name)
|
||||
if (input?.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: MTBWidget): void {
|
||||
widget.type = widget.origType!
|
||||
widget.computeSize = widget.origComputeSize!
|
||||
widget.serializeValue = widget.origSerializeValue
|
||||
|
||||
delete widget.origType
|
||||
delete widget.origComputeSize
|
||||
delete widget.origSerializeValue
|
||||
|
||||
// Show any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
showWidget(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function convertToWidget(node: MTBNode, widget: MTBWidget): void {
|
||||
showWidget(widget)
|
||||
const sz = node.size
|
||||
const inputIndex = node.inputs.findIndex((i) => i.widget?.name === widget.name)
|
||||
if (inputIndex >= 0) {
|
||||
node.removeInput(inputIndex)
|
||||
}
|
||||
|
||||
if (node.widgets) {
|
||||
for (const w of node.widgets) {
|
||||
if (w.last_y !== undefined) {
|
||||
w.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])])
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the type and link type from a widget config object.
|
||||
*/
|
||||
export function getWidgetType(config: unknown[]): { type: string; linkType: string } {
|
||||
let type = config?.[0]
|
||||
let linkType = type as string
|
||||
if (Array.isArray(type)) {
|
||||
linkType = type.join(',')
|
||||
type = 'COMBO'
|
||||
}
|
||||
return { type: type as string, linkType }
|
||||
}
|
||||
|
||||
export function convertToInput(
|
||||
node: MTBNode,
|
||||
widget: MTBWidget,
|
||||
config: unknown[],
|
||||
): void {
|
||||
hideWidget(node, widget)
|
||||
|
||||
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 },
|
||||
})
|
||||
|
||||
if (node.widgets) {
|
||||
for (const w of node.widgets) {
|
||||
if (w.last_y !== undefined) {
|
||||
w.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])])
|
||||
}
|
||||
|
||||
export function hideWidgetForGood(node: MTBNode, widget: MTBWidget, suffix = ''): void {
|
||||
widget.origType = widget.type as string
|
||||
widget.origComputeSize = widget.computeSize
|
||||
widget.origSerializeValue = widget.serializeValue
|
||||
widget.computeSize = () => [0, -4]
|
||||
widget.hidden = true
|
||||
widget.type = CONVERTED_TYPE + suffix
|
||||
|
||||
// Hide any linked widgets
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
hideWidgetForGood(node, w, `:${widget.name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fixWidgets(node: MTBNode): void {
|
||||
if (!node.inputs) return
|
||||
|
||||
for (const input of node.inputs) {
|
||||
log(input)
|
||||
if (input.widget || node.widgets) {
|
||||
const matching_widget = node.widgets?.find((w) => w.name === input.name)
|
||||
if (matching_widget) {
|
||||
const w = node.widgets?.find((w) => w.name === matching_widget.name)
|
||||
if (w && w.type !== CONVERTED_TYPE) {
|
||||
log(w)
|
||||
log(`hiding ${w.name}(${w.type}) from ${node.type}`)
|
||||
log(node)
|
||||
hideWidget(node, w)
|
||||
} else {
|
||||
log(`converting to widget ${w}`)
|
||||
if (w) convertToWidget(node, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function inner_value_change(
|
||||
node: MTBNode,
|
||||
widget: MTBWidget,
|
||||
val: unknown,
|
||||
pos?: [number, number],
|
||||
event?: Event,
|
||||
): void {
|
||||
let value = val
|
||||
if (widget.type === 'number' || widget.type === 'BBOX') {
|
||||
value = Number(value)
|
||||
} else if (widget.type === 'BOOL') {
|
||||
value = Boolean(value)
|
||||
}
|
||||
widget.value = value
|
||||
|
||||
const property = (widget.options as { property?: string } | undefined)?.property
|
||||
if (property && node.properties[property] !== undefined) {
|
||||
node.setProperty(property, value)
|
||||
}
|
||||
|
||||
if (widget.callback) {
|
||||
// @ts-expect-error - app is global in ComfyUI
|
||||
widget.callback(widget.value, app.canvas, node, pos, event)
|
||||
}
|
||||
}
|
||||
|
||||
export const getNamedWidget = <T extends string>(
|
||||
node: MTBNode,
|
||||
...names: T[]
|
||||
): Record<T, MTBWidget | undefined> => {
|
||||
const out = {} as Record<T, MTBWidget | undefined>
|
||||
|
||||
for (const name of names) {
|
||||
out[name] = node.widgets?.find((w) => w.name === name)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
export const nodesFromLink = (node: MTBNode, link: LLink | number): LinkInfo => {
|
||||
let resolvedLink: LLink
|
||||
if (typeof link === 'number') {
|
||||
resolvedLink = node.graph.getLink(link)
|
||||
} else {
|
||||
resolvedLink = link
|
||||
}
|
||||
|
||||
const fromNode = node.graph.getNodeById(resolvedLink.origin_id)
|
||||
const toNode = node.graph.getNodeById(resolvedLink.target_id)
|
||||
|
||||
let tp: 'error' | 'incoming' | 'outgoing' = 'error'
|
||||
|
||||
if (fromNode.id === node.id) {
|
||||
tp = 'outgoing'
|
||||
} else if (toNode.id === node.id) {
|
||||
tp = 'incoming'
|
||||
}
|
||||
|
||||
return { to: toNode, from: fromNode, type: tp }
|
||||
}
|
||||
|
||||
export const hasWidgets = (node: MTBNode): boolean => {
|
||||
if (!node.widgets || !node.widgets?.[Symbol.iterator]) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const cleanupNode = (node: MTBNode): void => {
|
||||
if (!hasWidgets(node)) {
|
||||
return
|
||||
}
|
||||
for (const w of node.widgets!) {
|
||||
if (w.canvas) {
|
||||
w.canvas.remove()
|
||||
}
|
||||
if (w.inputEl) {
|
||||
w.inputEl.remove()
|
||||
}
|
||||
w.onRemoved?.()
|
||||
}
|
||||
}
|
||||
|
||||
export function offsetDOMWidget(
|
||||
widget: MTBWidget,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
node: MTBNode,
|
||||
widgetWidth: number,
|
||||
widgetY: number,
|
||||
height?: number,
|
||||
): void {
|
||||
if (!widget.inputEl) return
|
||||
|
||||
const margin = 10
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + widgetY)
|
||||
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
Object.assign(widget.inputEl.style, {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale.toString(),
|
||||
left: `${transform.a + transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: `${widgetWidth - margin * 2}px`,
|
||||
height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`,
|
||||
position: 'absolute',
|
||||
background: !node.color ? '' : node.color,
|
||||
color: !node.color ? '' : 'white',
|
||||
zIndex: '5',
|
||||
})
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// import './app.css'
|
||||
// import App from './App.svelte'
|
||||
import Inspector from './lib/Inspector.svelte'
|
||||
|
||||
export const createOutliner = (target, opts) => {
|
||||
const options = opts || {}
|
||||
const tgt = target || document.body
|
||||
|
||||
const app = new Inspector({
|
||||
// target: document.getElementById('app'),
|
||||
target: tgt,
|
||||
props: options,
|
||||
})
|
||||
return app
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import { resizeHandle } from './actions.js'
|
||||
|
||||
import InspectorPreview from './InspectorPreview.svelte'
|
||||
import Tabs from './Tabs.svelte'
|
||||
import TabUi from './TabUI.svelte'
|
||||
import TabHelp from './TabHelp.svelte'
|
||||
import { inComfy } from './utils'
|
||||
import { onMount } from 'svelte'
|
||||
import { graphToPrompt } from '../mtb_api/graph-to-prompt'
|
||||
|
||||
interface InputItem {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
options?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
let {
|
||||
visible = true,
|
||||
inputs = {} as Record<string, InputItem>,
|
||||
} = $props()
|
||||
|
||||
const items = $derived(
|
||||
Object.keys(inputs).map((k) => ({
|
||||
...inputs[k],
|
||||
original_name: inputs[k].original_name ?? k,
|
||||
}))
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
console.log('Mounted API Inspector')
|
||||
})
|
||||
|
||||
async function exportApi() {
|
||||
if (!inComfy()) return
|
||||
|
||||
// @ts-expect-error - app is global in ComfyUI
|
||||
const { output } = await graphToPrompt(app)
|
||||
|
||||
const json = JSON.stringify(output, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'workflow_api.json'
|
||||
a.click()
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div transition:fade={{ duration: 120 }} use:resizeHandle class="mtb-panel">
|
||||
<header class="mtb-panel-header">
|
||||
<div class="mtb-panel-title">
|
||||
<span class="mtb-panel-icon">⚡</span>
|
||||
<span>API Controls</span>
|
||||
</div>
|
||||
<span class="mtb-panel-badge">{items.length}</span>
|
||||
</header>
|
||||
|
||||
<div class="mtb-panel-content">
|
||||
<InspectorPreview />
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
label: 'Inputs',
|
||||
value: 1,
|
||||
component: TabUi,
|
||||
props: { inputs: items },
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
value: 2,
|
||||
component: TabHelp,
|
||||
props: {},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer class="mtb-panel-footer">
|
||||
<button class="mtb-btn mtb-btn-primary" onclick={() => {
|
||||
if (!inComfy()) return
|
||||
// @ts-expect-error - app is global in ComfyUI
|
||||
app.queuePrompt(0, 1)
|
||||
}}>
|
||||
<span class="mtb-btn-icon">▶</span>
|
||||
Queue
|
||||
</button>
|
||||
<button class="mtb-btn mtb-btn-secondary" onclick={exportApi}>Export</button>
|
||||
</footer>
|
||||
|
||||
<div class="mtb-panel-status">
|
||||
<span class="mtb-status-dot"></span>
|
||||
<span>Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* MTB Panel - Vercel-inspired minimal design */
|
||||
.mtb-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--comfy-menu-bg, #1a1a1a);
|
||||
color: var(--fg-color, #fafafa);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.mtb-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.08));
|
||||
background: var(--comfy-menu-bg, #1a1a1a);
|
||||
}
|
||||
|
||||
.mtb-panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.mtb-panel-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.mtb-panel-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
background: var(--mtb-api-color, #2930b0);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.mtb-panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--fg-color, #888) transparent;
|
||||
}
|
||||
|
||||
.mtb-panel-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.mtb-panel-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mtb-panel-content::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color, rgba(255,255,255,0.15));
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mtb-panel-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--fg-color, rgba(255,255,255,0.25));
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.mtb-panel-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.mtb-btn {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.mtb-btn-primary {
|
||||
background: var(--mtb-api-color, #2930b0);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mtb-btn-primary:hover {
|
||||
background: color-mix(in srgb, var(--mtb-api-color, #2930b0), white 10%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mtb-btn-secondary {
|
||||
background: var(--comfy-input-bg, rgba(255,255,255,0.05));
|
||||
color: var(--fg-color, #fafafa);
|
||||
border: 1px solid var(--border-color, rgba(255,255,255,0.1));
|
||||
}
|
||||
|
||||
.mtb-btn-secondary:hover {
|
||||
background: var(--border-color, rgba(255,255,255,0.1));
|
||||
border-color: var(--fg-color, rgba(255,255,255,0.2));
|
||||
}
|
||||
|
||||
.mtb-btn-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Status bar */
|
||||
.mtb-panel-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 16px;
|
||||
font-size: 11px;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.5));
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.mtb-status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,212 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ColorPicker, { ChromeVariant } from 'svelte-awesome-color-picker'
|
||||
import Wrapper from './Wrapper.svelte'
|
||||
|
||||
let element: HTMLElement
|
||||
|
||||
let {
|
||||
item = { type: 'NUMBER' },
|
||||
onInput = (e: Event, val?: unknown) => console.log(e, val),
|
||||
id = undefined,
|
||||
value = $bindable(''),
|
||||
} = $props()
|
||||
|
||||
const shared = {
|
||||
autocomplete: 'off',
|
||||
'data-lpignore': true,
|
||||
'data-form-type': 'other',
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mtb-input-wrapper">
|
||||
{#if item.type === 'NUMBER'}
|
||||
<input
|
||||
class="mtb-input"
|
||||
oninput={onInput}
|
||||
{...shared}
|
||||
{id}
|
||||
bind:value
|
||||
bind:this={element}
|
||||
type="number"
|
||||
/>
|
||||
{:else if item.type === 'MODEL' || item.type === 'COMBO'}
|
||||
{@const options = item.options || item.widgets?.[0]?.options?.values || []}
|
||||
<select class="mtb-select" oninput={onInput} bind:value {...shared} {id} bind:this={element}>
|
||||
{#each options as option}
|
||||
<option selected={item.widget?.value === option} value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if item.type === 'STRING'}
|
||||
{#if item.widgets?.[0]?.type === 'customtext'}
|
||||
<textarea
|
||||
class="mtb-textarea"
|
||||
oninput={onInput}
|
||||
bind:value
|
||||
{...shared}
|
||||
{id}
|
||||
bind:this={element}
|
||||
rows="3"
|
||||
></textarea>
|
||||
{:else}
|
||||
<input
|
||||
class="mtb-input"
|
||||
oninput={onInput}
|
||||
bind:value
|
||||
{...shared}
|
||||
{id}
|
||||
type="text"
|
||||
bind:this={element}
|
||||
/>
|
||||
{/if}
|
||||
{:else if item.type === 'BOOLEAN'}
|
||||
{@const isChecked = value === true || value === 'true' || value === 1 || value === '1' || value === 'on'}
|
||||
<label class="mtb-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onchange={(e) => {
|
||||
// ComfyUI toggles use booleans - 'off' string is truthy so doesn't work!
|
||||
const newValue = e.currentTarget.checked
|
||||
value = newValue
|
||||
onInput(e, newValue)
|
||||
}}
|
||||
/>
|
||||
<span class="mtb-toggle-slider"></span>
|
||||
<span class="mtb-toggle-label">{isChecked ? 'On' : 'Off'}</span>
|
||||
</label>
|
||||
{:else if item.type === 'COLOR'}
|
||||
<ColorPicker
|
||||
label=""
|
||||
bind:hex={value}
|
||||
components={{ ...ChromeVariant, wrapper: Wrapper }}
|
||||
sliderDirection="horizontal"
|
||||
--picker-z-index="9000"
|
||||
oninput={(event) => {
|
||||
value = event.detail.hex
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<input class="mtb-input" oninput={onInput} bind:value {...shared} {id} bind:this={element} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mtb-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Base input styles */
|
||||
.mtb-input,
|
||||
.mtb-select,
|
||||
.mtb-textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
color: var(--fg-color, #fafafa);
|
||||
background: var(--comfy-input-bg, rgba(255,255,255,0.05));
|
||||
border: 1px solid var(--border-color, rgba(255,255,255,0.1));
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.mtb-input:hover,
|
||||
.mtb-select:hover,
|
||||
.mtb-textarea:hover {
|
||||
border-color: var(--border-color, rgba(255,255,255,0.2));
|
||||
}
|
||||
|
||||
.mtb-input:focus,
|
||||
.mtb-select:focus,
|
||||
.mtb-textarea:focus {
|
||||
border-color: var(--mtb-api-color, #2930b0);
|
||||
box-shadow: 0 0 0 3px rgba(41, 48, 176, 0.15);
|
||||
}
|
||||
|
||||
/* Number input */
|
||||
.mtb-input[type='number'] {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.mtb-input[type='number']::-webkit-inner-spin-button,
|
||||
.mtb-input[type='number']::-webkit-outer-spin-button {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mtb-input[type='number']:hover::-webkit-inner-spin-button,
|
||||
.mtb-input[type='number']:hover::-webkit-outer-spin-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Select */
|
||||
.mtb-select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.mtb-select option {
|
||||
background: var(--comfy-menu-bg, #1a1a1a);
|
||||
color: var(--fg-color, #fafafa);
|
||||
}
|
||||
|
||||
/* Textarea */
|
||||
.mtb-textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
.mtb-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mtb-toggle input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.mtb-toggle-slider {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: var(--border-color, rgba(255,255,255,0.15));
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mtb-toggle-slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.mtb-toggle input:checked + .mtb-toggle-slider {
|
||||
background: var(--mtb-api-color, #2930b0);
|
||||
}
|
||||
|
||||
.mtb-toggle input:checked + .mtb-toggle-slider::after {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.mtb-toggle-label {
|
||||
font-size: 12px;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.6));
|
||||
}
|
||||
</style>
|
||||
@@ -1,221 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte'
|
||||
import InspectorInput from './InspectorInput.svelte'
|
||||
import { inComfy } from './utils.js'
|
||||
|
||||
interface ItemType {
|
||||
name: string
|
||||
type: string
|
||||
node_id?: number
|
||||
id: number
|
||||
value?: unknown
|
||||
widgets?: { value: unknown }[]
|
||||
options?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface Action {
|
||||
label: string
|
||||
icon?: string
|
||||
callback: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
item = {
|
||||
name: 'KSampler',
|
||||
type: 'STRING',
|
||||
node_id: 123,
|
||||
id: 4,
|
||||
} as ItemType,
|
||||
extra_actions = {} as Record<string, Action>,
|
||||
} = $props()
|
||||
|
||||
let actions = $state<Record<string, Action>>({})
|
||||
|
||||
// Derive value from widget - updates when widget value changes
|
||||
const getWidgetValue = () => item.widgets?.[0]?.value ?? item.value ?? ''
|
||||
let value = $state<unknown>(getWidgetValue())
|
||||
|
||||
// Sync value from widget when it changes (Node → Panel)
|
||||
// Use untrack to prevent reacting to local value changes (avoids reverting user input)
|
||||
$effect(() => {
|
||||
const widgetVal = getWidgetValue()
|
||||
untrack(() => {
|
||||
if (widgetVal !== value) {
|
||||
value = widgetVal
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
actions = {
|
||||
goToNode: {
|
||||
label: 'Focus',
|
||||
icon: '↗',
|
||||
callback: () => {
|
||||
const id = item.node_id
|
||||
if (!id) return
|
||||
if (!inComfy()) return
|
||||
// @ts-expect-error - app is global in ComfyUI
|
||||
const app = window.app
|
||||
const node = app.graph.getNodeById(id)
|
||||
app.canvas.centerOnNode(node)
|
||||
app.canvas.setZoom(1)
|
||||
app.canvas.selectNode(node)
|
||||
},
|
||||
},
|
||||
...extra_actions,
|
||||
}
|
||||
if (item.name.toLowerCase() === 'seed') {
|
||||
actions.randomize = {
|
||||
label: 'Random',
|
||||
icon: '⚄',
|
||||
callback: () => {
|
||||
value = Math.floor(Math.random() * 1e9)
|
||||
if (!inComfy()) return
|
||||
onInput(null, value)
|
||||
// @ts-expect-error - app is global in ComfyUI
|
||||
window.app.canvas.setDirty(true)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const onInput = (e: Event | null, val?: unknown) => {
|
||||
if (!inComfy()) return
|
||||
if (item.widgets) {
|
||||
for (const w of item.widgets as Array<{
|
||||
value: unknown
|
||||
callback?: (value: unknown) => void
|
||||
options?: { callback?: (value: unknown) => void }
|
||||
}>) {
|
||||
const newVal = val ?? (e?.target as HTMLInputElement)?.value
|
||||
w.value = newVal
|
||||
// Trigger widget callback to notify ComfyUI of the change
|
||||
w.callback?.(newVal)
|
||||
w.options?.callback?.(newVal)
|
||||
}
|
||||
// @ts-expect-error - app is global in ComfyUI
|
||||
window.app.canvas.setDirty(true)
|
||||
}
|
||||
}
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
STRING: 'Text',
|
||||
NUMBER: 'Number',
|
||||
COMBO: 'Select',
|
||||
MODEL: 'Model',
|
||||
BOOLEAN: 'Toggle',
|
||||
IMAGE: 'Image',
|
||||
COLOR: 'Color',
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mtb-input-row">
|
||||
<div class="mtb-input-header">
|
||||
<label class="mtb-input-label" for="input-{item.id}">{item.name}</label>
|
||||
<span class="mtb-input-type">{typeLabels[item.type] || item.type}</span>
|
||||
</div>
|
||||
|
||||
<div class="mtb-input-field">
|
||||
<InspectorInput {onInput} bind:value id={item.id} {item} />
|
||||
</div>
|
||||
|
||||
{#if Object.keys(actions).length > 0}
|
||||
<div class="mtb-input-actions">
|
||||
{#each Object.keys(actions) as k}
|
||||
{@const action = actions[k]}
|
||||
<button
|
||||
class="mtb-action-btn"
|
||||
onclick={action.callback}
|
||||
title={action.label}
|
||||
>
|
||||
{#if action.icon}
|
||||
<span class="mtb-action-icon">{action.icon}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mtb-input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06));
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.mtb-input-row:hover {
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
.mtb-input-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mtb-input-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mtb-input-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--fg-color, #fafafa);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.mtb-input-type {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.4));
|
||||
padding: 2px 6px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mtb-input-field {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mtb-input-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mtb-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color, rgba(255,255,255,0.1));
|
||||
border-radius: 4px;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.5));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.mtb-action-btn:hover {
|
||||
background: var(--mtb-api-color, #2930b0);
|
||||
border-color: var(--mtb-api-color, #2930b0);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mtb-action-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,107 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { inComfy } from './utils'
|
||||
import type { NodeId } from '@comfyorg/litegraph'
|
||||
import type { ComfyApi } from '@comfyorg/comfyui-frontend-types'
|
||||
|
||||
let nodeId: NodeId | undefined = $state()
|
||||
let img: string | undefined = $state()
|
||||
|
||||
const show = (src: string, node?: NodeId | null) => {
|
||||
img = src
|
||||
nodeId = Number(node)
|
||||
}
|
||||
let api: ComfyApi
|
||||
|
||||
onMount(() => {
|
||||
if (!inComfy()) return
|
||||
import('@/scripts/api').then((e) => {
|
||||
api = e.api
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
const images = detail?.output?.images
|
||||
if (!images || !images.length) return
|
||||
const format = window.app?.getPreviewFormatParam()
|
||||
const src = [
|
||||
`./view?filename=${encodeURIComponent(images[0].filename)}`,
|
||||
`type=${images[0].type}`,
|
||||
`subfolder=${encodeURIComponent(images[0].subfolder)}`,
|
||||
`t=${+new Date()}${format}`,
|
||||
].join('&')
|
||||
show(src, detail.node)
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview', ({ detail }) => {
|
||||
show(URL.createObjectURL(detail), window.app?.runningNodeId)
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="mtb-preview"
|
||||
onclick={(e) => {
|
||||
if (!inComfy()) return
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const node = window.app?.graph.getNodeById(nodeId)
|
||||
if (!node) return
|
||||
window.app?.canvas.centerOnNode(node)
|
||||
window.app?.canvas.setZoom(1)
|
||||
}}
|
||||
>
|
||||
{#if img}
|
||||
<img src={img} alt="Preview output" />
|
||||
{:else}
|
||||
<div class="mtb-preview-empty">
|
||||
<span class="mtb-preview-icon">🖼</span>
|
||||
<span class="mtb-preview-text">No preview yet</span>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.mtb-preview {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
max-height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06));
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mtb-preview:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.mtb-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 320px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mtb-preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.mtb-preview-icon {
|
||||
font-size: 32px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.mtb-preview-text {
|
||||
font-size: 12px;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.3));
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<h3>How to use this panel?</h3>
|
||||
<p>
|
||||
This panel was mainly developed to ease the authoring of workflows to use
|
||||
externaly with the API
|
||||
</p>
|
||||
|
||||
<style>
|
||||
section {
|
||||
width: 100%;
|
||||
padding: 0.3em;
|
||||
overflow: auto;
|
||||
}
|
||||
div {
|
||||
padding: 0.2em;
|
||||
margin: 0.15em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,161 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { dndzone } from 'svelte-dnd-action'
|
||||
import InspectorItem from './InspectorItem.svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { notifyOrderChanged } from '../mtb_api/panel.svelte'
|
||||
|
||||
interface InputItem {
|
||||
id: number
|
||||
node_id?: number
|
||||
original_name?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
let { inputs = [] as InputItem[] } = $props()
|
||||
|
||||
const flipDurationMs = 100
|
||||
let locked = $state(false)
|
||||
|
||||
function handleDndConsider(e: CustomEvent<{ items: InputItem[] }>) {
|
||||
inputs = e.detail.items
|
||||
}
|
||||
|
||||
function handleDndFinalize(e: CustomEvent<{ items: InputItem[] }>) {
|
||||
inputs = e.detail.items
|
||||
// Persist the new order back to the nodes
|
||||
const orderedInputs = inputs.map((item, index) => ({
|
||||
node_id: item.node_id as number,
|
||||
original_name: item.original_name as string,
|
||||
order: index,
|
||||
}))
|
||||
notifyOrderChanged(orderedInputs)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mtb-tab-ui">
|
||||
<div class="mtb-tab-toolbar">
|
||||
<button
|
||||
class="mtb-toolbar-btn"
|
||||
class:active={locked}
|
||||
onclick={() => (locked = !locked)}
|
||||
title={locked ? 'Unlock reordering' : 'Lock reordering'}
|
||||
>
|
||||
{#if locked}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 9.9-1"></path>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<span class="mtb-toolbar-label">{inputs.length} input{inputs.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
|
||||
{#if inputs.length === 0}
|
||||
<div class="mtb-empty-state">
|
||||
<div class="mtb-empty-icon">⚡</div>
|
||||
<p class="mtb-empty-text">No API inputs</p>
|
||||
<p class="mtb-empty-hint">Right-click a node and select "Mark API" to expose its inputs</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="mtb-inputs-list"
|
||||
use:dndzone={{ items: inputs, flipDurationMs, dragDisabled: locked }}
|
||||
onconsider={handleDndConsider}
|
||||
onfinalize={handleDndFinalize}
|
||||
>
|
||||
{#each inputs as item (item.id)}
|
||||
<div animate:flip={{ duration: flipDurationMs }}>
|
||||
<InspectorItem {item} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mtb-tab-ui {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mtb-tab-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06));
|
||||
}
|
||||
|
||||
.mtb-toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color, rgba(255,255,255,0.1));
|
||||
border-radius: 6px;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.5));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.mtb-toolbar-btn:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.mtb-toolbar-btn.active {
|
||||
background: var(--mtb-api-color, #2930b0);
|
||||
border-color: var(--mtb-api-color, #2930b0);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mtb-toolbar-label {
|
||||
font-size: 11px;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.4));
|
||||
}
|
||||
|
||||
.mtb-inputs-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.mtb-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mtb-empty-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.mtb-empty-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--fg-color, #fafafa);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.mtb-empty-hint {
|
||||
font-size: 12px;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.4));
|
||||
margin: 0;
|
||||
max-width: 200px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,88 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Component as SvelteComponent } from 'svelte'
|
||||
|
||||
interface TabItem {
|
||||
label: string
|
||||
value: number
|
||||
component: SvelteComponent<unknown>
|
||||
props: Record<string, unknown>
|
||||
}
|
||||
|
||||
let { items = [] as TabItem[] } = $props()
|
||||
|
||||
let activeTabValue = $state(items[0]?.value ?? 1)
|
||||
|
||||
function handleClick(value: number) {
|
||||
return () => {
|
||||
activeTabValue = value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mtb-tabs">
|
||||
<nav class="mtb-tabs-nav" role="tablist">
|
||||
{#each items as item}
|
||||
<button
|
||||
class="mtb-tab"
|
||||
class:active={activeTabValue === item.value}
|
||||
role="tab"
|
||||
aria-selected={activeTabValue === item.value}
|
||||
onclick={handleClick(item.value)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="mtb-tabs-content">
|
||||
{#each items as item}
|
||||
{#if activeTabValue === item.value}
|
||||
<svelte:component this={item.component} {...item.props} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mtb-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mtb-tabs-nav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.06));
|
||||
}
|
||||
|
||||
.mtb-tab {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--descrip-text, rgba(255,255,255,0.5));
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.mtb-tab:hover {
|
||||
color: var(--fg-color, #fafafa);
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.mtb-tab.active {
|
||||
color: var(--fg-color, #fafafa);
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.mtb-tabs-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { portal } from 'svelte-portal'
|
||||
|
||||
export let wrapper
|
||||
export let isOpen
|
||||
export let isPopup
|
||||
/* svelte-ignore unused-export-let */
|
||||
export let toRight
|
||||
</script>
|
||||
|
||||
<div
|
||||
use:portal={'#inside'}
|
||||
class="wrapper"
|
||||
bind:this={wrapper}
|
||||
class:isOpen
|
||||
class:isPopup
|
||||
role={isPopup ? 'dialog' : undefined}
|
||||
aria-label="color picker"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 8px 5px 5px 8px;
|
||||
margin: 0 10px 10px;
|
||||
border: 1px solid black;
|
||||
border-radius: 12px;
|
||||
display: none;
|
||||
width: max-content;
|
||||
}
|
||||
.isOpen {
|
||||
display: block;
|
||||
}
|
||||
.isPopup {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
z-index: var(--picker-z-index, 2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
export function dragMe(node) {
|
||||
let offsetX = 300
|
||||
let offsetY = 200
|
||||
let isDragging = false
|
||||
|
||||
node.style.left = `${300}px`
|
||||
node.style.top = `${200}px`
|
||||
node.style.position = 'absolute'
|
||||
|
||||
const dragPanel = (e) => {
|
||||
if (!isDragging) return
|
||||
const newX = e.clientX - offsetX
|
||||
const newY = e.clientY - offsetY
|
||||
node.style.left = `${newX}px`
|
||||
node.style.top = `${newY}px`
|
||||
}
|
||||
|
||||
const startDragging = (e: MouseEvent) => {
|
||||
const target = e.target
|
||||
const isBlankSpace = target === node
|
||||
console.log({ target, node, isBlankSpace, contains: node.contains(target) })
|
||||
if (isBlankSpace) {
|
||||
isDragging = true
|
||||
offsetX = e.clientX - node.offsetLeft
|
||||
offsetY = e.clientY - node.offsetTop
|
||||
node.style.cursor = 'grabbing'
|
||||
document.addEventListener('mousemove', dragPanel)
|
||||
document.addEventListener('mouseup', stopDragging)
|
||||
}
|
||||
}
|
||||
|
||||
const stopDragging = () => {
|
||||
isDragging = false
|
||||
node.style.cursor = 'grab'
|
||||
document.removeEventListener('mousemove', dragPanel)
|
||||
document.removeEventListener('mouseup', stopDragging)
|
||||
}
|
||||
// Prevent child elements from receiving mouse events during dragging
|
||||
for (const el of node.querySelectorAll('input, button')) {
|
||||
el.addEventListener('mousedown', (e) => e.stopPropagation())
|
||||
}
|
||||
|
||||
node.addEventListener('mousedown', startDragging)
|
||||
return {
|
||||
destroy() {
|
||||
stopDragging()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function resizeHandle(node) {
|
||||
let resizing = false
|
||||
let initialWidth
|
||||
let initialHeight
|
||||
let initialX
|
||||
let initialY
|
||||
|
||||
const handle = document.createElement('div')
|
||||
Object.assign(handle.style, {
|
||||
position: 'absolute',
|
||||
width: '0',
|
||||
height: '0',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
cursor: 'nwse-resize',
|
||||
|
||||
borderTop: '10px solid transparent',
|
||||
borderLeft: '10px solid transparent',
|
||||
borderBottom: '10px solid var(--border-color)',
|
||||
borderRight: '10px solid var(--border-color)',
|
||||
pointerEvents: 'auto',
|
||||
})
|
||||
|
||||
node.appendChild(handle)
|
||||
|
||||
const resizeHandler = (event) => {
|
||||
resizing = true
|
||||
initialWidth = node.offsetWidth
|
||||
initialHeight = node.offsetHeight
|
||||
initialX = event.clientX
|
||||
initialY = event.clientY
|
||||
|
||||
const mouseMoveHandler = (event) => {
|
||||
if (resizing) {
|
||||
const deltaX = event.clientX - initialX
|
||||
const deltaY = event.clientY - initialY
|
||||
node.style.width = `${initialWidth + deltaX}px`
|
||||
node.style.height = `${initialHeight + deltaY}px`
|
||||
const rect = node.getBoundingClientRect()
|
||||
const newLeft = rect.left - deltaX
|
||||
const newTop = rect.top - deltaY
|
||||
node.style.left = `${newLeft}px`
|
||||
node.style.top = `${newTop}px`
|
||||
}
|
||||
}
|
||||
|
||||
const mouseUpHandler = () => {
|
||||
resizing = false
|
||||
window.removeEventListener('mousemove', mouseMoveHandler)
|
||||
window.removeEventListener('mouseup', mouseUpHandler)
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', mouseMoveHandler)
|
||||
window.addEventListener('mouseup', mouseUpHandler)
|
||||
}
|
||||
|
||||
handle.addEventListener('mousedown', resizeHandler)
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
handle.removeEventListener('mousedown', resizeHandler)
|
||||
handle.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,547 +0,0 @@
|
||||
/**
|
||||
* Adds a named stylesheet to the document with an optional ability to replace an existing one.
|
||||
*
|
||||
* @param name - The unique name (ID) of the stylesheet.
|
||||
* @param css - The CSS rules as a string.
|
||||
* @param force - Whether to replace the existing stylesheet if it exists.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function addNamedStyleSheet(
|
||||
name: string,
|
||||
css: string,
|
||||
force = false,
|
||||
): void {
|
||||
const existingStyleSheet = document.getElementById(name)
|
||||
|
||||
if (existingStyleSheet && !force) {
|
||||
console.debug(
|
||||
`Stylesheet with name "${name}" already exists. Skipping addition.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (existingStyleSheet && force) {
|
||||
console.debug(`Stylesheet with name "${name}" exists. Replacing...`)
|
||||
existingStyleSheet.remove()
|
||||
}
|
||||
|
||||
const styleElement = document.createElement('style')
|
||||
styleElement.id = name
|
||||
styleElement.type = 'text/css'
|
||||
|
||||
styleElement.appendChild(document.createTextNode(css))
|
||||
document.head.appendChild(styleElement)
|
||||
|
||||
console.debug(`Stylesheet with name "${name}" added.`)
|
||||
}
|
||||
|
||||
export const ensureMTBStyles = () => {
|
||||
const S = {
|
||||
fg: 'var(--fg-color)',
|
||||
bgi: 'var(--comfy-input-bg)',
|
||||
bgm: 'var(--comfy-menu-bg)',
|
||||
border: 'var(--comfy-border)',
|
||||
borderHover: 'var(--comfy-border-hover)',
|
||||
box: 'var(--comfy-box)',
|
||||
accent: 'var(--p-button-text-primary-color)',
|
||||
}
|
||||
const common = `
|
||||
.mtb_sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: ${S.bgm};
|
||||
}
|
||||
.mtb_img_grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: scroll;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.mtb_tools {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
const inputs = `
|
||||
/* SELECT */
|
||||
.mtb_select {
|
||||
appearance: none;
|
||||
display: grid;
|
||||
grid-template-areas: "select";
|
||||
padding: 10px;
|
||||
background-color: ${S.bgi};
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
color: ${S.fg};
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@supports (-moz-appearance:none) {
|
||||
.mtb_select{
|
||||
grid-area: select;
|
||||
background: ${S.bgi} url('data:image/gif;base64,R0lGODlhBgAGAKEDAFVVVX9/f9TU1CgmNyH5BAEKAAMALAAAAAAGAAYAAAIODA4hCDKWxlhNvmCnGwUAOw==') right center no-repeat !important;
|
||||
background-position: calc(100% - 5px) center !important;
|
||||
-moz-appearance:none !important;
|
||||
}
|
||||
|
||||
/* styling the dropdown arrow for browsers that support it */
|
||||
.mtb_select:after {
|
||||
content: "";
|
||||
width: 0.8em;
|
||||
height: 0.5em;
|
||||
background-color: ${S.fg};
|
||||
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
|
||||
}
|
||||
|
||||
.mtb_select:focus {
|
||||
outline: none;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.mtb_select > option {
|
||||
padding: 10px;
|
||||
background-color: ${S.bgi};
|
||||
border:none;
|
||||
color: ${S.fg};
|
||||
}
|
||||
|
||||
.mtb_select > option:hover {
|
||||
background-color: red;
|
||||
color: ${S.fg};
|
||||
}
|
||||
|
||||
/* SLIDER */
|
||||
.mtb_slider[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: ${S.bgm};
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
opacity: 0.7;
|
||||
transition: opacity .2s;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
/* slider track */
|
||||
.mtb_slider[type="range"]::-webkit-slider-runnable-track,
|
||||
.mtb_slider[type="range"]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: ${S.bgi};
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* progress */
|
||||
.mtb_slider[type="range"]::-moz-range-progress {
|
||||
background-color: ${S.accent};
|
||||
height:10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* slider thumb (the handle) */
|
||||
.mtb_slider[type="range"]::-webkit-slider-thumb,
|
||||
.mtb_slider[type="range"]::-moz-range-thumb
|
||||
{
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background: ${S.fg};
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
filter: drop-shadow(1px 1px 4px black);
|
||||
}
|
||||
|
||||
.mtb_slider[type="range"]:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mtb_slider[type=range]:-moz-focusring{
|
||||
outline: 1px solid red;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.mtb_slider[type="range"]:hover::-webkit-slider-thumb,
|
||||
.mtb_slider[type="range"]:active::-webkit-slider-thumb {
|
||||
background-color: ${S.accent};
|
||||
}
|
||||
`
|
||||
addNamedStyleSheet(
|
||||
'mtb_ui',
|
||||
`
|
||||
${common}
|
||||
${inputs}
|
||||
`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an element with a div
|
||||
*
|
||||
* @param {Object} [style] - CSS styles to apply to the element.
|
||||
* @returns {HTMLElement} - The created DOM element.
|
||||
*/
|
||||
export const wrapElement = (element, style = {}) => {
|
||||
const container = makeElement('div', style)
|
||||
container.appendChild(element)
|
||||
return container
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DOM element with optional styles, class, and id.
|
||||
*
|
||||
* @param kind - The tag name of the element. Supports class and id syntax (e.g. 'div.class#id').
|
||||
* @param style - CSS styles to apply to the element.
|
||||
* @param parent - A parent to append to
|
||||
* @returns {HTMLElement} - The created DOM element.
|
||||
*/
|
||||
export const makeElement = (
|
||||
kind: string,
|
||||
style?: object,
|
||||
parent?: HTMLElement,
|
||||
): HTMLElement => {
|
||||
let [real_kind, className] = kind.split('.')
|
||||
let id: string | undefined
|
||||
|
||||
if (className?.includes('#')) {
|
||||
;[className, id] = className.split('#')
|
||||
}
|
||||
|
||||
const el = document.createElement(real_kind)
|
||||
|
||||
if (style) {
|
||||
Object.assign(el.style, style)
|
||||
}
|
||||
|
||||
if (className) {
|
||||
el.classList.add(...className.split(' ')) // Support multiple classes
|
||||
}
|
||||
|
||||
if (id) {
|
||||
el.id = id
|
||||
}
|
||||
if (parent) {
|
||||
parent.appendChild(el)
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
/**
|
||||
* Clears all child elements of the given parent element.
|
||||
*
|
||||
* @param el - The parent element whose children should be removed.
|
||||
*/
|
||||
export const clearElement = (el: HTMLElement) => {
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.firstChild)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates a labeled element (input, select, etc.).
|
||||
*
|
||||
* @param {HTMLElement} el - The element to label.
|
||||
* @param {string} labelText - The label text.
|
||||
* @returns {HTMLDivElement} - A div containing the label and the element.
|
||||
*/
|
||||
export const makeLabeledElement = (
|
||||
el: HTMLElement,
|
||||
labelText: string,
|
||||
): HTMLDivElement => {
|
||||
const wrapper = makeElement('div.mtb_labeled_element', {
|
||||
marginBottom: '1em',
|
||||
})
|
||||
const label = makeElement('label', {
|
||||
display: 'block',
|
||||
marginBottom: '0.5em',
|
||||
})
|
||||
label.textContent = labelText
|
||||
wrapper.appendChild(label)
|
||||
wrapper.appendChild(el)
|
||||
return wrapper as HTMLDivElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a camelCase CSS property to kebab-case.
|
||||
*
|
||||
* @param prop - The camelCase CSS property.
|
||||
* @returns - The kebab-case CSS property.
|
||||
*/
|
||||
const camelToKebab = (prop: string): string =>
|
||||
prop.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
|
||||
|
||||
/**
|
||||
* Parses the style string into an object of CSS property-value pairs.
|
||||
*
|
||||
* @param {string} styleString - The CSS rule text (e.g., "color: red; background-color: blue;").
|
||||
* @returns {Object} - An object with camelCase CSS properties.
|
||||
*/
|
||||
const parseStyleString = (styleString: string): object => {
|
||||
const styleObj: Record<string, unknown> = {}
|
||||
for (const rule of styleString.split(';')) {
|
||||
const [property, value] = rule.split(':').map((item) => item.trim())
|
||||
if (property && value) {
|
||||
const camelProp = property.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
|
||||
styleObj[camelProp] = value
|
||||
}
|
||||
}
|
||||
return styleObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a new CSS class with the provided styles, or skips if the class already exists.
|
||||
*
|
||||
* @param {string} className - The name of the CSS class to define.
|
||||
* @param {Object} classStyles - An object containing camelCase CSS property-value pairs.
|
||||
*/
|
||||
export function defineCSSClass(className: string, classStyles: object) {
|
||||
const styleSheets = document.styleSheets
|
||||
let classExists = false
|
||||
let existingStyleString = ''
|
||||
const classExistsInStyleSheet = (styleSheet: CSSStyleSheet) => {
|
||||
const rules = styleSheet.cssRules
|
||||
for (const rule of rules) {
|
||||
if (rule.selectorText === `.${className}`) {
|
||||
classExists = true
|
||||
existingStyleString = rule.style.cssText // Capture existing styles
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for (const styleSheet of styleSheets) {
|
||||
if (classExistsInStyleSheet(styleSheet)) {
|
||||
console.debug(`Class ${className} already exists, merging styles...`)
|
||||
break
|
||||
}
|
||||
}
|
||||
const existingStyles = classExists
|
||||
? parseStyleString(existingStyleString)
|
||||
: {}
|
||||
const mergedStyles = { ...existingStyles, ...classStyles }
|
||||
|
||||
const stylesString = Object.entries(mergedStyles)
|
||||
.map(([key, value]) => `${camelToKebab(key)}: ${value};`)
|
||||
.join(' ')
|
||||
|
||||
if (!classExists) {
|
||||
console.debug(`Defining new class ${className}...`)
|
||||
styleSheets[0].insertRule(`.${className} { ${stylesString} }`, 0)
|
||||
} else {
|
||||
console.debug(`Updating existing class ${className} with merged styles...`)
|
||||
for (const styleSheet of styleSheets) {
|
||||
const rules = styleSheet.cssRules
|
||||
for (const rule of rules) {
|
||||
if (rule.selectorText === `.${className}`) {
|
||||
rule.style.cssText = stylesString // Update the existing rule
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`Class ${className} has been defined/updated with styles:`,
|
||||
mergedStyles,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a sidebar and ensures it resizes correctly when the window is resized.
|
||||
*
|
||||
* @param el - The element where the sidebar is rendered.
|
||||
* @param cont - The content container of the sidebar.
|
||||
* @param elems - Array of elements to append to the sidebar.
|
||||
* @returns - A handle with a method to unregister the resize event.
|
||||
*/
|
||||
export const renderSidebar = (
|
||||
el: HTMLElement,
|
||||
cont: HTMLElement,
|
||||
elems: HTMLElement[],
|
||||
) => {
|
||||
el.appendChild(cont)
|
||||
|
||||
if (!el.parentNode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!('style' in el.parentNode) && !('offsetHeight' in el.parentNode)) {
|
||||
return
|
||||
}
|
||||
el.parentNode.style.overflowY = 'clip'
|
||||
cont.style.height = `${el.parentNode.offsetHeight}px`
|
||||
|
||||
const resizeHandler = () => {
|
||||
cont.style.height = `${el.parentNode.offsetHeight}px`
|
||||
}
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
|
||||
for (const elem of elems) {
|
||||
cont.appendChild(elem)
|
||||
}
|
||||
|
||||
return {
|
||||
unregister: () => {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a <select> dropdown with given options.
|
||||
*
|
||||
* @param {string[]} options - The options for the select element.
|
||||
* @param {string} [current] - The currently selected option (optional).
|
||||
* @returns {HTMLSelectElement} - The created <select> element.
|
||||
*/
|
||||
export const makeSelect = (
|
||||
options: string[],
|
||||
current?: string,
|
||||
): HTMLSelectElement => {
|
||||
const selector = makeElement('select.mtb_select', {
|
||||
width: 'auto',
|
||||
margin: '1em',
|
||||
}) as HTMLSelectElement
|
||||
|
||||
for (const option of options) {
|
||||
const opt = makeElement('option') as HTMLOptionElement
|
||||
opt.value = option
|
||||
opt.innerHTML = option
|
||||
selector.appendChild(opt)
|
||||
}
|
||||
|
||||
if (current !== undefined) {
|
||||
if (options.includes(current)) {
|
||||
selector.value = current
|
||||
} else {
|
||||
console.error(
|
||||
`You tried to select an option that doesn't exist (${current}). Options: ${options}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return selector
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an <input type="range"> slider element with given parameters.
|
||||
*
|
||||
* @param {number} min - Minimum value of the slider.
|
||||
* @param {number} max - Maximum value of the slider.
|
||||
* @param {number} [value] - Initial value of the slider.
|
||||
* @param {number} [step] - Step value for the slider.
|
||||
* @returns {HTMLInputElement} - The created slider element.
|
||||
*/
|
||||
export const makeSlider = (
|
||||
min: number,
|
||||
max: number,
|
||||
value?: number,
|
||||
step?: number,
|
||||
): HTMLInputElement => {
|
||||
const slider = makeElement('input.mtb_slider', {
|
||||
width: '100%',
|
||||
}) as HTMLInputElement
|
||||
|
||||
slider.type = 'range'
|
||||
slider.min = (min || 0).toString()
|
||||
slider.max = (max || 100).toString()
|
||||
slider.value = (value || slider.min).toString()
|
||||
slider.step = (step || 1).toString()
|
||||
|
||||
return slider
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a button element.
|
||||
*
|
||||
* @param {string} label - The label for the button.
|
||||
* @param {Object} [style] - Optional styles to apply to the button.
|
||||
* @param {Function} [onClick] - Optional click handler.
|
||||
* @returns {HTMLButtonElement} - The created button element.
|
||||
*/
|
||||
export const makeButton = (
|
||||
label: string,
|
||||
style: object = {},
|
||||
onClick?: (e: MouseEvent) => void,
|
||||
): HTMLButtonElement => {
|
||||
const button = makeElement('button.mtb_button', style) as HTMLButtonElement
|
||||
button.textContent = label
|
||||
|
||||
if (onClick) {
|
||||
button.addEventListener('click', onClick)
|
||||
}
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a resizable splitter between two elements.
|
||||
*
|
||||
* @param {HTMLElement} el1 - The first element.
|
||||
* @param {HTMLElement} el2 - The second element.
|
||||
* @param {'vertical' | 'horizontal'} direction - Splitter direction (vertical or horizontal).
|
||||
* @param {'absolute' | 'normal'} mode - Splitter mode: 'absolute' for free resizing, 'normal' for layout-based resizing.
|
||||
* @returns {HTMLDivElement} - The container with resizable splitter.
|
||||
*/
|
||||
export const makeSplitter = (
|
||||
el1: HTMLElement,
|
||||
el2: HTMLElement,
|
||||
direction: 'vertical' | 'horizontal' = 'vertical',
|
||||
mode: 'absolute' | 'normal' = 'normal',
|
||||
): HTMLDivElement => {
|
||||
const container = makeElement('div.mtb_splitter_container', {
|
||||
display: mode === 'absolute' ? 'block' : 'flex',
|
||||
flexDirection: direction === 'vertical' ? 'row' : 'column',
|
||||
position: mode === 'absolute' ? 'relative' : 'static',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}) as HTMLDivElement
|
||||
|
||||
const handle = makeElement('div.mtb_splitter_handle', {
|
||||
backgroundColor: '#ccc',
|
||||
cursor: direction === 'vertical' ? 'col-resize' : 'row-resize',
|
||||
width: direction === 'vertical' ? '5px' : '100%',
|
||||
height: direction === 'horizontal' ? '5px' : '100%',
|
||||
})
|
||||
|
||||
let isResizing = false
|
||||
|
||||
handle.addEventListener('mousedown', () => {
|
||||
isResizing = true
|
||||
})
|
||||
|
||||
window.addEventListener('mouseup', () => {
|
||||
isResizing = false
|
||||
})
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
if (!isResizing) return
|
||||
if (direction === 'vertical') {
|
||||
const newWidth = e.clientX - container.offsetLeft
|
||||
el1.style.width = `${newWidth}px`
|
||||
el2.style.width = `${container.offsetWidth - newWidth}px`
|
||||
} else {
|
||||
const newHeight = e.clientY - container.offsetTop
|
||||
el1.style.height = `${newHeight}px`
|
||||
el2.style.height = `${container.offsetHeight - newHeight}px`
|
||||
}
|
||||
})
|
||||
|
||||
container.appendChild(el1)
|
||||
container.appendChild(handle)
|
||||
container.appendChild(el2)
|
||||
|
||||
return container
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const inComfy = () => {
|
||||
return window !== undefined && window.app !== undefined
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
// target: document.getElementById('app'),
|
||||
target: document.querySelector('body'),
|
||||
})
|
||||
|
||||
export default app
|
||||
@@ -1,20 +0,0 @@
|
||||
// src/mocks/api.ts
|
||||
|
||||
export const api = {
|
||||
addEventListener: (event: string, callback: (e: CustomEvent) => void) => {
|
||||
console.log(`Mock api.addEventListener: ${event}`)
|
||||
},
|
||||
removeEventListener: (event: string, callback: (e: CustomEvent) => void) => {
|
||||
console.log(`Mock api.removeEventListener: ${event}`)
|
||||
},
|
||||
getItems: async (type: string) => {
|
||||
console.log(`Mock api.getItems: ${type}`)
|
||||
return { Running: [], Pending: [] }
|
||||
},
|
||||
deleteItem: async (type: string, id: string) => {
|
||||
console.log(`Mock api.deleteItem: ${type} ${id}`)
|
||||
},
|
||||
clearItems: async (type: string) => {
|
||||
console.log(`Mock api.clearItems: ${type}`)
|
||||
},
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// src/mocks/app.ts
|
||||
|
||||
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types'
|
||||
|
||||
export const app: ComfyApp = {
|
||||
canvas: document.createElement('canvas'),
|
||||
graph: {
|
||||
// Mock graph methods if you use them
|
||||
add: (node: any) => console.log('Mock graph.add', node),
|
||||
// ...
|
||||
} as any, // Cast to any if you don't want to fully mock LGraph
|
||||
ui: {
|
||||
// Mock UI methods
|
||||
dialog: {
|
||||
show: (content: any) => console.log('Mock dialog.show', content),
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
} as any, // Cast to any if you don't want to fully mock UI
|
||||
// Mock common methods
|
||||
onReady: async () => {
|
||||
console.log('Mock app.onReady called')
|
||||
// Simulate a delay if your code expects async behavior
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
},
|
||||
registerCustomNodeMapping: (mapping: any) =>
|
||||
console.log('Mock registerCustomNodeMapping', mapping),
|
||||
// Add any other properties/methods your code directly accesses
|
||||
// that would cause errors if undefined.
|
||||
// You can use `jest.fn()` or similar if you're using a testing framework.
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// src/mocks/comfy_shared.ts
|
||||
// Mock for comfy_shared.js utilities
|
||||
|
||||
export function deepMerge(target: object, ...sources: object[]): object {
|
||||
for (const source of sources) {
|
||||
for (const key in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
const sourceValue = (source as Record<string, unknown>)[key]
|
||||
const targetValue = (target as Record<string, unknown>)[key]
|
||||
if (
|
||||
sourceValue &&
|
||||
typeof sourceValue === 'object' &&
|
||||
!Array.isArray(sourceValue)
|
||||
) {
|
||||
;(target as Record<string, unknown>)[key] = deepMerge(
|
||||
(targetValue as object) || {},
|
||||
sourceValue as object,
|
||||
)
|
||||
} else {
|
||||
;(target as Record<string, unknown>)[key] = sourceValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
export function addMenuHandler(
|
||||
nodeType: unknown,
|
||||
callback: (...args: unknown[]) => void,
|
||||
): void {
|
||||
console.log('Mock addMenuHandler called')
|
||||
}
|
||||
|
||||
export function extendPrototype(
|
||||
prototype: object,
|
||||
methodName: string,
|
||||
callback: (...args: unknown[]) => void,
|
||||
): void {
|
||||
console.log(`Mock extendPrototype: ${methodName}`)
|
||||
}
|
||||
|
||||
export function getNodes(includeAll = false): unknown[] {
|
||||
console.log('Mock getNodes called')
|
||||
return []
|
||||
}
|
||||
|
||||
export function infoLogger(...args: unknown[]): void {
|
||||
console.log('[MTB Info]', ...args)
|
||||
}
|
||||
|
||||
export function warnLogger(...args: unknown[]): void {
|
||||
console.warn('[MTB Warn]', ...args)
|
||||
}
|
||||
|
||||
export function errorLogger(...args: unknown[]): void {
|
||||
console.error('[MTB Error]', ...args)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// src/mocks/ui.ts
|
||||
|
||||
type ElementOptions = {
|
||||
textContent?: string
|
||||
onclick?: () => void
|
||||
type?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock implementation of ComfyUI's $el helper
|
||||
*/
|
||||
export function $el(
|
||||
tag: string,
|
||||
opts?: ElementOptions | HTMLElement[],
|
||||
children?: HTMLElement[],
|
||||
): HTMLElement {
|
||||
const parts = tag.split('.')
|
||||
const tagName = parts[0] || 'div'
|
||||
const className = parts.slice(1).join(' ')
|
||||
|
||||
const element = document.createElement(tagName)
|
||||
if (className) {
|
||||
element.className = className
|
||||
}
|
||||
|
||||
if (Array.isArray(opts)) {
|
||||
// opts is children
|
||||
opts.forEach((child) => element.appendChild(child))
|
||||
} else if (opts) {
|
||||
// opts is options
|
||||
Object.entries(opts).forEach(([key, value]) => {
|
||||
if (key === 'textContent') {
|
||||
element.textContent = value as string
|
||||
} else if (key === 'onclick') {
|
||||
element.onclick = value as () => void
|
||||
} else if (key.startsWith('on')) {
|
||||
element.addEventListener(key.slice(2).toLowerCase(), value as EventListener)
|
||||
} else {
|
||||
element.setAttribute(key, String(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (children) {
|
||||
children.forEach((child) => element.appendChild(child))
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
/* MTB API Authoring Layer - On-Node Widget Styles */
|
||||
:root {
|
||||
--mtb-api-color: #4f6ef7;
|
||||
--mtb-api-output-color: #f7b84f;
|
||||
--mtb-api-radius: 8px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
API Settings Widget Container (on nodes)
|
||||
============================================ */
|
||||
.mtb_api_settings {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 12px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(79, 110, 247, 0.15) 0%,
|
||||
rgba(79, 110, 247, 0.05) 100%
|
||||
);
|
||||
border: 1px solid rgba(79, 110, 247, 0.3);
|
||||
border-radius: var(--mtb-api-radius);
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mtb_api_output {
|
||||
background: transparent;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Input Section (per widget)
|
||||
============================================ */
|
||||
.mtb_api_section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mtb_api_section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Checkbox + Title Row */
|
||||
.mtb_api_checkbox_label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mtb_api_checkbox_label input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--mtb-api-color);
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mtb_api_title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* Content Container (name/type fields) */
|
||||
.mtb_api_section > div:not(.mtb_api_checkbox_label) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 6px 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.mtb_api_section label {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Input Fields */
|
||||
.mtb_api_section input[type="text"],
|
||||
.mtb_api_section select {
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
pointer-events: auto;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.mtb_api_section input[type="text"]:focus,
|
||||
.mtb_api_section select:focus {
|
||||
border-color: var(--mtb-api-color);
|
||||
box-shadow: 0 0 0 2px rgba(79, 110, 247, 0.2);
|
||||
}
|
||||
|
||||
.mtb_api_section select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.mtb_api_section hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
/* Disabled State */
|
||||
.mtb_api_disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Node Settings Section
|
||||
============================================ */
|
||||
.mtb_api_node_settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
margin-top: 4px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mtb_api_node_settings_label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.mtb_api_toggle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mtb_api_toggle input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.mtb_api_toggle_track {
|
||||
width: 32px;
|
||||
height: 18px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 9px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.mtb_api_toggle_track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.mtb_api_toggle input:checked + .mtb_api_toggle_track {
|
||||
background: var(--mtb-api-color);
|
||||
}
|
||||
|
||||
.mtb_api_toggle input:checked + .mtb_api_toggle_track::after {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
|
||||
.mtb_api_toggle_label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Type-Specific Config Section
|
||||
============================================ */
|
||||
.mtb_api_type_config {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mtb_api_config_row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mtb_api_config_row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mtb_api_config_row label {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mtb_api_config_row input[type="number"],
|
||||
.mtb_api_config_row input[type="text"] {
|
||||
width: 60px;
|
||||
height: 24px;
|
||||
padding: 0 6px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
pointer-events: auto;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.mtb_api_config_row input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.mtb_api_config_row input[type="number"]:focus,
|
||||
.mtb_api_config_row input[type="text"]:focus {
|
||||
border-color: var(--mtb-api-color);
|
||||
box-shadow: 0 0 0 2px rgba(79, 110, 247, 0.2);
|
||||
}
|
||||
|
||||
.mtb_api_config_row input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mtb_api_config_row span {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/** Color for API-marked nodes (refined blue) */
|
||||
export const API_COLOR = '#4f6ef7'
|
||||
|
||||
/** Color for API output nodes (warm gold) */
|
||||
export const OUTPUT_COLOR = '#f7b84f'
|
||||
|
||||
/** CSS custom properties for API styling */
|
||||
export const API_CSS_VARS = `
|
||||
:root {
|
||||
--mtb-api-color: ${API_COLOR};
|
||||
--mtb-api-output-color: ${OUTPUT_COLOR};
|
||||
--mtb-api-radius: 8px;
|
||||
}
|
||||
`
|
||||
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* MTB API Extension Registration
|
||||
* Registers the API authoring layer with ComfyUI
|
||||
*/
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import * as shared from '@mtb/shared'
|
||||
import { apiSettingsWidgetManager } from './widget-manager'
|
||||
import { getAPIPanel, notifyAPIChanged } from './panel.svelte'
|
||||
import type { MTBNode } from './types'
|
||||
|
||||
interface ContextMenuItem {
|
||||
content: string
|
||||
callback: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
interface NodeType {
|
||||
prototype: {
|
||||
onDrawForeground?: (...args: unknown[]) => void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the MTB API extension with ComfyUI
|
||||
*/
|
||||
export function registerMtbApiExtension(): void {
|
||||
app.registerExtension({
|
||||
name: 'mtb.api',
|
||||
|
||||
setup() {
|
||||
// Keyboard shortcut: Ctrl+Shift+A
|
||||
const panel = getAPIPanel()
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'A') {
|
||||
e.preventDefault()
|
||||
panel.toggle()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
init() {
|
||||
// Register sidebar tab
|
||||
// @ts-expect-error - extensionManager is ComfyUI API
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: 'mtb-api-panel',
|
||||
icon: 'pi pi-bolt',
|
||||
title: 'API Authoring',
|
||||
tooltip: 'MTB: Configure API inputs and outputs',
|
||||
type: 'custom',
|
||||
render: (el: HTMLElement) => {
|
||||
const panel = getAPIPanel()
|
||||
panel.renderInto(el)
|
||||
},
|
||||
destroy: () => {
|
||||
// Cleanup if needed
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(
|
||||
nodeType: NodeType,
|
||||
_nodeData: unknown,
|
||||
_app: unknown,
|
||||
) {
|
||||
// Add API menu to all nodes
|
||||
shared.addMenuHandler(nodeType, function (
|
||||
this: MTBNode,
|
||||
_app: unknown,
|
||||
options: ContextMenuItem[],
|
||||
) {
|
||||
// Mark/Unmark as API input
|
||||
const markApiItem: ContextMenuItem = {
|
||||
content: this.properties.useAPI ? 'Unmark API ⚡' : 'Mark API ⚡',
|
||||
callback: (...args: unknown[]) => {
|
||||
const node = args[4] as MTBNode
|
||||
if (node.properties.useAPI) {
|
||||
node.setProperty('useAPI', false)
|
||||
apiSettingsWidgetManager.ensureWidgets(node)
|
||||
} else {
|
||||
node.setProperty('useAPI', true)
|
||||
}
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}
|
||||
options.push(markApiItem)
|
||||
|
||||
// Mark/Remove as API output
|
||||
const markOutputItem: ContextMenuItem = {
|
||||
content: this.properties.mtb_api?.isAPIOutput
|
||||
? 'Remove Output (API) ⚡'
|
||||
: 'Mark Output (API) ⚡',
|
||||
callback: (...args: unknown[]) => {
|
||||
const node = args[4] as MTBNode
|
||||
if (node.properties.mtb_api?.isAPIOutput) {
|
||||
apiSettingsWidgetManager.applySettings(node, { isAPIOutput: false })
|
||||
apiSettingsWidgetManager.ensureWidgets(node)
|
||||
} else {
|
||||
apiSettingsWidgetManager.applySettings(node, { isAPIOutput: true })
|
||||
node.setProperty('useAPI', true)
|
||||
}
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}
|
||||
options.push(markOutputItem)
|
||||
|
||||
// Show/Hide disabled inputs
|
||||
const currentShow =
|
||||
this.properties.mtb_api?.showDisabled === undefined
|
||||
? true
|
||||
: this.properties.mtb_api?.showDisabled
|
||||
|
||||
const hideDisabledItem: ContextMenuItem = {
|
||||
content: currentShow
|
||||
? 'Hide Disabled (API) ⚡'
|
||||
: 'Show Disabled (API) ⚡',
|
||||
callback: (...args: unknown[]) => {
|
||||
const node = args[4] as MTBNode
|
||||
const oldApi = node.properties.mtb_api || {}
|
||||
if (oldApi?.showDisabled !== undefined) {
|
||||
node.setProperty('mtb_api', {
|
||||
...oldApi,
|
||||
showDisabled: !oldApi.showDisabled,
|
||||
})
|
||||
} else {
|
||||
node.setProperty('mtb_api', {
|
||||
...oldApi,
|
||||
showDisabled: false,
|
||||
})
|
||||
}
|
||||
apiSettingsWidgetManager.ensureWidgets(node, true)
|
||||
},
|
||||
}
|
||||
options.push(hideDisabledItem)
|
||||
|
||||
return [markApiItem, hideDisabledItem]
|
||||
})
|
||||
|
||||
// Extend onDrawForeground to draw API indicators
|
||||
const origDrawForeground = nodeType.prototype.onDrawForeground
|
||||
nodeType.prototype.onDrawForeground = function (
|
||||
this: MTBNode,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvas: unknown,
|
||||
) {
|
||||
origDrawForeground?.apply(this, [ctx, canvas])
|
||||
apiSettingsWidgetManager.drawForeground(this, ctx, canvas)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Graph to Prompt conversion for MTB API
|
||||
* Uses ComfyUI's graphToPrompt and post-processes to add MTB API metadata
|
||||
*/
|
||||
|
||||
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types'
|
||||
import type { GraphPromptResult, APINodeSettings, MTBNode } from './types'
|
||||
|
||||
/**
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* Includes MTB API metadata for SDK consumers
|
||||
*/
|
||||
export async function graphToPrompt(app: ComfyApp): Promise<GraphPromptResult> {
|
||||
// Use ComfyUI's built-in graphToPrompt which handles all complexity
|
||||
// (subgraphs, DTOs, virtual nodes, etc.)
|
||||
const result = await app.graphToPrompt()
|
||||
|
||||
// Post-process to add MTB API metadata to nodes that have it
|
||||
for (const nodeId of Object.keys(result.output)) {
|
||||
const nodeData = result.output[nodeId]
|
||||
|
||||
// Find the original node in the graph to get MTB API settings
|
||||
const node = app.graph.getNodeById(Number(nodeId)) as MTBNode | null
|
||||
|
||||
if (node?.properties?.mtb_api) {
|
||||
const apiSettings = { ...node.properties.mtb_api }
|
||||
const enabledInputs: Record<string, Partial<APINodeSettings['inputs']>[string]> = {}
|
||||
|
||||
// Clean up internal-only settings
|
||||
delete apiSettings.showDisabled
|
||||
|
||||
// Filter to only enabled inputs
|
||||
if (apiSettings.inputs) {
|
||||
for (const k of Object.keys(apiSettings.inputs)) {
|
||||
const current = apiSettings.inputs[k]
|
||||
if (current.enabled) {
|
||||
enabledInputs[k] = { ...current }
|
||||
delete (enabledInputs[k] as { enabled?: boolean }).enabled
|
||||
}
|
||||
}
|
||||
apiSettings.inputs = enabledInputs as typeof apiSettings.inputs
|
||||
}
|
||||
|
||||
// Add MTB API metadata
|
||||
nodeData._meta = {
|
||||
...nodeData._meta,
|
||||
apiSettings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result as GraphPromptResult
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* MTB API Authoring Layer
|
||||
* Main entry point for the API extension
|
||||
*
|
||||
* NOTE: This bundle does NOT auto-register to avoid side-effects.
|
||||
* ComfyUI loads all .js files in web/, so dist/ files must be side-effect free.
|
||||
* Call registerMtbApiExtension() explicitly from the main entry point.
|
||||
*/
|
||||
|
||||
// Import CSS for injection (this is bundled, not a side-effect at runtime)
|
||||
import './api_nodes.css'
|
||||
|
||||
// Re-export types
|
||||
export * from './types'
|
||||
|
||||
// Re-export constants
|
||||
export { API_COLOR, OUTPUT_COLOR } from './constants'
|
||||
|
||||
// Re-export core modules
|
||||
export { graphToPrompt } from './graph-to-prompt'
|
||||
export { APISettingsWidgetManager, apiSettingsWidgetManager } from './widget-manager'
|
||||
export { APIPanel, getAPIPanel, notifyAPIChanged, MTB_API_CHANGED_EVENT } from './panel.svelte'
|
||||
export { registerMtbApiExtension } from './extension'
|
||||
@@ -1,239 +0,0 @@
|
||||
/**
|
||||
* API Panel Controller
|
||||
* Manages the Svelte-based API panel for controlling exposed inputs
|
||||
*
|
||||
* NOTE: This file uses .svelte.ts extension to enable Svelte 5 runes ($state)
|
||||
*/
|
||||
|
||||
import { mount, unmount } from 'svelte'
|
||||
import * as shared from '@mtb/shared'
|
||||
import Inspector from '../lib/Inspector.svelte'
|
||||
import type { APIInput, MTBNode } from './types'
|
||||
|
||||
interface InputItem {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
options?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactive props state for the Inspector component
|
||||
* Using $state makes this reactive - changes auto-update the component
|
||||
*/
|
||||
function createPanelProps() {
|
||||
let props = $state({
|
||||
visible: true,
|
||||
inputs: {} as Record<string, InputItem>,
|
||||
})
|
||||
return props
|
||||
}
|
||||
|
||||
/** Custom event for API changes */
|
||||
export const MTB_API_CHANGED_EVENT = 'mtb:api:changed'
|
||||
|
||||
/** Custom event for order changes from drag-drop */
|
||||
export const MTB_API_ORDER_CHANGED_EVENT = 'mtb:api:order-changed'
|
||||
|
||||
/** Dispatch event to notify panel of changes */
|
||||
export function notifyAPIChanged(): void {
|
||||
window.dispatchEvent(new CustomEvent(MTB_API_CHANGED_EVENT))
|
||||
}
|
||||
|
||||
/** Dispatch event with new order after drag-drop */
|
||||
export function notifyOrderChanged(orderedInputs: { node_id: number; original_name: string; order: number }[]): void {
|
||||
window.dispatchEvent(new CustomEvent(MTB_API_ORDER_CHANGED_EVENT, { detail: orderedInputs }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the API panel UI for managing exposed workflow inputs
|
||||
*/
|
||||
export class APIPanel {
|
||||
private component: ReturnType<typeof mount> | null = null
|
||||
private props = createPanelProps()
|
||||
private sidebarMode = false
|
||||
private listening = false
|
||||
|
||||
constructor() {
|
||||
// Don't mount automatically - wait for renderInto or show
|
||||
this.setupChangeListener()
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for API changes and update panel reactively
|
||||
*/
|
||||
private setupChangeListener(): void {
|
||||
if (this.listening) return
|
||||
this.listening = true
|
||||
|
||||
window.addEventListener(MTB_API_CHANGED_EVENT, () => {
|
||||
// Debounce updates slightly
|
||||
requestAnimationFrame(() => {
|
||||
this.updateContent()
|
||||
})
|
||||
})
|
||||
|
||||
// Listen for order changes from drag-drop
|
||||
window.addEventListener(MTB_API_ORDER_CHANGED_EVENT, ((e: CustomEvent<{ node_id: number; original_name: string; order: number }[]>) => {
|
||||
this.applyInputOrder(e.detail)
|
||||
}) as EventListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies new order to node properties after drag-drop reorder
|
||||
*/
|
||||
private applyInputOrder(orderedInputs: { node_id: number; original_name: string; order: number }[]): void {
|
||||
for (const node of shared.getNodes(true) as MTBNode[]) {
|
||||
const nodeInputs = orderedInputs.filter(i => i.node_id === node.id)
|
||||
if (nodeInputs.length === 0) continue
|
||||
|
||||
for (const input of nodeInputs) {
|
||||
if (node.properties.mtb_api?.inputs?.[input.original_name]) {
|
||||
node.properties.mtb_api.inputs[input.original_name].order = input.order
|
||||
}
|
||||
}
|
||||
// Trigger property update
|
||||
node.setProperty('mtb_api', node.properties.mtb_api)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and mounts the Inspector Svelte component
|
||||
*/
|
||||
private createPanel(target: HTMLElement = document.body) {
|
||||
return mount(Inspector, {
|
||||
target,
|
||||
props: this.props,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the panel into a sidebar element
|
||||
*/
|
||||
renderInto(el: HTMLElement): void {
|
||||
this.sidebarMode = true
|
||||
this.props.visible = true
|
||||
|
||||
// Destroy existing component if any
|
||||
if (this.component) {
|
||||
unmount(this.component)
|
||||
}
|
||||
|
||||
this.component = this.createPanel(el)
|
||||
this.updateContent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the panel and updates its content
|
||||
*/
|
||||
show(): void {
|
||||
if (!this.component) {
|
||||
this.component = this.createPanel()
|
||||
}
|
||||
this.updateContent()
|
||||
this.props.visible = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the panel
|
||||
*/
|
||||
hide(): void {
|
||||
this.props.visible = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the panel is currently visible
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
return this.props.visible
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles panel visibility
|
||||
*/
|
||||
toggle(): void {
|
||||
if (this.isVisible()) {
|
||||
this.hide()
|
||||
} else {
|
||||
this.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all API inputs from marked nodes in the graph
|
||||
*/
|
||||
getAPIInputs(): Record<string, APIInput> {
|
||||
const inputsList: (APIInput & { original_name: string })[] = []
|
||||
|
||||
for (const node of shared.getNodes(true) as MTBNode[]) {
|
||||
const widgets = node.widgets
|
||||
|
||||
if (node.properties.mtb_api && node.properties.useAPI) {
|
||||
if (node.properties.mtb_api.inputs) {
|
||||
for (const currentName in node.properties.mtb_api.inputs) {
|
||||
const current = node.properties.mtb_api.inputs[currentName]
|
||||
if (current.enabled) {
|
||||
const inputName = current.name || currentName
|
||||
const widget = widgets?.find((w) => w.name === currentName)
|
||||
if (!widget) continue
|
||||
|
||||
inputsList.push({
|
||||
...current,
|
||||
id: 0, // Will be assigned after sorting
|
||||
name: inputName,
|
||||
original_name: currentName,
|
||||
type: current.type,
|
||||
node_id: node.id,
|
||||
widgets: [widget],
|
||||
// Extract current value from widget
|
||||
value: widget.value,
|
||||
// For COMBO types, extract options
|
||||
options: (widget.options as { values?: string[] })?.values,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by order (undefined order goes to end)
|
||||
inputsList.sort((a, b) => {
|
||||
const orderA = a.order ?? Number.MAX_SAFE_INTEGER
|
||||
const orderB = b.order ?? Number.MAX_SAFE_INTEGER
|
||||
return orderA - orderB
|
||||
})
|
||||
|
||||
// Convert to Record and assign sequential IDs
|
||||
const inputs: Record<string, APIInput> = {}
|
||||
inputsList.forEach((input, index) => {
|
||||
input.id = index + 1
|
||||
inputs[input.name] = input
|
||||
})
|
||||
|
||||
return inputs
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the panel content with current API inputs
|
||||
*/
|
||||
updateContent(): void {
|
||||
const newInputs = this.getAPIInputs()
|
||||
// Update the reactive props - this auto-updates the component
|
||||
this.props.inputs = newInputs
|
||||
console.log('Found API inputs:', newInputs)
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton panel instance */
|
||||
let panelInstance: APIPanel | null = null
|
||||
|
||||
/**
|
||||
* Gets or creates the singleton panel instance
|
||||
*/
|
||||
export function getAPIPanel(): APIPanel {
|
||||
if (!panelInstance) {
|
||||
panelInstance = new APIPanel()
|
||||
}
|
||||
return panelInstance
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import type { LGraphNode, IWidget, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
|
||||
/** Supported API input types */
|
||||
export const API_INPUT_TYPES = [
|
||||
'STRING',
|
||||
'IMAGE',
|
||||
'AUDIO',
|
||||
'VIDEO',
|
||||
'COMBO',
|
||||
'MODEL',
|
||||
'NUMBER',
|
||||
'FLOATS',
|
||||
'BOOLEAN',
|
||||
] as const
|
||||
|
||||
export type APIInputType = (typeof API_INPUT_TYPES)[number]
|
||||
|
||||
/** Settings for a single API input on a node */
|
||||
export interface APIInputSettings {
|
||||
enabled: boolean
|
||||
type: APIInputType
|
||||
name: string
|
||||
/** Display order (lower = first) */
|
||||
order?: number
|
||||
|
||||
// NUMBER config
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
|
||||
// STRING config
|
||||
multiline?: boolean
|
||||
|
||||
// AUDIO/VIDEO config
|
||||
accept?: string // e.g., ".mp3,.wav" or ".mp4,.webm"
|
||||
maxDuration?: number // seconds
|
||||
trimEnabled?: boolean
|
||||
}
|
||||
|
||||
/** API configuration stored in node.properties.mtb_api */
|
||||
export interface APINodeSettings {
|
||||
isAPIOutput?: boolean
|
||||
showDisabled?: boolean
|
||||
includeInExport?: boolean
|
||||
inputs?: Record<string, APIInputSettings>
|
||||
}
|
||||
|
||||
/** Extended node properties with MTB API fields */
|
||||
export interface MTBNodeProperties {
|
||||
useAPI?: boolean
|
||||
mtb_api?: APINodeSettings
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** LGraphNode with MTB API properties */
|
||||
export interface MTBNode extends LGraphNode {
|
||||
properties: MTBNodeProperties
|
||||
}
|
||||
|
||||
/** Collected API input with widget references */
|
||||
export interface APIInput {
|
||||
id: number
|
||||
name: string
|
||||
original_name?: string
|
||||
type: APIInputType
|
||||
node_id: number
|
||||
widgets: IWidget[]
|
||||
options?: string[]
|
||||
widget?: IWidget
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
/** Result of graphToPrompt conversion */
|
||||
export interface GraphPromptResult {
|
||||
workflow: object
|
||||
output: Record<
|
||||
string,
|
||||
{
|
||||
inputs: Record<string, unknown>
|
||||
class_type: string
|
||||
_meta?: {
|
||||
title: string
|
||||
apiSettings?: Partial<APINodeSettings>
|
||||
}
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
/** Props for the API Panel component */
|
||||
export interface APIPanelProps {
|
||||
visible?: boolean
|
||||
inputs?: Record<string, APIInput>
|
||||
}
|
||||
|
||||
/** Interface for the API Settings Widget Manager */
|
||||
export interface IAPISettingsWidgetManager {
|
||||
createAPISettingsWidget(node: MTBNode, force?: boolean): void
|
||||
removeAPISettingsWidget(node: MTBNode, widget?: IWidget): void
|
||||
applySettings(
|
||||
node: MTBNode,
|
||||
settings: Partial<APINodeSettings>,
|
||||
): APINodeSettings
|
||||
ensureWidgets(node: MTBNode, force?: boolean): void
|
||||
drawForeground(
|
||||
node: MTBNode,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
canvas: LGraphCanvas,
|
||||
): void
|
||||
}
|
||||
@@ -1,597 +0,0 @@
|
||||
/**
|
||||
* API Settings Widget Manager
|
||||
* Manages DOM widgets for configuring API inputs on nodes
|
||||
*/
|
||||
|
||||
import type { IWidget, LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { $el } from '@/scripts/ui'
|
||||
import * as shared from '@mtb/shared'
|
||||
import { API_COLOR, OUTPUT_COLOR } from './constants'
|
||||
import { API_INPUT_TYPES, type APIInputType, type APIInputSettings, type APINodeSettings, type MTBNode } from './types'
|
||||
import { notifyAPIChanged } from './panel.svelte'
|
||||
import CSS from './api_nodes.css?inline'
|
||||
|
||||
/** Symbol to store original callbacks on widgets */
|
||||
const ORIGINAL_CALLBACK = Symbol('mtb_original_callback')
|
||||
|
||||
declare const LiteGraph: {
|
||||
NODE_TITLE_HEIGHT: number
|
||||
NODE_COLLAPSED_RADIUS: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the creation and lifecycle of API settings widgets on nodes
|
||||
*/
|
||||
export class APISettingsWidgetManager {
|
||||
/**
|
||||
* Wraps widget callbacks to notify panel of value changes
|
||||
*/
|
||||
private wrapWidgetCallbacks(node: MTBNode): void {
|
||||
if (!node.widgets) return
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.name === 'apiSettings') continue
|
||||
|
||||
const w = widget as IWidget & {
|
||||
callback?: (...args: unknown[]) => void
|
||||
[ORIGINAL_CALLBACK]?: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
// Skip if already wrapped
|
||||
if (w[ORIGINAL_CALLBACK]) continue
|
||||
|
||||
// Store original and wrap
|
||||
w[ORIGINAL_CALLBACK] = w.callback
|
||||
w.callback = (...args: unknown[]) => {
|
||||
w[ORIGINAL_CALLBACK]?.(...args)
|
||||
notifyAPIChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores original widget callbacks
|
||||
*/
|
||||
private unwrapWidgetCallbacks(node: MTBNode): void {
|
||||
if (!node.widgets) return
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
const w = widget as IWidget & {
|
||||
callback?: (...args: unknown[]) => void
|
||||
[ORIGINAL_CALLBACK]?: (...args: unknown[]) => void
|
||||
}
|
||||
|
||||
if (w[ORIGINAL_CALLBACK]) {
|
||||
w.callback = w[ORIGINAL_CALLBACK]
|
||||
delete w[ORIGINAL_CALLBACK]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the API settings DOM widget for a node
|
||||
*/
|
||||
createAPISettingsWidget(node: MTBNode, force?: boolean): void {
|
||||
const existingWidget = node.widgets?.find((w) => w.name === 'apiSettings')
|
||||
|
||||
if (!existingWidget || force) {
|
||||
if (existingWidget) {
|
||||
this.removeAPISettingsWidget(node, existingWidget)
|
||||
}
|
||||
|
||||
const element = document.createElement('div')
|
||||
element.classList.add(
|
||||
node.properties.mtb_api?.isAPIOutput
|
||||
? 'mtb_api_output'
|
||||
: 'mtb_api_settings',
|
||||
)
|
||||
|
||||
if (node.widgets) {
|
||||
const otherWidgets = node.widgets.filter(
|
||||
(w) => w.name !== 'apiSettings',
|
||||
)
|
||||
for (const w of otherWidgets) {
|
||||
const section = this.createWidgetSection(node, w)
|
||||
if (section) {
|
||||
element.appendChild(section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.createNodeSettingsSection(node, element)
|
||||
|
||||
// @ts-expect-error - addDOMWidget is a ComfyUI extension
|
||||
node.addDOMWidget('apiSettings', 'API_SETTINGS', element, {
|
||||
hideOnZoom: false,
|
||||
getHeight: () => element.children.length * 80,
|
||||
})
|
||||
|
||||
// Wrap widget callbacks to sync Node → Panel
|
||||
this.wrapWidgetCallbacks(node)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the node-level settings section with export toggle
|
||||
*/
|
||||
createNodeSettingsSection(node: MTBNode, element: HTMLElement): void {
|
||||
const includeInExport = node.properties.mtb_api?.includeInExport !== false
|
||||
|
||||
const nodeSettings = $el('div.mtb_api_node_settings', {}, [
|
||||
$el('span.mtb_api_node_settings_label', { textContent: 'Include in API export' }),
|
||||
$el('label.mtb_api_toggle', {}, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
checked: includeInExport,
|
||||
onchange: (e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked
|
||||
this.applySettings(node, { includeInExport: checked })
|
||||
},
|
||||
}),
|
||||
$el('span.mtb_api_toggle_track'),
|
||||
]),
|
||||
])
|
||||
element.appendChild(nodeSettings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies new settings to the node, merging with existing
|
||||
*/
|
||||
applySettings(
|
||||
node: MTBNode,
|
||||
newSettings: Partial<APINodeSettings>,
|
||||
): APINodeSettings {
|
||||
const settings = node.properties.mtb_api || {}
|
||||
const result = shared.deepMerge(settings, newSettings) as APINodeSettings
|
||||
|
||||
node.setProperty('mtb_api', result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures widgets are created/removed based on node state
|
||||
*/
|
||||
ensureWidgets(node: MTBNode, force?: boolean): void {
|
||||
const apiSettings = node.widgets?.find((w) => w.name === 'apiSettings')
|
||||
if (node.properties.useAPI) {
|
||||
this.createAPISettingsWidget(node, force)
|
||||
} else {
|
||||
this.removeAPISettingsWidget(node, apiSettings)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the API settings widget from a node
|
||||
*/
|
||||
removeAPISettingsWidget(node: MTBNode, apiSettings?: IWidget): void {
|
||||
if (apiSettings) {
|
||||
;(apiSettings as IWidget & { onRemoved?: () => void }).onRemoved?.()
|
||||
;(apiSettings as IWidget & { onRemove?: () => void }).onRemove?.()
|
||||
node.widgets = node.widgets?.filter((w) => w.name !== 'apiSettings')
|
||||
|
||||
// Restore original widget callbacks
|
||||
this.unwrapWidgetCallbacks(node)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the CSS styles are injected into the document
|
||||
*/
|
||||
updateStyleElement(): void {
|
||||
let styleElement = document.getElementById('mtb-api-widget-styles')
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement('style')
|
||||
styleElement.id = 'mtb-api-widget-styles'
|
||||
document.head.appendChild(styleElement)
|
||||
styleElement.textContent = CSS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a ComfyUI widget type to an API input type
|
||||
*/
|
||||
apiTypeFromComfyType(comfyType: string): APIInputType {
|
||||
switch (comfyType) {
|
||||
case 'number':
|
||||
return 'NUMBER'
|
||||
case 'text':
|
||||
case 'string':
|
||||
case 'customtext':
|
||||
return 'STRING'
|
||||
case 'combo':
|
||||
return 'COMBO'
|
||||
case 'toggle':
|
||||
case 'boolean':
|
||||
return 'BOOLEAN'
|
||||
default:
|
||||
console.log('UNHANDLED widget type:', comfyType, '- defaulting to STRING')
|
||||
return 'STRING'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates type-specific configuration UI for a widget
|
||||
*/
|
||||
private createTypeSpecificConfig(
|
||||
node: MTBNode,
|
||||
widgetName: string,
|
||||
type: APIInputType,
|
||||
container: HTMLElement,
|
||||
): void {
|
||||
const settings = node.properties.mtb_api?.inputs?.[widgetName] || {}
|
||||
|
||||
// Clear previous config UI
|
||||
const existingConfig = container.querySelector('.mtb_api_type_config')
|
||||
if (existingConfig) existingConfig.remove()
|
||||
|
||||
const configDiv = document.createElement('div')
|
||||
configDiv.classList.add('mtb_api_type_config')
|
||||
|
||||
switch (type) {
|
||||
case 'NUMBER':
|
||||
this.createNumberConfig(node, widgetName, settings, configDiv)
|
||||
break
|
||||
case 'STRING':
|
||||
this.createStringConfig(node, widgetName, settings, configDiv)
|
||||
break
|
||||
case 'AUDIO':
|
||||
case 'VIDEO':
|
||||
this.createMediaConfig(node, widgetName, settings, configDiv)
|
||||
break
|
||||
}
|
||||
|
||||
if (configDiv.children.length > 0) {
|
||||
container.appendChild(configDiv)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates NUMBER-specific config (min, max, step)
|
||||
*/
|
||||
private createNumberConfig(
|
||||
node: MTBNode,
|
||||
widgetName: string,
|
||||
settings: Partial<APIInputSettings>,
|
||||
container: HTMLElement,
|
||||
): void {
|
||||
const row = $el('div.mtb_api_config_row', {}, [
|
||||
$el('label', { textContent: 'min:' }),
|
||||
$el('input', {
|
||||
type: 'number',
|
||||
value: settings.min ?? '',
|
||||
placeholder: 'auto',
|
||||
onchange: (e: Event) => {
|
||||
const val = parseFloat((e.target as HTMLInputElement).value)
|
||||
this.applySettings(node, {
|
||||
inputs: { [widgetName]: { min: isNaN(val) ? undefined : val } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}),
|
||||
$el('label', { textContent: 'max:' }),
|
||||
$el('input', {
|
||||
type: 'number',
|
||||
value: settings.max ?? '',
|
||||
placeholder: 'auto',
|
||||
onchange: (e: Event) => {
|
||||
const val = parseFloat((e.target as HTMLInputElement).value)
|
||||
this.applySettings(node, {
|
||||
inputs: { [widgetName]: { max: isNaN(val) ? undefined : val } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}),
|
||||
$el('label', { textContent: 'step:' }),
|
||||
$el('input', {
|
||||
type: 'number',
|
||||
value: settings.step ?? '',
|
||||
placeholder: 'auto',
|
||||
onchange: (e: Event) => {
|
||||
const val = parseFloat((e.target as HTMLInputElement).value)
|
||||
this.applySettings(node, {
|
||||
inputs: { [widgetName]: { step: isNaN(val) ? undefined : val } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}),
|
||||
])
|
||||
container.appendChild(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates STRING-specific config (multiline)
|
||||
*/
|
||||
private createStringConfig(
|
||||
node: MTBNode,
|
||||
widgetName: string,
|
||||
settings: Partial<APIInputSettings>,
|
||||
container: HTMLElement,
|
||||
): void {
|
||||
const row = $el('div.mtb_api_config_row', {}, [
|
||||
$el('label.mtb_api_checkbox_label', {}, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
checked: settings.multiline ?? false,
|
||||
onchange: (e: Event) => {
|
||||
this.applySettings(node, {
|
||||
inputs: { [widgetName]: { multiline: (e.target as HTMLInputElement).checked } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}),
|
||||
$el('span', { textContent: 'Multiline' }),
|
||||
]),
|
||||
])
|
||||
container.appendChild(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates AUDIO/VIDEO-specific config (accept, maxDuration, trimEnabled)
|
||||
*/
|
||||
private createMediaConfig(
|
||||
node: MTBNode,
|
||||
widgetName: string,
|
||||
settings: Partial<APIInputSettings>,
|
||||
container: HTMLElement,
|
||||
): void {
|
||||
const acceptRow = $el('div.mtb_api_config_row', {}, [
|
||||
$el('label', { textContent: 'accept:' }),
|
||||
$el('input', {
|
||||
type: 'text',
|
||||
value: settings.accept ?? '',
|
||||
placeholder: 'audio/* or .mp3,.wav',
|
||||
onchange: (e: Event) => {
|
||||
const val = (e.target as HTMLInputElement).value
|
||||
this.applySettings(node, {
|
||||
inputs: { [widgetName]: { accept: val || undefined } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const trimRow = $el('div.mtb_api_config_row', {}, [
|
||||
$el('label', { textContent: 'max duration:' }),
|
||||
$el('input', {
|
||||
type: 'number',
|
||||
value: settings.maxDuration ?? '',
|
||||
placeholder: 'unlimited',
|
||||
onchange: (e: Event) => {
|
||||
const val = parseFloat((e.target as HTMLInputElement).value)
|
||||
this.applySettings(node, {
|
||||
inputs: { [widgetName]: { maxDuration: isNaN(val) ? undefined : val } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}),
|
||||
$el('span', { textContent: 's' }),
|
||||
$el('label.mtb_api_checkbox_label', {}, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
checked: settings.trimEnabled ?? true,
|
||||
onchange: (e: Event) => {
|
||||
this.applySettings(node, {
|
||||
inputs: { [widgetName]: { trimEnabled: (e.target as HTMLInputElement).checked } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
},
|
||||
}),
|
||||
$el('span', { textContent: 'Enable trim' }),
|
||||
]),
|
||||
])
|
||||
|
||||
container.appendChild(acceptRow)
|
||||
container.appendChild(trimRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration section for a single widget
|
||||
*/
|
||||
createWidgetSection(node: MTBNode, widget: IWidget): HTMLElement | undefined {
|
||||
const section = document.createElement('div')
|
||||
const options = node.properties.mtb_api || {}
|
||||
section.classList.add('mtb_api_section')
|
||||
|
||||
// Checkbox to enable/disable section
|
||||
const enableCheckbox = document.createElement('input')
|
||||
enableCheckbox.type = 'checkbox'
|
||||
|
||||
let enabled = options.inputs?.[widget.name]?.enabled
|
||||
enabled = enabled === undefined ? true : enabled
|
||||
|
||||
this.applySettings(node, {
|
||||
inputs: { [widget.name]: { enabled } },
|
||||
})
|
||||
|
||||
if (options.showDisabled === false && !enabled) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
enableCheckbox.checked = enabled
|
||||
|
||||
const checkboxLabel = document.createElement('label')
|
||||
checkboxLabel.classList.add('mtb_api_checkbox_label')
|
||||
checkboxLabel.appendChild(enableCheckbox)
|
||||
checkboxLabel.appendChild(document.createTextNode('Enable '))
|
||||
|
||||
const contentContainer = document.createElement('div')
|
||||
|
||||
enableCheckbox.addEventListener('change', () => {
|
||||
if (enableCheckbox.checked) {
|
||||
this.applySettings(node, {
|
||||
inputs: { [widget.name]: { enabled: true } },
|
||||
})
|
||||
contentContainer.classList.remove('mtb_api_disabled')
|
||||
} else {
|
||||
this.applySettings(node, {
|
||||
inputs: { [widget.name]: { enabled: false } },
|
||||
})
|
||||
contentContainer.classList.add('mtb_api_disabled')
|
||||
}
|
||||
notifyAPIChanged()
|
||||
})
|
||||
|
||||
const title = document.createElement('span')
|
||||
title.classList.add('mtb_api_title')
|
||||
title.textContent = widget.name
|
||||
|
||||
checkboxLabel.appendChild(title)
|
||||
|
||||
// Type selector
|
||||
const typeLabel = document.createElement('label')
|
||||
typeLabel.textContent = 'type:'
|
||||
typeLabel.htmlFor = 'inputType'
|
||||
|
||||
const typeSelect = document.createElement('select')
|
||||
typeSelect.id = 'inputType'
|
||||
|
||||
const selectedValue =
|
||||
options.inputs?.[widget.name]?.type ||
|
||||
this.apiTypeFromComfyType(widget.type as string)
|
||||
|
||||
for (const option of API_INPUT_TYPES) {
|
||||
const optionElement = document.createElement('option')
|
||||
optionElement.value = option
|
||||
optionElement.text = option
|
||||
if (option === selectedValue) {
|
||||
this.applySettings(node, {
|
||||
inputs: { [widget.name]: { type: option } },
|
||||
})
|
||||
optionElement.selected = true
|
||||
}
|
||||
typeSelect.appendChild(optionElement)
|
||||
}
|
||||
|
||||
typeSelect.addEventListener('change', () => {
|
||||
const newType = typeSelect.value as APIInputType
|
||||
this.applySettings(node, {
|
||||
inputs: { [widget.name]: { type: newType } },
|
||||
})
|
||||
// Regenerate type-specific config
|
||||
this.createTypeSpecificConfig(node, widget.name, newType, contentContainer)
|
||||
notifyAPIChanged()
|
||||
})
|
||||
|
||||
// Name input
|
||||
const nameLabel = document.createElement('label')
|
||||
nameLabel.textContent = 'name:'
|
||||
nameLabel.htmlFor = 'name'
|
||||
|
||||
const nameInput = document.createElement('input')
|
||||
nameInput.type = 'text'
|
||||
nameInput.id = 'name'
|
||||
nameInput.value = options.inputs?.[widget.name]?.name || widget.name
|
||||
|
||||
nameInput.addEventListener('change', () => {
|
||||
this.applySettings(node, {
|
||||
inputs: { [widget.name]: { name: nameInput.value } },
|
||||
})
|
||||
notifyAPIChanged()
|
||||
})
|
||||
|
||||
const separator = document.createElement('hr')
|
||||
|
||||
contentContainer.appendChild(nameLabel)
|
||||
contentContainer.appendChild(nameInput)
|
||||
contentContainer.appendChild(typeLabel)
|
||||
contentContainer.appendChild(typeSelect)
|
||||
|
||||
// Add type-specific config for initial type
|
||||
this.createTypeSpecificConfig(node, widget.name, selectedValue, contentContainer)
|
||||
|
||||
if (!enabled) {
|
||||
contentContainer.classList.add('mtb_api_disabled')
|
||||
}
|
||||
|
||||
section.appendChild(checkboxLabel)
|
||||
section.appendChild(contentContainer)
|
||||
section.appendChild(separator)
|
||||
this.updateStyleElement()
|
||||
|
||||
return section
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the API indicator overlay on the node
|
||||
* Clean, minimal design with subtle glow effect
|
||||
*/
|
||||
drawForeground(
|
||||
node: MTBNode,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
_canvas: LGraphCanvas,
|
||||
): void {
|
||||
const isOutput = node.properties.mtb_api?.isAPIOutput === true
|
||||
const isInput = node.properties.useAPI
|
||||
|
||||
if (!isInput && !isOutput) {
|
||||
return
|
||||
}
|
||||
|
||||
this.ensureWidgets(node)
|
||||
|
||||
const color = isOutput ? OUTPUT_COLOR : API_COLOR
|
||||
const label = isOutput ? 'OUTPUT' : 'API'
|
||||
const padding = 4
|
||||
const borderRadius = 8
|
||||
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
|
||||
|
||||
ctx.save()
|
||||
|
||||
// Subtle outer glow
|
||||
ctx.shadowColor = color
|
||||
ctx.shadowBlur = 12
|
||||
ctx.shadowOffsetX = 0
|
||||
ctx.shadowOffsetY = 0
|
||||
|
||||
// Draw subtle border
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(
|
||||
-padding,
|
||||
-(titleHeight + padding),
|
||||
node.size[0] + padding * 2,
|
||||
node.size[1] + titleHeight + padding * 2,
|
||||
borderRadius,
|
||||
)
|
||||
ctx.stroke()
|
||||
|
||||
// Reset shadow for badge
|
||||
ctx.shadowBlur = 0
|
||||
|
||||
// Draw compact badge
|
||||
const badgeHeight = 18
|
||||
const badgeY = -(titleHeight + padding + badgeHeight + 4)
|
||||
|
||||
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, sans-serif'
|
||||
const textMetrics = ctx.measureText(label)
|
||||
const badgeWidth = textMetrics.width + 12
|
||||
|
||||
// Badge background
|
||||
ctx.fillStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(0, badgeY, badgeWidth, badgeHeight, 4)
|
||||
ctx.fill()
|
||||
|
||||
// Badge text
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(label, 6, badgeY + badgeHeight / 2)
|
||||
|
||||
// Draw small icon indicator in corner
|
||||
const iconSize = 8
|
||||
const iconX = node.size[0] - iconSize - 4
|
||||
const iconY = -titleHeight + 4
|
||||
|
||||
ctx.fillStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.arc(iconX + iconSize / 2, iconY + iconSize / 2, iconSize / 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton instance */
|
||||
export const apiSettingsWidgetManager = new APISettingsWidgetManager()
|
||||
@@ -1,17 +0,0 @@
|
||||
// import './app.css'
|
||||
// import App from './App.svelte'
|
||||
import { mount } from 'svelte'
|
||||
|
||||
import Inspector from '../lib/Inspector.svelte'
|
||||
|
||||
export const createOutliner = (target: HTMLElement, opts: unknown) => {
|
||||
const options = opts || {}
|
||||
const tgt = target || document.body
|
||||
|
||||
const app = mount(Inspector, {
|
||||
// target: document.getElementById('app'),
|
||||
target: tgt,
|
||||
props: options,
|
||||
})
|
||||
return app
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,528 +0,0 @@
|
||||
/** Python REPL for the frontend (uses rich)*/
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import * as shared from './comfy_shared.js'
|
||||
import * as mtb_ui from './mtb_ui.js'
|
||||
|
||||
class ComfyREPL extends LiteGraph.LGraphNode {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.shape = LiteGraph.BOX_SHAPE
|
||||
this.isVirtualNode = true
|
||||
this.category = 'mtb/repl'
|
||||
this.title = '🐍 REPL (mtb)'
|
||||
|
||||
this.uuid = shared.makeUUID()
|
||||
|
||||
this.size = [600, 400]
|
||||
|
||||
// Create a container for our custom widgets
|
||||
this.widget = this.addDOMWidget('HTML', 'html', this.createREPLWidget())
|
||||
|
||||
this.loadAceEditor()
|
||||
|
||||
// Store input and output for persistence
|
||||
this.properties = {
|
||||
inputCode: '',
|
||||
outputHistory: '',
|
||||
}
|
||||
|
||||
this.outputArea.innerHTML = this.properties.outputHistory
|
||||
this.outputArea.scrollTop = this.outputArea.scrollHeight
|
||||
|
||||
// Debounced linting function
|
||||
this.debouncedLint = shared.debounce(this.lintCode.bind(this), 500)
|
||||
|
||||
// Resizing state variables
|
||||
this.isResizing = false
|
||||
this.initialMouseY = 0
|
||||
this.initialInputHeight = 0
|
||||
this.initialOutputHeight = 0
|
||||
}
|
||||
|
||||
loadAceEditor() {
|
||||
if (window.MTB_ACE) {
|
||||
return
|
||||
}
|
||||
let NEED_PATCH = false
|
||||
if (window.ace) {
|
||||
shared.infoLogger(
|
||||
'A global ace was found in scope, to avoid issues with it we will patch it',
|
||||
)
|
||||
NEED_PATCH = true
|
||||
window._backupAce = window.ace
|
||||
window.ace = null
|
||||
}
|
||||
|
||||
shared
|
||||
.loadScript('/mtb_async/ace/ace.js')
|
||||
.then((m) => {
|
||||
shared.infoLogger('ACE was loaded', m)
|
||||
window.MTB_ACE = window.ace
|
||||
this.initAceEditor()
|
||||
|
||||
this.aceEditor.setValue(this.properties.inputCode, -1)
|
||||
})
|
||||
.catch((e) => {
|
||||
shared.errorLogger(e)
|
||||
})
|
||||
.finally(() => {
|
||||
if (NEED_PATCH) {
|
||||
console.log('Patching back window object')
|
||||
window.ace = window._backupAce
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
initAceEditor() {
|
||||
if (typeof MTB_ACE === 'undefined' || !MTB_ACE.edit) {
|
||||
console.error('ACE editor not loaded. Cannot set up editors.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.inputDiv) {
|
||||
console.error('Input div not found for Ace editor initialization.')
|
||||
return
|
||||
}
|
||||
this.aceEditor = MTB_ACE.edit(this.inputDiv)
|
||||
this.aceEditor.setTheme('ace/theme/monokai') //"ace/theme/dracula", "ace/theme/github"
|
||||
this.aceEditor.session.setMode('ace/mode/python')
|
||||
this.aceEditor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace',
|
||||
showPrintMargin: false,
|
||||
wrap: true,
|
||||
tabSize: 4,
|
||||
useSoftTabs: true,
|
||||
highlightActiveLine: true,
|
||||
highlightSelectedWord: true,
|
||||
cursorStyle: 'ace', // "ace" | "slim" | "smooth" | "wide"
|
||||
behavioursEnabled: true,
|
||||
displayIndentGuides: true,
|
||||
fixedWidthGutter: true,
|
||||
scrollPastEnd: 0.5,
|
||||
})
|
||||
|
||||
// Custom keybinding for Ctrl+Enter
|
||||
this.aceEditor.commands.addCommand({
|
||||
name: 'runCode',
|
||||
bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
|
||||
exec: () => this.executeCode(),
|
||||
})
|
||||
|
||||
// Listen for changes to trigger linting
|
||||
let lintDisabled = false
|
||||
this.aceEditor.session.on('change', () => {
|
||||
if (!lintDisabled) {
|
||||
this.debouncedLint()
|
||||
}
|
||||
})
|
||||
|
||||
this.outputArea.scrollTop = this.outputArea.scrollHeight
|
||||
}
|
||||
|
||||
addOutput(html) {
|
||||
this.outputArea.innerHTML += html
|
||||
this.properties.outputHistory += html
|
||||
this.outputArea.scrollTop = this.outputArea.scrollHeight
|
||||
}
|
||||
|
||||
createREPLWidget() {
|
||||
const container = mtb_ui.makeElement('div', {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
padding: '5px',
|
||||
})
|
||||
|
||||
this.inputDiv = mtb_ui.makeElement(
|
||||
'div',
|
||||
{
|
||||
width: 'calc(100% - 10px)',
|
||||
height: '100px',
|
||||
backgroundColor: '#333',
|
||||
color: '#eee',
|
||||
border: '1px solid #555',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '5px',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
container,
|
||||
)
|
||||
// Resizable Handle
|
||||
this.handleDiv = mtb_ui.makeElement(
|
||||
'div',
|
||||
{
|
||||
width: '100%',
|
||||
height: '5px',
|
||||
backgroundColor: '#666',
|
||||
cursor: 'ns-resize',
|
||||
marginBottom: '5px',
|
||||
borderRadius: '2px',
|
||||
},
|
||||
container,
|
||||
)
|
||||
this.handleDiv.addEventListener('mousedown', this.startResizing.bind(this))
|
||||
|
||||
// Run Button
|
||||
this.runButton = mtb_ui.makeElement(
|
||||
'button',
|
||||
{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
backgroundColor: '#555',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '5px',
|
||||
fontSize: '14px',
|
||||
},
|
||||
container,
|
||||
)
|
||||
this.runButton.textContent = 'Run Code (Ctrl+Enter)'
|
||||
this.runButton.onclick = () => this.executeCode()
|
||||
|
||||
// Clear Button
|
||||
this.clearButton = mtb_ui.makeElement(
|
||||
'button',
|
||||
{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
backgroundColor: '#555',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '5px',
|
||||
fontSize: '14px',
|
||||
},
|
||||
container,
|
||||
)
|
||||
this.clearButton.textContent = 'Clear Output'
|
||||
this.clearButton.onclick = () => {
|
||||
this.outputArea.innerHTML = ''
|
||||
this.properties.outputHistory = ''
|
||||
}
|
||||
|
||||
// Output Area
|
||||
this.outputArea = mtb_ui.makeElement(
|
||||
'div',
|
||||
{
|
||||
flexGrow: '1',
|
||||
width: 'calc(100% - 10px)',
|
||||
backgroundColor: '#222',
|
||||
color: '#ddd',
|
||||
border: '1px solid #555',
|
||||
borderRadius: '4px',
|
||||
padding: '5px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '14px',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
container,
|
||||
)
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
// --- Resizing Logic ---
|
||||
startResizing(e) {
|
||||
if (!this.inputDiv) {
|
||||
shared.infoLogger("The input div isn't ready", this)
|
||||
shared.errorLogger("The input div isn't ready")
|
||||
return
|
||||
}
|
||||
this.isResizing = true
|
||||
this.initialMouseY = e.clientY
|
||||
this.initialInputHeight = this.inputDiv.offsetHeight
|
||||
this.initialOutputHeight = this.outputArea.offsetHeight
|
||||
|
||||
document.addEventListener('mousemove', this.doResize.bind(this))
|
||||
document.addEventListener('mouseup', this.stopResizing.bind(this))
|
||||
document.body.style.cursor = 'ns-resize' // Change cursor globally
|
||||
}
|
||||
doResize(e) {
|
||||
if (!this.isResizing) return
|
||||
|
||||
const deltaY = e.clientY - this.initialMouseY
|
||||
|
||||
let new_input_height = this.initialInputHeight + deltaY
|
||||
let new_output_height = this.initialOutputHeight - deltaY
|
||||
|
||||
const minInputHeight = 50 // Minimum height for Ace editor
|
||||
const minOutputHeight = 50 // Minimum height for output area
|
||||
|
||||
// Clamp heights to minimums
|
||||
if (new_input_height < minInputHeight) {
|
||||
new_input_height = minInputHeight
|
||||
new_output_height =
|
||||
this.initialInputHeight + this.initialOutputHeight - minInputHeight
|
||||
}
|
||||
if (new_output_height < minOutputHeight) {
|
||||
new_output_height = minOutputHeight
|
||||
new_input_height =
|
||||
this.initialInputHeight + this.initialOutputHeight - minOutputHeight
|
||||
}
|
||||
|
||||
this.inputDiv.style.height = `${new_input_height}px`
|
||||
this.outputArea.style.height = `${new_output_height}px`
|
||||
|
||||
// Update the stored ratio for persistence
|
||||
const totalDynamicHeight =
|
||||
this.inputDiv.offsetHeight + this.outputArea.offsetHeight
|
||||
if (totalDynamicHeight > 0) {
|
||||
this.properties.inputHeightRatio = new_input_height / totalDynamicHeight
|
||||
}
|
||||
|
||||
this.aceEditor.resize() // Important for Ace to redraw
|
||||
}
|
||||
|
||||
stopResizing() {
|
||||
this.isResizing = false
|
||||
document.removeEventListener('mousemove', this.doResize)
|
||||
document.removeEventListener('mouseup', this.stopResizing)
|
||||
document.body.style.cursor = '' // Restore default cursor
|
||||
}
|
||||
// --- End Resizing Logic ---
|
||||
|
||||
async executeCode() {
|
||||
const code = this.aceEditor.getValue()
|
||||
|
||||
if (!code.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
const inputPrompt = `<div style="color:#888; margin-top: 10px;">>>> ${code}</div>`
|
||||
|
||||
this.addOutput(inputPrompt)
|
||||
|
||||
try {
|
||||
const response = await fetch('/mtb/execute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ code: code, name: this.uuid }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.debug('Received from backend', result)
|
||||
const outputHtml = result.output_html || ''
|
||||
const error = result.error
|
||||
|
||||
if (error) {
|
||||
this.addOutput(
|
||||
`<div style="color: #f00; font-weight: bold;">Error:</div>${outputHtml}`,
|
||||
)
|
||||
} else {
|
||||
this.addOutput(outputHtml)
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = `<div style="color: #f00;">Frontend Error: ${e.message}</div>`
|
||||
|
||||
this.addOutput(errorMessage)
|
||||
console.error('ComfyREPL Frontend Error:', e)
|
||||
} finally {
|
||||
// Not clearing
|
||||
// this.inputArea.value = '' // Clear input after execution
|
||||
// this.properties.inputCode = '' // Clear persisted input
|
||||
}
|
||||
}
|
||||
|
||||
async lintCode() {
|
||||
if (!this.aceEditor) {
|
||||
return
|
||||
}
|
||||
const code = this.aceEditor.getValue()
|
||||
if (!code.trim()) {
|
||||
this.aceEditor.session.setAnnotations([]) // Clear annotations if empty
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/mtb/lint', {
|
||||
// New linting endpoint
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ code: code, name: this.uuid }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(response)
|
||||
throw new Error(
|
||||
`HTTP error! status: ${response.status} ${response.statusText}`,
|
||||
)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
// result.diagnostics should be an array of {row, column, text, type}
|
||||
this.aceEditor.session.setAnnotations(result.diagnostics)
|
||||
} catch (e) {
|
||||
console.error('ComfyREPL Linting Error:', e)
|
||||
this.aceEditor.session.setAnnotations([
|
||||
{
|
||||
row: 0,
|
||||
column: 0,
|
||||
text: `Linting failed: ${e.message}`,
|
||||
type: 'error',
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// Restore properties when loading a graph
|
||||
onConfigure() {
|
||||
if (this.properties.inputCode && this.aceEditor) {
|
||||
this.aceEditor.setValue(this.properties.inputCode, -1)
|
||||
}
|
||||
// if (this.properties.inputCode) {
|
||||
// this.inputArea.value = this.properties.inputCode
|
||||
// }
|
||||
if (this.properties.outputHistory) {
|
||||
this.outputArea.innerHTML = this.properties.outputHistory
|
||||
this.outputArea.scrollTop = this.outputArea.scrollHeight
|
||||
}
|
||||
if (this.properties.uuid) {
|
||||
this.uuid = this.properties.uuid
|
||||
}
|
||||
this.debouncedLint()
|
||||
this.onResize(this.size)
|
||||
}
|
||||
|
||||
// Save properties when saving a graph
|
||||
onSerialize(o) {
|
||||
if (this.aceEditor) {
|
||||
o.properties.inputCode = this.aceEditor.getValue() //this.inputArea.value
|
||||
}
|
||||
o.properties.outputHistory = this.outputArea.innerHTML
|
||||
o.properties.uuid = this.uuid
|
||||
o.properties.inputHeightRatio = this.properties.inputHeightRatio
|
||||
}
|
||||
|
||||
onRemoved() {
|
||||
// Clean up DOM elements when node is removed
|
||||
if (this.widget?.element?.parentNode) {
|
||||
this.widget.element.parentNode.removeChild(this.widget.element)
|
||||
}
|
||||
// Destroy Ace editor instance to prevent memory leaks
|
||||
if (this.aceEditor) {
|
||||
this.aceEditor.destroy()
|
||||
this.aceEditor.container.remove() // Remove the Ace container div from DOM
|
||||
}
|
||||
// Clean up global event listeners if node is removed while resizing
|
||||
document.removeEventListener('mousemove', this.doResize)
|
||||
document.removeEventListener('mouseup', this.stopResizing)
|
||||
document.body.style.cursor = ''
|
||||
}
|
||||
// LiteGraph method to handle node resizing
|
||||
onResize(size) {
|
||||
// Call parent method if it exists (important for LiteGraph's internal sizing)
|
||||
if (super.onResize) {
|
||||
super.onResize(size)
|
||||
}
|
||||
|
||||
// Adjust container size
|
||||
const container = this.widget.element
|
||||
container.style.width = `${size[0] - 10}px` // Account for padding
|
||||
container.style.height = `${size[1] - 10}px`
|
||||
|
||||
// Adjust input and output area widths
|
||||
this.inputDiv.style.width = 'calc(100% - 10px)'
|
||||
this.outputArea.style.width = 'calc(100% - 10px)'
|
||||
//
|
||||
// const old = () => {
|
||||
// // Calculate remaining height for output area
|
||||
// // Ace editor manages its own height within this.inputDiv, so we use offsetHeight
|
||||
// const inputHeight = this.inputDiv.offsetHeight
|
||||
// const runButtonHeight = this.runButton.offsetHeight
|
||||
// const clearButtonHeight = this.clearButton.offsetHeight
|
||||
// const totalFixedHeight =
|
||||
// inputHeight + runButtonHeight + clearButtonHeight + 15 // 15 for margins/padding
|
||||
//
|
||||
// const remainingHeight = size[1] - 10 - totalFixedHeight
|
||||
// this.outputArea.style.height = `${Math.max(50, remainingHeight)}px` // Min height 50px
|
||||
// }
|
||||
// Calculate dynamic heights
|
||||
const containerHeight = size[1] - 10
|
||||
const handleHeight = this.handleDiv.offsetHeight
|
||||
const buttonHeights =
|
||||
this.runButton.offsetHeight + this.clearButton.offsetHeight + 15 // Sum of button heights + margins
|
||||
|
||||
const dynamicContentHeight = containerHeight - buttonHeights - handleHeight
|
||||
|
||||
const minInputHeight = 50
|
||||
const minOutputHeight = 50
|
||||
|
||||
let inputHeight = Math.max(
|
||||
minInputHeight,
|
||||
dynamicContentHeight * (this.properties.inputHeightRatio || 1.0),
|
||||
)
|
||||
let outputHeight = Math.max(
|
||||
minOutputHeight,
|
||||
dynamicContentHeight - inputHeight,
|
||||
)
|
||||
//
|
||||
// // Re-distribute if one hits its minimum
|
||||
// if (
|
||||
// inputHeight === minInputHeight &&
|
||||
// dynamicContentHeight - minInputHeight > minOutputHeight
|
||||
// ) {
|
||||
// outputHeight = dynamicContentHeight - minInputHeight
|
||||
// } else if (
|
||||
// outputHeight === minOutputHeight &&
|
||||
// dynamicContentHeight - minOutputHeight > minInputHeight
|
||||
// ) {
|
||||
// inputHeight = dynamicContentHeight - minOutputHeight
|
||||
// }
|
||||
//
|
||||
// // Final check to ensure total height matches available dynamic space
|
||||
// const currentTotal = inputHeight + outputHeight
|
||||
// if (currentTotal !== dynamicContentHeight) {
|
||||
// // Adjust one of them if there's a small discrepancy due to rounding
|
||||
// if (inputHeight > minInputHeight) {
|
||||
// inputHeight += dynamicContentHeight - currentTotal
|
||||
// } else if (outputHeight > minOutputHeight) {
|
||||
// outputHeight += dynamicContentHeight - currentTotal
|
||||
// }
|
||||
// }
|
||||
|
||||
this.inputDiv.style.height = `${inputHeight}px`
|
||||
this.outputArea.style.height = `${outputHeight}px`
|
||||
|
||||
// Update the ratio based on the actual heights set
|
||||
if (dynamicContentHeight > 0) {
|
||||
this.properties.inputHeightRatio = inputHeight / dynamicContentHeight
|
||||
}
|
||||
|
||||
// Inform Ace editor about the resize so it can redraw its content
|
||||
if (this.aceEditor) {
|
||||
this.aceEditor.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const repl = {
|
||||
name: 'mtb.repl',
|
||||
|
||||
registerCustomNodes() {
|
||||
LiteGraph.registerNodeType('Python REPL', ComfyREPL)
|
||||
},
|
||||
}
|
||||
|
||||
app.registerExtension(repl)
|
||||
Vendored
-30
@@ -1,30 +0,0 @@
|
||||
/// <reference types="comfyui-frontend-types" />
|
||||
|
||||
// Declare the ComfyUI 'app' module using the alias path
|
||||
declare module '@/scripts/app' {
|
||||
// Assuming ComfyApp is defined in src/types/comfyui.d.ts or an installed @types package
|
||||
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types' // Adjust path if ComfyApp is in a different file
|
||||
export const app: ComfyApp
|
||||
// You might also export other things if they are part of this module
|
||||
// export const api: ComfyApi;
|
||||
}
|
||||
|
||||
// Declare the ComfyUI 'app' module using the relative path
|
||||
// This is less ideal as it's a runtime path, but covers your existing imports
|
||||
declare module '../../scripts/app.js' {
|
||||
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types' // Adjust path
|
||||
export const app: ComfyApp
|
||||
}
|
||||
|
||||
// If you import other ComfyUI modules, declare them here too:
|
||||
declare module '@/scripts/api' {
|
||||
import type { ComfyApp } from '@comfyorg/comfyui-frontend-types' // Adjust path
|
||||
export const api: ComfyApi
|
||||
}
|
||||
|
||||
// declare module '@/scripts/utils' {
|
||||
// // Define types for utils if available
|
||||
// export const utils: any // Replace with actual type
|
||||
// }
|
||||
|
||||
// Add declarations for any other ComfyUI internal modules you import
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,7 +0,0 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
const macrosPlugin = require('./mtb_macros')
|
||||
|
||||
module.exports = {
|
||||
// Specify the custom plugin to use
|
||||
swcPlugins: [macrosPlugin()],
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020", // Or "esnext" for modern JS features
|
||||
"module": "esnext", // Or "commonjs" if you're using a bundler that transpiles
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["dom", "es2020"], // Include DOM types for browser environment
|
||||
"jsx": "react-jsx", // If you're using React/JSX
|
||||
"strict": true, // Enable all strict type-checking options
|
||||
"esModuleInterop": true, // Allows default imports from modules with no default export
|
||||
"skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts)
|
||||
"forceConsistentCasingInFileNames": true, // Ensure consistent file casing
|
||||
|
||||
// --- Crucial for ComfyUI imports ---
|
||||
"baseUrl": "./src", // Base directory for resolving non-relative module names
|
||||
"paths": {
|
||||
// Map the ComfyUI alias to a dummy location or just declare it
|
||||
// This tells TS that anything starting with '@/scripts/' should be looked for relative to baseUrl
|
||||
// However, for external modules like ComfyUI's, we'll primarily rely on `declare module`
|
||||
"@/scripts/*": ["src/types/shims/*"] // This path doesn't need to exist, it's just a placeholder for TS resolution
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types", // Default location for npm @types packages
|
||||
"./node_modules/@comfyorg",
|
||||
"./src/types" // Your custom types directory
|
||||
],
|
||||
"outDir": "./dist", // Where your compiled JS files will go
|
||||
"rootDir": "./src" // Your source code directory
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.svelte",
|
||||
"src/types/**/*.d.ts", // Include your custom declaration files
|
||||
"./node_modules/@comfyorg/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"files": [], // No files directly in this config
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// tsconfig.node.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true, // Recommended for project references
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler", // Or "node"
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2020",
|
||||
"lib": ["es2020"], // Node.js environment, no DOM
|
||||
"types": ["node"], // Explicitly include Node.js types
|
||||
"noEmit": true, // Don't emit JS files for this config, just for type checking
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["vite.config.ts"] // Only include the Vite config file here
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/// <reference types="node" />
|
||||
import { defineConfig, type Plugin } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
import path from 'node:path'
|
||||
|
||||
// import MtbBundle from './vite_mtb_bundle'
|
||||
|
||||
interface ComfyUIEnv {
|
||||
VITE_COMFYUI_HOST?: string
|
||||
}
|
||||
// region inlined
|
||||
const rewriteComfyImports = ({
|
||||
isDev,
|
||||
comfyuiHost,
|
||||
}: { isDev: boolean; comfyuiHost: string }): Plugin => {
|
||||
return {
|
||||
name: 'rewrite-comfy-imports',
|
||||
resolveId(source, importer, options) {
|
||||
// Only apply in development mode
|
||||
if (!isDev) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if the source is one of ComfyUI's core scripts
|
||||
// We'll handle both the direct '/scripts/app.js' and the aliased '@/scripts/app'
|
||||
const comfyScriptMatch = source.match(/^\/?scripts\/(app|api|ui)\.js$/)
|
||||
const comfyAliasMatch = source.match(/^@\/scripts\/(app|api|ui)$/)
|
||||
|
||||
if (comfyScriptMatch || comfyAliasMatch) {
|
||||
console.log('found match')
|
||||
const scriptName = comfyScriptMatch
|
||||
? comfyScriptMatch[1]
|
||||
: comfyAliasMatch[1]!
|
||||
// Return the full URL to the ComfyUI server's script
|
||||
return `${comfyuiHost}/scripts/${scriptName}.js`
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
// endregion inlined
|
||||
|
||||
const entryPoints = {
|
||||
mtb_inspector: path.resolve(__dirname, 'src/mtb_inspector/index.ts'),
|
||||
mtb_api: path.resolve(__dirname, 'src/mtb_api/index.ts'),
|
||||
comfy_shared: path.resolve(__dirname, 'src/comfy_shared/index.ts'),
|
||||
}
|
||||
|
||||
import noBundlePlugin from 'vite-plugin-no-bundle'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isDev = mode === 'development'
|
||||
const env = process.env as unknown as ComfyUIEnv
|
||||
const comfyuiHost = env.VITE_COMFYUI_HOST || 'https://mel-pc.tail3c8eb.ts.net'
|
||||
|
||||
return {
|
||||
// root: path.resolve(__dirname, 'src'),
|
||||
build: {
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
formats: ['es'],
|
||||
// name: 'mtb',
|
||||
// fileName: 'mtb_inspector',
|
||||
// entry: 'src/lib.js',
|
||||
entry: entryPoints,
|
||||
// fileName can be a function that receives the entry name
|
||||
// This will output files like 'mtb_inspector.js', 'mtb_react_widget.js'
|
||||
fileName: (format, entryName) => `${entryName}.js`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'/scripts/app.js',
|
||||
'/scripts/api.js',
|
||||
'/scripts/ui.js',
|
||||
'@mtb/shared',
|
||||
'/mtb_async/mtb_markdown.umd.js',
|
||||
'/mtb_async/mtb_markdown_plus.umd.js',
|
||||
],
|
||||
// input: entryPoints,
|
||||
output: {
|
||||
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name][extname]',
|
||||
// Remap external imports to actual runtime paths
|
||||
paths: {
|
||||
'@mtb/shared': '../comfy_shared.js',
|
||||
},
|
||||
manualChunks(id) {
|
||||
// Create a 'vendor' chunk for all node_modules
|
||||
console.log(id)
|
||||
if (id.includes('node_modules')) {
|
||||
return 'vendor'
|
||||
}
|
||||
// You can add more specific chunking logic here if needed
|
||||
// For example, to split React and Svelte into separate vendor chunks:
|
||||
// if (id.includes('node_modules/react') || id.includes('node_modules/react-dom')) {
|
||||
// return 'react-vendor';
|
||||
// }
|
||||
// if (id.includes('node_modules/svelte')) {
|
||||
// return 'svelte-vendor';
|
||||
// }
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
external: ['/scripts/app.js', '/scripts/api.js', '/scripts/ui.js'],
|
||||
},
|
||||
plugins: [
|
||||
rewriteComfyImports({ isDev, comfyuiHost }),
|
||||
svelte(),
|
||||
// MtbBundle(),
|
||||
|
||||
cssInjectedByJsPlugin({ styleId: 'mtb_inspector_style' }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@/scripts/app': isDev
|
||||
? path.resolve(__dirname, 'src/mocks/app.ts')
|
||||
: '/scripts/app.js',
|
||||
|
||||
'@/scripts/api': isDev
|
||||
? path.resolve(__dirname, 'src/mocks/api.ts')
|
||||
: '/scripts/api.js',
|
||||
|
||||
'@/scripts/ui': isDev
|
||||
? path.resolve(__dirname, 'src/mocks/ui.ts')
|
||||
: '/scripts/ui.js',
|
||||
|
||||
// comfy_shared.js - only alias in dev (prod uses external + output.paths)
|
||||
...(isDev && {
|
||||
'@mtb/shared': path.resolve(__dirname, 'src/mocks/comfy_shared.ts'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
import { build } from 'vite'
|
||||
|
||||
/**
|
||||
* @param {{dev:boolean, styleId:string}} user_config
|
||||
* @returns {import('vite').Plugin[]} plugin
|
||||
*/
|
||||
const MtbBundle = (user_config) => {
|
||||
let uconfig = user_config || {}
|
||||
uconfig = { dev: false, styleId: 'mtb_style', ...uconfig }
|
||||
/** @type {import('vite').ResolvedConfig} */
|
||||
let config
|
||||
/** @type {import('vite').Plugin[]} plugin */
|
||||
const plugins = [
|
||||
{
|
||||
apply: 'build',
|
||||
enforce: 'post',
|
||||
name: 'vite-mtb-bundle',
|
||||
config(config, env) {
|
||||
console.log(env.command)
|
||||
},
|
||||
configResolved(_config) {
|
||||
config = _config
|
||||
},
|
||||
async generateBundle(opts, bundle) {
|
||||
console.log(config.build)
|
||||
const cssAssets = Object.keys(bundle).filter(
|
||||
(i) =>
|
||||
bundle[i].type === 'asset' && bundle[i].fileName.endsWith('.css'),
|
||||
)
|
||||
const generatedStyleId =
|
||||
typeof uconfig.styleId === 'function'
|
||||
? uconfig.styleId()
|
||||
: uconfig.styleId
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (uconfig.dev) {
|
||||
plugins.push({
|
||||
name: 'vite-mtb-bundle-dev',
|
||||
apply: 'serve',
|
||||
enforce: 'post',
|
||||
transform(src, id) {
|
||||
return {
|
||||
code: src,
|
||||
map: null,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
export default MtbBundle
|
||||
Reference in New Issue
Block a user