diff --git a/README.md b/README.md index 063d986..8cce78e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/litegraph b/litegraph index 950a78d..fd56d0c 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 950a78df0327d55ddee76acbe1311b2a1515d359 +Subproject commit fd56d0c4e698b7a5bfccb1aa86e15db0f16120df diff --git a/package.json b/package.json index fd2c54b..23ae656 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f34575..0fccfd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cd2c7a0..0cf1b02 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,4 @@ packages: - 'gradio/js/*' - 'gradio/client/js' - - 'litegraph/packages/core' - - 'litegraph/packages/nodes-basic' - - 'litegraph/packages/tsconfig' + - 'litegraph/packages/*' diff --git a/src/App.svelte b/src/App.svelte index 8e91652..821d589 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,377 +1,15 @@ - + diff --git a/src/AppMobile.svelte b/src/AppMobile.svelte index e08e4e3..36abdfd 100644 --- a/src/AppMobile.svelte +++ b/src/AppMobile.svelte @@ -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" diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts new file mode 100644 index 0000000..28778be --- /dev/null +++ b/src/lib/ComfyGraph.ts @@ -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 = new EventEmitter() as TypedEmitter; + + 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); + } +} diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index 82b19ae..3a464bf 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -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, diff --git a/src/lib/GraphSync.ts b/src/lib/GraphSync.ts index 024e83b..9614594 100644 --- a/src/lib/GraphSync.ts +++ b/src/lib/GraphSync.ts @@ -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; - // nodeId -> widgetSubStores[] - private stores: Record = {} + // nodeId -> widgetSubStore + private stores: Record = {} - 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); } } diff --git a/src/lib/IComfyInputSlot.ts b/src/lib/IComfyInputSlot.ts new file mode 100644 index 0000000..612723b --- /dev/null +++ b/src/lib/IComfyInputSlot.ts @@ -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. +} diff --git a/src/lib/api.ts b/src/lib/api.ts index b7fd412..159d455 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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 }; } } diff --git a/src/lib/components/BlockContainer.svelte b/src/lib/components/BlockContainer.svelte new file mode 100644 index 0000000..263ebfb --- /dev/null +++ b/src/lib/components/BlockContainer.svelte @@ -0,0 +1,269 @@ + + +{#if container && children} +
1}> + + {#if container.attrs.showTitle} + + {/if} +
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)} +
+ + {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} +
+ {/if} +
+ {/each} +
+ {#if showHandles} +
+ {/if} + +
+{/if} + + diff --git a/src/lib/components/ComfyApp.svelte b/src/lib/components/ComfyApp.svelte index b4e5887..db027f5 100644 --- a/src/lib/components/ComfyApp.svelte +++ b/src/lib/components/ComfyApp.svelte @@ -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(".pane-wrapper") for (const wrapper of wrappers) { const paneNode = wrapper.parentNode as HTMLElement; // get the node inside the @@ -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() + } -
+
@@ -173,13 +177,29 @@ + + + - + + +
+ + diff --git a/src/lib/components/ComfyUIPane.svelte b/src/lib/components/ComfyUIPane.svelte index 7d29903..b9a95b1 100644 --- a/src/lib/components/ComfyUIPane.svelte +++ b/src/lib/components/ComfyUIPane.svelte @@ -1,104 +1,118 @@ -
- - - +
+
- diff --git a/src/lib/components/ComfyUIPane.ts b/src/lib/components/ComfyUIPane.ts deleted file mode 100644 index c555e03..0000000 --- a/src/lib/components/ComfyUIPane.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { LGraphNode } from "@litegraph-ts/core" - -export type DragItem = { - id: number, - node: LGraphNode, - isNodeExecuting?: boolean -} diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte new file mode 100644 index 0000000..1fd3b41 --- /dev/null +++ b/src/lib/components/WidgetContainer.svelte @@ -0,0 +1,93 @@ + + + +{#if container} + +{:else if widget && widget.node} +
1} + class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)} + class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id} + > + +
+ {#if showHandles} +
+ {/if} +{/if} + + diff --git a/src/lib/components/menu/ContextMenu.svelte b/src/lib/components/menu/ContextMenu.svelte new file mode 100644 index 0000000..a4c963a --- /dev/null +++ b/src/lib/components/menu/ContextMenu.svelte @@ -0,0 +1,47 @@ + + +{#if showMenu} + + + + + + + + Look! An icon! + + +{/if} + + diff --git a/src/lib/components/menu/Icon.svelte b/src/lib/components/menu/Icon.svelte new file mode 100644 index 0000000..0af8dc8 --- /dev/null +++ b/src/lib/components/menu/Icon.svelte @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/components/menu/Menu.svelte b/src/lib/components/menu/Menu.svelte new file mode 100644 index 0000000..6322a94 --- /dev/null +++ b/src/lib/components/menu/Menu.svelte @@ -0,0 +1,45 @@ + + + + + + diff --git a/src/lib/components/menu/MenuDivider.svelte b/src/lib/components/menu/MenuDivider.svelte new file mode 100644 index 0000000..13041d5 --- /dev/null +++ b/src/lib/components/menu/MenuDivider.svelte @@ -0,0 +1,9 @@ +
+ + diff --git a/src/lib/components/menu/MenuOption.svelte b/src/lib/components/menu/MenuOption.svelte new file mode 100644 index 0000000..bb7cca9 --- /dev/null +++ b/src/lib/components/menu/MenuOption.svelte @@ -0,0 +1,50 @@ + + +
+ {#if text} + {text} + {:else} + + {/if} +
+ + diff --git a/src/lib/components/menu/menu.ts b/src/lib/components/menu/menu.ts new file mode 100644 index 0000000..108b2fd --- /dev/null +++ b/src/lib/components/menu/menu.ts @@ -0,0 +1,3 @@ +const key = {}; + +export { key }; diff --git a/src/lib/defaultGraph.ts b/src/lib/defaultGraph.ts index eecb0e3..0ef7690 100644 --- a/src/lib/defaultGraph.ts +++ b/src/lib/defaultGraph.ts @@ -1,401 +1,3314 @@ import type SerializedAppState from "./ComfyApp" const defaultGraph: SerializedAppState = { - createdBy: "ComfyBox", - version: 1, - workflow: { - last_node_id: 9, - last_link_id: 9, - nodes: [ - { - id: 7, - type: "CLIPTextEncode", - pos: [ - 413, - 389 + createdBy: "ComfyBox", + version: 1, + workflow: { + last_node_id: 75, + last_link_id: 101, + nodes: [ + { + id: 19, + type: "ui/slider", + pos: [ + 77.09750000000287, + 235.9643000000003 + ], + size: [ + 210, + 158 + ], + flags: { + collapsed: true + }, + order: 0, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: null + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 18 + ], + _data: 8 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 0 + }, + { + name: "max", + type: "number", + links: null, + _data: 100 + }, + { + name: "step", + type: "number", + links: null, + _data: 0.5 + }, + { + name: "precision", + type: "number", + links: null, + _data: 0 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 8, + min: 0, + max: 100, + step: 0.5, + precision: 0 + }, + widgets_values: [ + "8.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 8, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 20, + type: "ui/combo", + pos: [ + 77.09750000000287, + 255.9643000000003 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 1, + mode: 0, + inputs: [ + { + name: "value", + type: "string", + link: null + } + ], + outputs: [ + { + name: "value", + type: "string", + links: [ + 19 + ], + _data: "euler" + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + } + ], + title: "UI.Combo", + properties: { + defaultValue: "euler", + values: [ + "euler", + "euler_ancestral", + "heun", + "dpm_2", + "dpm_2_ancestral", + "lms", + "dpm_fast", + "dpm_adaptive", + "dpmpp_2s_ancestral", + "dpmpp_sde", + "dpmpp_2m", + "ddim", + "uni_pc", + "uni_pc_bh2" + ] + }, + widgets_values: [ + "euler" + ], + color: "#223", + bgColor: "#335", + comfyValue: "euler", + shownOutputProperties: {} + }, + { + id: 21, + type: "ui/combo", + pos: [ + 77.09750000000287, + 275.9643000000003 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 2, + mode: 0, + inputs: [ + { + name: "value", + type: "string", + link: null + } + ], + outputs: [ + { + name: "value", + type: "string", + links: [ + 20 + ], + _data: "karras" + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + } + ], + title: "UI.Combo", + properties: { + defaultValue: "karras", + values: [ + "karras", + "normal", + "simple", + "ddim_uniform" + ] + }, + widgets_values: [ + "karras" + ], + color: "#223", + bgColor: "#335", + comfyValue: "karras", + shownOutputProperties: {} + }, + { + id: 22, + type: "ui/slider", + pos: [ + 77.09750000000287, + 355.9643000000003 + ], + size: [ + 210, + 158 + ], + flags: { + collapsed: true + }, + order: 3, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: null + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 21 + ], + _data: 1 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 0 + }, + { + name: "max", + type: "number", + links: null, + _data: 1 + }, + { + name: "step", + type: "number", + links: null, + _data: 0.01 + }, + { + name: "precision", + type: "number", + links: null, + _data: 0 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 1, + min: 0, + max: 1, + step: 0.01, + precision: 0 + }, + widgets_values: [ + "1.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 1, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 33, + type: "ui/text", + pos: [ + -347.6124999999959, + 309.06430000000114 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 4, + mode: 0, + inputs: [ + { + name: "value", + type: "string", + link: null + } + ], + outputs: [ + { + name: "value", + type: "string", + links: [ + 30 + ], + _data: "masterpiece, best quality, 1girl, dress, space opera, portrait, planet landscape, starry sky" + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + } + ], + title: "UI.Text", + properties: { + defaultValue: "", + multiline: true + }, + widgets_values: [ + "masterpiece, best quality, 1girl, dress, space opera, portrait, planet landscape, starry sky" + ], + color: "#223", + bgColor: "#335", + comfyValue: "masterpiece, best quality, 1girl, dress, space opera, portrait, planet landscape, starry sky", + shownOutputProperties: {} + }, + { + id: 35, + type: "ui/text", + pos: [ + -346.40249999999617, + 399.8143000000014 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 5, + mode: 0, + inputs: [ + { + name: "value", + type: "string", + link: null + } + ], + outputs: [ + { + name: "value", + type: "string", + links: [ + 31 + ], + _data: "worst quality, bad quality, nsfw" + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + } + ], + title: "UI.Text", + properties: { + defaultValue: "", + multiline: true + }, + widgets_values: [ + "worst quality, bad quality, nsfw" + ], + color: "#223", + bgColor: "#335", + comfyValue: "worst quality, bad quality, nsfw", + shownOutputProperties: {} + }, + { + id: 40, + type: "VAEDecode", + pos: [ + 532.5875000000013, + 160.9183000000009 + ], + size: [ + 210, + 46 + ], + flags: {}, + order: 31, + mode: 0, + inputs: [ + { + name: "samples", + type: "LATENT", + link: 40, + color_off: "orange", + color_on: "orange" + }, + { + name: "vae", + type: "VAE", + link: 101, + color_off: "orange", + color_on: "orange" + } + ], + outputs: [ + { + name: "IMAGE", + type: "IMAGE", + links: [ + 44 + ], + color_off: "orange", + color_on: "orange", + slot_index: 0 + } + ], + title: "VAEDecode", + properties: {}, + color: "#432", + bgColor: "#653" + }, + { + id: 45, + type: "ui/text", + pos: [ + 779.2900000000001, + 208.44999999999965 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 6, + mode: 0, + inputs: [ + { + name: "value", + type: "string", + link: null + } + ], + outputs: [ + { + name: "value", + type: "string", + links: [ + 43 + ], + _data: "ComfyUI" + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + } + ], + title: "UI.Text", + properties: { + defaultValue: "ComfyUI", + multiline: false + }, + widgets_values: [ + "ComfyUI" + ], + color: "#223", + bgColor: "#335", + comfyValue: "ComfyUI", + shownOutputProperties: {} + }, + { + id: 32, + type: "CLIPTextEncode", + pos: [ + -218, + 285 + ], + size: [ + 216.60000000000002, + 46 + ], + flags: {}, + order: 24, + mode: 0, + inputs: [ + { + name: "text", + type: "string", + link: 30, + config: { + defaultValue: "", + multiline: true + }, + serialize: true + }, + { + name: "clip", + type: "CLIP", + link: 99, + color_off: "orange", + color_on: "orange" + } + ], + outputs: [ + { + name: "CONDITIONING", + type: "CONDITIONING", + links: [ + 33 + ], + color_off: "orange", + color_on: "orange", + slot_index: 0 + } + ], + title: "CLIPTextEncode", + properties: {}, + color: "#432", + bgColor: "#653" + }, + { + id: 34, + type: "CLIPTextEncode", + pos: [ + -216, + 376 + ], + size: [ + 216.60000000000002, + 46 + ], + flags: {}, + order: 25, + mode: 0, + inputs: [ + { + name: "text", + type: "string", + link: 31, + config: { + defaultValue: "", + multiline: true + }, + serialize: true + }, + { + name: "clip", + type: "CLIP", + link: 100, + color_off: "orange", + color_on: "orange" + } + ], + outputs: [ + { + name: "CONDITIONING", + type: "CONDITIONING", + links: [ + 32 + ], + color_off: "orange", + color_on: "orange", + slot_index: 0 + } + ], + title: "CLIPTextEncode", + properties: {}, + color: "#432", + bgColor: "#653" + }, + { + id: 47, + type: "ui/gallery", + pos: [ + 1284, + 165 + ], + size: [ + 210, + 58 + ], + flags: {}, + order: 33, + mode: 0, + inputs: [ + { + name: "images", + type: "OUTPUT", + link: 46 + } + ], + outputs: [], + title: "UI.Gallery", + properties: { + defaultValue: [] + }, + widgets_values: [ + "Images: 4" + ], + color: "#223", + bgColor: "#335", + comfyValue: [ + { + name: null, + data: "http://localhost:8188/view?filename=ComfyUI_00352_.png&subfolder=&type=output" + }, + { + name: null, + data: "http://localhost:8188/view?filename=ComfyUI_00353_.png&subfolder=&type=output" + }, + { + name: null, + data: "http://localhost:8188/view?filename=ComfyUI_00354_.png&subfolder=&type=output" + }, + { + name: null, + data: "http://localhost:8188/view?filename=ComfyUI_00355_.png&subfolder=&type=output" + } + ], + shownOutputProperties: {} + }, + { + id: 49, + type: "events/log", + pos: [ + 771, + 22 + ], + size: [ + 140, + 26 + ], + flags: {}, + order: 17, + mode: 0, + inputs: [ + { + name: "event", + type: -1, + link: 47, + shape: 1 + } + ], + outputs: [], + title: "Log Event", + properties: {} + }, + { + id: 52, + type: "basic/watch", + pos: [ + 1063.1426107321954, + -118.99396962098862 + ], + size: [ + 140, + 26 + ], + flags: {}, + order: 22, + mode: 0, + inputs: [ + { + name: "value", + type: 0, + link: 50, + label: "null", + slot_index: 0 + } + ], + outputs: [], + title: "Watch", + properties: { + value: 1 + } + }, + { + id: 54, + type: "ui/slider", + pos: [ + 1038, + -226 + ], + size: [ + 210, + 158 + ], + flags: {}, + order: 23, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: 52 + } + ], + outputs: [ + { + name: "value", + type: "number", + links: null, + _data: 6 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 0 + }, + { + name: "max", + type: "number", + links: null, + _data: 10 + }, + { + name: "step", + type: "number", + links: null, + _data: 1 + }, + { + name: "precision", + type: "number", + links: null, + _data: 1 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 0, + min: 0, + max: 10, + step: 1, + precision: 1 + }, + widgets_values: [ + "6.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 6, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 48, + type: "ui/button", + pos: [ + 372, + 12 + ], + size: [ + 210, + 78 + ], + flags: {}, + order: 7, + mode: 0, + inputs: [], + outputs: [ + { + name: "event", + type: -2, + links: [ + 47, + 62 + ], + shape: 1, + slot_index: 0 + }, + { + name: "isClicked", + type: "boolean", + links: null, + slot_index: 1, + _data: false + } + ], + title: "UI.Button", + properties: { + defaultValue: false, + message: "bang" + }, + widgets_values: [ + "false" + ], + color: "#223", + bgColor: "#335", + comfyValue: false, + shownOutputProperties: {} + }, + { + id: 50, + type: "actions/copy", + pos: [ + 729, + -125 + ], + size: [ + 210, + 78 + ], + flags: {}, + order: 18, + mode: 0, + inputs: [ + { + name: "", + type: "*", + link: 66 + }, + { + name: "copy", + type: -1, + link: 62, + shape: 1 + } + ], + outputs: [ + { + name: "", + type: "*", + links: [ + 50, + 52 + ], + slot_index: 0 + } + ], + title: "Comfy.CopyAction", + properties: { + value: 6 + } + }, + { + id: 18, + type: "ui/slider", + pos: [ + 79, + 219 + ], + size: [ + 210, + 158 + ], + flags: { + collapsed: true + }, + order: 8, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: null + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 17 + ], + _data: 20 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 1 + }, + { + name: "max", + type: "number", + links: null, + _data: 10000 + }, + { + name: "step", + type: "number", + links: null, + _data: 1 + }, + { + name: "precision", + type: "number", + links: null, + _data: 0 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 20, + min: 1, + max: 10000, + step: 1, + precision: 0 + }, + widgets_values: [ + "20.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 20, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 53, + type: "ui/slider", + pos: [ + 369, + -174 + ], + size: [ + 210, + 158 + ], + flags: {}, + order: 9, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: null + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 66 + ], + slot_index: 0, + _data: 6 + }, + { + name: "changed", + type: -2, + links: [], + shape: 1, + slot_index: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 0 + }, + { + name: "max", + type: "number", + links: null, + _data: 10 + }, + { + name: "step", + type: "number", + links: null, + _data: 1 + }, + { + name: "precision", + type: "number", + links: null, + _data: 1 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 0, + min: 0, + max: 10, + step: 1, + precision: 1 + }, + widgets_values: [ + "6.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 6, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 64, + type: "basic/CompareValues", + pos: [ + -131, + -587 + ], + size: [ + 210, + 78 + ], + flags: {}, + order: 19, + mode: 0, + inputs: [ + { + name: "A", + type: 0, + link: 78 + }, + { + name: "B", + type: 0, + link: 79 + } + ], + outputs: [ + { + name: "true", + type: "boolean", + links: [ + 74 + ], + slot_index: 0, + _data: true + }, + { + name: "false", + type: "boolean", + links: null, + _data: false + } + ], + title: "GenericCompare", + properties: { + A: "randomize", + B: "randomize", + OP: "==", + enabled: true + } + }, + { + id: 39, + type: "ui/slider", + pos: [ + -350, + 554 + ], + size: [ + 210, + 158 + ], + flags: { + collapsed: true + }, + order: 10, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: null + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 38, + 83 + ], + _data: 4 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 1 + }, + { + name: "max", + type: "number", + links: null, + _data: 64 + }, + { + name: "step", + type: "number", + links: null, + _data: 1 + }, + { + name: "precision", + type: "number", + links: null, + _data: 0 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 1, + min: 1, + max: 64, + step: 1, + precision: 0 + }, + widgets_values: [ + "4.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 4, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 66, + type: "basic/string", + pos: [ + -363, + -578 + ], + size: [ + 210, + 38 + ], + flags: {}, + order: 11, + mode: 0, + inputs: [], + outputs: [ + { + name: "string", + type: "string", + links: [ + 78 + ], + slot_index: 0, + _data: "randomize" + } + ], + title: "Const String", + properties: { + value: "randomize" + } + }, + { + id: 57, + type: "ui/combo", + pos: [ + -378, + -304 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 12, + mode: 0, + inputs: [ + { + name: "value", + type: "string", + link: null + } + ], + outputs: [ + { + name: "value", + type: "string", + links: [ + 55, + 79 + ], + slot_index: 0, + _data: "randomize" + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + } + ], + title: "UI.Combo", + properties: { + defaultValue: "randomize", + values: [ + "fixed", + "increment", + "decrement", + "randomize" + ] + }, + widgets_values: [ + "randomize" + ], + color: "#223", + bgColor: "#335", + comfyValue: "randomize", + shownOutputProperties: {} + }, + { + id: 17, + type: "ui/slider", + pos: [ + -48, + -291 + ], + size: [ + 210, + 158 + ], + flags: { + collapsed: false + }, + order: 27, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: 67, + slot_index: 0 + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 68, + 80 + ], + slot_index: 0, + _data: 572904899609889 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: [ + 69 + ], + slot_index: 2, + _data: 0 + }, + { + name: "max", + type: "number", + links: [ + 70 + ], + slot_index: 3, + _data: 18446744073709552000 + }, + { + name: "step", + type: "number", + links: [ + 75 + ], + slot_index: 4, + _data: 1 + }, + { + name: "precision", + type: "number", + links: null, + _data: 0 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 0, + min: 0, + max: 18446744073709552000, + step: 1, + precision: 0 + }, + widgets_values: [ + "572904899609889.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 572904899609889, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 16, + type: "KSampler", + pos: [ + 242, + 156 + ], + size: [ + 241.79999999999998, + 206 + ], + flags: {}, + order: 26, + mode: 0, + inputs: [ + { + name: "model", + type: "MODEL", + link: 98, + color_off: "orange", + color_on: "orange" + }, + { + name: "seed", + type: "number", + link: 80, + config: { + min: 0, + max: 18446744073709552000, + step: 1, + precision: 0, + defaultValue: 0 + }, + serialize: true + }, + { + name: "steps", + type: "number", + link: 17, + config: { + min: 1, + max: 10000, + step: 1, + precision: 0, + defaultValue: 20 + }, + serialize: true + }, + { + name: "cfg", + type: "number", + link: 18, + config: { + min: 0, + max: 100, + step: 0.5, + precision: 0, + defaultValue: 8 + }, + serialize: true + }, + { + name: "sampler_name", + type: "string", + link: 19, + config: { + values: [ + "euler", + "euler_ancestral", + "heun", + "dpm_2", + "dpm_2_ancestral", + "lms", + "dpm_fast", + "dpm_adaptive", + "dpmpp_2s_ancestral", + "dpmpp_sde", + "dpmpp_2m", + "ddim", + "uni_pc", + "uni_pc_bh2" + ], + defaultValue: "euler" + }, + serialize: true + }, + { + name: "scheduler", + type: "string", + link: 20, + config: { + values: [ + "karras", + "normal", + "simple", + "ddim_uniform" + ], + defaultValue: "karras" + }, + serialize: true + }, + { + name: "positive", + type: "CONDITIONING", + link: 33, + color_off: "orange", + color_on: "orange" + }, + { + name: "negative", + type: "CONDITIONING", + link: 32, + color_off: "orange", + color_on: "orange" + }, + { + name: "latent_image", + type: "LATENT", + link: 39, + color_off: "orange", + color_on: "orange" + }, + { + name: "denoise", + type: "number", + link: 21, + config: { + min: 0, + max: 1, + step: 0.01, + precision: 0, + defaultValue: 1 + }, + serialize: true + } + ], + outputs: [ + { + name: "LATENT", + type: "LATENT", + links: [ + 40 + ], + color_off: "orange", + color_on: "orange", + slot_index: 0 + } + ], + title: "KSampler", + properties: {}, + color: "#432", + bgColor: "#653" + }, + { + id: 69, + type: "basic/integer", + pos: [ + 261, + -405 + ], + size: [ + 210, + 38 + ], + flags: {}, + order: 13, + mode: 0, + inputs: [], + outputs: [ + { + name: "value", + type: "number", + links: [ + 85 + ], + label: "1", + slot_index: 0, + _data: 1 + } + ], + title: "Const Integer", + properties: { + value: 1 + } + }, + { + id: 56, + type: "utils/value_control", + pos: [ + -252, + -372 + ], + size: [ + 151.2, + 126 + ], + flags: {}, + order: 34, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: 68 + }, + { + name: "trigger", + type: -1, + link: 73, + shape: 1 + }, + { + name: "action", + type: "string", + link: 55, + config: { + defaultValue: "randomize", + values: [ + "fixed", + "increment", + "decrement", + "randomize" + ] + } + }, + { + name: "min", + type: "number", + link: 69 + }, + { + name: "max", + type: "number", + link: 70 + }, + { + name: "step", + type: "number", + link: 88 + } + ], + outputs: [ + { + name: "value", + type: "*", + links: [ + 67 + ], + slot_index: 0 + } + ], + title: "Comfy.ValueControl", + properties: { + value: 572904899609889, + action: "randomize", + min: 0, + max: 18446744073709552000, + step: 1 + } + }, + { + id: 63, + type: "utils/selector2", + pos: [ + 174, + -565 + ], + size: [ + 140, + 66 + ], + flags: {}, + order: 35, + mode: 0, + inputs: [ + { + name: "select", + type: "boolean", + link: 74 + }, + { + name: "true", + type: "*", + link: 75 + }, + { + name: "false", + type: "*", + link: 86, + slot_index: 2 + } + ], + outputs: [ + { + name: "out", + type: "*", + links: [ + 87, + 88 + ], + slot_index: 0, + _data: 1 + } + ], + title: "Comfy.Selector2", + properties: { + value: null + } + }, + { + id: 36, + type: "EmptyLatentImage", + pos: [ + -220, + 490 + ], + size: [ + 216.60000000000002, + 66 + ], + flags: {}, + order: 28, + mode: 0, + inputs: [ + { + name: "width", + type: "number", + link: 93, + config: { + min: 64, + max: 8192, + step: 8, + precision: 0, + defaultValue: 512 + }, + serialize: true + }, + { + name: "height", + type: "number", + link: 94, + config: { + min: 64, + max: 8192, + step: 8, + precision: 0, + defaultValue: 512 + }, + serialize: true + }, + { + name: "batch_size", + type: "number", + link: 38, + config: { + min: 1, + max: 64, + step: 1, + precision: 0, + defaultValue: 1 + }, + serialize: true + } + ], + outputs: [ + { + name: "LATENT", + type: "LATENT", + links: [ + 39 + ], + color_off: "orange", + color_on: "orange", + slot_index: 0 + } + ], + title: "EmptyLatentImage", + properties: {}, + color: "#432", + bgColor: "#653" + }, + { + id: 38, + type: "ui/slider", + pos: [ + -351, + 532 + ], + size: [ + 210, + 158 + ], + flags: { + collapsed: true + }, + order: 30, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: 91 + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 90, + 94 + ], + slot_index: 0, + _data: 512 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 64 + }, + { + name: "max", + type: "number", + links: null, + _data: 8192 + }, + { + name: "step", + type: "number", + links: null, + _data: 8 + }, + { + name: "precision", + type: "number", + links: null, + _data: 0 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 512, + min: 64, + max: 8192, + step: 8, + precision: 0 + }, + widgets_values: [ + "512.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 512, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 37, + type: "ui/slider", + pos: [ + -352, + 509 + ], + size: [ + 210, + 158 + ], + flags: { + collapsed: true + }, + order: 29, + mode: 0, + inputs: [ + { + name: "value", + type: "number", + link: 92 + } + ], + outputs: [ + { + name: "value", + type: "number", + links: [ + 89, + 93 + ], + slot_index: 0, + _data: 848 + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + }, + { + name: "min", + type: "number", + links: null, + _data: 64 + }, + { + name: "max", + type: "number", + links: null, + _data: 8192 + }, + { + name: "step", + type: "number", + links: null, + _data: 8 + }, + { + name: "precision", + type: "number", + links: null, + _data: 0 + } + ], + title: "UI.Slider", + properties: { + defaultValue: 512, + min: 64, + max: 8192, + step: 8, + precision: 0 + }, + widgets_values: [ + "848.000" + ], + color: "#223", + bgColor: "#335", + comfyValue: 848, + shownOutputProperties: { + min: { + type: "number", + index: 2 + }, + max: { + type: "number", + index: 3 + }, + step: { + type: "number", + index: 4 + }, + precision: { + type: "number", + index: 5 + } + } + }, + { + id: 71, + type: "actions/swap", + pos: [ + -527, + 486 + ], + size: [ + 140, + 66 + ], + flags: { + collapsed: false + }, + order: 37, + mode: 0, + inputs: [ + { + name: "A", + type: "*", + link: 89 + }, + { + name: "B", + type: "*", + link: 90 + }, + { + name: "swap", + type: -1, + link: 95, + shape: 1 + } + ], + outputs: [ + { + name: "B", + type: "*", + links: [ + 91 + ], + slot_index: 0 + }, + { + name: "A", + type: "*", + links: [ + 92 + ], + slot_index: 1 + } + ], + title: "Comfy.SwapAction", + properties: {} + }, + { + id: 62, + type: "ui/button", + pos: [ + -648, + 550 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 14, + mode: 0, + inputs: [], + outputs: [ + { + name: "clicked", + type: -2, + links: [ + 95 + ], + shape: 1, + slot_index: 0 + }, + { + name: "isClicked", + type: "boolean", + links: null, + _data: false + } + ], + title: "UI.Button", + properties: { + defaultValue: false, + message: "bang" + }, + widgets_values: [ + "false" + ], + color: "#223", + bgColor: "#335", + comfyValue: false, + shownOutputProperties: {} + }, + { + id: 44, + type: "SaveImage", + pos: [ + 888, + 164 + ], + size: [ + 315, + 78 + ], + flags: {}, + order: 32, + mode: 0, + inputs: [ + { + name: "images", + type: "IMAGE", + link: 44, + color_off: "orange", + color_on: "orange", + slot_index: 0 + }, + { + name: "filename_prefix", + type: "string", + link: 43, + config: { + defaultValue: "ComfyUI", + multiline: false + }, + serialize: true + } + ], + outputs: [ + { + name: "output", + type: "OUTPUT", + links: [ + 46 + ], + slot_index: 0 + } + ], + title: "SaveImage", + properties: {}, + color: "#432", + bgColor: "#653" + }, + { + id: 55, + type: "actions/after_queued", + pos: [ + -591, + -349 + ], + size: [ + 193.2, + 46 + ], + flags: {}, + order: 15, + mode: 0, + inputs: [], + outputs: [ + { + name: "afterQueued", + type: -2, + links: [ + 73 + ], + shape: 1, + slot_index: 0, + _data: null + }, + { + name: "prompt", + type: "*", + links: null + } + ], + title: "Comfy.AfterQueuedAction", + properties: { + prompt: null + } + }, + { + id: 75, + type: "ui/combo", + pos: [ + -420.44163113999997, + 169.75962460075795 + ], + size: [ + 210, + 78 + ], + flags: { + collapsed: true + }, + order: 16, + mode: 0, + inputs: [ + { + name: "value", + type: "string", + link: null + } + ], + outputs: [ + { + name: "value", + type: "string", + links: [ + 97 + ], + _data: "refslaveV2_v2.safetensors" + }, + { + name: "changed", + type: -2, + links: null, + shape: 1 + } + ], + title: "UI.Combo", + properties: { + defaultValue: "AbyssOrangeMix2_nsfw.safetensors", + values: [ + "AbyssOrangeMix2_nsfw.safetensors", + "refslaveV2_v2.safetensors" + ] + }, + widgets_values: [ + "refslaveV2_v2.safetensors" + ], + color: "#223", + bgColor: "#335", + comfyValue: "refslaveV2_v2.safetensors", + shownOutputProperties: {} + }, + { + id: 74, + type: "CheckpointLoaderSimple", + pos: [ + -290, + 146 + ], + size: [ + 277.2, + 66 + ], + flags: {}, + order: 21, + mode: 0, + inputs: [ + { + name: "ckpt_name", + type: "string", + link: 97, + config: { + values: [ + "AbyssOrangeMix2_nsfw.safetensors", + "refslaveV2_v2.safetensors" + ], + defaultValue: "AbyssOrangeMix2_nsfw.safetensors" + }, + widgetNodeType: "ui/combo", + serialize: true, + defaultWidgetNode: null + } + ], + outputs: [ + { + name: "MODEL", + type: "MODEL", + links: [ + 98 + ], + slot_index: 0 + }, + { + name: "CLIP", + type: "CLIP", + links: [ + 99, + 100 + ], + slot_index: 1 + }, + { + name: "VAE", + type: "VAE", + links: [ + 101 + ], + slot_index: 2 + } + ], + title: "CheckpointLoaderSimple", + properties: {}, + color: "#432", + bgColor: "#653" + }, + { + id: 70, + type: "basic/watch", + pos: [ + 341, + -543 + ], + size: [ + 140, + 26 + ], + flags: { + collapsed: true + }, + order: 36, + mode: 0, + inputs: [ + { + name: "value", + type: 0, + link: 87, + label: "1.000" + } + ], + outputs: [], + title: "Watch", + properties: { + value: 1 + } + }, + { + id: 67, + type: "math/operation", + pos: [ + 519, + -409 + ], + size: [ + 140, + 46 + ], + flags: {}, + order: 20, + mode: 0, + inputs: [ + { + name: "A", + type: "number,array,object", + link: 85 + }, + { + name: "B", + type: "number", + link: 83 + } + ], + outputs: [ + { + name: "=", + type: "number", + links: [ + 86 + ], + slot_index: 0, + _data: 5 + } + ], + title: "Operation", + properties: { + A: 1, + B: 4, + OP: "+" + } + } ], - size: [ - 425.27801513671875, - 180.6060791015625 - ], - flags: {}, - order: 3, - mode: 0, - inputs: [ - { - name: "clip", - type: "CLIP", - link: 5 - } - ], - outputs: [ - { - name: "CONDITIONING", - type: "CONDITIONING", - links: [ - 6 + links: [ + [ + 17, + 18, + 0, + 16, + 2, + "number" ], - slot_index: 0 - } - ], - title: "CLIPTextEncode", - properties: {}, - widgets_values: [ - "bad hands" - ] - }, - { - id: 6, - type: "CLIPTextEncode", - pos: [ - 415, - 186 - ], - size: [ - 422.84503173828125, - 164.31304931640625 - ], - flags: {}, - order: 2, - mode: 0, - inputs: [ - { - name: "clip", - type: "CLIP", - link: 3 - } - ], - outputs: [ - { - name: "CONDITIONING", - type: "CONDITIONING", - links: [ - 4 + [ + 18, + 19, + 0, + 16, + 3, + "number" ], - slot_index: 0 - } - ], - title: "CLIPTextEncode", - properties: {}, - widgets_values: [ - "masterpiece best quality girl" - ] - }, - { - id: 5, - type: "EmptyLatentImage", - pos: [ - 473, - 609 - ], - size: [ - 315, - 106 - ], - flags: {}, - order: 0, - mode: 0, - inputs: [], - outputs: [ - { - name: "LATENT", - type: "LATENT", - links: [ - 2 + [ + 19, + 20, + 0, + 16, + 4, + "string" ], - slot_index: 0 - } - ], - title: "EmptyLatentImage", - properties: {}, - widgets_values: [ - 512, - 512, - 1 - ] - }, - { - id: 3, - type: "KSampler", - pos: [ - 863, - 186 - ], - size: [ - 315, - 262 - ], - flags: {}, - order: 4, - mode: 0, - inputs: [ - { - name: "model", - type: "MODEL", - link: 1 - }, - { - name: "positive", - type: "CONDITIONING", - link: 4 - }, - { - name: "negative", - type: "CONDITIONING", - link: 6 - }, - { - name: "latent_image", - type: "LATENT", - link: 2 - } - ], - outputs: [ - { - name: "LATENT", - type: "LATENT", - links: [ - 7 + [ + 20, + 21, + 0, + 16, + 5, + "string" ], - slot_index: 0 - } - ], - title: "KSampler", - properties: {}, - widgets_values: [ - 8566257, - "randomize", - 8, - 8, - "euler", - "normal", - 1 - ] - }, - { - id: 8, - type: "VAEDecode", - pos: [ - 1209, - 188 - ], - size: [ - 210, - 46 - ], - flags: {}, - order: 5, - mode: 0, - inputs: [ - { - name: "samples", - type: "LATENT", - link: 7 - }, - { - name: "vae", - type: "VAE", - link: 8 - } - ], - outputs: [ - { - name: "IMAGE", - type: "IMAGE", - links: [ - 9 + [ + 21, + 22, + 0, + 16, + 9, + "number" ], - slot_index: 0 - } - ], - title: "VAEDecode", - properties: {} - }, - { - id: 9, - type: "SaveImage", - pos: [ - 1451, - 189 - ], - size: [ - 210, - 82 - ], - flags: {}, - order: 6, - mode: 0, - inputs: [ - { - name: "images", - type: "IMAGE", - link: 9 - } - ], - outputs: [], - title: "SaveImage", - properties: {}, - widgets_values: [ - [], - "ComfyUI" - ] - }, - { - id: 4, - type: "CheckpointLoaderSimple", - pos: [ - 26, - 474 - ], - size: [ - 315, - 98 - ], - flags: {}, - order: 1, - mode: 0, - inputs: [], - outputs: [ - { - name: "MODEL", - type: "MODEL", - links: [ - 1 + [ + 30, + 33, + 0, + 32, + 0, + "string" ], - slot_index: 0 - }, - { - name: "CLIP", - type: "CLIP", - links: [ - 3, - 5 + [ + 31, + 35, + 0, + 34, + 0, + "string" ], - slot_index: 1 - }, - { - name: "VAE", - type: "VAE", - links: [ - 8 + [ + 32, + 34, + 0, + 16, + 7, + "CONDITIONING" ], - slot_index: 2 - } + [ + 33, + 32, + 0, + 16, + 6, + "CONDITIONING" + ], + [ + 38, + 39, + 0, + 36, + 2, + "number" + ], + [ + 39, + 36, + 0, + 16, + 8, + "LATENT" + ], + [ + 40, + 16, + 0, + 40, + 0, + "LATENT" + ], + [ + 43, + 45, + 0, + 44, + 1, + "string" + ], + [ + 44, + 40, + 0, + 44, + 0, + "IMAGE" + ], + [ + 46, + 44, + 0, + 47, + 0, + "OUTPUT" + ], + [ + 47, + 48, + 0, + 49, + 0, + -1 + ], + [ + 50, + 50, + 0, + 52, + 0, + "*" + ], + [ + 52, + 50, + 0, + 54, + 0, + "number" + ], + [ + 55, + 57, + 0, + 56, + 2, + "string" + ], + [ + 62, + 48, + 0, + 50, + 1, + -1 + ], + [ + 66, + 53, + 0, + 50, + 0, + "*" + ], + [ + 67, + 56, + 0, + 17, + 0, + "number" + ], + [ + 68, + 17, + 0, + 56, + 0, + "number" + ], + [ + 69, + 17, + 2, + 56, + 3, + "number" + ], + [ + 70, + 17, + 3, + 56, + 4, + "number" + ], + [ + 73, + 55, + 0, + 56, + 1, + -1 + ], + [ + 74, + 64, + 0, + 63, + 0, + "boolean" + ], + [ + 75, + 17, + 4, + 63, + 1, + "*" + ], + [ + 78, + 66, + 0, + 64, + 0, + "string" + ], + [ + 79, + 57, + 0, + 64, + 1, + "string" + ], + [ + 80, + 17, + 0, + 16, + 1, + "number" + ], + [ + 83, + 39, + 0, + 67, + 1, + "number" + ], + [ + 85, + 69, + 0, + 67, + 0, + "number,array,object" + ], + [ + 86, + 67, + 0, + 63, + 2, + "*" + ], + [ + 87, + 63, + 0, + 70, + 0, + "*" + ], + [ + 88, + 63, + 0, + 56, + 5, + "number" + ], + [ + 89, + 37, + 0, + 71, + 0, + "*" + ], + [ + 90, + 38, + 0, + 71, + 1, + "*" + ], + [ + 91, + 71, + 0, + 38, + 0, + "number" + ], + [ + 92, + 71, + 1, + 37, + 0, + "number" + ], + [ + 93, + 37, + 0, + 36, + 0, + "number" + ], + [ + 94, + 38, + 0, + 36, + 1, + "number" + ], + [ + 95, + 62, + 0, + 71, + 2, + -1 + ], + [ + 97, + 75, + 0, + 74, + 0, + "string" + ], + [ + 98, + 74, + 0, + 16, + 0, + "MODEL" + ], + [ + 99, + 74, + 1, + 32, + 1, + "CLIP" + ], + [ + 100, + 74, + 1, + 34, + 1, + "CLIP" + ], + [ + 101, + 74, + 2, + 40, + 1, + "VAE" + ] ], - title: "CheckpointLoaderSimple", - properties: {}, - widgets_values: [ - "v1-5-pruned-emaonly.ckpt" - ] - } - ], - links: [ - [ - 1, - 4, - 0, - 3, - 0, - "MODEL" - ], - [ - 2, - 5, - 0, - 3, - 3, - "LATENT" - ], - [ - 3, - 4, - 1, - 6, - 0, - "CLIP" - ], - [ - 4, - 6, - 0, - 3, - 1, - "CONDITIONING" - ], - [ - 5, - 4, - 1, - 7, - 0, - "CLIP" - ], - [ - 6, - 7, - 0, - 3, - 2, - "CONDITIONING" - ], - [ - 7, - 3, - 0, - 8, - 0, - "LATENT" - ], - [ - 8, - 4, - 2, - 8, - 1, - "VAE" - ], - [ - 9, - 8, - 0, - 9, - 0, - "IMAGE" - ] - ], - groups: [], - config: {}, - extra: {}, - version: 10 - }, - panes: { - panels: [ - [ - { - nodeId: 7 + groups: [], + config: {}, + extra: {}, + version: 10 + }, + layout: { + root: "0", + allItems: { + 0: { + dragItem: { + type: "container", + id: "0", + attrs: { + title: "Container", + showTitle: false, + direction: "horizontal", + classes: "" + } + }, + children: [ + "1", + "2" + ] + }, + 1: { + dragItem: { + type: "container", + id: "1", + attrs: { + title: "Container", + showTitle: false, + direction: "vertical", + classes: "" + } + }, + children: [ + "16", + "33", + "41", + "52" + ], + parent: "0" + }, + 2: { + dragItem: { + type: "container", + id: "2", + attrs: { + title: "Container", + showTitle: false, + direction: "vertical", + classes: "" + } + }, + children: [ + "37", + "35", + "27" + ], + parent: "0" + }, + 10: { + dragItem: { + type: "widget", + id: "10", + nodeId: 17, + attrs: { + title: "seed", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "48" + }, + 11: { + dragItem: { + type: "widget", + id: "11", + nodeId: 18, + attrs: { + title: "steps", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "16" + }, + 12: { + dragItem: { + type: "widget", + id: "12", + nodeId: 19, + attrs: { + title: "cfg", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "16" + }, + 13: { + dragItem: { + type: "widget", + id: "13", + nodeId: 20, + attrs: { + title: "sampler_name", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "16" + }, + 14: { + dragItem: { + type: "widget", + id: "14", + nodeId: 21, + attrs: { + title: "scheduler", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "16" + }, + 15: { + dragItem: { + type: "widget", + id: "15", + nodeId: 22, + attrs: { + title: "denoise", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "16" + }, + 16: { + dragItem: { + type: "container", + id: "16", + attrs: { + title: "KSampler", + showTitle: true, + direction: "vertical", + classes: "" + } + }, + children: [ + "48", + "11", + "12", + "13", + "14", + "15" + ], + parent: "1" + }, + 26: { + dragItem: { + type: "widget", + id: "26", + nodeId: 33, + attrs: { + title: "text", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "27" + }, + 27: { + dragItem: { + type: "container", + id: "27", + attrs: { + title: "Conditioning", + showTitle: true, + direction: "vertical", + classes: "" + } + }, + children: [ + "26", + "28" + ], + parent: "2" + }, + 28: { + dragItem: { + type: "widget", + id: "28", + nodeId: 35, + attrs: { + title: "text", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "27" + }, + 30: { + dragItem: { + type: "widget", + id: "30", + nodeId: 37, + attrs: { + title: "width", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "47" + }, + 31: { + dragItem: { + type: "widget", + id: "31", + nodeId: 38, + attrs: { + title: "height", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "47" + }, + 32: { + dragItem: { + type: "widget", + id: "32", + nodeId: 39, + attrs: { + title: "batch_size", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "33" + }, + 33: { + dragItem: { + type: "container", + id: "33", + attrs: { + title: "EmptyLatentImage", + showTitle: true, + direction: "vertical", + classes: "" + } + }, + children: [ + "32", + "47" + ], + parent: "1" + }, + 34: { + dragItem: { + type: "widget", + id: "34", + nodeId: 45, + attrs: { + title: "filename_prefix", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "35" + }, + 35: { + dragItem: { + type: "container", + id: "35", + attrs: { + title: "SaveImage", + showTitle: true, + direction: "vertical", + classes: "" + } + }, + children: [ + "34" + ], + parent: "2" + }, + 37: { + dragItem: { + type: "widget", + id: "37", + nodeId: 47, + attrs: { + title: "Widget", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "2" + }, + 38: { + dragItem: { + type: "widget", + id: "38", + nodeId: 48, + attrs: { + title: "copy", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "41" + }, + 39: { + dragItem: { + type: "widget", + id: "39", + nodeId: 53, + attrs: { + title: "value", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "41" + }, + 40: { + dragItem: { + type: "widget", + id: "40", + nodeId: 54, + attrs: { + title: "Widget", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "41" + }, + 41: { + dragItem: { + type: "container", + id: "41", + attrs: { + title: "Copy Test", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [ + "39", + "38", + "40" + ], + parent: "1" + }, + 42: { + dragItem: { + type: "widget", + id: "42", + nodeId: 57, + attrs: { + title: "action", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "48" + }, + 46: { + dragItem: { + type: "widget", + id: "46", + nodeId: 62, + attrs: { + title: "trigger", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "47" + }, + 47: { + dragItem: { + type: "container", + id: "47", + attrs: { + title: "Size", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [ + "30", + "46", + "31" + ], + parent: "33" + }, + 48: { + dragItem: { + type: "container", + id: "48", + attrs: { + title: "Seed", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [ + "10", + "42" + ], + parent: "16" + }, + 51: { + dragItem: { + type: "widget", + id: "51", + nodeId: 75, + attrs: { + title: "ckpt_name", + showTitle: true, + direction: "horizontal", + classes: "" + } + }, + children: [], + parent: "52" + }, + 52: { + dragItem: { + type: "container", + id: "52", + attrs: { + title: "CheckpointLoaderSimple", + showTitle: true, + direction: "vertical", + classes: "" + } + }, + children: [ + "51" + ], + parent: "1" + } }, - { - nodeId: 3 - } - ], - [ - { - nodeId: 6 - }, - { - nodeId: 9 - }, - { - nodeId: 4 - } - ], - [ - { - nodeId: 5 - } - ] - ] - } + currentId: 53 + }, + canvas: { + offset: [ + 0, + 0 + ], + scale: 1 + } +} + +const blankGraph: SerializedAppState = { + createdBy: "ComfyBox", + version: 1, + workflow: { + last_node_id: 0, + last_link_id: 0, + nodes: [], + links: [], + groups: [], + config: {}, + extra: {}, + version: 0 + }, + panes: {} } export default defaultGraph; +export { blankGraph } diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts new file mode 100644 index 0000000..971c029 --- /dev/null +++ b/src/lib/nodes/ComfyActionNodes.ts @@ -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 { + 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 { + 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( + "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 { +} + +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" +}) diff --git a/src/lib/nodes/ComfyBackendNode.ts b/src/lib/nodes/ComfyBackendNode.ts new file mode 100644 index 0000000..3cde566 --- /dev/null +++ b/src/lib/nodes/ComfyBackendNode.ts @@ -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(); + } + } + } + } + } +} diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index f3e6f86..acc8c24 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -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, +} 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 + } + } + } } diff --git a/src/lib/nodes/ComfyImageNodes.ts b/src/lib/nodes/ComfyImageNodes.ts deleted file mode 100644 index 996e1e8..0000000 --- a/src/lib/nodes/ComfyImageNodes.ts +++ /dev/null @@ -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 = []; - 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 { -} diff --git a/src/lib/nodes/ComfyReroute.ts b/src/lib/nodes/ComfyReroute.ts index 4127446..57d4e46 100644 --- a/src/lib/nodes/ComfyReroute.ts +++ b/src/lib/nodes/ComfyReroute.ts @@ -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; diff --git a/src/lib/nodes/ComfySelector.ts b/src/lib/nodes/ComfySelector.ts new file mode 100644 index 0000000..d152d9c --- /dev/null +++ b/src/lib/nodes/ComfySelector.ts @@ -0,0 +1,120 @@ +import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode from "./ComfyGraphNode"; + +export interface ComfySelectorProperties extends Record { + 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 { + 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" +}) diff --git a/src/lib/nodes/ComfyValueControl.ts b/src/lib/nodes/ComfyValueControl.ts new file mode 100644 index 0000000..fed07ec --- /dev/null +++ b/src/lib/nodes/ComfyValueControl.ts @@ -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 { + 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" +}) diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts new file mode 100644 index 0000000..7c127a7 --- /dev/null +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -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 { + 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 extends ComfyGraphNode { + abstract properties: ComfyWidgetProperties; + + value: Writable + propsChanged: Writable = 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 = {} + 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( + "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 { + 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 { + 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 { + 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 { + 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 { + 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" +}) diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index e355079..c2777d1 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -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" diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts new file mode 100644 index 0000000..a299319 --- /dev/null +++ b/src/lib/stores/layoutState.ts @@ -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, + allItemsByNode: Record, + 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, index: number) => ContainerLayout, + addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial, 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) => 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 & LayoutStateOps; +const store: Writable = 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 = {}, 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 = {}, 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 = {}): 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, + 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 = {} + 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 = {} + const allItemsByNode: Record = {} + 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; diff --git a/src/lib/stores/nodeState.ts b/src/lib/stores/nodeState.ts deleted file mode 100644 index ccd6eae..0000000 --- a/src/lib/stores/nodeState.ts +++ /dev/null @@ -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 - -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; -type WritableNodeStateStore = Writable & NodeStateOps; - -const store: Writable = 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; diff --git a/src/lib/stores/uiState.ts b/src/lib/stores/uiState.ts index 0fbb6b8..e9422be 100644 --- a/src/lib/stores/uiState.ts +++ b/src/lib/stores/uiState.ts @@ -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; -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 = { diff --git a/src/lib/stores/widgetState.ts b/src/lib/stores/widgetState.ts deleted file mode 100644 index 06084b9..0000000 --- a/src/lib/stores/widgetState.ts +++ /dev/null @@ -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 - -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) => void, - findWidgetByName: (nodeId: number, name: string) => WidgetUIState | null, - clear: () => void, -} - -export type WidgetStateStore = Record; -type WritableWidgetStateStore = Writable & WidgetStateOps; - -const store: Writable = 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).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) { - 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; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6f24cad..f5c47a7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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) { +}; diff --git a/src/lib/widgets.ts b/src/lib/widgets.ts index 2bf5bab..e3083aa 100644 --- a/src/lib/widgets.ts +++ b/src/lib/widgets.ts @@ -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 { + 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 -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 diff --git a/src/lib/widgets/ButtonWidget.svelte b/src/lib/widgets/ButtonWidget.svelte new file mode 100644 index 0000000..b2b8b6e --- /dev/null +++ b/src/lib/widgets/ButtonWidget.svelte @@ -0,0 +1,44 @@ + + +
+ {#if node !== null} + + {/if} +
+ + diff --git a/src/lib/widgets/ComboWidget.svelte b/src/lib/widgets/ComboWidget.svelte index 36a0103..6d1cedf 100644 --- a/src/lib/widgets/ComboWidget.svelte +++ b/src/lib/widgets/ComboWidget.svelte @@ -1,50 +1,117 @@ -
- {#if item !== null && option !== undefined} - + {/if} + {/key}
- diff --git a/src/lib/widgets/ComfyGalleryWidget.svelte b/src/lib/widgets/ComfyGalleryWidget.svelte index ef8228a..e69de29 100644 --- a/src/lib/widgets/ComfyGalleryWidget.svelte +++ b/src/lib/widgets/ComfyGalleryWidget.svelte @@ -1,57 +0,0 @@ - - - - diff --git a/src/lib/widgets/ComfyGalleryWidget.ts b/src/lib/widgets/ComfyGalleryWidget.ts deleted file mode 100644 index fa0b747..0000000 --- a/src/lib/widgets/ComfyGalleryWidget.ts +++ /dev/null @@ -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]; // src and alt/title, gradio format - -export interface ComfyGalleryWidgetOptions extends WidgetPanelOptions { -} - -export default class ComfyGalleryWidget extends ComfyWidget { - 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([]) - } - } -} diff --git a/src/lib/widgets/ComfyValueControlWidget.ts b/src/lib/widgets/ComfyValueControlWidget.ts deleted file mode 100644 index 065d6f9..0000000 --- a/src/lib/widgets/ComfyValueControlWidget.ts +++ /dev/null @@ -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 { - 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); - } -} diff --git a/src/lib/widgets/ComfyWidget.ts b/src/lib/widgets/ComfyWidget.ts index eec08ab..f90e86b 100644 --- a/src/lib/widgets/ComfyWidget.ts +++ b/src/lib/widgets/ComfyWidget.ts @@ -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 implements IWidget { name: string; @@ -27,7 +26,6 @@ export default abstract class ComfyWidget implements IWidget + 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 | null = null; + let propsChanged: Writable | 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('div.block div > img') + if (images != null) { + images.forEach(ImageViewer.instance.setupImageForLightbox.bind(ImageViewer.instance)); + } + ImageViewer.instance.updateOnBackgroundChange(); + }, 200) + } + + + + + diff --git a/src/lib/widgets/RangeWidget.svelte b/src/lib/widgets/RangeWidget.svelte index 20f1680..6e50522 100644 --- a/src/lib/widgets/RangeWidget.svelte +++ b/src/lib/widgets/RangeWidget.svelte @@ -1,35 +1,52 @@
- {#if item !== null && option !== null} + {#if node !== null && option !== null} - 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 | null = null; + let propsChanged: Writable | 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 + }
- {#if item !== null && itemValue !== null} + {#if node !== null && nodeValue !== null} - 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; diff --git a/src/mobile/routes/home.svelte b/src/mobile/routes/home.svelte index 5d61f2d..c26bb47 100644 --- a/src/mobile/routes/home.svelte +++ b/src/mobile/routes/home.svelte @@ -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); diff --git a/src/mobile/routes/list-subworkflows.svelte b/src/mobile/routes/list-subworkflows.svelte index 98f6706..c9a4fea 100644 --- a/src/mobile/routes/list-subworkflows.svelte +++ b/src/mobile/routes/list-subworkflows.svelte @@ -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" diff --git a/src/mobile/routes/subworkflow.svelte b/src/mobile/routes/subworkflow.svelte index 0e1b905..88840b8 100644 --- a/src/mobile/routes/subworkflow.svelte +++ b/src/mobile/routes/subworkflow.svelte @@ -1,22 +1,9 @@