@@ -20,7 +20,6 @@ This frontend isn't compatible with regular ComfyUI's workflow format since extr
|
||||
## Requirements
|
||||
|
||||
- `pnpm`
|
||||
- [Turborepo](https://turbo.build/repo/docs/installing)
|
||||
- An installation of vanilla [ComfyUI](https://github.com/comfyanonymous/ComfyUI) for the backend
|
||||
|
||||
## Installation
|
||||
@@ -32,6 +31,7 @@ git clone https://github.com/space-nuko/ComfyBox --recursive
|
||||
```
|
||||
|
||||
2. `pnpm install`
|
||||
3. `pnpm dev`
|
||||
4. Start ComfyUI as usual with `python main.py --enable-cors-header`
|
||||
5. Visit `http://localhost:3000` in your browser
|
||||
4. `pnpm build:css`
|
||||
5. `pnpm dev`
|
||||
6. Start ComfyUI as usual with `python main.py --enable-cors-header`
|
||||
7. Visit `http://localhost:3000` in your browser
|
||||
|
||||
Submodule litegraph updated: 950a78df03...fd56d0c4e6
@@ -15,6 +15,7 @@
|
||||
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@zerodevx/svelte-toast": "^0.9.3",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
@@ -42,6 +43,8 @@
|
||||
"@gradio/utils": "workspace:*",
|
||||
"@litegraph-ts/core": "workspace:*",
|
||||
"@litegraph-ts/nodes-basic": "workspace:*",
|
||||
"@litegraph-ts/nodes-events": "workspace:*",
|
||||
"@litegraph-ts/nodes-math": "workspace:*",
|
||||
"@litegraph-ts/tsconfig": "workspace:*",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.1",
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
|
||||
252
pnpm-lock.yaml
generated
252
pnpm-lock.yaml
generated
@@ -37,6 +37,12 @@ importers:
|
||||
'@litegraph-ts/nodes-basic':
|
||||
specifier: workspace:*
|
||||
version: link:litegraph/packages/nodes-basic
|
||||
'@litegraph-ts/nodes-events':
|
||||
specifier: workspace:*
|
||||
version: link:litegraph/packages/nodes-events
|
||||
'@litegraph-ts/nodes-math':
|
||||
specifier: workspace:*
|
||||
version: link:litegraph/packages/nodes-math
|
||||
'@litegraph-ts/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:litegraph/packages/tsconfig
|
||||
@@ -63,7 +69,7 @@ importers:
|
||||
version: 1.2.1
|
||||
svelte-preprocess:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
||||
version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
||||
svelte-select:
|
||||
specifier: ^5.5.3
|
||||
version: 5.5.3
|
||||
@@ -72,7 +78,7 @@ importers:
|
||||
version: 0.7.13(svelte@3.58.0)
|
||||
tailwindcss:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1(postcss@8.4.21)
|
||||
version: 3.3.1
|
||||
typed-emitter:
|
||||
specifier: github:andywer/typed-emitter
|
||||
version: github.com/andywer/typed-emitter/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4
|
||||
@@ -80,6 +86,9 @@ importers:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(vite@4.3.1)
|
||||
devDependencies:
|
||||
'@zerodevx/svelte-toast':
|
||||
specifier: ^0.9.3
|
||||
version: 0.9.3(svelte@3.58.0)
|
||||
eslint:
|
||||
specifier: ^8.37.0
|
||||
version: 8.37.0
|
||||
@@ -103,7 +112,7 @@ importers:
|
||||
version: 3.58.0
|
||||
svelte-check:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)
|
||||
version: 3.2.0(sass@1.61.0)(svelte@3.58.0)
|
||||
svelte-dnd-action:
|
||||
specifier: ^0.9.22
|
||||
version: 0.9.22(svelte@3.58.0)
|
||||
@@ -112,7 +121,7 @@ importers:
|
||||
version: 5.0.3
|
||||
vite:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||
version: 4.3.1(sass@1.61.0)
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^4.0.8
|
||||
version: 4.0.8(typescript@5.0.3)(vite@4.3.1)
|
||||
@@ -126,7 +135,7 @@ importers:
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^2.9.9
|
||||
version: 2.9.9(sass@1.61.0)
|
||||
version: 2.9.9
|
||||
|
||||
gradio/js/accordion: {}
|
||||
|
||||
@@ -691,7 +700,7 @@ importers:
|
||||
version: 1.0.0-next.91(@sveltejs/kit@1.15.2)
|
||||
'@sveltejs/kit':
|
||||
specifier: ^1.0.0-next.318
|
||||
version: 1.15.2(svelte@3.58.0)(vite@4.3.1)
|
||||
version: 1.15.2(svelte@3.58.0)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.2
|
||||
version: 10.4.2(postcss@8.4.21)
|
||||
@@ -703,13 +712,13 @@ importers:
|
||||
version: 3.1.1
|
||||
svelte-check:
|
||||
specifier: ^2.2.6
|
||||
version: 2.2.6(postcss-load-config@3.1.1)(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)
|
||||
version: 2.2.6(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)
|
||||
svelte-preprocess:
|
||||
specifier: ^4.10.1
|
||||
version: 4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@4.5.4)
|
||||
version: 4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4)
|
||||
tailwindcss:
|
||||
specifier: ^3.0.12
|
||||
version: 3.3.1(postcss@8.4.21)
|
||||
version: 3.3.1
|
||||
tslib:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
@@ -743,7 +752,7 @@ importers:
|
||||
version: 5.0.3
|
||||
vite:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(sass@1.61.0)
|
||||
version: 4.2.1
|
||||
vite-plugin-checker:
|
||||
specifier: ^0.5.6
|
||||
version: 0.5.6(eslint@8.37.0)(typescript@5.0.3)(vite@4.2.1)
|
||||
@@ -762,7 +771,39 @@ importers:
|
||||
version: 5.0.3
|
||||
vite:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(sass@1.61.0)
|
||||
version: 4.2.1
|
||||
|
||||
litegraph/packages/nodes-events:
|
||||
dependencies:
|
||||
'@litegraph-ts/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
devDependencies:
|
||||
'@litegraph-ts/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../tsconfig
|
||||
typescript:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
vite:
|
||||
specifier: ^4.2.1
|
||||
version: 4.3.1
|
||||
|
||||
litegraph/packages/nodes-math:
|
||||
dependencies:
|
||||
'@litegraph-ts/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
devDependencies:
|
||||
'@litegraph-ts/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../tsconfig
|
||||
typescript:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
vite:
|
||||
specifier: ^4.2.1
|
||||
version: 4.3.1
|
||||
|
||||
litegraph/packages/tsconfig: {}
|
||||
|
||||
@@ -1990,11 +2031,11 @@ packages:
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^1.0.0-next.587
|
||||
dependencies:
|
||||
'@sveltejs/kit': 1.15.2(svelte@3.58.0)(vite@4.3.1)
|
||||
'@sveltejs/kit': 1.15.2(svelte@3.58.0)
|
||||
import-meta-resolve: 2.2.2
|
||||
dev: true
|
||||
|
||||
/@sveltejs/kit@1.15.2(svelte@3.58.0)(vite@4.3.1):
|
||||
/@sveltejs/kit@1.15.2(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-rLNxZrjbrlPf8AWW8GAU4L/Vvu17e9v8EYl7pUip7x72lTft7RcxeP3z7tsrHpMSBBxC9o4XdKzFvz1vMZyXZw==}
|
||||
engines: {node: ^16.14 || >=18}
|
||||
hasBin: true
|
||||
@@ -2003,7 +2044,7 @@ packages:
|
||||
svelte: ^3.54.0
|
||||
vite: ^4.0.0
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 2.1.1(svelte@3.58.0)(vite@4.3.1)
|
||||
'@sveltejs/vite-plugin-svelte': 2.1.1(svelte@3.58.0)
|
||||
'@types/cookie': 0.5.1
|
||||
cookie: 0.5.0
|
||||
devalue: 4.3.0
|
||||
@@ -2017,7 +2058,24 @@ packages:
|
||||
svelte: 3.58.0
|
||||
tiny-glob: 0.2.9
|
||||
undici: 5.20.0
|
||||
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@sveltejs/vite-plugin-svelte@2.1.1(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==}
|
||||
engines: {node: ^14.18.0 || >= 16}
|
||||
peerDependencies:
|
||||
svelte: ^3.54.0
|
||||
vite: ^4.0.0
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
deepmerge: 4.3.1
|
||||
kleur: 4.1.5
|
||||
magic-string: 0.30.0
|
||||
svelte: 3.58.0
|
||||
svelte-hmr: 0.15.1(svelte@3.58.0)
|
||||
vitefu: 0.2.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -2035,10 +2093,11 @@ packages:
|
||||
magic-string: 0.30.0
|
||||
svelte: 3.58.0
|
||||
svelte-hmr: 0.15.1(svelte@3.58.0)
|
||||
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||
vite: 4.3.1(sass@1.61.0)
|
||||
vitefu: 0.2.4(vite@4.3.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@ts-morph/common@0.18.1:
|
||||
resolution: {integrity: sha512-RVE+zSRICWRsfrkAw5qCAK+4ZH9kwEFv5h0+/YeHTLieWP7F4wWq4JsKFuNWG+fYh/KF+8rAtgdj5zb2mm+DVA==}
|
||||
@@ -2178,6 +2237,7 @@ packages:
|
||||
|
||||
/@types/node@18.16.0:
|
||||
resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==}
|
||||
dev: true
|
||||
|
||||
/@types/node@8.10.66:
|
||||
resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==}
|
||||
@@ -2281,6 +2341,14 @@ packages:
|
||||
eslint-visitor-keys: 3.4.0
|
||||
dev: true
|
||||
|
||||
/@zerodevx/svelte-toast@0.9.3(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==}
|
||||
peerDependencies:
|
||||
svelte: ^3.57.0
|
||||
dependencies:
|
||||
svelte: 3.58.0
|
||||
dev: true
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.8.2):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -4292,7 +4360,7 @@ packages:
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
import-local: 3.1.0
|
||||
jest-config: 29.5.0(@types/node@18.16.0)
|
||||
jest-config: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
jest-validate: 29.5.0
|
||||
prompts: 2.4.2
|
||||
@@ -4303,6 +4371,44 @@ packages:
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/jest-config@29.5.0:
|
||||
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
ts-node: '>=9.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.21.4
|
||||
'@jest/test-sequencer': 29.5.0
|
||||
'@jest/types': 29.5.0
|
||||
babel-jest: 29.5.0(@babel/core@7.21.4)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.5.0
|
||||
jest-environment-node: 29.5.0
|
||||
jest-get-type: 29.4.3
|
||||
jest-regex-util: 29.4.3
|
||||
jest-resolve: 29.5.0
|
||||
jest-runner: 29.5.0
|
||||
jest-util: 29.5.0
|
||||
jest-validate: 29.5.0
|
||||
micromatch: 4.0.5
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.5.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/jest-config@29.5.0(@types/node@18.16.0):
|
||||
resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -5882,7 +5988,7 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
/svelte-check@2.2.6(postcss-load-config@3.1.1)(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0):
|
||||
/svelte-check@2.2.6(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5896,7 +6002,7 @@ packages:
|
||||
sade: 1.8.1
|
||||
source-map: 0.7.4
|
||||
svelte: 3.58.0
|
||||
svelte-preprocess: 4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
||||
svelte-preprocess: 4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@5.0.3)
|
||||
typescript: 5.0.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -5911,7 +6017,7 @@ packages:
|
||||
- sugarss
|
||||
dev: true
|
||||
|
||||
/svelte-check@3.2.0(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0):
|
||||
/svelte-check@3.2.0(sass@1.61.0)(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5924,7 +6030,7 @@ packages:
|
||||
picocolors: 1.0.0
|
||||
sade: 1.8.1
|
||||
svelte: 3.58.0
|
||||
svelte-preprocess: 5.0.3(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
||||
svelte-preprocess: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
||||
typescript: 5.0.3
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -5976,7 +6082,7 @@ packages:
|
||||
tiny-glob: 0.2.9
|
||||
dev: false
|
||||
|
||||
/svelte-preprocess@4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@4.5.4):
|
||||
/svelte-preprocess@4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4):
|
||||
resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==}
|
||||
engines: {node: '>= 9.11.2'}
|
||||
requiresBuild: true
|
||||
@@ -6023,14 +6129,13 @@ packages:
|
||||
magic-string: 0.25.9
|
||||
postcss: 8.4.21
|
||||
postcss-load-config: 3.1.1
|
||||
sass: 1.61.0
|
||||
sorcery: 0.10.0
|
||||
strip-indent: 3.0.0
|
||||
svelte: 3.58.0
|
||||
typescript: 4.5.4
|
||||
dev: true
|
||||
|
||||
/svelte-preprocess@4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3):
|
||||
/svelte-preprocess@4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@5.0.3):
|
||||
resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==}
|
||||
engines: {node: '>= 9.11.2'}
|
||||
requiresBuild: true
|
||||
@@ -6077,14 +6182,13 @@ packages:
|
||||
magic-string: 0.25.9
|
||||
postcss: 8.4.21
|
||||
postcss-load-config: 3.1.1
|
||||
sass: 1.61.0
|
||||
sorcery: 0.10.0
|
||||
strip-indent: 3.0.0
|
||||
svelte: 3.58.0
|
||||
typescript: 5.0.3
|
||||
dev: true
|
||||
|
||||
/svelte-preprocess@5.0.3(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3):
|
||||
/svelte-preprocess@5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3):
|
||||
resolution: {integrity: sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==}
|
||||
engines: {node: '>= 14.10.0'}
|
||||
requiresBuild: true
|
||||
@@ -6125,7 +6229,6 @@ packages:
|
||||
'@types/pug': 2.0.6
|
||||
detect-indent: 6.1.0
|
||||
magic-string: 0.27.0
|
||||
postcss: 8.4.21
|
||||
sass: 1.61.0
|
||||
sorcery: 0.11.0
|
||||
strip-indent: 3.0.0
|
||||
@@ -6190,12 +6293,10 @@ packages:
|
||||
get-port: 3.2.0
|
||||
dev: false
|
||||
|
||||
/tailwindcss@3.3.1(postcss@8.4.21):
|
||||
/tailwindcss@3.3.1:
|
||||
resolution: {integrity: sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
postcss: ^8.0.9
|
||||
dependencies:
|
||||
arg: 5.0.2
|
||||
chokidar: 3.5.3
|
||||
@@ -6924,7 +7025,7 @@ packages:
|
||||
strip-ansi: 6.0.1
|
||||
tiny-invariant: 1.3.1
|
||||
typescript: 5.0.3
|
||||
vite: 4.2.1(sass@1.61.0)
|
||||
vite: 4.2.1
|
||||
vscode-languageclient: 7.0.0
|
||||
vscode-languageserver: 7.0.0
|
||||
vscode-languageserver-textdocument: 1.0.8
|
||||
@@ -6947,7 +7048,7 @@ packages:
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.29.0
|
||||
ts-morph: 17.0.1
|
||||
vite: 4.2.1(sass@1.61.0)
|
||||
vite: 4.2.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
@@ -6961,7 +7062,7 @@ packages:
|
||||
dependencies:
|
||||
picocolors: 1.0.0
|
||||
picomatch: 2.3.1
|
||||
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||
vite: 4.3.1(sass@1.61.0)
|
||||
dev: false
|
||||
|
||||
/vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.1):
|
||||
@@ -6975,13 +7076,13 @@ packages:
|
||||
debug: 4.3.4
|
||||
globrex: 0.1.2
|
||||
tsconfck: 2.1.1(typescript@5.0.3)
|
||||
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||
vite: 4.3.1(sass@1.61.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/vite@2.9.9(sass@1.61.0):
|
||||
/vite@2.9.9:
|
||||
resolution: {integrity: sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==}
|
||||
engines: {node: '>=12.2.0'}
|
||||
hasBin: true
|
||||
@@ -7001,12 +7102,11 @@ packages:
|
||||
postcss: 8.4.21
|
||||
resolve: 1.22.2
|
||||
rollup: 2.79.1
|
||||
sass: 1.61.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vite@4.2.1(sass@1.61.0):
|
||||
/vite@4.2.1:
|
||||
resolution: {integrity: sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
@@ -7035,10 +7135,41 @@ packages:
|
||||
postcss: 8.4.21
|
||||
resolve: 1.22.2
|
||||
rollup: 3.21.0
|
||||
sass: 1.61.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
/vite@4.3.1:
|
||||
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': '>= 14'
|
||||
less: '*'
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
esbuild: 0.17.18
|
||||
postcss: 8.4.21
|
||||
rollup: 3.21.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vite@4.3.1(@types/node@18.16.0)(sass@1.61.0):
|
||||
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
@@ -7071,6 +7202,48 @@ packages:
|
||||
sass: 1.61.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vite@4.3.1(sass@1.61.0):
|
||||
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': '>= 14'
|
||||
less: '*'
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
esbuild: 0.17.18
|
||||
postcss: 8.4.21
|
||||
rollup: 3.21.0
|
||||
sass: 1.61.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
/vitefu@0.2.4:
|
||||
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
|
||||
peerDependencies:
|
||||
vite: ^3.0.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/vitefu@0.2.4(vite@4.3.1):
|
||||
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
|
||||
@@ -7080,7 +7253,8 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
dependencies:
|
||||
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||
vite: 4.3.1(sass@1.61.0)
|
||||
dev: false
|
||||
|
||||
/vitest@0.25.8(sass@1.61.0):
|
||||
resolution: {integrity: sha512-X75TApG2wZTJn299E/TIYevr4E9/nBo1sUtZzn0Ci5oK8qnpZAZyhwg0qCeMSakGIWtc6oRwcQFyFfW14aOFWg==}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
packages:
|
||||
- 'gradio/js/*'
|
||||
- 'gradio/client/js'
|
||||
- 'litegraph/packages/core'
|
||||
- 'litegraph/packages/nodes-basic'
|
||||
- 'litegraph/packages/tsconfig'
|
||||
- 'litegraph/packages/*'
|
||||
|
||||
370
src/App.svelte
370
src/App.svelte
@@ -1,377 +1,15 @@
|
||||
<script lang="ts">
|
||||
import ComfyApp from "$lib/components/ComfyApp.svelte"
|
||||
import { default as ComfyAppState } from "$lib/components/ComfyApp"
|
||||
import "@litegraph-ts/core/css/litegraph.css";
|
||||
import "./scss/global.scss";
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const app = new ComfyAppState();
|
||||
</script>
|
||||
|
||||
<ComfyApp/>
|
||||
<ComfyApp {app}/>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--primary-50: #fff7ed;
|
||||
--primary-100: #ffedd5;
|
||||
--primary-200: #fed7aa;
|
||||
--primary-300: #fdba74;
|
||||
--primary-400: #fb923c;
|
||||
--primary-500: #f97316;
|
||||
--primary-600: #ea580c;
|
||||
--primary-700: #c2410c;
|
||||
--primary-800: #9a3412;
|
||||
--primary-900: #7c2d12;
|
||||
--primary-950: #6c2e12;
|
||||
--secondary-50: #eff6ff;
|
||||
--secondary-100: #dbeafe;
|
||||
--secondary-200: #bfdbfe;
|
||||
--secondary-300: #93c5fd;
|
||||
--secondary-400: #60a5fa;
|
||||
--secondary-500: #3b82f6;
|
||||
--secondary-600: #2563eb;
|
||||
--secondary-700: #1d4ed8;
|
||||
--secondary-800: #1e40af;
|
||||
--secondary-900: #1e3a8a;
|
||||
--secondary-950: #1d3660;
|
||||
--neutral-50: #f9fafb;
|
||||
--neutral-100: #f3f4f6;
|
||||
--neutral-200: #e5e7eb;
|
||||
--neutral-300: #d1d5db;
|
||||
--neutral-400: #9ca3af;
|
||||
--neutral-500: #6b7280;
|
||||
--neutral-600: #4b5563;
|
||||
--neutral-700: #374151;
|
||||
--neutral-800: #1f2937;
|
||||
--neutral-900: #111827;
|
||||
--neutral-950: #0b0f19;
|
||||
--spacing-xxs: 1px;
|
||||
--spacing-xs: 2px;
|
||||
--spacing-sm: 4px;
|
||||
--spacing-md: 6px;
|
||||
--spacing-lg: 8px;
|
||||
--spacing-xl: 10px;
|
||||
--spacing-xxl: 16px;
|
||||
--radius-xxs: 1px;
|
||||
--radius-xs: 2px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius-xxl: 22px;
|
||||
--text-xxs: 9px;
|
||||
--text-xs: 10px;
|
||||
--text-sm: 12px;
|
||||
--text-md: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 22px;
|
||||
--text-xxl: 26px;
|
||||
--color-accent: var(--primary-500);
|
||||
--color-accent-soft: var(--primary-50);
|
||||
--background-fill-primary: white;
|
||||
--background-fill-secondary: var(--neutral-50);
|
||||
--border-color-accent: var(--primary-300);
|
||||
--border-color-primary: var(--neutral-200);
|
||||
--text-color-code-background-fill: var(--neutral-200);
|
||||
--text-color-code-border: var(--border-color-primary);
|
||||
--link-text-color: var(--secondary-600);
|
||||
--link-text-color-active: var(--secondary-600);
|
||||
--link-text-color-hover: var(--secondary-700);
|
||||
--link-text-color-visited: var(--secondary-500);
|
||||
--body-text-color-subdued: var(--neutral-400);
|
||||
--body-background-fill: var(--background-fill-primary);
|
||||
--body-text-color: var(--neutral-800);
|
||||
--body-text-size: var(--text-md);
|
||||
--body-text-weight: 400;
|
||||
--embed-radius: var(--radius-lg);
|
||||
--shadow-drop: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
--shadow-drop-lg: 0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-inset: rgba(0, 0, 0, 0.05) 0px 2px 4px 0px inset;
|
||||
--shadow-spread: 3px;
|
||||
--block-background-fill: var(--background-fill-primary);
|
||||
--block-border-color: var(--border-color-primary);
|
||||
--block-border-width: 1px;
|
||||
--block-info-text-color: var(--body-text-color-subdued);
|
||||
--block-info-text-size: var(--text-sm);
|
||||
--block-info-text-weight: 400;
|
||||
--block-label-background-fill: var(--background-fill-primary);
|
||||
--block-label-border-color: var(--border-color-primary);
|
||||
--block-label-border-width: 1px;
|
||||
--block-label-text-color: var(--neutral-500);
|
||||
--block-label-icon-color: var(--block-label-text-color);
|
||||
--block-label-margin: 0;
|
||||
--block-label-padding: var(--spacing-sm) var(--spacing-lg);
|
||||
--block-label-radius: calc(var(--radius-lg) - 1px) 0
|
||||
calc(var(--radius-lg) - 1px) 0;
|
||||
--block-label-right-radius: 0 calc(var(--radius-lg) - 1px) 0
|
||||
calc(var(--radius-lg) - 1px);
|
||||
--block-label-text-size: var(--text-sm);
|
||||
--block-label-text-weight: 400;
|
||||
--block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px);
|
||||
--block-radius: var(--radius-lg);
|
||||
--block-shadow: var(--shadow-drop);
|
||||
--block-title-background-fill: none;
|
||||
--block-title-border-color: none;
|
||||
--block-title-border-width: 0px;
|
||||
--block-title-text-color: var(--neutral-500);
|
||||
--block-title-padding: 0;
|
||||
--block-title-radius: none;
|
||||
--block-title-text-size: var(--text-md);
|
||||
--block-title-text-weight: 400;
|
||||
--container-radius: var(--radius-lg);
|
||||
--form-gap-width: 1px;
|
||||
--layout-gap: var(--spacing-xxl);
|
||||
--panel-background-fill: var(--background-fill-secondary);
|
||||
--panel-border-color: var(--border-color-primary);
|
||||
--panel-border-width: 0;
|
||||
--section-header-text-size: var(--text-md);
|
||||
--section-header-text-weight: 400;
|
||||
--checkbox-background-color: var(--background-fill-primary);
|
||||
--checkbox-background-color-focus: var(--background-fill-primary);
|
||||
--checkbox-background-color-hover: var(--background-fill-primary);
|
||||
--checkbox-background-color-selected: var(--secondary-600);
|
||||
--checkbox-border-color: var(--neutral-300);
|
||||
--checkbox-border-color-focus: var(--secondary-500);
|
||||
--checkbox-border-color-hover: var(--neutral-300);
|
||||
--checkbox-border-color-selected: var(--secondary-600);
|
||||
--checkbox-border-radius: var(--radius-sm);
|
||||
--checkbox-border-width: var(--input-border-width);
|
||||
--checkbox-label-background-fill: linear-gradient(
|
||||
to top,
|
||||
var(--neutral-50),
|
||||
white
|
||||
);
|
||||
--checkbox-label-background-fill-hover: linear-gradient(
|
||||
to top,
|
||||
var(--neutral-100),
|
||||
white
|
||||
);
|
||||
--checkbox-label-background-fill-selected: var(
|
||||
--checkbox-label-background-fill
|
||||
);
|
||||
--checkbox-label-border-color: var(--border-color-primary);
|
||||
--checkbox-label-border-color-hover: var(--border-color-primary);
|
||||
--checkbox-label-border-width: var(--input-border-width);
|
||||
--checkbox-label-gap: var(--spacing-lg);
|
||||
--checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md));
|
||||
--checkbox-label-shadow: var(--shadow-drop);
|
||||
--checkbox-label-text-size: var(--text-md);
|
||||
--checkbox-label-text-weight: 400;
|
||||
--checkbox-shadow: var(--input-shadow);
|
||||
--checkbox-label-text-color: var(--body-text-color);
|
||||
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
|
||||
--error-background-fill: linear-gradient(
|
||||
to right,
|
||||
#fee2e2,
|
||||
var(--background-fill-secondary)
|
||||
);
|
||||
--error-border-color: #fecaca;
|
||||
--error-border-width: 1px;
|
||||
--error-text-color: #ef4444;
|
||||
--prose-header-text-weight: 600;
|
||||
--input-background-fill: white;
|
||||
--input-background-fill-focus: var(--secondary-500);
|
||||
--input-background-fill-hover: var(--input-background-fill);
|
||||
--input-border-color: var(--border-color-primary);
|
||||
--input-border-color-focus: var(--secondary-300);
|
||||
--input-border-color-hover: var(--border-color-primary);
|
||||
--input-border-width: 1px;
|
||||
--input-padding: var(--spacing-xl);
|
||||
--input-placeholder-color: var(--neutral-400);
|
||||
--input-radius: var(--radius-lg);
|
||||
--input-shadow: 0 0 0 var(--shadow-spread) transparent, var(--shadow-inset);
|
||||
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--secondary-50),
|
||||
var(--shadow-inset);
|
||||
--input-text-size: var(--text-md);
|
||||
--input-text-weight: 400;
|
||||
--loader-color: var(--color-accent);
|
||||
--prose-text-size: var(--text-md);
|
||||
--prose-text-weight: 400;
|
||||
--stat-background-fill: linear-gradient(
|
||||
to right,
|
||||
var(--primary-400),
|
||||
var(--primary-200)
|
||||
);
|
||||
--table-border-color: var(--neutral-300);
|
||||
--table-even-background-fill: white;
|
||||
--table-odd-background-fill: var(--neutral-50);
|
||||
--table-radius: var(--radius-lg);
|
||||
--table-row-focus: var(--color-accent-soft);
|
||||
--button-border-width: var(--input-border-width);
|
||||
--button-cancel-background-fill: linear-gradient(
|
||||
to bottom right,
|
||||
#fee2e2,
|
||||
#fecaca
|
||||
);
|
||||
--button-cancel-background-fill-hover: linear-gradient(
|
||||
to bottom right,
|
||||
#fee2e2,
|
||||
#fee2e2
|
||||
);
|
||||
--button-cancel-border-color: #fecaca;
|
||||
--button-cancel-border-color-hover: var(--button-cancel-border-color);
|
||||
--button-cancel-text-color: #dc2626;
|
||||
--button-cancel-text-color-hover: var(--button-cancel-text-color);
|
||||
--button-large-padding: var(--spacing-lg) calc(2 * var(--spacing-lg));
|
||||
--button-large-radius: var(--radius-lg);
|
||||
--button-large-text-size: var(--text-lg);
|
||||
--button-large-text-weight: 600;
|
||||
--button-primary-background-fill: linear-gradient(
|
||||
to bottom right,
|
||||
var(--primary-100),
|
||||
var(--primary-300)
|
||||
);
|
||||
--button-primary-background-fill-hover: linear-gradient(
|
||||
to bottom right,
|
||||
var(--primary-100),
|
||||
var(--primary-200)
|
||||
);
|
||||
--button-primary-border-color: var(--primary-200);
|
||||
--button-primary-border-color-hover: var(--button-primary-border-color);
|
||||
--button-primary-text-color: var(--primary-600);
|
||||
--button-primary-text-color-hover: var(--button-primary-text-color);
|
||||
--button-secondary-background-fill: linear-gradient(
|
||||
to bottom right,
|
||||
var(--neutral-100),
|
||||
var(--neutral-200)
|
||||
);
|
||||
--button-secondary-background-fill-hover: linear-gradient(
|
||||
to bottom right,
|
||||
var(--neutral-100),
|
||||
var(--neutral-100)
|
||||
);
|
||||
--button-secondary-border-color: var(--neutral-200);
|
||||
--button-secondary-border-color-hover: var(--button-secondary-border-color);
|
||||
--button-secondary-text-color: var(--neutral-700);
|
||||
--button-secondary-text-color-hover: var(--button-secondary-text-color);
|
||||
--button-shadow: var(--shadow-drop);
|
||||
--button-shadow-active: var(--shadow-inset);
|
||||
--button-shadow-hover: var(--shadow-drop-lg);
|
||||
--button-small-padding: var(--spacing-sm) calc(2 * var(--spacing-sm));
|
||||
--button-small-radius: var(--radius-lg);
|
||||
--button-small-text-size: var(--text-md);
|
||||
--button-small-text-weight: 400;
|
||||
--button-transition: none;
|
||||
}
|
||||
.dark {
|
||||
--color-accent-soft: var(--neutral-900);
|
||||
--background-fill-primary: var(--neutral-950);
|
||||
--background-fill-secondary: var(--neutral-900);
|
||||
--border-color-accent: var(--neutral-600);
|
||||
--border-color-primary: var(--neutral-700);
|
||||
--text-color-code-background-fill: var(--neutral-800);
|
||||
--link-text-color-active: var(--secondary-500);
|
||||
--link-text-color: var(--secondary-500);
|
||||
--link-text-color-hover: var(--secondary-400);
|
||||
--link-text-color-visited: var(--secondary-600);
|
||||
--body-text-color-subdued: var(--neutral-400);
|
||||
--body-background-fill: var(--background-fill-primary);
|
||||
--body-text-color: var(--neutral-100);
|
||||
--shadow-spread: 1px;
|
||||
--block-background-fill: var(--neutral-800);
|
||||
--block-border-color: var(--border-color-primary);
|
||||
--block-border-width: 1px;
|
||||
--block-info-text-color: var(--body-text-color-subdued);
|
||||
--block-label-background-fill: var(--background-fill-secondary);
|
||||
--block-label-border-color: var(--border-color-primary);
|
||||
--block-label-border-width: 1px;
|
||||
--block-label-text-color: var(--neutral-200);
|
||||
--block-shadow: none;
|
||||
--block-title-background-fill: none;
|
||||
--block-title-border-color: none;
|
||||
--block-title-border-width: 0px;
|
||||
--block-title-text-color: var(--neutral-200);
|
||||
--panel-background-fill: var(--background-fill-secondary);
|
||||
--panel-border-color: var(--border-color-primary);
|
||||
--checkbox-background-color: var(--neutral-800);
|
||||
--checkbox-background-color-focus: var(--checkbox-background-color);
|
||||
--checkbox-background-color-hover: var(--checkbox-background-color);
|
||||
--checkbox-background-color-selected: var(--secondary-600);
|
||||
--checkbox-border-color: var(--neutral-700);
|
||||
--checkbox-border-color-focus: var(--secondary-500);
|
||||
--checkbox-border-color-hover: var(--neutral-600);
|
||||
--checkbox-border-color-selected: var(--secondary-600);
|
||||
--checkbox-label-background-fill: linear-gradient(
|
||||
to top,
|
||||
var(--neutral-900),
|
||||
var(--neutral-800)
|
||||
);
|
||||
--checkbox-label-background-fill-hover: linear-gradient(
|
||||
to top,
|
||||
var(--neutral-900),
|
||||
var(--neutral-800)
|
||||
);
|
||||
--checkbox-label-background-fill-selected: var(
|
||||
--checkbox-label-background-fill
|
||||
);
|
||||
--checkbox-label-border-color: var(--border-color-primary);
|
||||
--checkbox-label-border-color-hover: var(--border-color-primary);
|
||||
--checkbox-label-text-color: var(--body-text-color);
|
||||
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
|
||||
--error-background-fill: var(--background-fill-primary);
|
||||
--error-border-color: var(--border-color-primary);
|
||||
--error-border-width: var(--error-border-width);
|
||||
--error-text-color: #ef4444;
|
||||
--input-background-fill: var(--neutral-800);
|
||||
--input-background-fill-focus: var(--secondary-600);
|
||||
--input-background-fill-hover: var(--input-background-fill);
|
||||
--input-border-color: var(--border-color-primary);
|
||||
--input-border-color-focus: var(--neutral-700);
|
||||
--input-border-color-hover: var(--border-color-primary);
|
||||
--input-placeholder-color: var(--neutral-500);
|
||||
--input-shadow: var(--input-shadow);
|
||||
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--neutral-700),
|
||||
var(--shadow-inset);
|
||||
--loader-color: var(--loader-color);
|
||||
--stat-background-fill: linear-gradient(
|
||||
to right,
|
||||
var(--primary-400),
|
||||
var(--primary-600)
|
||||
);
|
||||
--table-border-color: var(--neutral-700);
|
||||
--table-even-background-fill: var(--neutral-950);
|
||||
--table-odd-background-fill: var(--neutral-900);
|
||||
--table-row-focus: var(--color-accent-soft);
|
||||
--button-cancel-background-fill: linear-gradient(
|
||||
to bottom right,
|
||||
#dc2626,
|
||||
#b91c1c
|
||||
);
|
||||
--button-cancel-background-fill-hover: linear-gradient(
|
||||
to bottom right,
|
||||
#dc2626,
|
||||
#dc2626
|
||||
);
|
||||
--button-cancel-border-color: #dc2626;
|
||||
--button-cancel-border-color-hover: var(--button-cancel-border-color);
|
||||
--button-cancel-text-color: white;
|
||||
--button-cancel-text-color-hover: var(--button-cancel-text-color);
|
||||
--button-primary-background-fill: linear-gradient(
|
||||
to bottom right,
|
||||
var(--primary-600),
|
||||
var(--primary-700)
|
||||
);
|
||||
--button-primary-background-fill-hover: linear-gradient(
|
||||
to bottom right,
|
||||
var(--primary-600),
|
||||
var(--primary-600)
|
||||
);
|
||||
--button-primary-border-color: var(--primary-600);
|
||||
--button-primary-border-color-hover: var(--button-primary-border-color);
|
||||
--button-primary-text-color: white;
|
||||
--button-primary-text-color-hover: var(--button-primary-text-color);
|
||||
--button-secondary-background-fill: linear-gradient(
|
||||
to bottom right,
|
||||
var(--neutral-600),
|
||||
var(--neutral-700)
|
||||
);
|
||||
--button-secondary-background-fill-hover: linear-gradient(
|
||||
to bottom right,
|
||||
var(--neutral-600),
|
||||
var(--neutral-600)
|
||||
);
|
||||
--button-secondary-border-color: var(--neutral-600);
|
||||
--button-secondary-border-color-hover: var(--button-secondary-border-color);
|
||||
--button-secondary-text-color: white;
|
||||
--button-secondary-text-color-hover: var(--button-secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
import { Button } from "@gradio/button";
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import nodeState from "$lib/stores/nodeState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
|
||||
124
src/lib/ComfyGraph.ts
Normal file
124
src/lib/ComfyGraph.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions } from "@litegraph-ts/core";
|
||||
import GraphSync from "./GraphSync";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
import layoutState from "./stores/layoutState";
|
||||
import uiState from "./stores/uiState";
|
||||
import { get } from "svelte/store";
|
||||
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
||||
import type { ComfyWidgetNode } from "./nodes";
|
||||
|
||||
type ComfyGraphEvents = {
|
||||
configured: (graph: LGraph) => void
|
||||
nodeAdded: (node: LGraphNode) => void
|
||||
nodeRemoved: (node: LGraphNode) => void
|
||||
nodeConnectionChanged: (kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) => void
|
||||
cleared: () => void
|
||||
beforeChange: (graph: LGraph, param: any) => void
|
||||
afterChange: (graph: LGraph, param: any) => void
|
||||
afterExecute: () => void
|
||||
}
|
||||
|
||||
export default class ComfyGraph extends LGraph {
|
||||
graphSync: GraphSync;
|
||||
eventBus: TypedEmitter<ComfyGraphEvents> = new EventEmitter() as TypedEmitter<ComfyGraphEvents>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.graphSync = new GraphSync(this)
|
||||
}
|
||||
|
||||
override onConfigure() {
|
||||
console.debug("Configured");
|
||||
this.eventBus.emit("configured", this);
|
||||
}
|
||||
|
||||
override onBeforeChange(graph: LGraph, info: any) {
|
||||
console.debug("BeforeChange", info);
|
||||
this.eventBus.emit("beforeChange", graph, info);
|
||||
}
|
||||
|
||||
override onAfterChange(graph: LGraph, info: any) {
|
||||
console.debug("AfterChange", info);
|
||||
this.eventBus.emit("afterChange", graph, info);
|
||||
}
|
||||
|
||||
override onAfterExecute() {
|
||||
this.eventBus.emit("afterExecute");
|
||||
}
|
||||
|
||||
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||
layoutState.nodeAdded(node)
|
||||
this.graphSync.onNodeAdded(node);
|
||||
|
||||
if ("outputProperties" in node) {
|
||||
const widgetNode = node as ComfyWidgetNode;
|
||||
for (const propName of widgetNode.outputProperties) {
|
||||
widgetNode.addPropertyAsOutput(propName.name, propName.type)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the class declared a default widget layout
|
||||
if ("defaultWidgets" in node && !("svelteComponentType" in node)) {
|
||||
const comfyNode = node as ComfyGraphNode;
|
||||
const widgets = comfyNode.defaultWidgets;
|
||||
|
||||
if (widgets) {
|
||||
if (widgets.inputs) {
|
||||
for (const pair of Object.entries(comfyNode.defaultWidgets.inputs)) {
|
||||
const [index, spec] = pair
|
||||
const input = comfyNode.inputs[index] as IComfyInputSlot;
|
||||
input.defaultWidgetNode = spec.defaultWidgetNode;
|
||||
if (spec.config)
|
||||
input.config = spec.config
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (get(uiState).autoAddUI) {
|
||||
if (!("svelteComponentType" in node) && !options.addedByDeserialize) {
|
||||
console.debug("[ComfyGraph] AutoAdd UI")
|
||||
const comfyNode = node as ComfyGraphNode;
|
||||
const widgetNodesAdded = []
|
||||
for (let index = 0; index < comfyNode.inputs.length; index++) {
|
||||
const input = comfyNode.inputs[index];
|
||||
if ("config" in input) {
|
||||
const comfyInput = input as IComfyInputSlot;
|
||||
if (comfyInput.defaultWidgetNode) {
|
||||
const widgetNode = LiteGraph.createNode(comfyInput.defaultWidgetNode)
|
||||
const inputPos = comfyNode.getConnectionPos(true, index);
|
||||
this.add(widgetNode)
|
||||
widgetNode.connect(0, comfyNode, index);
|
||||
widgetNode.collapse();
|
||||
widgetNode.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
|
||||
widgetNodesAdded.push(widgetNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
const dragItems = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem).filter(di => di)
|
||||
console.debug("[ComfyGraph] Group new widgets", dragItems)
|
||||
|
||||
layoutState.groupItems(dragItems, { title: node.title })
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("Added", node);
|
||||
this.eventBus.emit("nodeAdded", node);
|
||||
}
|
||||
|
||||
override onNodeRemoved(node: LGraphNode) {
|
||||
layoutState.nodeRemoved(node);
|
||||
this.graphSync.onNodeRemoved(node);
|
||||
|
||||
console.debug("Removed", node);
|
||||
this.eventBus.emit("nodeRemoved", node);
|
||||
}
|
||||
|
||||
override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) {
|
||||
console.debug("ConnectionChange", node);
|
||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ import type ComfyApp from "./components/ComfyApp";
|
||||
import queueState from "./stores/queueState";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export type SerializedGraphCanvasState = {
|
||||
offset: Vector2,
|
||||
scale: number
|
||||
}
|
||||
|
||||
export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
app: ComfyApp
|
||||
|
||||
@@ -20,6 +25,23 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
serialize(): SerializedGraphCanvasState {
|
||||
return {
|
||||
offset: this.ds.offset,
|
||||
scale: this.ds.scale
|
||||
}
|
||||
}
|
||||
|
||||
deserialize(data: SerializedGraphCanvasState) {
|
||||
this.ds.offset = data.offset;
|
||||
this.ds.scale = data.scale;
|
||||
}
|
||||
|
||||
recenter() {
|
||||
this.ds.reset();
|
||||
this.setDirty(true, true)
|
||||
}
|
||||
|
||||
override drawNodeShape(
|
||||
node: LGraphNode,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import type { LGraph } from "@litegraph-ts/core";
|
||||
import widgetState, { type WidgetStateStore, type WidgetUIState, type WidgetUIStateStore } from "./stores/widgetState";
|
||||
import nodeState, { type NodeStateStore, type NodeUIState, type NodeUIStateStore } from "./stores/nodeState";
|
||||
import type { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import type ComfyApp from "./components/ComfyApp";
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import type { Unsubscriber, Writable } from "svelte/store";
|
||||
import type { ComfyWidgetNode } from "./nodes";
|
||||
import type ComfyGraph from "./ComfyGraph";
|
||||
|
||||
type WidgetSubStore = {
|
||||
store: WidgetUIStateStore,
|
||||
unsubscribe: Unsubscriber
|
||||
}
|
||||
|
||||
type NodeSubStore = {
|
||||
store: NodeUIStateStore,
|
||||
unsubscribe: Unsubscriber
|
||||
}
|
||||
|
||||
/*
|
||||
* Responsible for watching for and synchronizing state changes from the
|
||||
* frontend to the litegraph instance.
|
||||
@@ -30,80 +25,53 @@ type NodeSubStore = {
|
||||
*/
|
||||
export default class GraphSync {
|
||||
graph: LGraph;
|
||||
private _unsubscribe: Unsubscriber;
|
||||
private _finalizer: FinalizationRegistry<number>;
|
||||
|
||||
// nodeId -> widgetSubStores[]
|
||||
private stores: Record<string, WidgetSubStore[]> = {}
|
||||
// nodeId -> widgetSubStore
|
||||
private stores: Record<string, WidgetSubStore> = {}
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
this.graph = app.lGraph;
|
||||
this._unsubscribeWidget = widgetState.subscribe(this.onAllWidgetStateChanged.bind(this));
|
||||
this._unsubscribeNode = nodeState.subscribe(this.onAllNodeStateChanged.bind(this));
|
||||
this._finalizer = new FinalizationRegistry((id: number) => {
|
||||
console.log(`${this} has been garbage collected`);
|
||||
this._unsubscribeWidget();
|
||||
this._unsubscribeNode();
|
||||
});
|
||||
constructor(graph: ComfyGraph) {
|
||||
this.graph = graph;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fired when the entire widget graph changes.
|
||||
*/
|
||||
private onAllWidgetStateChanged(state: WidgetStateStore) {
|
||||
onNodeAdded(node: LGraphNode) {
|
||||
// TODO assumes only a single graph's widget state.
|
||||
|
||||
for (let nodeId in state) {
|
||||
if (!this.stores[nodeId]) {
|
||||
this.addStores(state, nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
for (let nodeId in this.stores) {
|
||||
if (!state[nodeId]) {
|
||||
this.removeStores(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onAllNodeStateChanged(state: NodeStateStore) {
|
||||
// TODO assumes only a single graph's widget state.
|
||||
|
||||
for (let nodeId in state) {
|
||||
state[nodeId].node.name = state[nodeId].name;
|
||||
if ("svelteComponentType" in node) {
|
||||
this.addStore(node as ComfyWidgetNode);
|
||||
}
|
||||
|
||||
this.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
private addStores(state: WidgetStateStore, nodeId: string) {
|
||||
if (this.stores[nodeId]) {
|
||||
console.warn("Stores already exist!", nodeId, this.stores[nodeId])
|
||||
onNodeRemoved(node: LGraphNode) {
|
||||
if ("svelteComponentType" in node) {
|
||||
this.removeStore(node as ComfyWidgetNode);
|
||||
}
|
||||
|
||||
this.stores[nodeId] = []
|
||||
|
||||
for (const wuis of state[nodeId]) {
|
||||
const unsub = wuis.value.subscribe((v) => this.onWidgetStateChanged(wuis, v))
|
||||
this.stores[nodeId].push({ store: wuis.value, unsubscribe: unsub });
|
||||
}
|
||||
|
||||
console.debug("NEWSTORES", this.stores[nodeId])
|
||||
this.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
private removeStores(nodeId: string) {
|
||||
console.debug("DELSTORES", this.stores[nodeId])
|
||||
for (const ss of this.stores[nodeId]) {
|
||||
ss.unsubscribe();
|
||||
private addStore(node: ComfyWidgetNode) {
|
||||
if (this.stores[node.id]) {
|
||||
console.warn("[GraphSync] Stores already exist!", node.id, this.stores[node.id])
|
||||
}
|
||||
delete this.stores[nodeId]
|
||||
|
||||
const unsub = node.value.subscribe((v) => this.onWidgetStateChanged(node, v))
|
||||
this.stores[node.id] = ({ store: node.value, unsubscribe: unsub });
|
||||
|
||||
console.debug("[GraphSync] NEWSTORE", this.stores[node.id])
|
||||
}
|
||||
|
||||
private removeStore(node: ComfyWidgetNode) {
|
||||
console.debug("[GraphSync] DELSTORE", this.stores[node.id])
|
||||
this.stores[node.id].unsubscribe()
|
||||
delete this.stores[node.id]
|
||||
}
|
||||
|
||||
/*
|
||||
* Fired when a single widget's value changes.
|
||||
*/
|
||||
private onWidgetStateChanged(wuis: WidgetUIState, value: any) {
|
||||
wuis.widget.value = value;
|
||||
private onWidgetStateChanged(node: ComfyWidgetNode, value: any) {
|
||||
this.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/lib/IComfyInputSlot.ts
Normal file
20
src/lib/IComfyInputSlot.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { INodeInputSlot } from "@litegraph-ts/core";
|
||||
import type { ComfyWidgetNode } from "./nodes";
|
||||
|
||||
// TODO generalize
|
||||
export type ComfyInputConfig = {
|
||||
min?: number,
|
||||
max?: number,
|
||||
step?: number,
|
||||
precision?: number,
|
||||
defaultValue?: any,
|
||||
values?: any[],
|
||||
multiline?: boolean
|
||||
}
|
||||
|
||||
export default interface IComfyInputSlot extends INodeInputSlot {
|
||||
serialize: boolean,
|
||||
defaultWidgetNode: new (name?: string) => ComfyWidgetNode
|
||||
widgetNodeType?: string,
|
||||
config: ComfyInputConfig, // stores range min/max/step, etc.
|
||||
}
|
||||
@@ -231,7 +231,7 @@ export default class ComfyAPI extends EventTarget {
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { Running: [], Pending: [] };
|
||||
return { Running: [], Pending: [], error };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ export default class ComfyAPI extends EventTarget {
|
||||
return { History: Object.values(await res.json()) };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { History: [] };
|
||||
return { History: [], error };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
269
src/lib/components/BlockContainer.svelte
Normal file
269
src/lib/components/BlockContainer.svelte
Normal file
@@ -0,0 +1,269 @@
|
||||
<script lang="ts">
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import WidgetContainer from "./WidgetContainer.svelte"
|
||||
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
|
||||
import {fade} from 'svelte/transition';
|
||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { flip } from 'svelte/animate';
|
||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
|
||||
export let container: ContainerLayout | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
export let showHandles: boolean = false;
|
||||
let children: IDragItem[] | null = null;
|
||||
const flipDurationMs = 100;
|
||||
|
||||
$: if (container) {
|
||||
children = $layoutState.allItems[container.id].children;
|
||||
}
|
||||
else {
|
||||
children = null;
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// console.log(dragItems);
|
||||
};
|
||||
|
||||
function handleFinalize(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if container && children}
|
||||
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')}"
|
||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)}
|
||||
class:root-container={zIndex === 0}
|
||||
class:is-executing={container.isNodeExecuting}
|
||||
class:container-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1}>
|
||||
<Block>
|
||||
{#if container.attrs.showTitle}
|
||||
<label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}>
|
||||
<BlockTitle>
|
||||
{#if $uiState.uiEditMode === "widgets"}
|
||||
<input class="edit-title" bind:value={container.attrs.title} type="text" minlength="1" />
|
||||
{:else}
|
||||
{container.attrs.title}
|
||||
{/if}
|
||||
</BlockTitle>
|
||||
</label>
|
||||
{/if}
|
||||
<div class="v-pane"
|
||||
class:empty={children.length === 0}
|
||||
class:edit={$uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
use:dndzone="{{
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
centreDraggedOnCursor: true,
|
||||
morphDisabled: true,
|
||||
dropFromOthersDisabled: zIndex === 0,
|
||||
dragDisabled: zIndex === 0 || $layoutState.currentSelection.length > 2 || $uiState.uiEditMode === "disabled"
|
||||
}}"
|
||||
on:consider="{handleConsider}"
|
||||
on:finalize="{handleFinalize}"
|
||||
>
|
||||
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||
<div class="animation-wrapper"
|
||||
animate:flip={{duration:flipDurationMs}}>
|
||||
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
|
||||
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if showHandles}
|
||||
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
||||
{/if}
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.v-pane {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
|
||||
.edit {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
border-width: 3px;
|
||||
border-color: var(--color-grey-400);
|
||||
border-radius: var(--block-radius);
|
||||
background: var(--color-grey-300);
|
||||
min-height: 100px;
|
||||
border-style: dashed;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: grab;
|
||||
z-index: 99999;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handle-container:hover {
|
||||
background-color: #d8ade680;
|
||||
}
|
||||
|
||||
.drag-item-shadow {
|
||||
position: absolute;
|
||||
top: 0; left:0; right: 0; bottom: 0;
|
||||
visibility: visible;
|
||||
border: 1px dashed grey;
|
||||
background: lightblue;
|
||||
opacity: 0.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
:global(.block) {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--layout-gap);
|
||||
width: var(--size-full);
|
||||
|
||||
> :global(.block > .v-pane) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
> :global(*), > :global(.form > *) {
|
||||
flex: 1 1 0%;
|
||||
flex-wrap: wrap;
|
||||
min-width: min(160px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
position: relative;
|
||||
|
||||
> :global(.block > .v-pane) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
> :global(*), > :global(.form > *), .v-pane {
|
||||
width: var(--size-full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container.selected > :global(.block) {
|
||||
background: var(--color-yellow-300);
|
||||
}
|
||||
|
||||
.is-executing {
|
||||
border: 3px dashed var(--color-green-600) !important;
|
||||
margin: 0.2em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
|
||||
&:not(.edit) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: grab;
|
||||
z-index: 99999;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
background-color: #add8e680;
|
||||
}
|
||||
|
||||
.handle-container:hover {
|
||||
background-color: #d8ade680;
|
||||
}
|
||||
|
||||
.drag-item-shadow {
|
||||
position: absolute;
|
||||
top: 0; left:0; right: 0; bottom: 0;
|
||||
visibility: visible;
|
||||
border: 1px dashed grey;
|
||||
background: lightblue;
|
||||
opacity: 0.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.node-type {
|
||||
font-size: smaller;
|
||||
color: var(--neutral-400);
|
||||
}
|
||||
|
||||
.edit-title-label {
|
||||
z-index: 10000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.edit-title {
|
||||
z-index: 10000;
|
||||
display: block;
|
||||
position: relative;
|
||||
outline: none !important;
|
||||
box-shadow: var(--input-shadow);
|
||||
border: var(--input-border-width) solid var(--input-border-color);
|
||||
border-radius: var(--input-radius);
|
||||
background: var(--input-background-fill);
|
||||
padding: var(--input-padding);
|
||||
width: 100%;
|
||||
color: var(--body-text-color);
|
||||
font-weight: var(--input-text-weight);
|
||||
font-size: var(--input-text-size);
|
||||
line-height: var(--line-sm);
|
||||
}
|
||||
|
||||
.edit-title:focus {
|
||||
box-shadow: var(--input-shadow-focus);
|
||||
border-color: var(--input-border-color-focus);
|
||||
}
|
||||
|
||||
.edit-title::placeholder {
|
||||
color: var(--input-placeholder-color);
|
||||
}
|
||||
|
||||
.container-edit-outline > :global(.block) {
|
||||
border-color: var(--color-pink-500);
|
||||
border-width: 2px;
|
||||
border-style: dashed !important;
|
||||
margin: 0.2em;
|
||||
padding: 1.4em;
|
||||
}
|
||||
|
||||
.widget-edit-outline {
|
||||
border: 2px dashed var(--color-blue-400);
|
||||
margin: 0.2em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.root-container > :global(.block) {
|
||||
padding: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -6,26 +6,34 @@
|
||||
import ComfyUIPane from "./ComfyUIPane.svelte";
|
||||
import ComfyApp, { type SerializedAppState } from "./ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import nodeState from "$lib/stores/nodeState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
|
||||
import { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import LightboxModal from "./LightboxModal.svelte";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
import type { ComfyAPIStatus } from "$lib/api";
|
||||
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
|
||||
|
||||
import { LGraph } from "@litegraph-ts/core";
|
||||
import LightboxModal from "./LightboxModal.svelte";
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
|
||||
let app: ComfyApp = undefined;
|
||||
export let app: ComfyApp = undefined;
|
||||
let imageViewer: ImageViewer;
|
||||
let uiPane: ComfyUIPane = undefined;
|
||||
let queue: ComfyQueue = undefined;
|
||||
let mainElem: HTMLDivElement;
|
||||
let uiPane: ComfyUIPane = undefined;
|
||||
let containerElem: HTMLDivElement;
|
||||
let resizeTimeout: typeof Timer = -1;
|
||||
let resizeTimeout: NodeJS.Timeout | null;
|
||||
let hasShownUIHelpToast: boolean = false;
|
||||
|
||||
let debugLayout: boolean = false;
|
||||
|
||||
const toastOptions = {
|
||||
intro: { duration: 200 },
|
||||
theme: {
|
||||
'--toastBarHeight': 0
|
||||
}
|
||||
}
|
||||
|
||||
function refreshView(event?: Event) {
|
||||
clearTimeout(resizeTimeout);
|
||||
@@ -37,8 +45,11 @@
|
||||
app.queuePrompt(0, 1);
|
||||
}
|
||||
|
||||
$: if (app) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
|
||||
$: if (app) app.lCanvas.allow_interaction = !$uiState.graphLocked;
|
||||
$: if (app?.lCanvas) app.lCanvas.allow_dragnodes = !$uiState.nodesLocked;
|
||||
$: if (app?.lCanvas) app.lCanvas.allow_interaction = !$uiState.graphLocked;
|
||||
|
||||
$: if ($uiState.uiEditMode)
|
||||
$layoutState.currentSelection = []
|
||||
|
||||
let graphSize = null;
|
||||
|
||||
@@ -64,69 +75,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
let graphResizeTimer: typeof Timer = -1;
|
||||
|
||||
function serializeAppState(): SerializedAppState {
|
||||
const graph = app.lGraph;
|
||||
|
||||
const serializedGraph = graph.serialize()
|
||||
const serializedPaneOrder = uiPane.serialize()
|
||||
|
||||
return {
|
||||
createdBy: "ComfyBox",
|
||||
version: 1,
|
||||
workflow: serializedGraph,
|
||||
panes: serializedPaneOrder
|
||||
}
|
||||
}
|
||||
|
||||
function doAutosave(graph: LGraph): void {
|
||||
const savedWorkflow = serializeAppState();
|
||||
localStorage.setItem("workflow", JSON.stringify(savedWorkflow))
|
||||
}
|
||||
|
||||
function doRestore(workflow: SerializedAppState) {
|
||||
uiPane.restore(workflow.panes);
|
||||
}
|
||||
|
||||
function doSave(): void {
|
||||
if (!app?.lGraph)
|
||||
return;
|
||||
|
||||
const date = new Date();
|
||||
const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", "");
|
||||
|
||||
download(`workflow-${formattedDate}.json`, JSON.stringify(serializeAppState()), "application/json")
|
||||
app.saveStateToLocalStorage();
|
||||
toast.push("Saved to local storage.")
|
||||
//
|
||||
// const date = new Date();
|
||||
// const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", "");
|
||||
//
|
||||
// download(`workflow-${formattedDate}.json`, JSON.stringify(app.serialize()), "application/json")
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
app = new ComfyApp();
|
||||
function doReset(): void {
|
||||
var confirmed = confirm("Are you sure you want to clear the current workflow?");
|
||||
if (confirmed) {
|
||||
app.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO dedup
|
||||
app.eventBus.on("nodeAdded", nodeState.nodeAdded);
|
||||
app.eventBus.on("nodeRemoved", nodeState.nodeRemoved);
|
||||
app.eventBus.on("configured", nodeState.configureFinished);
|
||||
app.eventBus.on("cleared", nodeState.clear);
|
||||
function doRecenter(): void {
|
||||
app.lCanvas.recenter();
|
||||
}
|
||||
|
||||
app.eventBus.on("nodeAdded", widgetState.nodeAdded);
|
||||
app.eventBus.on("nodeRemoved", widgetState.nodeRemoved);
|
||||
app.eventBus.on("configured", widgetState.configureFinished);
|
||||
app.eventBus.on("cleared", widgetState.clear);
|
||||
app.eventBus.on("autosave", doAutosave);
|
||||
app.eventBus.on("restored", doRestore);
|
||||
$: if ($uiState.uiEditMode !== "disabled" && !hasShownUIHelpToast) {
|
||||
hasShownUIHelpToast = true;
|
||||
toast.push("Right-click to open context menu.")
|
||||
}
|
||||
|
||||
app.api.addEventListener("status", (ev: CustomEvent) => {
|
||||
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
|
||||
});
|
||||
if (debugLayout) {
|
||||
layoutState.subscribe(s => {
|
||||
console.warn("UPDATESTATE", s)
|
||||
})
|
||||
}
|
||||
|
||||
await app.setup();
|
||||
(window as any).app = app;
|
||||
(window as any).appPane = uiPane;
|
||||
|
||||
refreshView();
|
||||
app.api.addEventListener("status", (ev: CustomEvent) => {
|
||||
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
|
||||
});
|
||||
|
||||
$: if (app.rootEl && !imageViewer) {
|
||||
imageViewer = new ImageViewer(app.rootEl);
|
||||
}
|
||||
|
||||
$: if (containerElem) {
|
||||
let wrappers = containerElem.querySelectorAll<HTMLDivElement>(".pane-wrapper")
|
||||
for (const wrapper of wrappers) {
|
||||
const paneNode = wrapper.parentNode as HTMLElement; // get the node inside the <Pane/>
|
||||
@@ -134,10 +126,22 @@
|
||||
app.resizeCanvas()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await app.setup();
|
||||
(window as any).app = app;
|
||||
(window as any).appPane = uiPane;
|
||||
|
||||
refreshView();
|
||||
})
|
||||
|
||||
async function doRefreshCombos() {
|
||||
await app.refreshComboInNodes()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="main" bind:this={mainElem}>
|
||||
<div id="main">
|
||||
<div id="dropzone" class="dropzone"></div>
|
||||
<div id="container" bind:this={containerElem}>
|
||||
<Splitpanes theme="comfy" on:resize={refreshView}>
|
||||
@@ -173,13 +177,29 @@
|
||||
<Button variant="secondary" on:click={doSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doReset}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doRecenter}>
|
||||
Recenter
|
||||
</Button>
|
||||
<Button variant="secondary" on:click={doRefreshCombos}>
|
||||
🔄
|
||||
</Button>
|
||||
<Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
|
||||
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/>
|
||||
<Checkbox label="Enable UI Editing" bind:value={$uiState.unlocked}/>
|
||||
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
|
||||
<label for="enable-ui-editing">Enable UI Editing</label>
|
||||
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="widgets">Widgets</option>
|
||||
</select>
|
||||
</div>
|
||||
<LightboxModal />
|
||||
</div>
|
||||
|
||||
<SvelteToast options={toastOptions} />
|
||||
|
||||
<style lang="scss">
|
||||
#container {
|
||||
height: calc(100vh - 60px);
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink } from "@litegraph-ts/core";
|
||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType } from "@litegraph-ts/core";
|
||||
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||
import ComfyAPI from "$lib/api"
|
||||
import { ComfyWidgets } from "$lib/widgets"
|
||||
import defaultGraph from "$lib/defaultGraph"
|
||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
|
||||
// Import nodes
|
||||
import * as basic from "@litegraph-ts/nodes-basic"
|
||||
import "@litegraph-ts/nodes-basic"
|
||||
import "@litegraph-ts/nodes-events"
|
||||
import "@litegraph-ts/nodes-math"
|
||||
import * as nodes from "$lib/nodes/index"
|
||||
import ComfyGraphCanvas from "$lib/ComfyGraphCanvas";
|
||||
|
||||
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
|
||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import type { WidgetStateStore, WidgetUIState } from "$lib/stores/widgetState";
|
||||
import * as widgets from "$lib/widgets/index"
|
||||
import type ComfyWidget from "$lib/widgets/ComfyWidget";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import GraphSync from "$lib/GraphSync";
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import type { SerializedLayoutState } from "$lib/stores/layoutState";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import ComfyGraph from "$lib/ComfyGraph";
|
||||
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
|
||||
LiteGraph.catch_exceptions = false;
|
||||
|
||||
@@ -27,33 +36,25 @@ if (typeof window !== "undefined") {
|
||||
|
||||
type QueueItem = { num: number, batchCount: number }
|
||||
|
||||
export type SerializedPanes = {
|
||||
panels: { nodeId: number }[][]
|
||||
}
|
||||
|
||||
export type SerializedAppState = {
|
||||
createdBy: "ComfyBox",
|
||||
version: number,
|
||||
panes: SerializedPanes,
|
||||
workflow: SerializedLGraph
|
||||
workflow: SerializedLGraph,
|
||||
layout: SerializedLayoutState,
|
||||
canvas: SerializedGraphCanvasState
|
||||
}
|
||||
|
||||
type ComfyAppEvents = {
|
||||
configured: (graph: LGraph) => void
|
||||
nodeAdded: (node: LGraphNode) => void
|
||||
nodeRemoved: (node: LGraphNode) => void
|
||||
nodeConnectionChanged: (kind: LConnectionKind, node: LGraphNode, slot: INodeSlot, targetNode: LGraphNode, targetSlot: INodeSlot) => void
|
||||
cleared: () => void
|
||||
beforeChange: (graph: LGraph, param: any) => void
|
||||
afterChange: (graph: LGraph, param: any) => void
|
||||
autosave: (graph: LGraph) => void
|
||||
restored: (workflow: SerializedAppState) => void
|
||||
/** [link origin, link index] | value */
|
||||
export type SerializedPromptInput = [string, number] | any
|
||||
|
||||
export type SerializedPromptInputs = {
|
||||
inputs: Record<string, SerializedPromptInput>,
|
||||
class_type: string
|
||||
}
|
||||
|
||||
interface ComfyGraphNodeExecutable extends LGraphNodeExecutable {
|
||||
comfyClass: string
|
||||
isVirtualNode?: boolean;
|
||||
applyToGraph(workflow: SerializedLGraph<SerializedLGraphNode<LGraphNode>, SerializedLLink, SerializedLGraphGroup>): void;
|
||||
export type SerializedPrompt = {
|
||||
workflow: SerializedLGraph,
|
||||
output: Record<string, SerializedPromptInputs>
|
||||
}
|
||||
|
||||
export type Progress = {
|
||||
@@ -66,12 +67,10 @@ export default class ComfyApp {
|
||||
rootEl: HTMLDivElement | null = null;
|
||||
canvasEl: HTMLCanvasElement | null = null;
|
||||
canvasCtx: CanvasRenderingContext2D | null = null;
|
||||
lGraph: LGraph | null = null;
|
||||
lCanvas: LGraphCanvas | null = null;
|
||||
lGraph: ComfyGraph | null = null;
|
||||
lCanvas: ComfyGraphCanvas | null = null;
|
||||
dropZone: HTMLElement | null = null;
|
||||
nodeOutputs: Record<string, any> = {};
|
||||
eventBus: TypedEmitter<ComfyAppEvents> = new EventEmitter() as TypedEmitter<ComfyAppEvents>;
|
||||
graphSync: GraphSync;
|
||||
|
||||
dragOverNode: LGraphNode | null = null;
|
||||
shiftDown: boolean = false;
|
||||
@@ -79,30 +78,28 @@ export default class ComfyApp {
|
||||
|
||||
private queueItems: QueueItem[] = [];
|
||||
private processingQueue: boolean = false;
|
||||
private alreadySetup = false;
|
||||
|
||||
constructor() {
|
||||
this.api = new ComfyAPI();
|
||||
}
|
||||
|
||||
async setup(): Promise<void> {
|
||||
if (this.alreadySetup) {
|
||||
console.error("Already setup")
|
||||
return;
|
||||
}
|
||||
|
||||
this.rootEl = document.getElementById("main") as HTMLDivElement;
|
||||
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
|
||||
this.lGraph = new LGraph();
|
||||
this.lGraph = new ComfyGraph();
|
||||
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
|
||||
this.canvasCtx = this.canvasEl.getContext("2d");
|
||||
this.graphSync = new GraphSync(this);
|
||||
|
||||
this.addGraphLifecycleHooks();
|
||||
|
||||
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||
LiteGraph.alt_drag_do_clone_nodes = true;
|
||||
LiteGraph.ignore_all_widget_events = true;
|
||||
|
||||
this.lGraph.start();
|
||||
|
||||
// await this.#invokeExtensionsAsync("init");
|
||||
this.registerNodeTypeOverrides();
|
||||
this.registerWidgetTypeOverrides();
|
||||
await this.registerNodes();
|
||||
|
||||
// Load previous workflow
|
||||
@@ -110,9 +107,8 @@ export default class ComfyApp {
|
||||
try {
|
||||
const json = localStorage.getItem("workflow");
|
||||
if (json) {
|
||||
const workflow = JSON.parse(json) as SerializedAppState;
|
||||
this.loadGraphData(workflow["workflow"]);
|
||||
this.eventBus.emit("restored", workflow);
|
||||
const state = JSON.parse(json) as SerializedAppState;
|
||||
this.deserialize(state)
|
||||
restored = true;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -121,17 +117,18 @@ export default class ComfyApp {
|
||||
|
||||
// We failed to restore a workflow so load the default
|
||||
if (!restored) {
|
||||
this.loadGraphData();
|
||||
this.initDefaultGraph();
|
||||
}
|
||||
|
||||
// Save current workflow automatically
|
||||
setInterval(this.requestAutosave.bind(this), 15000);
|
||||
// setInterval(this.saveStateToLocalStorage.bind(this), 1000);
|
||||
|
||||
this.addApiUpdateHandlers();
|
||||
this.addDropHandler();
|
||||
this.addPasteHandler();
|
||||
this.addKeyboardHandler();
|
||||
|
||||
this.setupColorScheme()
|
||||
|
||||
// await this.#invokeExtensionsAsync("setup");
|
||||
|
||||
@@ -139,6 +136,11 @@ export default class ComfyApp {
|
||||
this.resizeCanvas();
|
||||
window.addEventListener("resize", this.resizeCanvas.bind(this));
|
||||
|
||||
this.lGraph.start();
|
||||
this.lGraph.eventBus.on("afterExecute", () => this.lCanvas.draw(true))
|
||||
|
||||
this.alreadySetup = true;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -150,82 +152,20 @@ export default class ComfyApp {
|
||||
this.lCanvas.draw(true, true);
|
||||
}
|
||||
|
||||
private graphOnConfigure() {
|
||||
console.debug("Configured");
|
||||
this.eventBus.emit("configured", this.lGraph);
|
||||
saveStateToLocalStorage() {
|
||||
const savedWorkflow = this.serialize();
|
||||
const json = JSON.stringify(savedWorkflow);
|
||||
localStorage.setItem("workflow", json)
|
||||
}
|
||||
|
||||
private graphOnBeforeChange(graph: LGraph, info: any) {
|
||||
console.debug("BeforeChange", info);
|
||||
this.eventBus.emit("beforeChange", graph, info);
|
||||
}
|
||||
|
||||
private graphOnAfterChange(graph: LGraph, info: any) {
|
||||
console.debug("AfterChange", info);
|
||||
this.eventBus.emit("afterChange", graph, info);
|
||||
}
|
||||
|
||||
private graphOnNodeAdded(node: LGraphNode) {
|
||||
console.debug("Added", node);
|
||||
this.eventBus.emit("nodeAdded", node);
|
||||
}
|
||||
|
||||
private graphOnNodeRemoved(node: LGraphNode) {
|
||||
console.debug("Removed", node);
|
||||
this.eventBus.emit("nodeRemoved", node);
|
||||
}
|
||||
|
||||
private graphOnNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: INodeSlot, targetNode: LGraphNode, targetSlot: INodeSlot) {
|
||||
console.debug("ConnectionChange", node);
|
||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||
}
|
||||
|
||||
private canvasOnClear() {
|
||||
console.debug("CanvasClear");
|
||||
this.eventBus.emit("cleared");
|
||||
}
|
||||
|
||||
private requestAutosave() {
|
||||
this.eventBus.emit("autosave", this.lGraph);
|
||||
}
|
||||
|
||||
private addGraphLifecycleHooks() {
|
||||
this.lGraph.onConfigure = this.graphOnConfigure.bind(this);
|
||||
this.lGraph.onBeforeChange = this.graphOnBeforeChange.bind(this);
|
||||
this.lGraph.onAfterChange = this.graphOnAfterChange.bind(this);
|
||||
this.lGraph.onNodeAdded = this.graphOnNodeAdded.bind(this);
|
||||
this.lGraph.onNodeRemoved = this.graphOnNodeRemoved.bind(this);
|
||||
this.lGraph.onNodeConnectionChange = this.graphOnNodeConnectionChange.bind(this);
|
||||
|
||||
this.lCanvas.onClear = this.canvasOnClear.bind(this);
|
||||
}
|
||||
|
||||
static node_type_overrides: Record<string, typeof ComfyGraphNode> = {}
|
||||
static widget_type_overrides: Record<string, Function> = {}
|
||||
|
||||
private registerNodeTypeOverrides() {
|
||||
ComfyApp.node_type_overrides["SaveImage"] = nodes.ComfySaveImageNode;
|
||||
ComfyApp.node_type_overrides["PreviewImage"] = nodes.ComfyPreviewImageNode;
|
||||
}
|
||||
|
||||
private registerWidgetTypeOverrides() {
|
||||
ComfyApp.widget_type_overrides["comfy/gallery"] = widgets.ComfyGalleryWidget_Svelte;
|
||||
}
|
||||
static node_type_overrides: Record<string, typeof ComfyBackendNode> = {}
|
||||
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
||||
|
||||
private async registerNodes() {
|
||||
const app = this;
|
||||
|
||||
// Load node definitions from the backend
|
||||
const defs = await this.api.getNodeDefs();
|
||||
// await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
|
||||
|
||||
// Generate list of known widgets
|
||||
const widgets = ComfyWidgets;
|
||||
// const widgets = Object.assign(
|
||||
// {},
|
||||
// ComfyWidgets,
|
||||
// ...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
|
||||
// );
|
||||
|
||||
// Register a node for each definition
|
||||
for (const nodeId in defs) {
|
||||
@@ -234,56 +174,11 @@ export default class ComfyApp {
|
||||
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
||||
if (typeOverride)
|
||||
console.debug("Attaching custom type to received node:", nodeId, typeOverride)
|
||||
const baseClass: typeof LGraphNode = typeOverride || LGraphNode;
|
||||
const baseClass: typeof ComfyBackendNode = typeOverride || ComfyBackendNode;
|
||||
|
||||
const ctor = class extends baseClass {
|
||||
constructor(title?: string) {
|
||||
super(title);
|
||||
this.type = nodeId; // XXX: workaround dependency in LGraphNode.addInput()
|
||||
(this as any).comfyClass = nodeId;
|
||||
var inputs = nodeData["input"]["required"];
|
||||
if (nodeData["input"]["optional"] != undefined) {
|
||||
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
|
||||
}
|
||||
const config = { minWidth: 1, minHeight: 1 };
|
||||
for (const inputName in inputs) {
|
||||
const inputData = inputs[inputName];
|
||||
const type = inputData[0];
|
||||
|
||||
if (inputData[1]?.forceInput) {
|
||||
this.addInput(inputName, type);
|
||||
} else {
|
||||
if (Array.isArray(type)) {
|
||||
// Enums
|
||||
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
|
||||
} else if (`${type}:${inputName}` in widgets) {
|
||||
// Support custom widgets by Type:Name
|
||||
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
|
||||
} else if (type in widgets) {
|
||||
// Standard type widgets
|
||||
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
|
||||
} else {
|
||||
// Node connection inputs
|
||||
this.addInput(inputName, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const o in nodeData["output"]) {
|
||||
const output = nodeData["output"][o];
|
||||
const outputName = nodeData["output_name"][o] || output;
|
||||
this.addOutput(outputName, output);
|
||||
}
|
||||
|
||||
const s = this.computeSize();
|
||||
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
||||
s[1] = Math.max(config.minHeight, s[1]);
|
||||
this.size = s;
|
||||
this.serialize_widgets = true;
|
||||
|
||||
// app.#invokeExtensionsAsync("nodeCreated", this);
|
||||
|
||||
return this;
|
||||
super(title, nodeId, nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,15 +189,9 @@ export default class ComfyApp {
|
||||
desc: `ComfyNode: ${nodeId}`
|
||||
}
|
||||
|
||||
// this.#addNodeContextMenuHandler(node);
|
||||
// this.#addDrawBackgroundHandler(node, app);
|
||||
|
||||
// await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData);
|
||||
LiteGraph.registerNodeType(node);
|
||||
node.category = nodeData.category;
|
||||
}
|
||||
|
||||
// await this.#invokeExtensionsAsync("registerCustomNodes");
|
||||
}
|
||||
|
||||
private showDropZone() {
|
||||
@@ -351,24 +240,24 @@ export default class ComfyApp {
|
||||
* Adds a handler on paste that extracts and loads workflows from pasted JSON data
|
||||
*/
|
||||
private addPasteHandler() {
|
||||
document.addEventListener("paste", (e) => {
|
||||
let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain");
|
||||
let workflow;
|
||||
try {
|
||||
data = data.slice(data.indexOf("{"));
|
||||
workflow = JSON.parse(data);
|
||||
} catch (err) {
|
||||
try {
|
||||
data = data.slice(data.indexOf("workflow\n"));
|
||||
data = data.slice(data.indexOf("{"));
|
||||
workflow = JSON.parse(data);
|
||||
} catch (error) { }
|
||||
}
|
||||
// document.addEventListener("paste", (e) => {
|
||||
// let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain");
|
||||
// let workflow;
|
||||
// try {
|
||||
// data = data.slice(data.indexOf("{"));
|
||||
// workflow = JSON.parse(data);
|
||||
// } catch (err) {
|
||||
// try {
|
||||
// data = data.slice(data.indexOf("workflow\n"));
|
||||
// data = data.slice(data.indexOf("{"));
|
||||
// workflow = JSON.parse(data);
|
||||
// } catch (error) { }
|
||||
// }
|
||||
|
||||
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
|
||||
this.loadGraphData(workflow);
|
||||
}
|
||||
});
|
||||
// if (workflow && workflow.version && workflow.nodes && workflow.extra) {
|
||||
// this.loadGraphData(workflow);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,17 +311,72 @@ export default class ComfyApp {
|
||||
});
|
||||
}
|
||||
|
||||
private setupColorScheme() {
|
||||
const setColor = (type: any, color: string) => {
|
||||
this.lCanvas.link_type_colors[type] = color
|
||||
this.lCanvas.default_connection_color_byType[type] = color
|
||||
}
|
||||
|
||||
// Distinguish frontend/backend connections
|
||||
const BACKEND_TYPES = ["CLIP", "CLIP_VISION", "CLIP_VISION_OUTPUT", "CONDITIONING", "CONTROL_NET", "IMAGE", "LATENT", "MASK", "MODEL", "STYLE_MODEL", "VAE"]
|
||||
for (const type of BACKEND_TYPES) {
|
||||
setColor(type, "orange")
|
||||
}
|
||||
|
||||
setColor("OUTPUT", "rebeccapurple")
|
||||
setColor(BuiltInSlotType.EVENT, "lightseagreen")
|
||||
setColor(BuiltInSlotType.ACTION, "lightseagreen")
|
||||
}
|
||||
|
||||
serialize(): SerializedAppState {
|
||||
const graph = this.lGraph;
|
||||
|
||||
const serializedGraph = graph.serialize()
|
||||
const serializedLayout = layoutState.serialize()
|
||||
const serializedCanvas = this.lCanvas.serialize();
|
||||
|
||||
return {
|
||||
createdBy: "ComfyBox",
|
||||
version: COMFYBOX_SERIAL_VERSION,
|
||||
workflow: serializedGraph,
|
||||
layout: serializedLayout,
|
||||
canvas: serializedCanvas
|
||||
}
|
||||
}
|
||||
|
||||
deserialize(data: SerializedAppState) {
|
||||
if (data.version !== COMFYBOX_SERIAL_VERSION) {
|
||||
throw `Invalid ComfyBox saved data format: ${data.version}`
|
||||
}
|
||||
|
||||
// Ensure loadGraphData does not trigger any state changes in layoutState
|
||||
// (isConfiguring is set to true here)
|
||||
// lGraph.configure will add new nodes, triggering onNodeAdded, but we
|
||||
// want to restore the layoutState ourselves
|
||||
layoutState.onStartConfigure();
|
||||
|
||||
this.loadGraphData(data.workflow)
|
||||
|
||||
// Now restore the layout
|
||||
// Subsequent added nodes will add the UI data to layoutState
|
||||
layoutState.deserialize(data.layout, this.lGraph)
|
||||
|
||||
// Restore canvas offset/zoom
|
||||
this.lCanvas.deserialize(data.canvas)
|
||||
}
|
||||
|
||||
initDefaultGraph() {
|
||||
const state = structuredClone(defaultGraph)
|
||||
this.deserialize(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the graph with the specified workflow data
|
||||
* @param {*} graphData A serialized graph object
|
||||
*/
|
||||
loadGraphData(graphData?: SerializedLGraph) {
|
||||
loadGraphData(graphData: SerializedLGraph) {
|
||||
this.clean();
|
||||
|
||||
if (!graphData) {
|
||||
graphData = structuredClone(defaultGraph.workflow)
|
||||
}
|
||||
|
||||
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||
for (let n of graphData.nodes) {
|
||||
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||
@@ -445,84 +389,118 @@ export default class ComfyApp {
|
||||
size[0] = Math.max(node.size[0], size[0]);
|
||||
size[1] = Math.max(node.size[1], size[1]);
|
||||
node.size = size;
|
||||
|
||||
if (node.widgets) {
|
||||
// If you break something in the backend and want to patch workflows in the frontend
|
||||
// This is the place to do this
|
||||
for (let widget of node.widgets) {
|
||||
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
|
||||
if (widget.name == "sampler_name") {
|
||||
if (widget.value.constructor === String && widget.value.startsWith("sample_")) {
|
||||
widget.value = widget.value.slice(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this.#invokeExtensions("loadedGraphNode", node);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.clean();
|
||||
|
||||
const blankGraph: SerializedLGraph = {
|
||||
last_node_id: 0,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
version: 0
|
||||
}
|
||||
|
||||
layoutState.onStartConfigure();
|
||||
this.lGraph.configure(blankGraph)
|
||||
layoutState.initDefaultLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt() {
|
||||
async graphToPrompt(): Promise<SerializedPrompt> {
|
||||
// Run frontend-only logic
|
||||
this.lGraph.runStep(1)
|
||||
|
||||
const workflow = this.lGraph.serialize();
|
||||
|
||||
const output = {};
|
||||
// Process nodes in order of execution
|
||||
for (const node of this.lGraph.computeExecutionOrder<ComfyGraphNodeExecutable>(false, null)) {
|
||||
const n = workflow.nodes.find((n) => n.id === node.id);
|
||||
for (const node_ of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) {
|
||||
const n = workflow.nodes.find((n) => n.id === node_.id);
|
||||
|
||||
if (node.isVirtualNode || !node.comfyClass) {
|
||||
console.debug("Not serializing node: ", node.type)
|
||||
// Don't serialize frontend only nodes but let them make changes
|
||||
if (node.applyToGraph) {
|
||||
node.applyToGraph(workflow);
|
||||
}
|
||||
if (!node_.isBackendNode) {
|
||||
console.debug("Not serializing node: ", node_.type)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.mode === 2) {
|
||||
const node = node_ as ComfyBackendNode;
|
||||
|
||||
if (node.mode === NodeMode.NEVER) {
|
||||
// Don't serialize muted nodes
|
||||
continue;
|
||||
}
|
||||
|
||||
const inputs = {};
|
||||
const widgets = node.widgets;
|
||||
|
||||
// Store all widget values
|
||||
if (widgets) {
|
||||
for (let i = 0; i < widgets.length; i++) {
|
||||
const widget = widgets[i];
|
||||
let isVirtual = false;
|
||||
if ("isVirtual" in widget)
|
||||
isVirtual = (widget as ComfyWidget<any, any>).isVirtual;
|
||||
if ((!widget.options || widget.options.serialize !== false) && !isVirtual) {
|
||||
let value = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
|
||||
inputs[widget.name] = value
|
||||
// Store all link values
|
||||
if (node.inputs) {
|
||||
for (let i = 0; i < node.inputs.length; i++) {
|
||||
const inp = node.inputs[i];
|
||||
const inputLink = node.getInputLink(i)
|
||||
const inputNode = node.getInputNode(i)
|
||||
if (!inputLink || !inputNode) {
|
||||
if ("config" in inp) {
|
||||
const defaultValue = (inp as IComfyInputSlot).config?.defaultValue
|
||||
if (defaultValue !== null && defaultValue !== undefined)
|
||||
inputs[inp.name] = defaultValue
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let serialize = true;
|
||||
if ("config" in inp)
|
||||
serialize = (inp as IComfyInputSlot).serialize
|
||||
|
||||
let isBackendNode = node.isBackendNode;
|
||||
let isInputBackendNode = false;
|
||||
if ("isBackendNode" in inputNode)
|
||||
isInputBackendNode = (inputNode as ComfyGraphNode).isBackendNode;
|
||||
|
||||
// The reasoning behind this check:
|
||||
// We only want to serialize inputs to nodes with backend equivalents.
|
||||
// And in ComfyBox, the nodes in litegraph *never* have widgets, instead they're all inputs.
|
||||
// All values are passed by separate frontend-only nodes,
|
||||
// either UI-bound or something like ConstantInteger.
|
||||
// So we know that any value passed into a backend node *must* come from
|
||||
// a frontend node.
|
||||
// The rest (links between backend nodes) will be serialized after this bit runs.
|
||||
if (serialize && isBackendNode && !isInputBackendNode) {
|
||||
inputs[inp.name] = inputLink.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store all node links
|
||||
// Store all links between nodes
|
||||
for (let i = 0; i < node.inputs.length; i++) {
|
||||
let parent: ComfyGraphNodeExecutable = node.getInputNode(i) as ComfyGraphNodeExecutable;
|
||||
let parent: ComfyGraphNode = node.getInputNode(i) as ComfyGraphNode;
|
||||
if (parent) {
|
||||
const seen = {}
|
||||
let link = node.getInputLink(i);
|
||||
while (parent && parent.isVirtualNode) {
|
||||
while (parent && !parent.isBackendNode) {
|
||||
link = parent.getInputLink(link.origin_slot);
|
||||
if (link) {
|
||||
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNodeExecutable;
|
||||
if (link && !seen[link.id]) {
|
||||
seen[link.id] = true
|
||||
parent = parent.getInputNode(link.origin_slot) as ComfyGraphNode;
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (link) {
|
||||
inputs[node.inputs[i].name] = [String(link.origin_id), link.origin_slot];
|
||||
if (link && parent && parent.isBackendNode) {
|
||||
const input = node.inputs[i]
|
||||
// TODO can null be a legitimate value in some cases?
|
||||
// Nodes like CLIPLoader will never have a value in the frontend, hence "null".
|
||||
if (!(input.name in inputs))
|
||||
inputs[input.name] = [String(link.origin_id), link.origin_slot];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -569,20 +547,20 @@ export default class ComfyApp {
|
||||
await this.api.queuePrompt(num, p);
|
||||
} catch (error) {
|
||||
// this.ui.dialog.show(error.response || error.toString());
|
||||
console.error(error.response || error.toString())
|
||||
const mes = error.response || error.toString()
|
||||
toast.push(`Error queuing prompt:\n${mes}`, {
|
||||
theme: {
|
||||
'--toastBackground': 'var(--color-red-500)',
|
||||
}
|
||||
})
|
||||
console.error("Error queuing prompt", mes, num, p)
|
||||
break;
|
||||
}
|
||||
|
||||
for (const n of p.workflow.nodes) {
|
||||
const node = this.lGraph.getNodeById(n.id);
|
||||
if (node.widgets) {
|
||||
for (const widget of node.widgets) {
|
||||
// Allow widgets to run callbacks after a prompt has been queued
|
||||
// e.g. random seed after every gen
|
||||
if ("afterQueued" in widget) {
|
||||
(widget as ComfyWidget<any, any>).afterQueued();
|
||||
}
|
||||
}
|
||||
if ("afterQueued" in node) {
|
||||
(node as ComfyGraphNode).afterQueued(p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,14 +617,26 @@ export default class ComfyApp {
|
||||
|
||||
const def = defs[node.type];
|
||||
|
||||
for (const widgetNum in node.widgets) {
|
||||
const widget = node.widgets[widgetNum]
|
||||
for (let index = 0; index < node.inputs.length; index++) {
|
||||
const input = node.inputs[index];
|
||||
if ("config" in input) {
|
||||
const comfyInput = input as IComfyInputSlot;
|
||||
|
||||
if (widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
|
||||
widget.options.values = def["input"]["required"][widget.name][0];
|
||||
console.warn("RefreshCombo", comfyInput.defaultWidgetNode, comfyInput)
|
||||
|
||||
if (!widget.options.values.includes(widget.value)) {
|
||||
widget.value = widget.options.values[0];
|
||||
if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) {
|
||||
comfyInput.config.values = def["input"]["required"][comfyInput.name][0];
|
||||
|
||||
console.warn("RefreshCombo", comfyInput.config.values, def["input"]["required"][comfyInput.name])
|
||||
const inputNode = node.getInputNode(index)
|
||||
|
||||
if (inputNode && "doAutoConfig" in inputNode) {
|
||||
const comfyInputNode = inputNode as nodes.ComfyWidgetNode;
|
||||
comfyInputNode.doAutoConfig(comfyInput)
|
||||
if (!comfyInput.config.values.includes(get(comfyInputNode.value))) {
|
||||
comfyInputNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from "svelte";
|
||||
import { get } from "svelte/store"
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { Move } from 'radix-icons-svelte';
|
||||
import widgetState, { type WidgetDrawState, type WidgetUIState } from "$lib/stores/widgetState";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import nodeState from "$lib/stores/nodeState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action';
|
||||
|
||||
import {fade} from 'svelte/transition';
|
||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { flip } from 'svelte/animate';
|
||||
import ComfyApp from "./ComfyApp";
|
||||
import type { LGraphNode } from "@litegraph-ts/core";
|
||||
import type { DragItem } from "./ComfyUIPane";
|
||||
import { getComponentForWidgetState } from "$lib/utils"
|
||||
|
||||
export let dragItems: DragItem[] = [];
|
||||
let dragDisabled = true;
|
||||
let unlockUI = false;
|
||||
const flipDurationMs = 200;
|
||||
|
||||
$: dragDisabled = !$uiState.unlocked;
|
||||
|
||||
const handleConsider = evt => {
|
||||
dragItems = evt.detail.items;
|
||||
// console.log(dragItems);
|
||||
};
|
||||
const handleFinalize = evt => {
|
||||
dragItems = evt.detail.items;
|
||||
// Ensure dragging is stopped on drag finish
|
||||
dragDisabled = true;
|
||||
};
|
||||
|
||||
const startDrag = () => {
|
||||
if (!$uiState.unlocked)
|
||||
return
|
||||
dragDisabled = false;
|
||||
};
|
||||
const stopDrag = () => {
|
||||
if (!$uiState.unlocked)
|
||||
return
|
||||
dragDisabled = true;
|
||||
};
|
||||
|
||||
const unsubscribe = widgetState.subscribe(state => {
|
||||
dragItems = dragItems.filter(item => item.node.id in state);
|
||||
});
|
||||
|
||||
onDestroy(unsubscribe);
|
||||
|
||||
$: if ($queueState) {
|
||||
for (let dragItem of dragItems) {
|
||||
dragItem.isNodeExecuting = $queueState.runningNodeId === dragItem.node.id;
|
||||
}
|
||||
dragItems = dragItems;
|
||||
}
|
||||
|
||||
function updateNodeName(node: LGraphNode, value: string) {
|
||||
nodeState.nodeStateChanged(node);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="v-pane"
|
||||
use:dndzone="{{ items: dragItems, dragDisabled, flipDurationMs }}"
|
||||
on:consider="{handleConsider}"
|
||||
on:finalize="{handleFinalize}"
|
||||
>
|
||||
{#each dragItems as dragItem(dragItem.id)}
|
||||
{@const node = dragItem.node}
|
||||
{@const id = node.id}
|
||||
<div class="animation-wrapper" class:is-executing={dragItem.isNodeExecuting} animate:flip={{duration:flipDurationMs}}>
|
||||
<Block>
|
||||
<label for={String(id)} class={$uiState.unlocked ? "edit-title-label" : ""}>
|
||||
<BlockTitle>
|
||||
{#if $uiState.unlocked}
|
||||
<input class="edit-title" bind:value={dragItem.node.title} type="text" minlength="1" on:input="{(v) => { updateNodeName(node, v) }}"/>
|
||||
{:else}
|
||||
{node.title}
|
||||
{/if}
|
||||
{#if node.title !== node.type}
|
||||
<span class="node-type">({node.type})</span>
|
||||
{/if}
|
||||
</BlockTitle>
|
||||
</label>
|
||||
{#each $widgetState[id] as item}
|
||||
<svelte:component this={getComponentForWidgetState(item)} {item} />
|
||||
{#if dragItem[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if $uiState.unlocked}
|
||||
<div class="handle" on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
||||
{/if}
|
||||
</Block>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.v-pane {
|
||||
border: 1px solid grey;
|
||||
float: left;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.is-executing :global(.block) {
|
||||
border: 5px dashed var(--color-green-600) !important;
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: grab;
|
||||
z-index: 99999;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handle:hover {
|
||||
background-color: #add8e680;
|
||||
}
|
||||
|
||||
.drag-item-shadow {
|
||||
position: absolute;
|
||||
top: 0; left:0; right: 0; bottom: 0;
|
||||
visibility: visible;
|
||||
border: 1px dashed grey;
|
||||
background: lightblue;
|
||||
opacity: 0.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.node-type {
|
||||
font-size: smaller;
|
||||
color: var(--neutral-400);
|
||||
}
|
||||
|
||||
.edit-title-label {
|
||||
position: relative;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.edit-title {
|
||||
z-index: 100000;
|
||||
display: block;
|
||||
position: relative;
|
||||
outline: none !important;
|
||||
box-shadow: var(--input-shadow);
|
||||
border: var(--input-border-width) solid var(--input-border-color);
|
||||
border-radius: var(--input-radius);
|
||||
background: var(--input-background-fill);
|
||||
padding: var(--input-padding);
|
||||
width: 100%;
|
||||
color: var(--body-text-color);
|
||||
font-weight: var(--input-text-weight);
|
||||
font-size: var(--input-text-size);
|
||||
line-height: var(--line-sm);
|
||||
}
|
||||
|
||||
.edit-title:focus {
|
||||
box-shadow: var(--input-shadow-focus);
|
||||
border-color: var(--input-border-color-focus);
|
||||
}
|
||||
|
||||
.edit-title::placeholder {
|
||||
color: var(--input-placeholder-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,104 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte'
|
||||
import { get } from "svelte/store";
|
||||
import { LGraphNode, LGraph } from "@litegraph-ts/core";
|
||||
import type { IWidget } from "@litegraph-ts/core";
|
||||
import ComfyApp from "./ComfyApp";
|
||||
import type { SerializedPanes } from "./ComfyApp"
|
||||
import ComfyPane from "./ComfyPane.svelte";
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import type { DragItem } from "./ComfyUIPane";
|
||||
import WidgetContainer from "./WidgetContainer.svelte";
|
||||
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
|
||||
import Menu from './menu/Menu.svelte';
|
||||
import MenuOption from './menu/MenuOption.svelte';
|
||||
import MenuDivider from './menu/MenuDivider.svelte';
|
||||
import Icon from './menu/Icon.svelte'
|
||||
|
||||
export let app: ComfyApp;
|
||||
let root: IDragItem | null;
|
||||
let dragConfigured: boolean = false;
|
||||
export let dragItems: DragItem[][] = []
|
||||
|
||||
export let totalId = 0;
|
||||
|
||||
function findLeastPopulatedPaneIndex(): number {
|
||||
let minWidgetCount = 2 ** 64;
|
||||
let minIndex = 0;
|
||||
let state = get(widgetState);
|
||||
for (let i = 0; i < dragItems.length; i++) {
|
||||
let widgetCount = 0;
|
||||
for (let j = 0; j < dragItems[i].length; j++) {
|
||||
const nodeID = dragItems[i][j].node.id;
|
||||
widgetCount += state[nodeID].length;
|
||||
}
|
||||
if (widgetCount < minWidgetCount) {
|
||||
minWidgetCount = widgetCount
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
return minIndex
|
||||
}
|
||||
|
||||
function addUIForNewNode(node: LGraphNode, paneIndex?: number) {
|
||||
if (!paneIndex)
|
||||
paneIndex = findLeastPopulatedPaneIndex();
|
||||
dragItems[paneIndex].push({ id: totalId++, node: node });
|
||||
}
|
||||
|
||||
$: if(app && !dragConfigured) {
|
||||
dragConfigured = true;
|
||||
app.eventBus.on("nodeAdded", addUIForNewNode);
|
||||
}
|
||||
|
||||
/*
|
||||
* Serialize UI panel order so it can be restored when workflow is loaded
|
||||
*/
|
||||
export function serialize(): any {
|
||||
let panels = []
|
||||
for (let i = 0; i < dragItems.length; i++) {
|
||||
panels[i] = [];
|
||||
for (let j = 0; j < dragItems[i].length; j++) {
|
||||
panels[i].push({ nodeId: dragItems[i][j].node.id });
|
||||
}
|
||||
}
|
||||
return {
|
||||
panels
|
||||
}
|
||||
// TODO
|
||||
}
|
||||
|
||||
export function restore(panels: SerializedPanes) {
|
||||
let nodeIdToDragItem: Record<number, DragItem> = {};
|
||||
for (let i = 0; i < dragItems.length; i++) {
|
||||
for (const dragItem of dragItems[i]) {
|
||||
nodeIdToDragItem[dragItem.node.id] = dragItem
|
||||
}
|
||||
// TODO
|
||||
}
|
||||
|
||||
function groupWidgets(horizontal: boolean) {
|
||||
const items = layoutState.getCurrentSelection()
|
||||
$layoutState.currentSelection = []
|
||||
layoutState.groupItems(items, { direction: horizontal ? "horizontal" : "vertical" })
|
||||
}
|
||||
|
||||
let canUngroup = false;
|
||||
let isDeleteGroup = false;
|
||||
$: canUngroup = $layoutState.currentSelection.length === 1
|
||||
&& layoutState.getCurrentSelection()[0].type === "container"
|
||||
$: if (canUngroup) {
|
||||
const dragItem = layoutState.getCurrentSelection()[0];
|
||||
const entry = $layoutState.allItems[dragItem.id];
|
||||
isDeleteGroup = entry.children.length === 0
|
||||
}
|
||||
else {
|
||||
isDeleteGroup = false
|
||||
}
|
||||
|
||||
function ungroup() {
|
||||
const item = layoutState.getCurrentSelection()[0]
|
||||
if (item.type !== "container")
|
||||
return;
|
||||
|
||||
$layoutState.currentSelection = []
|
||||
layoutState.ungroup(item as ContainerLayout)
|
||||
}
|
||||
|
||||
let menuPos = { x: 0, y: 0 };
|
||||
let showMenu = false;
|
||||
|
||||
$: $layoutState.isMenuOpen = showMenu;
|
||||
|
||||
$: if ($layoutState.root) {
|
||||
root = $layoutState.root
|
||||
} else {
|
||||
root = null;
|
||||
}
|
||||
|
||||
async function onRightClick(e) {
|
||||
if ($uiState.uiEditMode === "disabled")
|
||||
return;
|
||||
|
||||
e.preventDefault();
|
||||
if (showMenu) {
|
||||
showMenu = false;
|
||||
await new Promise(res => setTimeout(res, 100));
|
||||
}
|
||||
|
||||
for (let i = 0; i < panels.panels.length; i++) {
|
||||
dragItems[i].length = 0;
|
||||
for (const panel of panels.panels[i]) {
|
||||
const dragItem = nodeIdToDragItem[panel.nodeId];
|
||||
if (dragItem) {
|
||||
delete nodeIdToDragItem[panel.nodeId];
|
||||
dragItems[i].push(dragItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
menuPos = { x: e.clientX, y: e.clientY };
|
||||
showMenu = true;
|
||||
}
|
||||
|
||||
// Put everything left over into other columns
|
||||
if (Object.keys(nodeIdToDragItem).length > 0) {
|
||||
console.warn("Extra panels without ordering found", nodeIdToDragItem, panels)
|
||||
for (const nodeId in nodeIdToDragItem) {
|
||||
const dragItem = nodeIdToDragItem[nodeId];
|
||||
const paneIndex = findLeastPopulatedPaneIndex();
|
||||
dragItems[paneIndex].push(dragItem);
|
||||
}
|
||||
}
|
||||
function closeMenu() {
|
||||
showMenu = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="comfy-ui-panes" >
|
||||
<ComfyPane bind:dragItems={dragItems[0]} />
|
||||
<ComfyPane bind:dragItems={dragItems[1]} />
|
||||
<ComfyPane bind:dragItems={dragItems[2]} />
|
||||
<div id="comfy-ui-panes" on:contextmenu={onRightClick}>
|
||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{#if showMenu}
|
||||
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||
<MenuOption
|
||||
isDisabled={$layoutState.currentSelection.length === 0}
|
||||
on:click={() => groupWidgets(false)}
|
||||
text="Group" />
|
||||
<MenuOption
|
||||
isDisabled={$layoutState.currentSelection.length === 0}
|
||||
on:click={() => groupWidgets(true)}
|
||||
text="Group Horizontally" />
|
||||
<MenuOption
|
||||
isDisabled={!canUngroup}
|
||||
on:click={ungroup}
|
||||
text={isDeleteGroup ? "Delete Group" : "Ungroup"} />
|
||||
</Menu>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
#comfy-ui-panes {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { LGraphNode } from "@litegraph-ts/core"
|
||||
|
||||
export type DragItem = {
|
||||
id: number,
|
||||
node: LGraphNode,
|
||||
isNodeExecuting?: boolean
|
||||
}
|
||||
93
src/lib/components/WidgetContainer.svelte
Normal file
93
src/lib/components/WidgetContainer.svelte
Normal file
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
|
||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import BlockContainer from "./BlockContainer.svelte"
|
||||
|
||||
export let dragItem: IDragItem | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
let container: ContainerLayout | null = null;
|
||||
let widget: WidgetLayout | null = null;
|
||||
let showHandles: boolean = false;
|
||||
|
||||
$: if (!dragItem || !$layoutState.allItems[dragItem.id]) {
|
||||
dragItem = null;
|
||||
container = null;
|
||||
widget = null;
|
||||
}
|
||||
else if (dragItem.type === "container") {
|
||||
container = dragItem as ContainerLayout;
|
||||
widget = null;
|
||||
}
|
||||
else if (dragItem.type === "widget") {
|
||||
widget = dragItem as WidgetLayout;
|
||||
container = null;
|
||||
}
|
||||
|
||||
$: showHandles = $uiState.uiEditMode === "widgets" // TODO
|
||||
&& zIndex > 1
|
||||
&& !$layoutState.isMenuOpen
|
||||
|
||||
|
||||
$: if ($queueState && widget && widget.node) {
|
||||
dragItem.isNodeExecuting = $queueState.runningNodeId === widget.node.id;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{#if container}
|
||||
<BlockContainer {container} {classes} {zIndex} {showHandles} />
|
||||
{:else if widget && widget.node}
|
||||
<div class="widget" class:widget-edit-outline={$uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
|
||||
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
|
||||
>
|
||||
<svelte:component this={widget.node.svelteComponentType} {widget} />
|
||||
</div>
|
||||
{#if showHandles}
|
||||
<div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.widget.selected {
|
||||
background: var(--color-yellow-200);
|
||||
}
|
||||
.container.selected {
|
||||
background: var(--color-yellow-400);
|
||||
}
|
||||
|
||||
.is-executing {
|
||||
border: 3px dashed var(--color-green-600) !important;
|
||||
margin: 0.2em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: grab;
|
||||
z-index: 99999;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
background-color: #add8e680;
|
||||
}
|
||||
|
||||
.node-type {
|
||||
font-size: smaller;
|
||||
color: var(--neutral-400);
|
||||
}
|
||||
|
||||
.widget-edit-outline {
|
||||
border: 2px dashed var(--color-blue-400);
|
||||
margin: 0.2em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
</style>
|
||||
47
src/lib/components/menu/ContextMenu.svelte
Normal file
47
src/lib/components/menu/ContextMenu.svelte
Normal file
@@ -0,0 +1,47 @@
|
||||
<script>
|
||||
import Menu from './Menu.svelte';
|
||||
import MenuOption from './MenuOption.svelte';
|
||||
import MenuDivider from './MenuDivider.svelte';
|
||||
import { tick } from 'svelte'
|
||||
|
||||
import Icon from './Icon.svelte'
|
||||
|
||||
let pos = { x: 0, y: 0 };
|
||||
let showMenu = false;
|
||||
|
||||
async function onRightClick(e) {
|
||||
if (showMenu) {
|
||||
showMenu = false;
|
||||
await new Promise(res => setTimeout(res, 100));
|
||||
}
|
||||
|
||||
pos = { x: e.clientX, y: e.clientY };
|
||||
showMenu = true;
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
showMenu = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showMenu}
|
||||
<Menu {...pos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||
<MenuOption
|
||||
on:click={console.log}
|
||||
text="Do nothing" />
|
||||
<MenuOption
|
||||
on:click={console.log}
|
||||
text="Do nothing, but twice" />
|
||||
<MenuDivider />
|
||||
<MenuOption
|
||||
isDisabled={true}
|
||||
on:click={console.log}
|
||||
text="Whoops, disabled!" />
|
||||
<MenuOption on:click={console.log}>
|
||||
<Icon />
|
||||
<span>Look! An icon!</span>
|
||||
</MenuOption>
|
||||
</Menu>
|
||||
{/if}
|
||||
|
||||
<svelte:body on:contextmenu|preventDefault={onRightClick} />
|
||||
3
src/lib/components/menu/Icon.svelte
Normal file
3
src/lib/components/menu/Icon.svelte
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M11.2584 13.039C10.3201 13.647 9.2013 14 8 14C4.68629 14 2 11.3137 2 8C2 6.66837 2.4338 5.43806 3.1678 4.44268L11.2584 13.039ZM12.7332 11.6878L4.60537 3.05194C5.57075 2.38838 6.74002 2 8 2C11.3137 2 14 4.68629 14 8C14 9.39043 13.527 10.6704 12.7332 11.6878ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 548 B |
45
src/lib/components/menu/Menu.svelte
Normal file
45
src/lib/components/menu/Menu.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import { setContext, createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { key } from './menu.ts';
|
||||
|
||||
export let x;
|
||||
export let y;
|
||||
|
||||
// whenever x and y is changed, restrict box to be within bounds
|
||||
$: (() => {
|
||||
if (!menuEl) return;
|
||||
|
||||
const rect = menuEl.getBoundingClientRect();
|
||||
x = Math.min(window.innerWidth - rect.width, x);
|
||||
if (y > window.innerHeight - rect.height) y -= rect.height;
|
||||
})(x, y);
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
setContext(key, {
|
||||
dispatchClick: () => dispatch('click')
|
||||
});
|
||||
|
||||
let menuEl;
|
||||
function onPageClick(e) {
|
||||
if (e.target === menuEl || menuEl.contains(e.target)) return;
|
||||
dispatch('clickoutside');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:body on:click={onPageClick} />
|
||||
<div class="menu" bind:this={menuEl} style="top: {y}px; left: {x}px;">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.menu {
|
||||
z-index: var(--layer-top);
|
||||
position: absolute;
|
||||
display: grid;
|
||||
border: 1px solid #0003;
|
||||
box-shadow: 2px 2px 5px 0px #0002;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
9
src/lib/components/menu/MenuDivider.svelte
Normal file
9
src/lib/components/menu/MenuDivider.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<hr />
|
||||
|
||||
<style>
|
||||
hr {
|
||||
border-top: 1px solid #0003;
|
||||
width: 100%;
|
||||
margin: 2px 0;
|
||||
}
|
||||
</style>
|
||||
50
src/lib/components/menu/MenuOption.svelte
Normal file
50
src/lib/components/menu/MenuOption.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { key } from './menu.ts';
|
||||
|
||||
export let isDisabled = false;
|
||||
export let text = '';
|
||||
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const { dispatchClick } = getContext(key);
|
||||
|
||||
const handleClick = e => {
|
||||
if (isDisabled) return;
|
||||
|
||||
dispatch('click');
|
||||
dispatchClick();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:disabled={isDisabled}
|
||||
on:click={handleClick}
|
||||
>
|
||||
{#if text}
|
||||
{text}
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 4px 15px;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-gap: 5px;
|
||||
}
|
||||
div:hover {
|
||||
background: #0002;
|
||||
}
|
||||
div.disabled {
|
||||
color: #0006;
|
||||
}
|
||||
div.disabled:hover {
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
3
src/lib/components/menu/menu.ts
Normal file
3
src/lib/components/menu/menu.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const key = {};
|
||||
|
||||
export { key };
|
||||
File diff suppressed because it is too large
Load Diff
132
src/lib/nodes/ComfyActionNodes.ts
Normal file
132
src/lib/nodes/ComfyActionNodes.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
|
||||
export interface ComfyAfterQueuedAction extends Record<any, any> {
|
||||
prompt: SerializedPrompt
|
||||
}
|
||||
|
||||
export class ComfyAfterQueuedAction extends ComfyGraphNode {
|
||||
override properties: ComfyCopyActionProperties = {
|
||||
prompt: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
{ name: "afterQueued", type: BuiltInSlotType.EVENT },
|
||||
{ name: "prompt", type: "*" }
|
||||
],
|
||||
}
|
||||
|
||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||
if (property === "value") {
|
||||
this.setOutputData(0, this.properties.prompt)
|
||||
}
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
this.setOutputData(0, this.properties.prompt)
|
||||
}
|
||||
|
||||
override afterQueued(p: SerializedPrompt) {
|
||||
this.setProperty("value", p)
|
||||
this.triggerSlot(0, "bang")
|
||||
}
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
super.onSerialize(o)
|
||||
o.properties = { prompt: null }
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyAfterQueuedAction,
|
||||
title: "Comfy.AfterQueuedAction",
|
||||
desc: "Triggers a 'bang' event when a prompt is queued.",
|
||||
type: "actions/after_queued"
|
||||
})
|
||||
|
||||
export interface ComfyCopyActionProperties extends Record<any, any> {
|
||||
value: any
|
||||
}
|
||||
|
||||
export class ComfyCopyAction extends ComfyGraphNode {
|
||||
override properties: ComfyCopyActionProperties = {
|
||||
value: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "in", type: "*" },
|
||||
{ name: "copy", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "out", type: "*" }
|
||||
],
|
||||
}
|
||||
|
||||
displayWidget: ITextWidget;
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title);
|
||||
this.displayWidget = this.addWidget<ITextWidget>(
|
||||
"text",
|
||||
"Value",
|
||||
"",
|
||||
"value"
|
||||
);
|
||||
this.displayWidget.disabled = true;
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
this.setProperty("value", this.getInputData(0))
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
this.setProperty("value", this.getInputData(0))
|
||||
this.setOutputData(0, this.properties.value)
|
||||
console.log("setData", this.properties.value)
|
||||
};
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyCopyAction,
|
||||
title: "Comfy.CopyAction",
|
||||
desc: "Copies its input to its output when an event is received",
|
||||
type: "actions/copy"
|
||||
})
|
||||
|
||||
export interface ComfySwapActionProperties extends Record<any, any> {
|
||||
}
|
||||
|
||||
export class ComfySwapAction extends ComfyGraphNode {
|
||||
override properties: ComfySwapActionProperties = {
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "A", type: "*" },
|
||||
{ name: "B", type: "*" },
|
||||
{ name: "swap", type: BuiltInSlotType.ACTION }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "B", type: "*" },
|
||||
{ name: "A", type: "*" }
|
||||
],
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
const a = this.getInputData(0)
|
||||
const b = this.getInputData(1)
|
||||
this.setOutputData(0, a)
|
||||
this.setOutputData(1, b)
|
||||
};
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfySwapAction,
|
||||
title: "Comfy.SwapAction",
|
||||
desc: "Swaps two inputs when triggered",
|
||||
type: "actions/swap"
|
||||
})
|
||||
92
src/lib/nodes/ComfyBackendNode.ts
Normal file
92
src/lib/nodes/ComfyBackendNode.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import ComfyWidgets from "$lib/widgets"
|
||||
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
|
||||
|
||||
/*
|
||||
* Base class for any node with configuration sent by the backend.
|
||||
*/
|
||||
export class ComfyBackendNode extends ComfyGraphNode {
|
||||
comfyClass: string;
|
||||
|
||||
constructor(title: string, comfyClass: string, nodeData: any) {
|
||||
super(title)
|
||||
this.type = comfyClass; // XXX: workaround dependency in LGraphNode.addInput()
|
||||
this.comfyClass = comfyClass;
|
||||
this.isBackendNode = true;
|
||||
|
||||
const color = LGraphCanvas.node_colors["yellow"];
|
||||
this.color = color.color
|
||||
this.bgColor = color.bgColor
|
||||
|
||||
this.setup(nodeData)
|
||||
|
||||
// ComfyUI has no obvious way to identify if a node will return outputs back to the frontend based on its properties.
|
||||
// It just returns a hash like { "ui": { "images": results } } internally.
|
||||
// So this will need to be hardcoded for now.
|
||||
if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
|
||||
this.addOutput("output", "OUTPUT");
|
||||
}
|
||||
}
|
||||
|
||||
private setup(nodeData: any) {
|
||||
var inputs = nodeData["input"]["required"];
|
||||
if (nodeData["input"]["optional"] != undefined) {
|
||||
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
|
||||
}
|
||||
|
||||
const config = { minWidth: 1, minHeight: 1 };
|
||||
for (const inputName in inputs) {
|
||||
const inputData = inputs[inputName];
|
||||
const type = inputData[0];
|
||||
|
||||
if (inputData[1]?.forceInput) {
|
||||
this.addInput(inputName, type);
|
||||
} else {
|
||||
if (Array.isArray(type)) {
|
||||
// Enums
|
||||
Object.assign(config, ComfyWidgets.COMBO(this, inputName, inputData) || {});
|
||||
} else if (`${type}:${inputName}` in ComfyWidgets) {
|
||||
// Support custom ComfyWidgets by Type:Name
|
||||
Object.assign(config, ComfyWidgets[`${type}:${inputName}`](this, inputName, inputData) || {});
|
||||
} else if (type in ComfyWidgets) {
|
||||
// Standard type ComfyWidgets
|
||||
Object.assign(config, ComfyWidgets[type](this, inputName, inputData) || {});
|
||||
} else {
|
||||
// Node connection inputs (backend)
|
||||
this.addInput(inputName, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const o in nodeData["output"]) {
|
||||
const output = nodeData["output"][o];
|
||||
const outputName = nodeData["output_name"][o] || output;
|
||||
this.addOutput(outputName, output);
|
||||
}
|
||||
|
||||
const s = this.computeSize();
|
||||
s[0] = Math.max(config.minWidth, s[0] * 1.5);
|
||||
s[1] = Math.max(config.minHeight, s[1]);
|
||||
this.size = s;
|
||||
this.serialize_widgets = false;
|
||||
|
||||
// app.#invokeExtensionsAsync("nodeCreated", this);
|
||||
}
|
||||
|
||||
override onExecuted(outputData: any) {
|
||||
console.warn("onExecuted outputs", outputData)
|
||||
for (let index = 0; index < this.outputs.length; index++) {
|
||||
const output = this.outputs[index]
|
||||
if (output.type === "OUTPUT") {
|
||||
this.setOutputData(index, outputData)
|
||||
for (const node of this.getOutputNodes(index)) {
|
||||
if ("receiveOutput" in node) {
|
||||
const widgetNode = node as ComfyWidgetNode;
|
||||
widgetNode.receiveOutput();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,56 @@
|
||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
||||
import { LGraphNode } from "@litegraph-ts/core";
|
||||
import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
|
||||
export type DefaultWidgetSpec = {
|
||||
defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
|
||||
config?: ComfyInputConfig
|
||||
}
|
||||
|
||||
export type DefaultWidgetLayout = {
|
||||
inputs?: Record<number, DefaultWidgetSpec>,
|
||||
}
|
||||
|
||||
export default class ComfyGraphNode extends LGraphNode {
|
||||
isVirtualNode: boolean = false;
|
||||
isBackendNode?: boolean;
|
||||
|
||||
afterQueued?(): void;
|
||||
afterQueued?(prompt: SerializedPrompt): void;
|
||||
onExecuted?(output: any): void;
|
||||
|
||||
defaultWidgets?: DefaultWidgetLayout
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
for (let index = 0; index < this.inputs.length; index++) {
|
||||
const input = this.inputs[index]
|
||||
const serInput = o.inputs[index]
|
||||
if ("defaultWidgetNode" in input) {
|
||||
const comfyInput = input as IComfyInputSlot
|
||||
const widgetNode = comfyInput.defaultWidgetNode
|
||||
const ty = Object.values(LiteGraph.registered_node_types)
|
||||
.find(v => v.class === widgetNode)
|
||||
if (ty)
|
||||
(serInput as any).widgetNodeType = ty.type;
|
||||
(serInput as any).defaultWidgetNode = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override onConfigure(o: SerializedLGraphNode) {
|
||||
for (let index = 0; index < this.inputs.length; index++) {
|
||||
const input = this.inputs[index]
|
||||
const serInput = o.inputs[index]
|
||||
if ("widgetNodeType" in serInput) {
|
||||
const comfyInput = input as IComfyInputSlot
|
||||
const ty: string = serInput.widgetNodeType as any
|
||||
const widgetNode = Object.values(LiteGraph.registered_node_types)
|
||||
.find(v => v.type === ty)
|
||||
if (widgetNode)
|
||||
comfyInput.defaultWidgetNode = widgetNode.class as any
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import ComfyGalleryWidget, { type ComfyGalleryEntry } from "$lib/widgets/ComfyGalleryWidget";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
|
||||
export type ComfyImageResult = {
|
||||
filename: string,
|
||||
subfolder: string,
|
||||
type: "output" | "temp"
|
||||
}
|
||||
export type ComfyImageExecOutput = {
|
||||
images: ComfyImageResult[]
|
||||
}
|
||||
|
||||
/*
|
||||
* Node with a single extra image output widget
|
||||
*/
|
||||
class ComfyImageNode extends ComfyGraphNode {
|
||||
private _imageResults: Array<ComfyImageResult> = [];
|
||||
private _galleryWidget: ComfyGalleryWidget;
|
||||
|
||||
constructor(title?: any) {
|
||||
super(title)
|
||||
this._galleryWidget = new ComfyGalleryWidget("Images", [], this);
|
||||
this.addCustomWidget(this._galleryWidget);
|
||||
}
|
||||
|
||||
override onExecuted(output: ComfyImageExecOutput) {
|
||||
this._imageResults = Array.from(output.images); // TODO append?
|
||||
const galleryItems = this._imageResults.map(r => {
|
||||
// TODO
|
||||
const url = "http://localhost:8188/view?"
|
||||
const params = new URLSearchParams(r)
|
||||
let entry: ComfyGalleryEntry = [url + params, null]
|
||||
return entry
|
||||
});
|
||||
this._galleryWidget.addImages(galleryItems);
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfySaveImageNode extends ComfyImageNode {
|
||||
}
|
||||
|
||||
export class ComfyPreviewImageNode extends ComfyImageNode {
|
||||
}
|
||||
@@ -18,9 +18,6 @@ export default class ComfyReroute extends ComfyGraphNode {
|
||||
}
|
||||
}
|
||||
|
||||
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
||||
override isVirtualNode: boolean = true;
|
||||
|
||||
override titleMode: TitleMode = TitleMode.NO_TITLE;
|
||||
override collapsable: boolean = false;
|
||||
|
||||
|
||||
120
src/lib/nodes/ComfySelector.ts
Normal file
120
src/lib/nodes/ComfySelector.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
|
||||
export interface ComfySelectorProperties extends Record<any, any> {
|
||||
value: any
|
||||
}
|
||||
|
||||
export default class ComfySelector extends ComfyGraphNode {
|
||||
override properties: ComfySelectorProperties = {
|
||||
value: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "select", type: "number" },
|
||||
{ name: "A", type: "*" },
|
||||
{ name: "B", type: "*" },
|
||||
{ name: "C", type: "*" },
|
||||
{ name: "D", type: "*" },
|
||||
],
|
||||
outputs: [
|
||||
{ name: "out", type: "*" }
|
||||
],
|
||||
}
|
||||
|
||||
private selected: number = 0;
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
override onDrawBackground(ctx: CanvasRenderingContext2D) {
|
||||
if (this.flags.collapsed) {
|
||||
return;
|
||||
}
|
||||
ctx.fillStyle = "#AFB";
|
||||
var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(50, y);
|
||||
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
|
||||
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
override onExecute() {
|
||||
var sel = this.getInputData(0);
|
||||
if (sel == null || sel.constructor !== Number)
|
||||
sel = 0;
|
||||
this.selected = sel = Math.round(sel) % (this.inputs.length - 1);
|
||||
var v = this.getInputData(sel + 1);
|
||||
if (v !== undefined) {
|
||||
this.setOutputData(0, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfySelector,
|
||||
title: "Comfy.Selector",
|
||||
desc: "Selects an output from two or more inputs",
|
||||
type: "utils/selector"
|
||||
})
|
||||
|
||||
export interface ComfySelectorTwoProperties extends Record<any, any> {
|
||||
value: any
|
||||
}
|
||||
|
||||
export class ComfySelectorTwo extends ComfyGraphNode {
|
||||
override properties: ComfySelectorTwoProperties = {
|
||||
value: null
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "select", type: "boolean" },
|
||||
{ name: "true", type: "*" },
|
||||
{ name: "false", type: "*" },
|
||||
],
|
||||
outputs: [
|
||||
{ name: "out", type: "*" }
|
||||
],
|
||||
}
|
||||
|
||||
private selected: number = 0;
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
override onDrawBackground(ctx: CanvasRenderingContext2D) {
|
||||
if (this.flags.collapsed) {
|
||||
return;
|
||||
}
|
||||
ctx.fillStyle = "#AFB";
|
||||
var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(50, y);
|
||||
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
|
||||
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
override onExecute() {
|
||||
var sel = this.getInputData(0);
|
||||
if (sel == null || sel.constructor !== Boolean)
|
||||
sel = 0;
|
||||
this.selected = sel ? 0 : 1;
|
||||
var v = this.getInputData(this.selected + 1);
|
||||
if (v !== undefined) {
|
||||
this.setOutputData(0, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfySelectorTwo,
|
||||
title: "Comfy.Selector2",
|
||||
desc: "Selects an output from two inputs with a boolean",
|
||||
type: "utils/selector2"
|
||||
})
|
||||
107
src/lib/nodes/ComfyValueControl.ts
Normal file
107
src/lib/nodes/ComfyValueControl.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode, { type DefaultWidgetLayout } from "./ComfyGraphNode";
|
||||
import { clamp } from "$lib/utils";
|
||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import { ComfyComboNode } from "./ComfyWidgetNodes";
|
||||
|
||||
export interface ComfyValueControlProperties extends Record<any, any> {
|
||||
value: any,
|
||||
action: "fixed" | "increment" | "decrement" | "randomize",
|
||||
min: number,
|
||||
max: number,
|
||||
step: number
|
||||
}
|
||||
|
||||
const INT_MAX = 1125899906842624;
|
||||
|
||||
export default class ComfyValueControl extends ComfyGraphNode {
|
||||
override properties: ComfyValueControlProperties = {
|
||||
value: null,
|
||||
action: "fixed",
|
||||
min: -INT_MAX,
|
||||
max: INT_MAX,
|
||||
step: 1
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "number" },
|
||||
{ name: "trigger", type: BuiltInSlotType.ACTION },
|
||||
{ name: "action", type: "string" },
|
||||
{ name: "min", type: "number" },
|
||||
{ name: "max", type: "number" },
|
||||
{ name: "step", type: "number" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "*" }
|
||||
],
|
||||
}
|
||||
|
||||
override defaultWidgets: DefaultWidgetLayout = {
|
||||
inputs: {
|
||||
2: {
|
||||
defaultWidgetNode: ComfyComboNode,
|
||||
config: {
|
||||
defaultValue: "randomize",
|
||||
values: ["fixed", "increment", "decrement", "randomize"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
override onExecute() {
|
||||
this.setProperty("action", this.getInputData(2) || "fixed")
|
||||
this.setProperty("min", this.getInputData(3))
|
||||
this.setProperty("max", this.getInputData(4))
|
||||
this.setProperty("step", this.getInputData(5) || 1)
|
||||
}
|
||||
|
||||
override onAction(action: any, param: any) {
|
||||
var v = this.getInputData(0)
|
||||
if (typeof v !== "number")
|
||||
return
|
||||
|
||||
let min = this.properties.min
|
||||
let max = this.properties.max
|
||||
if (min == null) min = -INT_MAX
|
||||
if (max == null) max = INT_MAX
|
||||
|
||||
// limit to something that javascript can handle
|
||||
min = Math.max(-INT_MAX, this.properties.min);
|
||||
max = Math.min(INT_MAX, this.properties.max);
|
||||
let range = (max - min) / (this.properties.step);
|
||||
|
||||
//adjust values based on valueControl Behaviour
|
||||
switch (this.properties.action) {
|
||||
case "fixed":
|
||||
break;
|
||||
case "increment":
|
||||
v += this.properties.step;
|
||||
break;
|
||||
case "decrement":
|
||||
v -= this.properties.step;
|
||||
break;
|
||||
case "randomize":
|
||||
v = Math.floor(Math.random() * range) * (this.properties.step) + min;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
v = clamp(v, min, max)
|
||||
this.setProperty("value", v)
|
||||
this.setOutputData(0, v)
|
||||
|
||||
console.debug("ValueControl", v, this.properties)
|
||||
};
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyValueControl,
|
||||
title: "Comfy.ValueControl",
|
||||
desc: "Adjusts an incoming value based on behavior",
|
||||
type: "utils/value_control"
|
||||
})
|
||||
518
src/lib/nodes/ComfyWidgetNodes.ts
Normal file
518
src/lib/nodes/ComfyWidgetNodes.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
|
||||
import { clamp } from "$lib/utils"
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
|
||||
export interface ComfyWidgetProperties extends Record<string, any> {
|
||||
defaultValue: any
|
||||
}
|
||||
|
||||
/*
|
||||
* A node that is tied to a UI widget in the frontend. When the frontend's
|
||||
* widget is changed, the value of the first output in the node is updated
|
||||
* in the litegraph instance.
|
||||
*/
|
||||
export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
abstract properties: ComfyWidgetProperties;
|
||||
|
||||
value: Writable<T>
|
||||
propsChanged: Writable<number> = writable(0) // dummy to indicate if props changed
|
||||
unsubscribe: Unsubscriber;
|
||||
|
||||
/** Svelte class for the frontend logic */
|
||||
abstract svelteComponentType: typeof SvelteComponentDev
|
||||
|
||||
/** If false, user manually set min/max/step, and should not be autoinherited from connected input */
|
||||
autoConfig: boolean = true;
|
||||
|
||||
copyFromInputLink: boolean = true;
|
||||
|
||||
/** Names of properties to add as inputs */
|
||||
// shownInputProperties: string[] = []
|
||||
|
||||
/** Names of properties to add as outputs */
|
||||
private shownOutputProperties: Record<string, { type: string, index: number }> = {}
|
||||
outputProperties: { name: string, type: string }[] = []
|
||||
|
||||
override isBackendNode = false;
|
||||
override serialize_widgets = true;
|
||||
|
||||
outputIndex: number = 0;
|
||||
inputIndex: number = 0;
|
||||
changedIndex: number = 1;
|
||||
|
||||
displayWidget: ITextWidget;
|
||||
|
||||
override size: Vector2 = [60, 40];
|
||||
|
||||
constructor(name: string, value: T) {
|
||||
const color = LGraphCanvas.node_colors["blue"]
|
||||
super(name)
|
||||
this.value = writable(value)
|
||||
this.color ||= color.color
|
||||
this.bgColor ||= color.bgColor
|
||||
this.displayWidget = this.addWidget<ITextWidget>(
|
||||
"text",
|
||||
"Value",
|
||||
""
|
||||
);
|
||||
this.displayWidget.disabled = true; // prevent editing
|
||||
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
|
||||
}
|
||||
|
||||
addPropertyAsOutput(propertyName: string, type: string) {
|
||||
if (this.shownOutputProperties[propertyName])
|
||||
return;
|
||||
|
||||
if (!(propertyName in this.properties)) {
|
||||
throw `No property named ${propertyName} found!`
|
||||
}
|
||||
|
||||
this.shownOutputProperties[propertyName] = { type, index: this.outputs.length }
|
||||
this.addOutput(propertyName, type)
|
||||
}
|
||||
|
||||
formatValue(value: any): string {
|
||||
return Watch.toString(value)
|
||||
}
|
||||
|
||||
private onValueUpdated(value: any) {
|
||||
console.debug("[Widget] valueUpdated", this, value)
|
||||
this.displayWidget.value = this.formatValue(value)
|
||||
|
||||
if (this.outputs.length >= this.outputIndex) {
|
||||
this.setOutputData(this.outputIndex, get(this.value))
|
||||
}
|
||||
if (this.outputs.length >= this.changedIndex) {
|
||||
const changedOutput = this.outputs[this.changedIndex]
|
||||
if (changedOutput.type === BuiltInSlotType.EVENT)
|
||||
this.triggerSlot(this.changedIndex, "changed")
|
||||
}
|
||||
}
|
||||
|
||||
setValue(value: any) {
|
||||
this.value.set(value)
|
||||
}
|
||||
|
||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||
const data = this.shownOutputProperties[property]
|
||||
if (data)
|
||||
this.setOutputData(data.index, value)
|
||||
}
|
||||
|
||||
/*
|
||||
* Logic to run if this widget can be treated as output (slider, combo, text)
|
||||
*/
|
||||
override onExecute() {
|
||||
if (this.copyFromInputLink) {
|
||||
if (this.inputs.length >= this.inputIndex) {
|
||||
const data = this.getInputData(this.inputIndex)
|
||||
if (data) { // TODO can "null" be a legitimate value here?
|
||||
console.log(data)
|
||||
this.setValue(data)
|
||||
const input = this.getInputLink(this.inputIndex)
|
||||
input.data = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.outputs.length >= this.outputIndex) {
|
||||
this.setOutputData(this.outputIndex, get(this.value))
|
||||
}
|
||||
for (const propName in this.shownOutputProperties) {
|
||||
const data = this.shownOutputProperties[propName]
|
||||
this.setOutputData(data.index, this.properties[propName])
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when a backend node sends a ComfyUI output over a link */
|
||||
receiveOutput() {
|
||||
}
|
||||
|
||||
onConnectOutput(
|
||||
outputIndex: number,
|
||||
inputType: INodeInputSlot["type"],
|
||||
input: INodeInputSlot,
|
||||
inputNode: LGraphNode,
|
||||
inputIndex: number
|
||||
): boolean {
|
||||
if (this.autoConfig && "config" in input) {
|
||||
this.doAutoConfig(input as IComfyInputSlot)
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
doAutoConfig(input: IComfyInputSlot) {
|
||||
// Copy properties from default config in input slot
|
||||
const comfyInput = input as IComfyInputSlot;
|
||||
for (const key in comfyInput.config)
|
||||
this.setProperty(key, comfyInput.config[key])
|
||||
|
||||
if ("defaultValue" in this.properties)
|
||||
this.setValue(this.properties.defaultValue)
|
||||
|
||||
const widget = layoutState.findLayoutForNode(this.id)
|
||||
if (widget && input.name !== "") {
|
||||
widget.attrs.title = input.name;
|
||||
}
|
||||
|
||||
console.debug("Property copy", input, this.properties)
|
||||
|
||||
this.setValue(get(this.value))
|
||||
this.propsChanged.set(get(this.propsChanged) + 1)
|
||||
}
|
||||
|
||||
onConnectionsChange(
|
||||
type: LConnectionKind,
|
||||
slotIndex: number,
|
||||
isConnected: boolean,
|
||||
link: LLink,
|
||||
ioSlot: (INodeOutputSlot | INodeInputSlot)
|
||||
): void {
|
||||
this.clampConfig();
|
||||
}
|
||||
|
||||
clampConfig() {
|
||||
let changed = false;
|
||||
for (const link of this.getOutputLinks(0)) {
|
||||
if (link) { // can be undefined if the link is removed
|
||||
const node = this.graph._nodes_by_id[link.target_id]
|
||||
if (node) {
|
||||
const input = node.inputs[link.target_slot]
|
||||
if (input && "config" in input) {
|
||||
this.clampOneConfig(input as IComfyInputSlot)
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force reactivity change so the frontend can be updated with the new props
|
||||
this.propsChanged.set(get(this.propsChanged) + 1)
|
||||
}
|
||||
|
||||
clampOneConfig(input: IComfyInputSlot) { }
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
super.onSerialize(o);
|
||||
(o as any).comfyValue = get(this.value);
|
||||
(o as any).shownOutputProperties = this.shownOutputProperties
|
||||
}
|
||||
|
||||
override onConfigure(o: SerializedLGraphNode) {
|
||||
this.value.set((o as any).comfyValue);
|
||||
this.shownOutputProperties = (o as any).shownOutputProperties;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComfySliderProperties extends ComfyWidgetProperties {
|
||||
min: number,
|
||||
max: number,
|
||||
step: number,
|
||||
precision: number
|
||||
}
|
||||
|
||||
export class ComfySliderNode extends ComfyWidgetNode<number> {
|
||||
override properties: ComfySliderProperties = {
|
||||
defaultValue: 0,
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
precision: 1
|
||||
}
|
||||
|
||||
override svelteComponentType = RangeWidget
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "number" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "number" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||
]
|
||||
}
|
||||
|
||||
override outputProperties = [
|
||||
{ name: "min", type: "number" },
|
||||
{ name: "max", type: "number" },
|
||||
{ name: "step", type: "number" },
|
||||
{ name: "precision", type: "number" },
|
||||
]
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, 0)
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
if (typeof value !== "number")
|
||||
return;
|
||||
super.setValue(clamp(value, this.properties.min, this.properties.max))
|
||||
}
|
||||
|
||||
override clampOneConfig(input: IComfyInputSlot) {
|
||||
// this.setProperty("min", clamp(this.properties.min, input.config.min, input.config.max))
|
||||
// this.setProperty("max", clamp(this.properties.max, input.config.max, input.config.min))
|
||||
// this.setProperty("step", Math.min(this.properties.step, input.config.step))
|
||||
this.setValue(this.properties.defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfySliderNode,
|
||||
title: "UI.Slider",
|
||||
desc: "Slider outputting a number value",
|
||||
type: "ui/slider"
|
||||
})
|
||||
|
||||
export interface ComfyComboProperties extends ComfyWidgetProperties {
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||
override properties: ComfyComboProperties = {
|
||||
defaultValue: "A",
|
||||
values: ["A", "B", "C", "D"]
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "string" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "string" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||
]
|
||||
}
|
||||
|
||||
override svelteComponentType = ComboWidget
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, "A")
|
||||
}
|
||||
|
||||
onConnectOutput(
|
||||
outputIndex: number,
|
||||
inputType: INodeInputSlot["type"],
|
||||
input: INodeInputSlot,
|
||||
inputNode: LGraphNode,
|
||||
inputIndex: number
|
||||
): boolean {
|
||||
if (!super.onConnectOutput(outputIndex, inputType, input, inputNode, inputIndex))
|
||||
return false;
|
||||
|
||||
const thisProps = this.properties;
|
||||
if (!("config" in input))
|
||||
return true;
|
||||
|
||||
const comfyInput = input as IComfyInputSlot;
|
||||
const otherProps = comfyInput.config;
|
||||
|
||||
// Ensure combo options match
|
||||
if (!(otherProps.values instanceof Array))
|
||||
return false;
|
||||
if (thisProps.values.find((v, i) => otherProps.values.indexOf(v) === -1))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
||||
return;
|
||||
super.setValue(value)
|
||||
}
|
||||
|
||||
override clampOneConfig(input: IComfyInputSlot) {
|
||||
if (input.config.values.indexOf(this.properties.value) === -1) {
|
||||
if (input.config.values.length === 0)
|
||||
this.setValue("")
|
||||
else
|
||||
this.setValue(input.config.defaultValue || input.config.values[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyComboNode,
|
||||
title: "UI.Combo",
|
||||
desc: "Combo box outputting a string value",
|
||||
type: "ui/combo"
|
||||
})
|
||||
|
||||
export interface ComfyTextProperties extends ComfyWidgetProperties {
|
||||
multiline: boolean;
|
||||
}
|
||||
|
||||
export class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||
override properties: ComfyTextProperties = {
|
||||
defaultValue: "",
|
||||
multiline: false
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "value", type: "string" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "string" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||
]
|
||||
}
|
||||
|
||||
override svelteComponentType = TextWidget
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, "")
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
super.setValue(`${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyTextNode,
|
||||
title: "UI.Text",
|
||||
desc: "Textbox outputting a string value",
|
||||
type: "ui/text"
|
||||
})
|
||||
|
||||
/** Raw output as received from ComfyUI's backend */
|
||||
export type GalleryOutput = {
|
||||
images: GalleryOutputEntry[]
|
||||
}
|
||||
|
||||
/** Raw output entry as received from ComfyUI's backend */
|
||||
export type GalleryOutputEntry = {
|
||||
filename: string,
|
||||
subfolder: string,
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||
}
|
||||
|
||||
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
override properties: ComfyGalleryProperties = {
|
||||
defaultValue: []
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "OUTPUT" }
|
||||
]
|
||||
}
|
||||
|
||||
override svelteComponentType = GalleryWidget
|
||||
override copyFromInputLink = false;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, [])
|
||||
}
|
||||
|
||||
override afterQueued() {
|
||||
let queue = get(queueState)
|
||||
if (!(typeof queue.queueRemaining === "number" && queue.queueRemaining > 1)) {
|
||||
this.setValue([])
|
||||
}
|
||||
}
|
||||
|
||||
override formatValue(value: GradioFileData[] | null): string {
|
||||
return `Images: ${value?.length || 0}`
|
||||
}
|
||||
|
||||
private convertItems(output: GalleryOutput): GradioFileData[] {
|
||||
return output.images.map(r => {
|
||||
// TODO configure backend URL
|
||||
const url = "http://localhost:8188/view?"
|
||||
const params = new URLSearchParams(r)
|
||||
return {
|
||||
name: null,
|
||||
data: url + params
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override setValue(value: any) {
|
||||
if (Array.isArray(value)) {
|
||||
super.setValue(value)
|
||||
}
|
||||
else {
|
||||
super.setValue([])
|
||||
}
|
||||
}
|
||||
|
||||
receiveOutput() {
|
||||
const link = this.getInputLink(0)
|
||||
if (link.data && "images" in link.data) {
|
||||
const data = link.data as GalleryOutput
|
||||
console.debug("[ComfyGalleryNode] Received output!", data)
|
||||
|
||||
const galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||
|
||||
const currentValue = get(this.value)
|
||||
this.setValue(currentValue.concat(galleryItems))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyGalleryNode,
|
||||
title: "UI.Gallery",
|
||||
desc: "Gallery that shows most recent outputs",
|
||||
type: "ui/gallery"
|
||||
})
|
||||
|
||||
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
||||
message: string
|
||||
}
|
||||
|
||||
export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||
override properties: ComfyButtonProperties = {
|
||||
defaultValue: false,
|
||||
message: "bang"
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
{ name: "clicked", type: BuiltInSlotType.EVENT },
|
||||
{ name: "isClicked", type: "boolean" },
|
||||
]
|
||||
}
|
||||
|
||||
override outputIndex = 1;
|
||||
override svelteComponentType = ButtonWidget;
|
||||
|
||||
override setValue(value: any) {
|
||||
super.setValue(Boolean(value))
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.setValue(true)
|
||||
this.triggerSlot(0, this.properties.message);
|
||||
this.setValue(false)
|
||||
}
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, false)
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
class: ComfyButtonNode,
|
||||
title: "UI.Button",
|
||||
desc: "Button that triggers an event when clicked",
|
||||
type: "ui/button"
|
||||
})
|
||||
@@ -1,2 +1,5 @@
|
||||
export { default as ComfyReroute } from "./ComfyReroute"
|
||||
export { ComfySaveImageNode, ComfyPreviewImageNode } from "./ComfyImageNodes"
|
||||
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
|
||||
export { ComfyCopyAction, ComfySwapAction } from "./ComfyActionNodes"
|
||||
export { default as ComfyValueControl } from "./ComfyValueControl"
|
||||
export { default as ComfySelector } from "./ComfySelector"
|
||||
|
||||
513
src/lib/stores/layoutState.ts
Normal file
513
src/lib/stores/layoutState.ts
Normal file
@@ -0,0 +1,513 @@
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type ComfyApp from "$lib/components/ComfyApp"
|
||||
import type { LGraphNode, IWidget, LGraph } from "@litegraph-ts/core"
|
||||
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
import type { ComfyWidgetNode } from '$lib/nodes';
|
||||
|
||||
type DragItemEntry = {
|
||||
dragItem: IDragItem,
|
||||
children: IDragItem[] | null,
|
||||
parent: IDragItem | null
|
||||
}
|
||||
|
||||
export type LayoutState = {
|
||||
root: IDragItem | null,
|
||||
allItems: Record<DragItemID, DragItemEntry>,
|
||||
allItemsByNode: Record<number, DragItemEntry>,
|
||||
currentId: number,
|
||||
currentSelection: DragItemID[],
|
||||
isConfiguring: boolean,
|
||||
isMenuOpen: boolean
|
||||
}
|
||||
|
||||
export type AttributesSpec = {
|
||||
name: string,
|
||||
type: string,
|
||||
editable: boolean
|
||||
}
|
||||
|
||||
export type AttributesCategorySpec = {
|
||||
categoryName: string,
|
||||
specs: AttributesSpec[]
|
||||
}
|
||||
|
||||
export type AttributesSpecList = AttributesCategorySpec[]
|
||||
|
||||
const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
{
|
||||
categoryName: "appearance",
|
||||
specs: [
|
||||
{
|
||||
name: "title",
|
||||
type: "string",
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "showTitle",
|
||||
type: "boolean",
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "direction",
|
||||
type: "string",
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: "classes",
|
||||
type: "string",
|
||||
editable: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
export { ALL_ATTRIBUTES };
|
||||
|
||||
export type Attributes = {
|
||||
direction: "horizontal" | "vertical",
|
||||
title: string,
|
||||
showTitle: boolean,
|
||||
classes: string
|
||||
}
|
||||
|
||||
export interface IDragItem {
|
||||
type: string,
|
||||
id: DragItemID,
|
||||
isNodeExecuting?: boolean,
|
||||
attrs: Attributes
|
||||
}
|
||||
|
||||
export interface ContainerLayout extends IDragItem {
|
||||
type: "container",
|
||||
}
|
||||
|
||||
export interface WidgetLayout extends IDragItem {
|
||||
type: "widget",
|
||||
node: ComfyWidgetNode
|
||||
}
|
||||
|
||||
type DragItemID = string;
|
||||
|
||||
type LayoutStateOps = {
|
||||
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index: number) => ContainerLayout,
|
||||
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index: number) => WidgetLayout,
|
||||
findDefaultContainerForInsertion: () => ContainerLayout | null,
|
||||
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
||||
nodeAdded: (node: LGraphNode) => void,
|
||||
nodeRemoved: (node: LGraphNode) => void,
|
||||
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout,
|
||||
ungroup: (container: ContainerLayout) => void,
|
||||
getCurrentSelection: () => IDragItem[],
|
||||
findLayoutForNode: (nodeId: number) => IDragItem | null;
|
||||
serialize: () => SerializedLayoutState,
|
||||
deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
|
||||
initDefaultLayout: () => void,
|
||||
onStartConfigure: () => void
|
||||
}
|
||||
|
||||
export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps;
|
||||
const store: Writable<LayoutState> = writable({
|
||||
root: null,
|
||||
allItems: {},
|
||||
allItemsByNode: {},
|
||||
currentId: 0,
|
||||
currentSelection: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: true
|
||||
})
|
||||
|
||||
function findDefaultContainerForInsertion(): ContainerLayout | null {
|
||||
const state = get(store);
|
||||
|
||||
if (state.root === null) {
|
||||
// Should never happen
|
||||
throw "Root container was null!";
|
||||
}
|
||||
|
||||
if (state.root.type === "container") {
|
||||
const container = state.root as ContainerLayout;
|
||||
const children: IDragItem[] = state.allItems[container.id]?.children || []
|
||||
const found = children.find((di) => di.type === "container")
|
||||
if (found && found.type === "container")
|
||||
return found as ContainerLayout;
|
||||
return container;
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index: number = -1): ContainerLayout {
|
||||
const state = get(store);
|
||||
const dragItem: ContainerLayout = {
|
||||
type: "container",
|
||||
id: `${state.currentId++}`,
|
||||
attrs: {
|
||||
title: "Container",
|
||||
showTitle: true,
|
||||
direction: "vertical",
|
||||
classes: "",
|
||||
...attrs
|
||||
}
|
||||
}
|
||||
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
||||
state.allItems[dragItem.id] = entry;
|
||||
if (parent) {
|
||||
moveItem(dragItem, parent)
|
||||
}
|
||||
console.debug("[layoutState] addContainer", state)
|
||||
store.set(state)
|
||||
return dragItem;
|
||||
}
|
||||
|
||||
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index: number = -1): WidgetLayout {
|
||||
const state = get(store);
|
||||
const widgetName = "Widget"
|
||||
const dragItem: WidgetLayout = {
|
||||
type: "widget",
|
||||
id: `${state.currentId++}`,
|
||||
node: node,
|
||||
attrs: {
|
||||
title: widgetName,
|
||||
showTitle: true,
|
||||
direction: "horizontal",
|
||||
classes: "",
|
||||
...attrs
|
||||
}
|
||||
}
|
||||
const parentEntry = state.allItems[parent.id]
|
||||
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
||||
state.allItems[dragItem.id] = entry;
|
||||
state.allItemsByNode[node.id] = entry;
|
||||
console.debug("[layoutState] addWidget", state)
|
||||
moveItem(dragItem, parent)
|
||||
return dragItem;
|
||||
}
|
||||
|
||||
function updateChildren(parent: IDragItem, newChildren?: IDragItem[]): IDragItem[] {
|
||||
const state = get(store);
|
||||
if (newChildren)
|
||||
state.allItems[parent.id].children = newChildren;
|
||||
for (const child of state.allItems[parent.id].children) {
|
||||
if (child.id === SHADOW_PLACEHOLDER_ITEM_ID)
|
||||
continue;
|
||||
state.allItems[child.id].parent = parent;
|
||||
}
|
||||
store.set(state)
|
||||
return state.allItems[parent.id].children
|
||||
}
|
||||
|
||||
function nodeAdded(node: LGraphNode) {
|
||||
const state = get(store)
|
||||
if (state.isConfiguring)
|
||||
return;
|
||||
|
||||
const parent = findDefaultContainerForInsertion();
|
||||
|
||||
// Two cases where we want to add nodes:
|
||||
// 1. User adds a new UI node, so we should instantiate its widget in the frontend.
|
||||
// 2. User adds a node with inputs that can be filled by frontend widgets.
|
||||
// Depending on config, this means we should instantiate default UI nodes connected to those inputs.
|
||||
|
||||
console.debug(node)
|
||||
if ("svelteComponentType" in node) {
|
||||
addWidget(parent, node as ComfyWidgetNode);
|
||||
}
|
||||
|
||||
// Add default node panel with all widgets autoinstantiated
|
||||
// if (node.widgets && node.widgets.length > 0) {
|
||||
// const container = addContainer(parent.id, { title: node.title, direction: "vertical", associatedNode: node.id });
|
||||
// for (const widget of node.widgets) {
|
||||
// addWidget(container.id, node, widget, { associatedNode: node.id });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
function removeEntry(state: LayoutState, id: DragItemID) {
|
||||
const entry = state.allItems[id]
|
||||
if (entry.children && entry.children.length > 0) {
|
||||
console.error(entry)
|
||||
throw `Tried removing entry ${id} but it still had children!`
|
||||
}
|
||||
const parent = entry.parent;
|
||||
if (parent) {
|
||||
const parentEntry = state.allItems[parent.id];
|
||||
parentEntry.children = parentEntry.children.filter(item => item.id !== id)
|
||||
}
|
||||
if (entry.dragItem.type === "widget") {
|
||||
const widget = entry.dragItem as WidgetLayout;
|
||||
delete state.allItemsByNode[widget.node.id]
|
||||
}
|
||||
delete state.allItems[id]
|
||||
}
|
||||
|
||||
function nodeRemoved(node: LGraphNode) {
|
||||
const state = get(store)
|
||||
|
||||
console.debug("[layoutState] nodeRemoved", node)
|
||||
|
||||
let del = Object.entries(state.allItems).filter(pair =>
|
||||
pair[1].dragItem.type === "widget"
|
||||
&& (pair[1].dragItem as WidgetLayout).node.id === node.id)
|
||||
|
||||
for (const pair of del) {
|
||||
const [id, dragItem] = pair;
|
||||
removeEntry(state, id)
|
||||
}
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
|
||||
const state = get(store)
|
||||
const entry = state.allItems[target.id]
|
||||
if (entry.parent && entry.parent.id === to.id)
|
||||
return;
|
||||
|
||||
if (entry.parent) {
|
||||
const parentEntry = state.allItems[entry.parent.id];
|
||||
const index = parentEntry.children.findIndex(c => c.id === target.id)
|
||||
if (index !== -1) {
|
||||
parentEntry.children.splice(index, 1)
|
||||
}
|
||||
else {
|
||||
console.error(parentEntry)
|
||||
console.error(target)
|
||||
throw "Child not found in parent!"
|
||||
}
|
||||
}
|
||||
|
||||
const toEntry = state.allItems[to.id];
|
||||
if (index !== -1)
|
||||
toEntry.children.splice(index, 0, target)
|
||||
else
|
||||
toEntry.children.push(target)
|
||||
state.allItems[target.id].parent = toEntry.dragItem;
|
||||
|
||||
console.debug("[layoutState] Move child", target, toEntry, index)
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function getCurrentSelection(): IDragItem[] {
|
||||
const state = get(store)
|
||||
return state.currentSelection.map(id => state.allItems[id].dragItem)
|
||||
}
|
||||
|
||||
function groupItems(dragItems: IDragItem[], attrs: Partial<Attributes> = {}): ContainerLayout {
|
||||
if (dragItems.length === 0)
|
||||
return;
|
||||
|
||||
const state = get(store)
|
||||
const parent = state.allItems[dragItems[0].id].parent || findDefaultContainerForInsertion();
|
||||
|
||||
if (parent === null || parent.type !== "container")
|
||||
return;
|
||||
|
||||
let index = undefined;
|
||||
if (parent) {
|
||||
const indexFound = state.allItems[parent.id].children.findIndex(c => c.id === dragItems[0].id)
|
||||
if (indexFound !== -1)
|
||||
index = indexFound
|
||||
}
|
||||
|
||||
const container = addContainer(parent as ContainerLayout, attrs, index)
|
||||
|
||||
for (const item of dragItems) {
|
||||
moveItem(item, container)
|
||||
}
|
||||
|
||||
console.debug("[layoutState] Grouped", container, parent, state.allItems[container.id].children, index)
|
||||
|
||||
store.set(state)
|
||||
return container
|
||||
}
|
||||
|
||||
function ungroup(container: ContainerLayout) {
|
||||
const state = get(store)
|
||||
|
||||
const parent = state.allItems[container.id].parent;
|
||||
if (!parent || parent.type !== "container") {
|
||||
console.warn("No parent to ungroup into!", container)
|
||||
return;
|
||||
}
|
||||
|
||||
let index = undefined;
|
||||
const parentChildren = state.allItems[parent.id].children;
|
||||
const indexFound = parentChildren.findIndex(c => c.id === container.id)
|
||||
if (indexFound !== -1)
|
||||
index = indexFound
|
||||
|
||||
const containerEntry = state.allItems[container.id]
|
||||
console.debug("[layoutState] About to ungroup", containerEntry, parent, parentChildren, index)
|
||||
|
||||
const children = [...containerEntry.children]
|
||||
for (const item of children) {
|
||||
moveItem(item, parent as ContainerLayout, index)
|
||||
}
|
||||
|
||||
removeEntry(state, container.id)
|
||||
|
||||
console.debug("[layoutState] Ungrouped", containerEntry, parent, parentChildren, index)
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function findLayoutForNode(nodeId: number): WidgetLayout | null {
|
||||
const state = get(store)
|
||||
const found = Object.entries(state.allItems).find(pair =>
|
||||
pair[1].dragItem.type === "widget"
|
||||
&& (pair[1].dragItem as WidgetLayout).node.id === nodeId)
|
||||
if (found)
|
||||
return found[1].dragItem as WidgetLayout
|
||||
return null;
|
||||
}
|
||||
|
||||
function initDefaultLayout() {
|
||||
store.set({
|
||||
root: null,
|
||||
allItems: {},
|
||||
currentId: 0,
|
||||
currentSelection: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false
|
||||
})
|
||||
|
||||
const root = addContainer(null, { direction: "horizontal", showTitle: false });
|
||||
const left = addContainer(root, { direction: "vertical", showTitle: false });
|
||||
const right = addContainer(root, { direction: "vertical", showTitle: false });
|
||||
|
||||
const state = get(store)
|
||||
state.root = root;
|
||||
store.set(state)
|
||||
|
||||
console.debug("[layoutState] initDefault", state)
|
||||
}
|
||||
|
||||
export type SerializedLayoutState = {
|
||||
root: DragItemID | null,
|
||||
allItems: Record<DragItemID, SerializedDragEntry>,
|
||||
currentId: number,
|
||||
}
|
||||
|
||||
export type SerializedDragEntry = {
|
||||
dragItem: SerializedDragItem,
|
||||
children: DragItemID[],
|
||||
parent: DragItemID | null
|
||||
}
|
||||
|
||||
export type SerializedDragItem = {
|
||||
type: string,
|
||||
id: DragItemID,
|
||||
nodeId: number | null,
|
||||
attrs: Attributes
|
||||
}
|
||||
|
||||
function serialize(): SerializedLayoutState {
|
||||
const state = get(store)
|
||||
|
||||
const allItems: Record<DragItemID, SerializedDragEntry> = {}
|
||||
for (const pair of Object.entries(state.allItems)) {
|
||||
const [id, entry] = pair;
|
||||
allItems[id] = {
|
||||
dragItem: {
|
||||
type: entry.dragItem.type,
|
||||
id: entry.dragItem.id,
|
||||
nodeId: (entry.dragItem as any).node?.id,
|
||||
attrs: entry.dragItem.attrs
|
||||
},
|
||||
children: entry.children.map((di) => di.id),
|
||||
parent: entry.parent?.id
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
root: state.root?.id,
|
||||
allItems,
|
||||
currentId: state.currentId,
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
const allItems: Record<DragItemID, DragItemEntry> = {}
|
||||
const allItemsByNode: Record<number, DragItemEntry> = {}
|
||||
for (const pair of Object.entries(data.allItems)) {
|
||||
const [id, entry] = pair;
|
||||
|
||||
const dragItem: IDragItem = {
|
||||
type: entry.dragItem.type,
|
||||
id: entry.dragItem.id,
|
||||
attrs: entry.dragItem.attrs
|
||||
};
|
||||
|
||||
const dragEntry: DragItemEntry = {
|
||||
dragItem,
|
||||
children: [],
|
||||
parent: null
|
||||
}
|
||||
|
||||
allItems[id] = dragEntry
|
||||
|
||||
if (dragItem.type === "widget") {
|
||||
const widget = dragItem as WidgetLayout;
|
||||
widget.node = graph.getNodeById(entry.dragItem.nodeId) as ComfyWidgetNode
|
||||
allItemsByNode[entry.dragItem.nodeId] = dragEntry
|
||||
}
|
||||
}
|
||||
|
||||
// reconnect parent/child tree
|
||||
for (const pair of Object.entries(data.allItems)) {
|
||||
const [id, entry] = pair;
|
||||
|
||||
for (const childId of entry.children) {
|
||||
allItems[id].children.push(allItems[childId].dragItem)
|
||||
}
|
||||
if (entry.parent) {
|
||||
allItems[id].parent = allItems[entry.parent].dragItem;
|
||||
}
|
||||
}
|
||||
|
||||
let root: IDragItem = null;
|
||||
if (data.root)
|
||||
root = allItems[data.root].dragItem
|
||||
|
||||
const state: LayoutState = {
|
||||
root,
|
||||
allItems,
|
||||
allItemsByNode,
|
||||
currentId: data.currentId,
|
||||
currentSelection: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false
|
||||
}
|
||||
|
||||
console.debug("[layoutState] deserialize", data, state)
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function onStartConfigure() {
|
||||
store.update(s => {
|
||||
s.isConfiguring = true;
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
const layoutStateStore: WritableLayoutStateStore =
|
||||
{
|
||||
...store,
|
||||
addContainer,
|
||||
addWidget,
|
||||
findDefaultContainerForInsertion,
|
||||
updateChildren,
|
||||
nodeAdded,
|
||||
nodeRemoved,
|
||||
getCurrentSelection,
|
||||
groupItems,
|
||||
findLayoutForNode,
|
||||
ungroup,
|
||||
initDefaultLayout,
|
||||
onStartConfigure,
|
||||
serialize,
|
||||
deserialize
|
||||
}
|
||||
export default layoutStateStore;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { writable, get } from 'svelte/store';
|
||||
import type { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type ComfyGraphNode from '$lib/nodes/ComfyGraphNode';
|
||||
|
||||
/** store for one node's state */
|
||||
export type NodeUIStateStore = Writable<any>
|
||||
|
||||
export type NodeUIState = {
|
||||
name: string,
|
||||
node: LGraphNode
|
||||
}
|
||||
|
||||
type NodeID = number;
|
||||
|
||||
type NodeStateOps = {
|
||||
nodeAdded: (node: LGraphNode) => void,
|
||||
nodeRemoved: (node: LGraphNode) => void,
|
||||
configureFinished: (graph: LGraph) => void,
|
||||
nodeStateChanged: (node: LGraphNode) => void,
|
||||
clear: () => void,
|
||||
}
|
||||
|
||||
export type NodeStateStore = Record<NodeID, NodeUIState>;
|
||||
type WritableNodeStateStore = Writable<NodeStateStore> & NodeStateOps;
|
||||
|
||||
const store: Writable<NodeStateStore> = writable({})
|
||||
|
||||
function clear() {
|
||||
store.set({})
|
||||
}
|
||||
|
||||
function nodeAdded(node: LGraphNode) {
|
||||
let state = get(store)
|
||||
state[node.id] = { node: node, name: node.name }
|
||||
store.set(state);
|
||||
}
|
||||
|
||||
function nodeRemoved(node: LGraphNode) {
|
||||
const state = get(store)
|
||||
delete state[node.id]
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function nodeStateChanged(node: LGraphNode) {
|
||||
const state = get(store)
|
||||
const nodeState = state[node.id]
|
||||
nodeState.name = node.name
|
||||
store.set(state);
|
||||
}
|
||||
|
||||
function configureFinished(graph: LGraph) {
|
||||
let state = get(store);
|
||||
|
||||
for (const node of graph.computeExecutionOrder(false, null)) {
|
||||
state[node.id].name = name;
|
||||
}
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
const nodeStateStore: WritableNodeStateStore =
|
||||
{
|
||||
...store,
|
||||
nodeAdded,
|
||||
nodeRemoved,
|
||||
nodeStateChanged,
|
||||
configureFinished,
|
||||
clear
|
||||
}
|
||||
export default nodeStateStore;
|
||||
@@ -2,15 +2,25 @@ import { writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type ComfyApp from "$lib/components/ComfyApp"
|
||||
|
||||
export type UIEditMode = "disabled" | "widgets" | "containers" | "layout";
|
||||
|
||||
export type UIState = {
|
||||
app: ComfyApp,
|
||||
nodesLocked: boolean,
|
||||
graphLocked: boolean,
|
||||
unlocked: boolean,
|
||||
autoAddUI: boolean,
|
||||
uiEditMode: UIEditMode
|
||||
}
|
||||
|
||||
export type WritableUIStateStore = Writable<UIState>;
|
||||
const store: WritableUIStateStore = writable({ unlocked: false, graphLocked: true, nodesLocked:false })
|
||||
const store: WritableUIStateStore = writable(
|
||||
{
|
||||
app: null,
|
||||
graphLocked: false,
|
||||
nodesLocked: false,
|
||||
autoAddUI: true,
|
||||
uiEditMode: "disabled",
|
||||
})
|
||||
|
||||
const uiStateStore: WritableUIStateStore =
|
||||
{
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { writable, get } from 'svelte/store';
|
||||
import type { LGraph, LGraphNode, IWidget } from "@litegraph-ts/core";
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type ComfyGraphNode from '$lib/nodes/ComfyGraphNode';
|
||||
import type ComfyWidget from '$lib/widgets/ComfyWidget';
|
||||
|
||||
/** store for one widget's state */
|
||||
export type WidgetUIStateStore = Writable<any>
|
||||
|
||||
export type WidgetUIState = {
|
||||
/** position in the node's list of widgets */
|
||||
index: number,
|
||||
/** parent node containing the widget */
|
||||
node: LGraphNode,
|
||||
/** actual widget instance */
|
||||
widget: IWidget,
|
||||
/** widget value as a store, to react to changes */
|
||||
value: WidgetUIStateStore,
|
||||
/**
|
||||
* true if this widget was added purely from the frontend. what this means:
|
||||
* - this widget's state will not be saved to the workflow
|
||||
* - the widget was added on startup by some subclass of ComfyGraphNode
|
||||
*/
|
||||
isVirtual: boolean
|
||||
}
|
||||
|
||||
export type WidgetDrawState = {
|
||||
isNodeExecuting: boolean
|
||||
}
|
||||
|
||||
type NodeID = number;
|
||||
|
||||
type WidgetStateOps = {
|
||||
nodeAdded: (node: LGraphNode) => void,
|
||||
nodeRemoved: (node: LGraphNode) => void,
|
||||
configureFinished: (graph: LGraph) => void,
|
||||
widgetStateChanged: (nodeId: number, widget: IWidget<any, any>) => void,
|
||||
findWidgetByName: (nodeId: number, name: string) => WidgetUIState | null,
|
||||
clear: () => void,
|
||||
}
|
||||
|
||||
export type WidgetStateStore = Record<NodeID, WidgetUIState[]>;
|
||||
type WritableWidgetStateStore = Writable<WidgetStateStore> & WidgetStateOps;
|
||||
|
||||
const store: Writable<WidgetStateStore> = writable({})
|
||||
|
||||
function clear() {
|
||||
store.set({})
|
||||
}
|
||||
|
||||
function nodeAdded(node: LGraphNode) {
|
||||
let state = get(store)
|
||||
|
||||
if (node.widgets) {
|
||||
for (const [index, widget] of node.widgets.entries()) {
|
||||
if (!state[node.id])
|
||||
state[node.id] = []
|
||||
let isVirtual = false;
|
||||
if ("isVirtual" in widget)
|
||||
isVirtual = (widget as ComfyWidget<any, any>).isVirtual;
|
||||
state[node.id].push({ index, node, widget, value: writable(widget.value), isVirtual: isVirtual })
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("NODEADDED", state)
|
||||
|
||||
store.set(state);
|
||||
}
|
||||
|
||||
function nodeRemoved(node: LGraphNode) {
|
||||
const state = get(store)
|
||||
delete state[node.id]
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function widgetStateChanged(nodeId: number, widget: IWidget<any, any>) {
|
||||
const state = get(store)
|
||||
const entries = state[nodeId]
|
||||
if (entries) {
|
||||
let widgetState = entries.find(e => e.widget === widget);
|
||||
if (widgetState) {
|
||||
widgetState.value.set(widget.value);
|
||||
store.set(state);
|
||||
}
|
||||
else {
|
||||
console.error("Widget state changed and node was found, but widget was not found in state!", widget, widget.node, entries)
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error("Widget state changed but node was not found in state!", widget, widget.node)
|
||||
}
|
||||
}
|
||||
|
||||
function configureFinished(graph: LGraph) {
|
||||
let state = get(store);
|
||||
|
||||
for (const node of graph.computeExecutionOrder(false, null)) {
|
||||
if (node.widgets_values) {
|
||||
for (const [i, value] of node.widgets_values.entries()) {
|
||||
if (i < state[node.id].length && !state[node.id][i].isVirtual) {
|
||||
state[node.id][i].value.set(value);
|
||||
}
|
||||
else {
|
||||
console.log("Skip virtual widget", node.id, node.type, state[node.id][i].widget)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function findWidgetByName(nodeId: number, name: string): WidgetUIState | null {
|
||||
let state = get(store);
|
||||
|
||||
if (!(nodeId in state))
|
||||
return null;
|
||||
|
||||
return state[nodeId].find((v) => v.widget.name === name);
|
||||
}
|
||||
|
||||
const widgetStateStore: WritableWidgetStateStore =
|
||||
{
|
||||
...store,
|
||||
nodeAdded,
|
||||
nodeRemoved,
|
||||
widgetStateChanged,
|
||||
configureFinished,
|
||||
findWidgetByName,
|
||||
clear
|
||||
}
|
||||
export default widgetStateStore;
|
||||
@@ -2,7 +2,13 @@ import ComfyApp from "./components/ComfyApp";
|
||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||
import widgetState, { type WidgetDrawState, type WidgetUIState } from "$lib/stores/widgetState";
|
||||
import { get } from "svelte/store"
|
||||
import layoutState from "$lib/stores/layoutState"
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(n, min), max)
|
||||
}
|
||||
|
||||
export function download(filename: string, text: string, type: string = "text/plain") {
|
||||
const blob = new Blob([text], { type: type });
|
||||
@@ -18,24 +24,31 @@ export function download(filename: string, text: string, type: string = "text/pl
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function getComponentForWidgetState(item: WidgetUIState): any {
|
||||
let ctor: any = null;
|
||||
export function startDrag(evt: MouseEvent) {
|
||||
const dragItemId: string = evt.target.dataset["dragItemId"];
|
||||
const ls = get(layoutState)
|
||||
|
||||
// custom widgets with TypeScript sources
|
||||
let override = ComfyApp.widget_type_overrides[item.widget.type]
|
||||
if (override) {
|
||||
return override;
|
||||
if (evt.button !== 0) {
|
||||
if (ls.currentSelection.length <= 1 && !ls.isMenuOpen)
|
||||
ls.currentSelection = [dragItemId]
|
||||
return;
|
||||
}
|
||||
|
||||
// litegraph.ts built-in widgets
|
||||
switch (item.widget.type) {
|
||||
case "combo":
|
||||
return ComboWidget;
|
||||
case "number":
|
||||
return RangeWidget;
|
||||
case "text":
|
||||
return TextWidget;
|
||||
}
|
||||
const item = ls.allItems[dragItemId].dragItem
|
||||
|
||||
return null;
|
||||
}
|
||||
if (evt.ctrlKey) {
|
||||
const index = ls.currentSelection.indexOf(item.id)
|
||||
if (index === -1)
|
||||
ls.currentSelection.push(item.id);
|
||||
else
|
||||
ls.currentSelection.splice(index, 1);
|
||||
ls.currentSelection = ls.currentSelection;
|
||||
}
|
||||
else {
|
||||
ls.currentSelection = [item.id]
|
||||
}
|
||||
layoutState.set(ls)
|
||||
};
|
||||
|
||||
export function stopDrag(evt: MouseEvent) {
|
||||
};
|
||||
|
||||
@@ -1,147 +1,80 @@
|
||||
import type { IWidget, LGraphNode } from "@litegraph-js/core";
|
||||
import type ComfyApp from "$lib/components/ComfyApp";
|
||||
import ComfyValueControlWidget from "./widgets/ComfyValueControlWidget";
|
||||
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||
import { BuiltInSlotShape, LiteGraph } from "@litegraph-ts/core";
|
||||
import { ComfyComboNode, ComfySliderNode, ComfyTextNode } from "./nodes";
|
||||
|
||||
export interface WidgetData {
|
||||
widget: IWidget,
|
||||
minWidth?: number,
|
||||
minHeight?: number
|
||||
}
|
||||
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot;
|
||||
|
||||
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app: ComfyApp) => WidgetData;
|
||||
|
||||
|
||||
type NumberConfig = { min: number, max: number, step: number, precision: number }
|
||||
type NumberDefaults = { val: number, config: NumberConfig }
|
||||
|
||||
function getNumberDefaults(inputData: any, defaultStep: number): NumberDefaults {
|
||||
let defaultVal = inputData[1]["default"];
|
||||
function getNumberDefaults(inputData: any, defaultStep: number): ComfyInputConfig {
|
||||
let defaultValue = inputData[1]["default"];
|
||||
let { min, max, step } = inputData[1];
|
||||
|
||||
if (defaultVal == undefined) defaultVal = 0;
|
||||
if (defaultValue == undefined) defaultValue = 0;
|
||||
if (min == undefined) min = 0;
|
||||
if (max == undefined) max = 2048;
|
||||
if (step == undefined) step = defaultStep;
|
||||
|
||||
return { val: defaultVal, config: { min, max, step: step, precision: 0 } };
|
||||
return { min, max, step: step, precision: 0, defaultValue };
|
||||
}
|
||||
|
||||
function addComfyInput(node: LGraphNode, inputName: string, extraInfo: Partial<IComfyInputSlot> = {}): IComfyInputSlot {
|
||||
const input = node.addInput(inputName) as IComfyInputSlot
|
||||
for (const [k, v] of Object.entries(extraInfo))
|
||||
input[k] = v
|
||||
|
||||
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
|
||||
const { val, config } = getNumberDefaults(inputData, 0.5);
|
||||
return { widget: node.addWidget("number", inputName, val, () => { }, config) };
|
||||
if (input.defaultWidgetNode) {
|
||||
const ty = Object.values(LiteGraph.registered_node_types)
|
||||
.find(v => v.class === input.defaultWidgetNode)
|
||||
if (ty)
|
||||
input.widgetNodeType = ty.type
|
||||
}
|
||||
|
||||
input.serialize = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||
const config = getNumberDefaults(inputData, 0.5);
|
||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
|
||||
}
|
||||
|
||||
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
|
||||
const { val, config } = getNumberDefaults(inputData, 1);
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
"number",
|
||||
inputName,
|
||||
val,
|
||||
function(v) {
|
||||
const s = this.options.step;
|
||||
this.value = Math.round(v / s) * s;
|
||||
},
|
||||
config
|
||||
),
|
||||
};
|
||||
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||
const config = getNumberDefaults(inputData, 1);
|
||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
|
||||
};
|
||||
|
||||
function seedWidget(node, inputName, inputData, app) {
|
||||
const seed = INT(node, inputName, inputData, app);
|
||||
const seedControl = new ComfyValueControlWidget("control_after_generate", "randomize", node, seed.widget);
|
||||
node.addCustomWidget(seedControl);
|
||||
|
||||
// seed.widget.linkedWidgets = [seedControl];
|
||||
return seed;
|
||||
}
|
||||
|
||||
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app: ComfyApp): WidgetData => {
|
||||
const defaultVal = inputData[1].default || "";
|
||||
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||
const defaultValue = inputData[1].default || "";
|
||||
const multiline = !!inputData[1].multiline;
|
||||
|
||||
// if (multiline) {
|
||||
// return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
|
||||
// } else {
|
||||
return { widget: node.addWidget("text", inputName, defaultVal, () => { }, { multiline }) };
|
||||
// }
|
||||
return addComfyInput(node, inputName, { type: "string", config: { defaultValue, multiline }, defaultWidgetNode: ComfyTextNode })
|
||||
};
|
||||
|
||||
const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
|
||||
const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||
const type = inputData[0];
|
||||
let defaultValue = type[0];
|
||||
if (inputData[1] && inputData[1].default) {
|
||||
defaultValue = inputData[1].default;
|
||||
}
|
||||
return { widget: node.addWidget("combo", inputName, defaultValue, () => { }, { values: type }) };
|
||||
return addComfyInput(node, inputName, { type: "string", config: { values: type, defaultValue }, defaultWidgetNode: ComfyComboNode })
|
||||
}
|
||||
|
||||
const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app): WidgetData => {
|
||||
const imageWidget = node.widgets.find((w) => w.name === "image");
|
||||
let uploadWidget: IWidget;
|
||||
|
||||
// async function uploadFile(file: File, updateNode: boolean) {
|
||||
// try {
|
||||
// // Wrap file in formdata so it includes filename
|
||||
// const body = new FormData();
|
||||
// body.append("image", file);
|
||||
// const resp = await fetch("/upload/image", {
|
||||
// method: "POST",
|
||||
// body,
|
||||
// });
|
||||
|
||||
// if (resp.status === 200) {
|
||||
// const data = await resp.json();
|
||||
// // Add the file as an option and update the widget value
|
||||
// if (!imageWidget.options.values.includes(data.name)) {
|
||||
// imageWidget.options.values.push(data.name);
|
||||
// }
|
||||
|
||||
// if (updateNode) {
|
||||
// // showImage(data.name);
|
||||
// imageWidget.value = data.name;
|
||||
// }
|
||||
// } else {
|
||||
// alert(resp.status + " - " + resp.statusText);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// alert(error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// const fileInput = document.createElement("input");
|
||||
// Object.assign(fileInput, {
|
||||
// type: "file",
|
||||
// accept: "image/jpeg,image/png",
|
||||
// style: "display: none",
|
||||
// onchange: async () => {
|
||||
// if (fileInput.files.length) {
|
||||
// await uploadFile(fileInput.files[0], true);
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
// document.body.append(fileInput);
|
||||
|
||||
// Create the button widget for selecting the files
|
||||
uploadWidget = node.addWidget("button", "choose file to upload", "image", () => {
|
||||
// fileInput.click();
|
||||
});
|
||||
uploadWidget.options = { serialize: false };
|
||||
|
||||
return { widget: uploadWidget };
|
||||
const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||
return addComfyInput(node, inputName, { type: "number", config: {} })
|
||||
}
|
||||
|
||||
|
||||
export type WidgetRepository = Record<string, WidgetFactory>
|
||||
|
||||
export const ComfyWidgets: WidgetRepository = {
|
||||
"INT:seed": seedWidget,
|
||||
"INT:noise_seed": seedWidget,
|
||||
const ComfyWidgets: WidgetRepository = {
|
||||
"INT:seed": INT,
|
||||
"INT:noise_seed": INT,
|
||||
FLOAT,
|
||||
INT,
|
||||
STRING,
|
||||
COMBO,
|
||||
IMAGEUPLOAD,
|
||||
}
|
||||
|
||||
export default ComfyWidgets
|
||||
|
||||
44
src/lib/widgets/ButtonWidget.svelte
Normal file
44
src/lib/widgets/ButtonWidget.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyButtonNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Button } from "@gradio/button";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
let node: ComfyButtonNode | null = null;
|
||||
let nodeValue: Writable<boolean> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfyButtonNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
}
|
||||
};
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
node.onClick();
|
||||
}
|
||||
|
||||
const style = {
|
||||
full_width: "100%"
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-button">
|
||||
{#if node !== null}
|
||||
<Button on:click={onClick} variant="primary" {style}>
|
||||
{widget.attrs.title}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +1,117 @@
|
||||
<script lang="ts">
|
||||
import type { WidgetDrawState, WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import { Dropdown } from "@gradio/form";
|
||||
import { get } from "svelte/store";
|
||||
import Select from 'svelte-select';
|
||||
export let item: WidgetUIState | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
let option: any;
|
||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
let node: ComfyComboNode | null = null;
|
||||
let nodeValue: Writable<string> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: any
|
||||
|
||||
$: if(item) {
|
||||
if (!itemValue)
|
||||
itemValue = item.value;
|
||||
if (!option)
|
||||
option = get(item.value);
|
||||
};
|
||||
export let debug: boolean = false;
|
||||
|
||||
$: if (option && itemValue) {
|
||||
$itemValue = option.value
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
|
||||
if (node.properties.values.indexOf(option.value) === -1) {
|
||||
setOption($nodeValue)
|
||||
$nodeValue = option
|
||||
}
|
||||
else {
|
||||
$nodeValue = option
|
||||
setOption($nodeValue)
|
||||
}
|
||||
setNodeValue(widget)
|
||||
node.properties = node.properties
|
||||
}
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if(widget) {
|
||||
node = widget.node as ComfyComboNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
setOption($nodeValue) // don't react on option
|
||||
}
|
||||
}
|
||||
|
||||
function setOption(value: any) {
|
||||
option = value;
|
||||
}
|
||||
|
||||
$: if (nodeValue && option && option.value) {
|
||||
$nodeValue = option.value;
|
||||
}
|
||||
|
||||
function getLinkValue() {
|
||||
if (!node)
|
||||
return "???";
|
||||
const links = node.getOutputLinks(0)
|
||||
if (links.length === 0)
|
||||
return "???";
|
||||
return links[0].data
|
||||
}
|
||||
|
||||
let lastPropsChanged: number = 0;
|
||||
let werePropsChanged: boolean = false;
|
||||
|
||||
$: if ($propsChanged !== lastPropsChanged) {
|
||||
werePropsChanged = true;
|
||||
lastPropsChanged = $propsChanged;
|
||||
setTimeout(() => (werePropsChanged = false), 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-combo">
|
||||
{#if item !== null && option !== undefined}
|
||||
<label>
|
||||
<BlockTitle show_label={true}>{item.widget.name}</BlockTitle>
|
||||
<Select
|
||||
bind:value={option}
|
||||
bind:items={item.widget.options.values}
|
||||
disabled={item.widget.options.values.length === 0}
|
||||
clearable={false}
|
||||
on:change
|
||||
on:select
|
||||
on:filter
|
||||
on:blur
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
<div class="wrapper gr-combo" class:updated={werePropsChanged}>
|
||||
{#key $propsChanged}
|
||||
{#if node !== null && nodeValue !== null}
|
||||
<label>
|
||||
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
|
||||
<Select
|
||||
bind:value={option}
|
||||
bind:items={node.properties.values}
|
||||
disabled={node.properties.values.length === 0}
|
||||
clearable={false}
|
||||
on:change
|
||||
on:select
|
||||
on:filter
|
||||
on:blur
|
||||
/>
|
||||
{#if debug}
|
||||
<div>Value: {option?.value}</div>
|
||||
<div>Items: {node.properties.values}</div>
|
||||
<div>NodeValue: {$nodeValue}</div>
|
||||
<div>LinkValue: {getLinkValue()}</div>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes -global-light-up {
|
||||
from {
|
||||
background-color: var(--color-yellow-400);
|
||||
}
|
||||
to {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
|
||||
.updated {
|
||||
animation: light-up 1s ease-out;
|
||||
:global(.svelte-select) {
|
||||
animation: light-up 1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.svelte-select-list) {
|
||||
z-index: var(--layer-5) !important;
|
||||
z-index: var(--layer-top) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Gallery } from "@gradio/gallery";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
|
||||
export let item: WidgetUIState | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null; // stores must be declared at top level
|
||||
|
||||
$: if(item) {
|
||||
itemValue = item.value;
|
||||
}
|
||||
|
||||
let style: Styles = {
|
||||
// grid_cols: [2],
|
||||
grid: [3],
|
||||
// object_fit: "cover",
|
||||
}
|
||||
let element: HTMLDivElement;
|
||||
|
||||
function updateForLightbox() {
|
||||
// Wait for gradio gallery to show the large preview image, if no timeout then
|
||||
// the event might fire too early
|
||||
setTimeout(() => {
|
||||
const images = element.querySelectorAll<HTMLImageElement>('div.block div > img')
|
||||
if (images != null) {
|
||||
images.forEach(ImageViewer.instance.setupImageForLightbox.bind(ImageViewer.instance));
|
||||
}
|
||||
ImageViewer.instance.updateOnBackgroundChange();
|
||||
}, 200)
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="wrapper comfy-gallery-widget gr-gallery" bind:this={element}>
|
||||
{#if item && itemValue}
|
||||
<Block variant="solid" padding={false}>
|
||||
<Gallery
|
||||
bind:value={$itemValue}
|
||||
label={item.widget.name}
|
||||
show_label={true}
|
||||
{style}
|
||||
root={""}
|
||||
root_url={""}
|
||||
on:select={updateForLightbox}
|
||||
/>
|
||||
</Block>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { get } from "svelte/store";
|
||||
import type { WidgetPanelOptions } from "@litegraph-ts/core";
|
||||
import ComfyWidget from "./ComfyWidget";
|
||||
import type { ComfyImageResult } from "$lib/nodes/ComfySaveImageNode";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
|
||||
export type ComfyGalleryEntry = [string, string | null]; // <img> src and alt/title, gradio format
|
||||
|
||||
export interface ComfyGalleryWidgetOptions extends WidgetPanelOptions {
|
||||
}
|
||||
|
||||
export default class ComfyGalleryWidget extends ComfyWidget<ComfyGalleryWidgetOptions, ComfyGalleryEntry[]> {
|
||||
override type = "comfy/gallery";
|
||||
override isVirtual = true;
|
||||
|
||||
addImages(images: ComfyImageResult[]) {
|
||||
this.setValue(this.value.concat(images));
|
||||
}
|
||||
|
||||
override afterQueued() {
|
||||
let queue = get(queueState)
|
||||
if (!(typeof queue.queueRemaining === "number" && queue.queueRemaining > 1)) {
|
||||
this.setValue([])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { IEnumWidget, IEnumWidgetOptions, INumberWidget, LGraphNode, WidgetPanelOptions } from "@litegraph-ts/core";
|
||||
import ComfyWidget from "./ComfyWidget";
|
||||
import type { ComfyImageResult } from "$lib/nodes/ComfySaveImageNode";
|
||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import widgetState from "$lib/stores/widgetState"
|
||||
|
||||
export interface ComfyValueControlWidgetOptions extends IEnumWidgetOptions {
|
||||
}
|
||||
|
||||
export default class ComfyValueControlWidget extends ComfyWidget<ComfyValueControlWidgetOptions, string> {
|
||||
override type = "combo";
|
||||
targetWidget: INumberWidget;
|
||||
|
||||
constructor(name: string, value: string, node: ComfyGraphNode, targetWidget: INumberWidget) {
|
||||
super(name, value, node)
|
||||
this.targetWidget = targetWidget;
|
||||
this.options = { values: ["fixed", "increment", "decrement", "randomize"], serialize: false };
|
||||
}
|
||||
|
||||
override afterQueued() {
|
||||
var v = this.value;
|
||||
|
||||
let min = this.targetWidget.options.min;
|
||||
let max = this.targetWidget.options.max;
|
||||
// limit to something that javascript can handle
|
||||
max = Math.min(1125899906842624, max);
|
||||
min = Math.max(-1125899906842624, min);
|
||||
let range = (max - min) / (this.targetWidget.options.step);
|
||||
|
||||
//adjust values based on valueControl Behaviour
|
||||
switch (v) {
|
||||
case "fixed":
|
||||
break;
|
||||
case "increment":
|
||||
this.targetWidget.value += this.targetWidget.options.step;
|
||||
break;
|
||||
case "decrement":
|
||||
this.targetWidget.value -= this.targetWidget.options.step;
|
||||
break;
|
||||
case "randomize":
|
||||
this.targetWidget.value = Math.floor(Math.random() * range) * (this.targetWidget.options.step) + min;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
/*check if values are over or under their respective
|
||||
* ranges and set them to min or max.*/
|
||||
if (this.targetWidget.value < min)
|
||||
this.targetWidget.value = min;
|
||||
|
||||
if (this.targetWidget.value > max)
|
||||
this.targetWidget.value = max;
|
||||
|
||||
widgetState.widgetStateChanged(this.node.id, this.targetWidget);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core";
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
|
||||
export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T, V> {
|
||||
name: string;
|
||||
@@ -27,7 +26,6 @@ export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T
|
||||
|
||||
setValue(value: V) {
|
||||
this.value = value;
|
||||
widgetState.widgetStateChanged(this.node.id, this);
|
||||
}
|
||||
|
||||
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number): void;
|
||||
|
||||
78
src/lib/widgets/GalleryWidget.svelte
Normal file
78
src/lib/widgets/GalleryWidget.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Gallery } from "@gradio/gallery";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
let node: ComfyGalleryNode | null = null;
|
||||
let nodeValue: Writable<GradioFileData[]> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfyGalleryNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
}
|
||||
};
|
||||
|
||||
let style: Styles = {
|
||||
// grid_cols: [2],
|
||||
grid: [3],
|
||||
// object_fit: "cover",
|
||||
}
|
||||
let element: HTMLDivElement;
|
||||
|
||||
function updateForLightbox() {
|
||||
// Wait for gradio gallery to show the large preview image, if no timeout then
|
||||
// the event might fire too early
|
||||
setTimeout(() => {
|
||||
const images = element.querySelectorAll<HTMLImageElement>('div.block div > img')
|
||||
if (images != null) {
|
||||
images.forEach(ImageViewer.instance.setupImageForLightbox.bind(ImageViewer.instance));
|
||||
}
|
||||
ImageViewer.instance.updateOnBackgroundChange();
|
||||
}, 200)
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="wrapper comfy-gallery-widget gr-gallery" bind:this={element}>
|
||||
{#if widget && node && nodeValue}
|
||||
<Block variant="solid" padding={false}>
|
||||
<div class="padding">
|
||||
<Gallery
|
||||
bind:value={$nodeValue}
|
||||
label={widget.attrs.title}
|
||||
show_label={true}
|
||||
{style}
|
||||
root={""}
|
||||
root_url={""}
|
||||
on:select={updateForLightbox}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.padding {
|
||||
height: 30rem;
|
||||
}
|
||||
|
||||
.wrapper :global(button.thumbnail-lg) {
|
||||
width: var(--size-32);
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +1,52 @@
|
||||
<script lang="ts">
|
||||
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Range } from "@gradio/form";
|
||||
import { get } from "svelte/store";
|
||||
export let item: WidgetUIState | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
import { get, type Writable } from "svelte/store";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
let node: ComfySliderNode | null = null;
|
||||
let nodeValue: Writable<number> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
|
||||
$: if (item) {
|
||||
itemValue = item.value;
|
||||
updateOption(); // don't react on option
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfySliderNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
setOption($nodeValue); // don't react on option
|
||||
}
|
||||
};
|
||||
|
||||
// I don't know why but this is necessary to watch for changes to node
|
||||
// properties from ComfyWidgetNode.
|
||||
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
|
||||
setOption($nodeValue)
|
||||
setNodeValue(widget)
|
||||
node.properties = node.properties
|
||||
}
|
||||
|
||||
function updateOption() {
|
||||
option = get(itemValue);
|
||||
function setOption(value: any) {
|
||||
option = value;
|
||||
}
|
||||
|
||||
function onRelease(e: Event) {
|
||||
if (itemValue && option) {
|
||||
$itemValue = option
|
||||
if (nodeValue && option) {
|
||||
$nodeValue = option
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-range">
|
||||
{#if item !== null && option !== null}
|
||||
{#if node !== null && option !== null}
|
||||
<Range
|
||||
bind:value={option}
|
||||
minimum={item.widget.options.min}
|
||||
maximum={item.widget.options.max}
|
||||
step={item.widget.options.step}
|
||||
label={item.widget.name}
|
||||
minimum={node.properties.min}
|
||||
maximum={node.properties.max}
|
||||
step={node.properties.step}
|
||||
label={widget.attrs.title}
|
||||
show_label={true}
|
||||
on:release={onRelease}
|
||||
on:change
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
<script lang="ts">
|
||||
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import { TextBox } from "@gradio/form";
|
||||
export let item: WidgetUIState | null = null;
|
||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
let node: ComfyComboNode | null = null;
|
||||
let nodeValue: Writable<string> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
$: if (item) { itemValue = item.value; }
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfySliderNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
}
|
||||
};
|
||||
|
||||
// I don't know why but this is necessary to watch for changes to node
|
||||
// properties from ComfyWidgetNode.
|
||||
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
|
||||
setNodeValue(widget)
|
||||
node.properties = node.properties
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper gr-textbox">
|
||||
{#if item !== null && itemValue !== null}
|
||||
{#if node !== null && nodeValue !== null}
|
||||
<TextBox
|
||||
bind:value={$itemValue}
|
||||
label={item.widget.name}
|
||||
lines={item.widget.options.multiline ? 5 : 1}
|
||||
max_lines={item.widget.options.multiline ? 5 : 1}
|
||||
bind:value={$nodeValue}
|
||||
label={widget.attrs.title}
|
||||
lines={node.properties.multiline ? 5 : 1}
|
||||
max_lines={node.properties.multiline ? 5 : 1}
|
||||
show_label={true}
|
||||
on:change
|
||||
on:submit
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import { Button } from "@gradio/button";
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import nodeState from "$lib/stores/nodeState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
|
||||
import { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import type { ComfyAPIStatus } from "$lib/api";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
|
||||
import { getComponentForWidgetState } from "$lib/utils"
|
||||
import { Link, Toolbar } from "framework7-svelte"
|
||||
import { f7 } from "framework7-svelte"
|
||||
|
||||
export let subworkflowID: number = -1;
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
import { Button } from "@gradio/button";
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import nodeState from "$lib/stores/nodeState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
@@ -18,47 +16,11 @@
|
||||
|
||||
let app: ComfyApp | null = null;
|
||||
|
||||
let serializedPaneOrder = {};
|
||||
|
||||
function serializeAppState(): SerializedAppState {
|
||||
const graph = app.lGraph;
|
||||
|
||||
const serializedGraph = graph.serialize()
|
||||
|
||||
return {
|
||||
createdBy: "ComfyBox",
|
||||
version: 1,
|
||||
workflow: serializedGraph,
|
||||
panes: serializedPaneOrder
|
||||
}
|
||||
}
|
||||
|
||||
function doAutosave(graph: LGraph): void {
|
||||
const savedWorkflow = serializeAppState();
|
||||
localStorage.setItem("workflow", JSON.stringify(savedWorkflow))
|
||||
}
|
||||
|
||||
function doRestore(workflow: SerializedAppState) {
|
||||
serializedPaneOrder = workflow.panes;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (app)
|
||||
return
|
||||
app = $uiState.app = new ComfyApp();
|
||||
|
||||
// TODO dedup
|
||||
app.eventBus.on("nodeAdded", nodeState.nodeAdded);
|
||||
app.eventBus.on("nodeRemoved", nodeState.nodeRemoved);
|
||||
app.eventBus.on("configured", nodeState.configureFinished);
|
||||
app.eventBus.on("cleared", nodeState.clear);
|
||||
|
||||
app.eventBus.on("nodeAdded", widgetState.nodeAdded);
|
||||
app.eventBus.on("nodeRemoved", widgetState.nodeRemoved);
|
||||
app.eventBus.on("configured", widgetState.configureFinished);
|
||||
app.eventBus.on("cleared", widgetState.clear);
|
||||
app.eventBus.on("autosave", doAutosave);
|
||||
app.eventBus.on("restored", doRestore);
|
||||
|
||||
app.api.addEventListener("status", (ev: CustomEvent) => {
|
||||
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
import { Button } from "@gradio/button";
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import nodeState from "$lib/stores/nodeState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import { Button } from "@gradio/button";
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import { Checkbox } from "@gradio/form"
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import nodeState from "$lib/stores/nodeState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import { download } from "$lib/utils"
|
||||
|
||||
import { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import type { ComfyAPIStatus } from "$lib/api";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
|
||||
import { getComponentForWidgetState } from "$lib/utils"
|
||||
import { f7 } from "framework7-svelte"
|
||||
|
||||
export let subworkflowID: number = -1;
|
||||
let app: ComfyApp = undefined;
|
||||
@@ -29,24 +16,7 @@
|
||||
<Page name="subworkflow">
|
||||
<Navbar title="Workflow {subworkflowID}" backLink="Back" />
|
||||
|
||||
{#each Object.entries($widgetState) as [id, ws]}
|
||||
{@const node = app.lGraph.getNodeById(id)}
|
||||
<div class:is-executing={$queueState.runningNodeId === node.id}>
|
||||
<Block>
|
||||
<label for={String(id)} class={$uiState.unlocked ? "edit-title-label" : ""}>
|
||||
<BlockTitle>
|
||||
{node.title}
|
||||
</BlockTitle>
|
||||
</label>
|
||||
{#each $widgetState[id] as item}
|
||||
<svelte:component this={getComponentForWidgetState(item)} {item} />
|
||||
{/each}
|
||||
{#if $uiState.unlocked}
|
||||
<div class="handle" on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
||||
{/if}
|
||||
</Block>
|
||||
</div>
|
||||
{/each}
|
||||
<div>Workflow!</div>
|
||||
</Page>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true
|
||||
"checkJs": true,
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"$lib": ["lib"],
|
||||
"$lib/*": ["lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
||||
Reference in New Issue
Block a user