@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"/>
|
<div id="app-root"/>
|
||||||
<script type="module" src='/src/main-desktop.ts'></script>
|
<script type="module" src='/src/main-desktop.ts'></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
2
klecks
2
klecks
Submodule klecks updated: a36de3203f...f08ba31888
Submodule litegraph updated: 95788fdcfb...db6a916757
@@ -8,7 +8,7 @@
|
|||||||
<meta name="theme-color" content="#2196f3">
|
<meta name="theme-color" content="#2196f3">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="mobile">
|
<div id="app-root" class="mobile">
|
||||||
<script type="module" src='/src/main-mobile.ts'></script>
|
<script type="module" src='/src/main-mobile.ts'></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
"test:inspect": "vitest --inspect-brk --single-thread",
|
||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
"format": "prettier --plugin-search-dir . --write .",
|
"format": "prettier --plugin-search-dir . --write .",
|
||||||
"svelte-check": "svelte-check",
|
"svelte-check": "svelte-check",
|
||||||
@@ -17,10 +18,14 @@
|
|||||||
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
|
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@floating-ui/core": "^1.2.6",
|
||||||
|
"@floating-ui/dom": "^1.2.8",
|
||||||
"@zerodevx/svelte-toast": "^0.9.3",
|
"@zerodevx/svelte-toast": "^0.9.3",
|
||||||
"eslint": "^8.37.0",
|
"eslint": "^8.37.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"happy-dom": "^9.18.3",
|
||||||
|
"jsdom": "^22.0.0",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"prettier-plugin-svelte": "^2.10.0",
|
"prettier-plugin-svelte": "^2.10.0",
|
||||||
"sass": "^1.61.0",
|
"sass": "^1.61.0",
|
||||||
@@ -33,7 +38,7 @@
|
|||||||
"vite-plugin-static-copy": "^0.14.0",
|
"vite-plugin-static-copy": "^0.14.0",
|
||||||
"vite-plugin-svelte-console-remover": "^1.0.10",
|
"vite-plugin-svelte-console-remover": "^1.0.10",
|
||||||
"vite-tsconfig-paths": "^4.0.8",
|
"vite-tsconfig-paths": "^4.0.8",
|
||||||
"vitest": "^0.25.8"
|
"vitest": "^0.27.3"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
331
pnpm-lock.yaml
generated
331
pnpm-lock.yaml
generated
@@ -122,6 +122,12 @@ importers:
|
|||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(vite@4.3.1)
|
version: 1.0.5(vite@4.3.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@floating-ui/core':
|
||||||
|
specifier: ^1.2.6
|
||||||
|
version: 1.2.6
|
||||||
|
'@floating-ui/dom':
|
||||||
|
specifier: ^1.2.8
|
||||||
|
version: 1.2.8
|
||||||
'@zerodevx/svelte-toast':
|
'@zerodevx/svelte-toast':
|
||||||
specifier: ^0.9.3
|
specifier: ^0.9.3
|
||||||
version: 0.9.3(svelte@3.58.0)
|
version: 0.9.3(svelte@3.58.0)
|
||||||
@@ -134,6 +140,12 @@ importers:
|
|||||||
eslint-plugin-svelte3:
|
eslint-plugin-svelte3:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0(eslint@8.37.0)(svelte@3.58.0)
|
version: 4.0.0(eslint@8.37.0)(svelte@3.58.0)
|
||||||
|
happy-dom:
|
||||||
|
specifier: ^9.18.3
|
||||||
|
version: 9.18.3
|
||||||
|
jsdom:
|
||||||
|
specifier: ^22.0.0
|
||||||
|
version: 22.0.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^2.8.7
|
specifier: ^2.8.7
|
||||||
version: 2.8.7
|
version: 2.8.7
|
||||||
@@ -171,8 +183,8 @@ importers:
|
|||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.0.8(typescript@5.0.3)(vite@4.3.1)
|
version: 4.0.8(typescript@5.0.3)(vite@4.3.1)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^0.25.8
|
specifier: ^0.27.3
|
||||||
version: 0.25.8(sass@1.61.0)
|
version: 0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0)
|
||||||
|
|
||||||
gradio/client/js:
|
gradio/client/js:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1677,13 +1689,11 @@ packages:
|
|||||||
|
|
||||||
/@floating-ui/core@1.2.6:
|
/@floating-ui/core@1.2.6:
|
||||||
resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==}
|
resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@floating-ui/dom@1.2.6:
|
/@floating-ui/dom@1.2.8:
|
||||||
resolution: {integrity: sha512-02vxFDuvuVPs22iJICacezYJyf7zwwOCWkPNkWNBr1U0Qt1cKFYzWvxts0AmqcOQGwt/3KJWcWIgtbUU38keyw==}
|
resolution: {integrity: sha512-XLwhYV90MxiHDq6S0rzFZj00fnDM+A1R9jhSioZoMsa7G0Q0i+Q4x40ajR8FHSdYDE1bgjG45mIWe6jtv9UPmg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/core': 1.2.6
|
'@floating-ui/core': 1.2.6
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.8:
|
/@humanwhocodes/config-array@0.11.8:
|
||||||
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
|
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
|
||||||
@@ -3053,6 +3063,11 @@ packages:
|
|||||||
tslib: 2.5.0
|
tslib: 2.5.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tootallnate/once@2.0.0:
|
||||||
|
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@trysound/sax@0.2.0:
|
/@trysound/sax@0.2.0:
|
||||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -3361,6 +3376,10 @@ packages:
|
|||||||
svelte: 3.58.0
|
svelte: 3.58.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/abab@2.0.6:
|
||||||
|
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/abbrev@1.1.1:
|
/abbrev@1.1.1:
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3395,6 +3414,15 @@ packages:
|
|||||||
pako: 2.1.0
|
pako: 2.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/agent-base@6.0.2:
|
||||||
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
|
engines: {node: '>= 6.0.0'}
|
||||||
|
dependencies:
|
||||||
|
debug: 4.3.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ajv@6.12.6:
|
/ajv@6.12.6:
|
||||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3476,7 +3504,6 @@ packages:
|
|||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/automation-events@6.0.0:
|
/automation-events@6.0.0:
|
||||||
resolution: {integrity: sha512-mSOnckbtuJso8cczB+broNsfcuDIQ+J4GFhlW/V3iD+LAXbS4XGHhdjYvtPnCKclKcvs9cVLtUMUkNncOzUQPg==}
|
resolution: {integrity: sha512-mSOnckbtuJso8cczB+broNsfcuDIQ+J4GFhlW/V3iD+LAXbS4XGHhdjYvtPnCKclKcvs9cVLtUMUkNncOzUQPg==}
|
||||||
@@ -3698,7 +3725,6 @@ packages:
|
|||||||
/cac@6.7.14:
|
/cac@6.7.14:
|
||||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/call-bind@1.0.2:
|
/call-bind@1.0.2:
|
||||||
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
||||||
@@ -3896,7 +3922,6 @@ packages:
|
|||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/commander@2.20.3:
|
/commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
@@ -4053,6 +4078,10 @@ packages:
|
|||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/css.escape@1.5.1:
|
||||||
|
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/cssesc@3.0.0:
|
/cssesc@3.0.0:
|
||||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4065,6 +4094,13 @@ packages:
|
|||||||
css-tree: 1.1.3
|
css-tree: 1.1.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/cssstyle@3.0.0:
|
||||||
|
resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
rrweb-cssom: 0.6.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/d3-array@3.2.2:
|
/d3-array@3.2.2:
|
||||||
resolution: {integrity: sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==}
|
resolution: {integrity: sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -4227,6 +4263,15 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/data-urls@4.0.0:
|
||||||
|
resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
abab: 2.0.6
|
||||||
|
whatwg-mimetype: 3.0.0
|
||||||
|
whatwg-url: 12.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/datalib@1.9.3:
|
/datalib@1.9.3:
|
||||||
resolution: {integrity: sha512-9rcwGd3zhvmJChyLzL5jjZ6UEtWO0SKa9Ycy6RVoQxSW43TSOBRbizj/Zn8UonfpBjCikHEQrJyE72Xw5eCY5A==}
|
resolution: {integrity: sha512-9rcwGd3zhvmJChyLzL5jjZ6UEtWO0SKa9Ycy6RVoQxSW43TSOBRbizj/Zn8UonfpBjCikHEQrJyE72Xw5eCY5A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4257,6 +4302,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|
||||||
|
/decimal.js@10.4.3:
|
||||||
|
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/dedent@0.7.0:
|
/dedent@0.7.0:
|
||||||
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
|
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -4284,7 +4333,6 @@ packages:
|
|||||||
/delayed-stream@1.0.0:
|
/delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/dequal@2.0.2:
|
/dequal@2.0.2:
|
||||||
resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==}
|
resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==}
|
||||||
@@ -4353,6 +4401,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/domexception@4.0.0:
|
||||||
|
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/domhandler@4.3.1:
|
/domhandler@4.3.1:
|
||||||
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
|
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -4415,6 +4470,11 @@ packages:
|
|||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/error-ex@1.3.2:
|
/error-ex@1.3.2:
|
||||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5045,6 +5105,15 @@ packages:
|
|||||||
mime-types: 2.1.34
|
mime-types: 2.1.34
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/form-data@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
mime-types: 2.1.34
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fraction.js@4.2.0:
|
/fraction.js@4.2.0:
|
||||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5328,6 +5397,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/happy-dom@9.18.3:
|
||||||
|
resolution: {integrity: sha512-b7iMGYeIXvUryNultA0AHEVU0FPpb2djJ/xSVlMDfP7HG4z7FomdqkCEpWtSv1zDL+t1gRUoBbpqFCoUBvjYtg==}
|
||||||
|
dependencies:
|
||||||
|
css.escape: 1.5.1
|
||||||
|
entities: 4.5.0
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
whatwg-encoding: 2.0.0
|
||||||
|
whatwg-mimetype: 3.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/har-schema@2.0.0:
|
/har-schema@2.0.0:
|
||||||
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
|
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -5365,6 +5445,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
|
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/html-encoding-sniffer@3.0.0:
|
||||||
|
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
whatwg-encoding: 2.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/html-escaper@2.0.2:
|
/html-escaper@2.0.2:
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5423,6 +5510,17 @@ packages:
|
|||||||
parse-cache-control: 1.0.1
|
parse-cache-control: 1.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/http-proxy-agent@5.0.0:
|
||||||
|
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
'@tootallnate/once': 2.0.0
|
||||||
|
agent-base: 6.0.2
|
||||||
|
debug: 4.3.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/http-response-object@3.0.2:
|
/http-response-object@3.0.2:
|
||||||
resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==}
|
resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5438,6 +5536,16 @@ packages:
|
|||||||
sshpk: 1.17.0
|
sshpk: 1.17.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/https-proxy-agent@5.0.1:
|
||||||
|
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
agent-base: 6.0.2
|
||||||
|
debug: 4.3.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/human-signals@2.1.0:
|
/human-signals@2.1.0:
|
||||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||||
engines: {node: '>=10.17.0'}
|
engines: {node: '>=10.17.0'}
|
||||||
@@ -5448,7 +5556,6 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
dev: false
|
|
||||||
|
|
||||||
/ieee754@1.2.1:
|
/ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
@@ -5565,6 +5672,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-potential-custom-element-name@1.0.1:
|
||||||
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-stream@2.0.1:
|
/is-stream@2.0.1:
|
||||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6136,6 +6247,44 @@ packages:
|
|||||||
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/jsdom@22.0.0:
|
||||||
|
resolution: {integrity: sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
peerDependencies:
|
||||||
|
canvas: ^2.5.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
canvas:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
abab: 2.0.6
|
||||||
|
cssstyle: 3.0.0
|
||||||
|
data-urls: 4.0.0
|
||||||
|
decimal.js: 10.4.3
|
||||||
|
domexception: 4.0.0
|
||||||
|
form-data: 4.0.0
|
||||||
|
html-encoding-sniffer: 3.0.0
|
||||||
|
http-proxy-agent: 5.0.0
|
||||||
|
https-proxy-agent: 5.0.1
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
nwsapi: 2.2.4
|
||||||
|
parse5: 7.1.2
|
||||||
|
rrweb-cssom: 0.6.0
|
||||||
|
saxes: 6.0.0
|
||||||
|
symbol-tree: 3.2.4
|
||||||
|
tough-cookie: 4.1.2
|
||||||
|
w3c-xmlserializer: 4.0.0
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
whatwg-encoding: 2.0.0
|
||||||
|
whatwg-mimetype: 3.0.0
|
||||||
|
whatwg-url: 12.0.1
|
||||||
|
ws: 8.13.0
|
||||||
|
xml-name-validator: 4.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: true
|
||||||
|
|
||||||
/jsesc@2.5.2:
|
/jsesc@2.5.2:
|
||||||
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
|
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -6171,7 +6320,6 @@ packages:
|
|||||||
|
|
||||||
/jsonc-parser@3.2.0:
|
/jsonc-parser@3.2.0:
|
||||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/jsonfile@4.0.0:
|
/jsonfile@4.0.0:
|
||||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||||
@@ -6515,14 +6663,12 @@ packages:
|
|||||||
/mime-db@1.51.0:
|
/mime-db@1.51.0:
|
||||||
resolution: {integrity: sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==}
|
resolution: {integrity: sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mime-types@2.1.34:
|
/mime-types@2.1.34:
|
||||||
resolution: {integrity: sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==}
|
resolution: {integrity: sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.51.0
|
mime-db: 1.51.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mime@3.0.0:
|
/mime@3.0.0:
|
||||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
@@ -6573,7 +6719,6 @@ packages:
|
|||||||
pathe: 1.1.0
|
pathe: 1.1.0
|
||||||
pkg-types: 1.0.3
|
pkg-types: 1.0.3
|
||||||
ufo: 1.1.2
|
ufo: 1.1.2
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mri@1.2.0:
|
/mri@1.2.0:
|
||||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||||
@@ -6715,6 +6860,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
|
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/nwsapi@2.2.4:
|
||||||
|
resolution: {integrity: sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/oauth-sign@0.9.0:
|
/oauth-sign@0.9.0:
|
||||||
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6860,6 +7009,12 @@ packages:
|
|||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
|
/parse5@7.1.2:
|
||||||
|
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||||
|
dependencies:
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/path-browserify@1.0.1:
|
/path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6889,9 +7044,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/pathe@0.2.0:
|
||||||
|
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/pathe@1.1.0:
|
/pathe@1.1.0:
|
||||||
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
|
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/pathval@1.1.1:
|
/pathval@1.1.1:
|
||||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||||
@@ -6928,7 +7086,6 @@ packages:
|
|||||||
jsonc-parser: 3.2.0
|
jsonc-parser: 3.2.0
|
||||||
mlly: 1.2.1
|
mlly: 1.2.1
|
||||||
pathe: 1.1.0
|
pathe: 1.1.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/plotly.js-dist-min@2.10.1:
|
/plotly.js-dist-min@2.10.1:
|
||||||
resolution: {integrity: sha512-H0ls1C2uu2U+qWw76djo4/zOGtUKfMILwFhu7tCOaG/wH5ypujrYGCH03N9SQVf1SXcctTfW57USf8LmagSiPQ==}
|
resolution: {integrity: sha512-H0ls1C2uu2U+qWw76djo4/zOGtUKfMILwFhu7tCOaG/wH5ypujrYGCH03N9SQVf1SXcctTfW57USf8LmagSiPQ==}
|
||||||
@@ -7114,7 +7271,6 @@ packages:
|
|||||||
|
|
||||||
/psl@1.9.0:
|
/psl@1.9.0:
|
||||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/punycode@2.3.0:
|
/punycode@2.3.0:
|
||||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||||
@@ -7136,6 +7292,10 @@ packages:
|
|||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/querystringify@2.2.0:
|
||||||
|
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -7252,6 +7412,10 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/requires-port@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/resize-observer-polyfill@1.5.1:
|
/resize-observer-polyfill@1.5.1:
|
||||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -7332,6 +7496,10 @@ packages:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
|
|
||||||
|
/rrweb-cssom@0.6.0:
|
||||||
|
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/run-parallel@1.2.0:
|
/run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7370,7 +7538,6 @@ packages:
|
|||||||
|
|
||||||
/safer-buffer@2.1.2:
|
/safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/sander@0.5.1:
|
/sander@0.5.1:
|
||||||
resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
|
resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
|
||||||
@@ -7389,6 +7556,13 @@ packages:
|
|||||||
immutable: 4.3.0
|
immutable: 4.3.0
|
||||||
source-map-js: 1.0.2
|
source-map-js: 1.0.2
|
||||||
|
|
||||||
|
/saxes@6.0.0:
|
||||||
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
|
engines: {node: '>=v12.22.7'}
|
||||||
|
dependencies:
|
||||||
|
xmlchars: 2.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/semver@5.7.1:
|
/semver@5.7.1:
|
||||||
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
|
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -7452,7 +7626,6 @@ packages:
|
|||||||
|
|
||||||
/siginfo@2.0.0:
|
/siginfo@2.0.0:
|
||||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/sigmund@1.0.1:
|
/sigmund@1.0.1:
|
||||||
resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==}
|
resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==}
|
||||||
@@ -7519,7 +7692,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
buffer-from: 1.1.2
|
buffer-from: 1.1.2
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/source-map@0.6.1:
|
/source-map@0.6.1:
|
||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
@@ -7577,7 +7749,6 @@ packages:
|
|||||||
|
|
||||||
/stackback@0.0.2:
|
/stackback@0.0.2:
|
||||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/standardized-audio-context@25.3.45:
|
/standardized-audio-context@25.3.45:
|
||||||
resolution: {integrity: sha512-d1UVvbz0mDmEqNehvoTKlpSevRJ3YiVZ6kdboeaWX+8cl94H1w8x7c5RNdg0nqxiE049LMeF4tFPuDl5Vm78Kg==}
|
resolution: {integrity: sha512-d1UVvbz0mDmEqNehvoTKlpSevRJ3YiVZ6kdboeaWX+8cl94H1w8x7c5RNdg0nqxiE049LMeF4tFPuDl5Vm78Kg==}
|
||||||
@@ -7589,7 +7760,6 @@ packages:
|
|||||||
|
|
||||||
/std-env@3.3.3:
|
/std-env@3.3.3:
|
||||||
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
|
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/streamsearch@1.1.0:
|
/streamsearch@1.1.0:
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
@@ -7782,7 +7952,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-8Ifi5CD2Ui7FX7NjJRmutFtXjrB8T/FMNoS2H8P81t5LHK4I9G4NIs007rLWG/nRl7y+zJUXa3tWuTjYXw/O5A==}
|
resolution: {integrity: sha512-8Ifi5CD2Ui7FX7NjJRmutFtXjrB8T/FMNoS2H8P81t5LHK4I9G4NIs007rLWG/nRl7y+zJUXa3tWuTjYXw/O5A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/core': 1.2.6
|
'@floating-ui/core': 1.2.6
|
||||||
'@floating-ui/dom': 1.2.6
|
'@floating-ui/dom': 1.2.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/svelte-hmr@0.15.1(svelte@3.58.0):
|
/svelte-hmr@0.15.1(svelte@3.58.0):
|
||||||
@@ -7953,7 +8123,7 @@ packages:
|
|||||||
/svelte-select@5.5.3:
|
/svelte-select@5.5.3:
|
||||||
resolution: {integrity: sha512-0QIiEmyon3bqoenFtR/BhamMpMmzOWWg1ctE7xxwP7nEnZhAGGoTra1HPPfEyT6C6gVaOFgCpdBaZoM8DEHlEw==}
|
resolution: {integrity: sha512-0QIiEmyon3bqoenFtR/BhamMpMmzOWWg1ctE7xxwP7nEnZhAGGoTra1HPPfEyT6C6gVaOFgCpdBaZoM8DEHlEw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/dom': 1.2.6
|
'@floating-ui/dom': 1.2.8
|
||||||
svelte-floating-ui: 1.2.8
|
svelte-floating-ui: 1.2.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -8007,6 +8177,10 @@ packages:
|
|||||||
ssr-window: 4.0.2
|
ssr-window: 4.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/symbol-tree@3.2.4:
|
||||||
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sync-request@6.1.0:
|
/sync-request@6.1.0:
|
||||||
resolution: {integrity: sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==}
|
resolution: {integrity: sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -8195,10 +8369,27 @@ packages:
|
|||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tough-cookie@4.1.2:
|
||||||
|
resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
psl: 1.9.0
|
||||||
|
punycode: 2.3.0
|
||||||
|
universalify: 0.2.0
|
||||||
|
url-parse: 1.5.10
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tr46@0.0.3:
|
/tr46@0.0.3:
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tr46@4.1.1:
|
||||||
|
resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ts-interface-checker@0.1.13:
|
/ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
@@ -8292,7 +8483,6 @@ packages:
|
|||||||
|
|
||||||
/ufo@1.1.2:
|
/ufo@1.1.2:
|
||||||
resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
|
resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/undici@5.20.0:
|
/undici@5.20.0:
|
||||||
resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}
|
resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}
|
||||||
@@ -8306,6 +8496,11 @@ packages:
|
|||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/universalify@0.2.0:
|
||||||
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
|
engines: {node: '>= 4.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/universalify@2.0.0:
|
/universalify@2.0.0:
|
||||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@@ -8325,6 +8520,13 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
|
|
||||||
|
/url-parse@1.5.10:
|
||||||
|
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||||
|
dependencies:
|
||||||
|
querystringify: 2.2.0
|
||||||
|
requires-port: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -8761,6 +8963,29 @@ packages:
|
|||||||
extsprintf: 1.3.0
|
extsprintf: 1.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/vite-node@0.27.3(@types/node@18.16.0)(sass@1.61.0):
|
||||||
|
resolution: {integrity: sha512-eyJYOO64o5HIp8poc4bJX+ZNBwMZeI3f6/JdiUmJgW02Mt7LnoCtDMRVmLaY9S05SIsjGe339ZK4uo2wQ+bF9g==}
|
||||||
|
engines: {node: '>=v14.16.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
cac: 6.7.14
|
||||||
|
debug: 4.3.4
|
||||||
|
mlly: 1.2.1
|
||||||
|
pathe: 0.2.0
|
||||||
|
picocolors: 1.0.0
|
||||||
|
source-map: 0.6.1
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- less
|
||||||
|
- sass
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
dev: true
|
||||||
|
|
||||||
/vite-node@0.31.0(@types/node@18.16.0):
|
/vite-node@0.31.0(@types/node@18.16.0):
|
||||||
resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==}
|
resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==}
|
||||||
engines: {node: '>=v14.18.0'}
|
engines: {node: '>=v14.18.0'}
|
||||||
@@ -9154,8 +9379,8 @@ packages:
|
|||||||
vite: 4.3.1(sass@1.61.0)
|
vite: 4.3.1(sass@1.61.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/vitest@0.25.8(sass@1.61.0):
|
/vitest@0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0):
|
||||||
resolution: {integrity: sha512-X75TApG2wZTJn299E/TIYevr4E9/nBo1sUtZzn0Ci5oK8qnpZAZyhwg0qCeMSakGIWtc6oRwcQFyFfW14aOFWg==}
|
resolution: {integrity: sha512-Ld3UVgRVhJUtqvQ3dW89GxiApFAgBsWJZBCWzK+gA3w2yG68csXlGZZ4WDJURf+8ecNfgrScga6xY+8YSOpiMg==}
|
||||||
engines: {node: '>=v14.16.0'}
|
engines: {node: '>=v14.16.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -9181,15 +9406,22 @@ packages:
|
|||||||
'@types/node': 18.16.0
|
'@types/node': 18.16.0
|
||||||
acorn: 8.8.2
|
acorn: 8.8.2
|
||||||
acorn-walk: 8.2.0
|
acorn-walk: 8.2.0
|
||||||
|
cac: 6.7.14
|
||||||
chai: 4.3.7
|
chai: 4.3.7
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
|
happy-dom: 9.18.3
|
||||||
|
jsdom: 22.0.0
|
||||||
local-pkg: 0.4.3
|
local-pkg: 0.4.3
|
||||||
|
picocolors: 1.0.0
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
std-env: 3.3.3
|
||||||
strip-literal: 1.0.1
|
strip-literal: 1.0.1
|
||||||
tinybench: 2.4.0
|
tinybench: 2.4.0
|
||||||
tinypool: 0.3.1
|
tinypool: 0.3.1
|
||||||
tinyspy: 1.1.1
|
tinyspy: 1.1.1
|
||||||
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0)
|
||||||
|
vite-node: 0.27.3(@types/node@18.16.0)(sass@1.61.0)
|
||||||
|
why-is-node-running: 2.2.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- less
|
- less
|
||||||
- sass
|
- sass
|
||||||
@@ -9308,6 +9540,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==}
|
resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/w3c-xmlserializer@4.0.0:
|
||||||
|
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
xml-name-validator: 4.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/walker@1.0.8:
|
/walker@1.0.8:
|
||||||
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9322,11 +9561,36 @@ packages:
|
|||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/webidl-conversions@7.0.0:
|
||||||
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/well-known-symbols@2.0.0:
|
/well-known-symbols@2.0.0:
|
||||||
resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==}
|
resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/whatwg-encoding@2.0.0:
|
||||||
|
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-mimetype@3.0.0:
|
||||||
|
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-url@12.0.1:
|
||||||
|
resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
tr46: 4.1.1
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/whatwg-url@5.0.0:
|
/whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9349,7 +9613,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
siginfo: 2.0.0
|
siginfo: 2.0.0
|
||||||
stackback: 0.0.2
|
stackback: 0.0.2
|
||||||
dev: false
|
|
||||||
|
|
||||||
/word-wrap@1.2.3:
|
/word-wrap@1.2.3:
|
||||||
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
|
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
|
||||||
@@ -9405,7 +9668,15 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
dev: false
|
|
||||||
|
/xml-name-validator@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/xmlchars@2.2.0:
|
||||||
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/xtend@2.2.0:
|
/xtend@2.2.0:
|
||||||
resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==}
|
resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const app = new ComfyAppState();
|
export let app: ComfyAppState;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ComfyApp {app}/>
|
<ComfyApp {app}/>
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await app.setup();
|
await app.setup();
|
||||||
(window as any).app = app;
|
|
||||||
window.addEventListener("backbutton", onBackKeyDown, false);
|
window.addEventListener("backbutton", onBackKeyDown, false);
|
||||||
window.addEventListener("popstate", onBackKeyDown, false);
|
window.addEventListener("popstate", onBackKeyDown, false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas, type LGraphRemoveNodeOptions } from "@litegraph-ts/core";
|
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas, type LGraphRemoveNodeOptions, Subgraph, type LGraphAddNodeMode } from "@litegraph-ts/core";
|
||||||
import GraphSync from "./GraphSync";
|
import GraphSync from "./GraphSync";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
@@ -8,7 +8,8 @@ import { get } from "svelte/store";
|
|||||||
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
||||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||||
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
|
||||||
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes";
|
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets";
|
||||||
|
import selectionState from "./stores/selectionState";
|
||||||
|
|
||||||
type ComfyGraphEvents = {
|
type ComfyGraphEvents = {
|
||||||
configured: (graph: LGraph) => void
|
configured: (graph: LGraph) => void
|
||||||
@@ -26,29 +27,48 @@ export default class ComfyGraph extends LGraph {
|
|||||||
|
|
||||||
override onConfigure() {
|
override onConfigure() {
|
||||||
console.debug("Configured");
|
console.debug("Configured");
|
||||||
this.eventBus.emit("configured", this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override onBeforeChange(graph: LGraph, info: any) {
|
override onBeforeChange(graph: LGraph, info: any) {
|
||||||
console.debug("BeforeChange", info);
|
console.debug("BeforeChange", info);
|
||||||
this.eventBus.emit("beforeChange", graph, info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override onAfterChange(graph: LGraph, info: any) {
|
override onAfterChange(graph: LGraph, info: any) {
|
||||||
console.debug("AfterChange", info);
|
console.debug("AfterChange", info);
|
||||||
this.eventBus.emit("afterChange", graph, info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override onAfterExecute() {
|
override onAfterExecute() {
|
||||||
this.eventBus.emit("afterExecute");
|
this.eventBus.emit("afterExecute");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE: This function will also be called by child subgraphs on their
|
||||||
|
* parent graphs. So we have to be sure the node that receives the callback
|
||||||
|
* is a root graph (this._is_subgraph is false). If a subgraph calls this
|
||||||
|
* then options.subgraphsh will have the list of subgraphs down the chain.
|
||||||
|
*/
|
||||||
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||||
|
// Don't add nodes in subgraphs until this callback reaches the root
|
||||||
|
// graph
|
||||||
|
if (node.getRootGraph() == null || this._is_subgraph)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.doAddNode(node, options);
|
||||||
|
|
||||||
|
// console.debug("Added", node);
|
||||||
|
this.eventBus.emit("nodeAdded", node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add widget UI/groups for newly added nodes.
|
||||||
|
*/
|
||||||
|
private doAddNode(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||||
layoutState.nodeAdded(node, options)
|
layoutState.nodeAdded(node, options)
|
||||||
|
|
||||||
// All nodes whether they come from base litegraph or ComfyBox should
|
// All nodes whether they come from base litegraph or ComfyBox should
|
||||||
// have tags added to them. Can't override serialization for existing
|
// have tags added to them. Can't override serialization for existing
|
||||||
// node types to add `tags` as anew field so putting it in properties is better.
|
// node types to add `tags` as a new field so putting it in properties
|
||||||
|
// is better.
|
||||||
if (node.properties.tags == null)
|
if (node.properties.tags == null)
|
||||||
node.properties.tags = []
|
node.properties.tags = []
|
||||||
|
|
||||||
@@ -88,7 +108,7 @@ export default class ComfyGraph extends LGraph {
|
|||||||
if (!("svelteComponentType" in node) && options.addedBy == null) {
|
if (!("svelteComponentType" in node) && options.addedBy == null) {
|
||||||
console.debug("[ComfyGraph] AutoAdd UI")
|
console.debug("[ComfyGraph] AutoAdd UI")
|
||||||
const comfyNode = node as ComfyGraphNode;
|
const comfyNode = node as ComfyGraphNode;
|
||||||
const widgetNodesAdded = []
|
const widgetNodesAdded: ComfyWidgetNode[] = []
|
||||||
for (let index = 0; index < comfyNode.inputs.length; index++) {
|
for (let index = 0; index < comfyNode.inputs.length; index++) {
|
||||||
const input = comfyNode.inputs[index];
|
const input = comfyNode.inputs[index];
|
||||||
if ("config" in input) {
|
if ("config" in input) {
|
||||||
@@ -96,7 +116,7 @@ export default class ComfyGraph extends LGraph {
|
|||||||
if (comfyInput.defaultWidgetNode) {
|
if (comfyInput.defaultWidgetNode) {
|
||||||
const widgetNode = LiteGraph.createNode(comfyInput.defaultWidgetNode)
|
const widgetNode = LiteGraph.createNode(comfyInput.defaultWidgetNode)
|
||||||
const inputPos = comfyNode.getConnectionPos(true, index);
|
const inputPos = comfyNode.getConnectionPos(true, index);
|
||||||
this.add(widgetNode)
|
node.graph.add(widgetNode)
|
||||||
widgetNode.connect(0, comfyNode, index);
|
widgetNode.connect(0, comfyNode, index);
|
||||||
widgetNode.collapse();
|
widgetNode.collapse();
|
||||||
widgetNode.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
|
widgetNode.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2];
|
||||||
@@ -109,26 +129,44 @@ export default class ComfyGraph extends LGraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dragItems = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem).filter(di => di)
|
const dragItemIDs = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem?.id).filter(Boolean)
|
||||||
console.debug("[ComfyGraph] Group new widgets", dragItems)
|
console.debug("[ComfyGraph] Group new widgets", dragItemIDs)
|
||||||
|
|
||||||
layoutState.groupItems(dragItems, { title: node.title })
|
// Use the default node title instead of custom node title, in
|
||||||
|
// case node was cloned
|
||||||
|
const reg = LiteGraph.registered_node_types[node.type]
|
||||||
|
|
||||||
|
layoutState.groupItems(dragItemIDs, { title: reg.title })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("Added", node);
|
// Handle nodes in subgraphs being attached to this graph indirectly
|
||||||
this.eventBus.emit("nodeAdded", node);
|
// ************** RECURSION ALERT ! **************
|
||||||
|
if (node.is(Subgraph)) {
|
||||||
|
for (const child of node.subgraph.iterateNodesInOrder()) {
|
||||||
|
this.doAddNode(child, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ************** RECURSION ALERT ! **************
|
||||||
}
|
}
|
||||||
|
|
||||||
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||||
|
selectionState.clear(); // safest option
|
||||||
layoutState.nodeRemoved(node, options);
|
layoutState.nodeRemoved(node, options);
|
||||||
|
|
||||||
console.debug("Removed", node);
|
// Handle subgraphs being removed
|
||||||
|
if (node.is(Subgraph)) {
|
||||||
|
for (const child of node.subgraph.iterateNodesInOrder()) {
|
||||||
|
this.onNodeRemoved(child, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug("Removed", node);
|
||||||
this.eventBus.emit("nodeRemoved", node);
|
this.eventBus.emit("nodeRemoved", node);
|
||||||
}
|
}
|
||||||
|
|
||||||
override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) {
|
override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) {
|
||||||
console.debug("ConnectionChange", node);
|
// console.debug("ConnectionChange", node);
|
||||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph, LLink } from "@litegraph-ts/core";
|
import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph, LLink, type NodeID } from "@litegraph-ts/core";
|
||||||
import type ComfyApp from "./components/ComfyApp";
|
import type ComfyApp from "./components/ComfyApp";
|
||||||
import queueState from "./stores/queueState";
|
import queueState from "./stores/queueState";
|
||||||
import { get } from "svelte/store";
|
import { get, type Unsubscriber } from "svelte/store";
|
||||||
import uiState from "./stores/uiState";
|
import uiState from "./stores/uiState";
|
||||||
import layoutState from "./stores/layoutState";
|
import layoutState from "./stores/layoutState";
|
||||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
import { ComfyReroute } from "./nodes";
|
import { ComfyReroute } from "./nodes";
|
||||||
|
import type { Progress } from "./components/ComfyApp";
|
||||||
|
import selectionState from "./stores/selectionState";
|
||||||
|
|
||||||
export type SerializedGraphCanvasState = {
|
export type SerializedGraphCanvasState = {
|
||||||
offset: Vector2,
|
offset: Vector2,
|
||||||
@@ -14,6 +16,7 @@ export type SerializedGraphCanvasState = {
|
|||||||
|
|
||||||
export default class ComfyGraphCanvas extends LGraphCanvas {
|
export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||||
app: ComfyApp | null;
|
app: ComfyApp | null;
|
||||||
|
private _unsubscribe: Unsubscriber;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
app: ComfyApp,
|
app: ComfyApp,
|
||||||
@@ -27,13 +30,33 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
) {
|
) {
|
||||||
super(canvas, app.lGraph, options);
|
super(canvas, app.lGraph, options);
|
||||||
this.app = app;
|
this.app = app;
|
||||||
|
this._unsubscribe = selectionState.subscribe(ss => {
|
||||||
|
for (const node of Object.values(this.selected_nodes)) {
|
||||||
|
node.is_selected = false;
|
||||||
|
}
|
||||||
|
this.selected_nodes = {}
|
||||||
|
for (const node of ss.currentSelectionNodes) {
|
||||||
|
this.selected_nodes[node.id] = node;
|
||||||
|
node.is_selected = true
|
||||||
|
}
|
||||||
|
this._selectedNodes = new Set()
|
||||||
|
this.setDirty(true, true);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_selectedNodes: Set<NodeID> = new Set();
|
||||||
|
|
||||||
serialize(): SerializedGraphCanvasState {
|
serialize(): SerializedGraphCanvasState {
|
||||||
return {
|
let offset = this.ds.offset;
|
||||||
offset: this.ds.offset,
|
let scale = this.ds.scale;
|
||||||
scale: this.ds.scale
|
|
||||||
|
// Use top-level graph for saved offset if we're viewing a subgraph
|
||||||
|
if (this._graph_stack?.length > 0) {
|
||||||
|
offset = this._graph_stack[0].offset;
|
||||||
|
scale = this._graph_stack[0].scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { offset, scale }
|
||||||
}
|
}
|
||||||
|
|
||||||
deserialize(data: SerializedGraphCanvasState) {
|
deserialize(data: SerializedGraphCanvasState) {
|
||||||
@@ -58,18 +81,36 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
super.drawNodeShape(node, ctx, size, fgColor, bgColor, selected, mouseOver);
|
super.drawNodeShape(node, ctx, size, fgColor, bgColor, selected, mouseOver);
|
||||||
|
|
||||||
let state = get(queueState);
|
let state = get(queueState);
|
||||||
|
let ss = get(selectionState);
|
||||||
|
const isRunningNode = node.id === state.runningNodeID
|
||||||
|
|
||||||
let color = null;
|
let color = null;
|
||||||
if (node.id === +state.runningNodeID) {
|
let thickness = 1;
|
||||||
|
// if (this._selectedNodes.has(node.id)) {
|
||||||
|
// color = "yellow";
|
||||||
|
// thickness = 5;
|
||||||
|
// }
|
||||||
|
if (ss.currentHoveredNodes.has(node.id)) {
|
||||||
|
color = "lightblue";
|
||||||
|
}
|
||||||
|
else if (isRunningNode) {
|
||||||
color = "#0f0";
|
color = "#0f0";
|
||||||
// this.app can be null inside the constructor if rendering is taking place already
|
|
||||||
} else if (this.app && this.app.dragOverNode && node.id === this.app.dragOverNode.id) {
|
|
||||||
color = "dodgerblue";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
|
this.drawNodeOutline(node, ctx, size, fgColor, bgColor, color, thickness)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunningNode && state.progress) {
|
||||||
|
ctx.fillStyle = "green";
|
||||||
|
ctx.fillRect(0, 0, size[0] * (state.progress.value / state.progress.max), 6);
|
||||||
|
ctx.fillStyle = bgColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, size: Vector2, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) {
|
||||||
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
|
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = outlineThickness;
|
||||||
ctx.globalAlpha = 0.8;
|
ctx.globalAlpha = 0.8;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
if (shape == BuiltInSlotShape.BOX_SHAPE)
|
if (shape == BuiltInSlotShape.BOX_SHAPE)
|
||||||
@@ -93,17 +134,10 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
);
|
);
|
||||||
else if (shape == BuiltInSlotShape.CIRCLE_SHAPE)
|
else if (shape == BuiltInSlotShape.CIRCLE_SHAPE)
|
||||||
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
|
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = outlineColor;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.strokeStyle = fgColor;
|
ctx.strokeStyle = fgColor;
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
if (state.progress) {
|
|
||||||
ctx.fillStyle = "green";
|
|
||||||
ctx.fillRect(0, 0, size[0] * (state.progress.value / state.progress.max), 6);
|
|
||||||
ctx.fillStyle = bgColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private alignToGrid(node: LGraphNode, ctx: CanvasRenderingContext2D) {
|
private alignToGrid(node: LGraphNode, ctx: CanvasRenderingContext2D) {
|
||||||
@@ -235,10 +269,43 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override onSelectionChange(nodes: Record<number, LGraphNode>) {
|
override onSelectionChange(nodes: Record<number, LGraphNode>) {
|
||||||
|
selectionState.update(ss => {
|
||||||
|
ss.currentSelectionNodes = Object.values(nodes)
|
||||||
|
ss.currentSelection = []
|
||||||
const ls = get(layoutState)
|
const ls = get(layoutState)
|
||||||
ls.currentSelectionNodes = Object.values(nodes)
|
for (const node of ss.currentSelectionNodes) {
|
||||||
ls.currentSelection = []
|
const widget = ls.allItemsByNode[node.id]
|
||||||
layoutState.set(ls)
|
if (widget)
|
||||||
|
ss.currentSelection.push(widget.dragItem.id)
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override onHoverChange(node: LGraphNode | null) {
|
||||||
|
selectionState.update(ss => {
|
||||||
|
ss.currentHoveredNodes.clear()
|
||||||
|
if (node) {
|
||||||
|
ss.currentHoveredNodes.add(node.id)
|
||||||
|
}
|
||||||
|
ss.currentHovered.clear()
|
||||||
|
const ls = get(layoutState)
|
||||||
|
for (const nodeID of ss.currentHoveredNodes) {
|
||||||
|
const widget = ls.allItemsByNode[nodeID]
|
||||||
|
if (widget)
|
||||||
|
ss.currentHovered.add(widget.dragItem.id)
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override clear() {
|
||||||
|
super.clear();
|
||||||
|
selectionState.update(ss => {
|
||||||
|
ss.currentSelectionNodes = [];
|
||||||
|
ss.currentHoveredNodes.clear()
|
||||||
|
return ss;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override onNodeMoved(node: LGraphNode) {
|
override onNodeMoved(node: LGraphNode) {
|
||||||
|
|||||||
60
src/lib/ComfyNodeDef.ts
Normal file
60
src/lib/ComfyNodeDef.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { range } from "./utils"
|
||||||
|
import ComfyWidgets from "./widgets"
|
||||||
|
|
||||||
|
export type ComfyNodeDef = {
|
||||||
|
name: string
|
||||||
|
display_name?: string
|
||||||
|
category: string
|
||||||
|
input: ComfyNodeDefInputs
|
||||||
|
/** Output type like "LATENT" or "IMAGE" */
|
||||||
|
output: string[]
|
||||||
|
output_name: string[]
|
||||||
|
output_is_list: boolean[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComfyNodeDefInputs = {
|
||||||
|
required: Record<string, ComfyNodeDefInput>,
|
||||||
|
optional?: Record<string, ComfyNodeDefInput>
|
||||||
|
}
|
||||||
|
export type ComfyNodeDefInput = [ComfyNodeDefInputType, ComfyNodeDefInputOptions | null]
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Array: Combo widget. Usually the values are strings but they can also be other stuff like booleans.
|
||||||
|
* - "INT"/"FLOAT"/etc.: Non-combo type widgets. See ComfyWidgets type.
|
||||||
|
* - other string: Must be an input type, usually something lke "IMAGE" or "LATENT".
|
||||||
|
*/
|
||||||
|
export type ComfyNodeDefInputType = any[] | keyof typeof ComfyWidgets | string
|
||||||
|
|
||||||
|
export type ComfyNodeDefInputOptions = {
|
||||||
|
forceInput?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO if/when comfy refactors
|
||||||
|
export type ComfyNodeDefOutput = {
|
||||||
|
type: string,
|
||||||
|
name: string,
|
||||||
|
is_list?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBackendNodeDefInputType(inputName: string, type: ComfyNodeDefInputType): type is string {
|
||||||
|
return !Array.isArray(type) && !(type in ComfyWidgets) && !(`${type}:${inputName}` in ComfyWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function iterateNodeDefInputs(def: ComfyNodeDef): Iterable<[string, ComfyNodeDefInput]> {
|
||||||
|
var inputs = def.input.required
|
||||||
|
if (def.input.optional != null) {
|
||||||
|
inputs = Object.assign({}, def.input.required, def.input.optional)
|
||||||
|
}
|
||||||
|
return Object.entries(inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function iterateNodeDefOutputs(def: ComfyNodeDef): Iterable<ComfyNodeDefOutput> {
|
||||||
|
return range(def.output.length).map(i => {
|
||||||
|
return {
|
||||||
|
type: def.output[i],
|
||||||
|
name: def.output_name[i] || def.output[i],
|
||||||
|
is_list: def.output_is_list[i],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -137,29 +137,9 @@ export class ImageViewer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupGalleryImageForLightbox(e: HTMLImageElement) {
|
showLightbox(source: HTMLImageElement) {
|
||||||
if (e.dataset.modded === "true")
|
|
||||||
return;
|
|
||||||
|
|
||||||
e.dataset.modded = "true";
|
|
||||||
e.style.cursor = 'pointer'
|
|
||||||
e.style.userSelect = 'none'
|
|
||||||
|
|
||||||
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
|
|
||||||
|
|
||||||
// For Firefox, listening on click first switched to next image then shows the lightbox.
|
|
||||||
// If you know how to fix this without switching to mousedown event, please.
|
|
||||||
// For other browsers the event is click to make it possiblr to drag picture.
|
|
||||||
var event = isFirefox ? 'mousedown' : 'click'
|
|
||||||
|
|
||||||
e.addEventListener(event, (evt) => {
|
|
||||||
// if (!opts.js_modal_lightbox || evt.button != 0) return;
|
|
||||||
|
|
||||||
const initiallyZoomed = true
|
const initiallyZoomed = true
|
||||||
this.modalZoomSet(this.modalImage, initiallyZoomed)
|
this.modalZoomSet(this.modalImage, initiallyZoomed)
|
||||||
evt.preventDefault()
|
|
||||||
|
|
||||||
const source = evt.target as HTMLImageElement;
|
|
||||||
|
|
||||||
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
||||||
console.debug("[ImageViewer] showModal", event, source, galleryElem);
|
console.debug("[ImageViewer] showModal", event, source, galleryElem);
|
||||||
@@ -173,9 +153,6 @@ export class ImageViewer {
|
|||||||
console.warn("Gallery!", index, urls, galleryElem)
|
console.warn("Gallery!", index, urls, galleryElem)
|
||||||
|
|
||||||
this.showModal(urls, index, galleryElem)
|
this.showModal(urls, index, galleryElem)
|
||||||
evt.stopPropagation();
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modalZoomSet(modalImage: HTMLImageElement, enable: boolean) {
|
modalZoomSet(modalImage: HTMLImageElement, enable: boolean) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
|
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutputs } from "./components/ComfyApp";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
|
import type { ComfyImageLocation } from "$lib/utils";
|
||||||
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
|
||||||
import type { SerializedLayoutState } from "./stores/layoutState";
|
import type { SerializedLayoutState } from "./stores/layoutState";
|
||||||
|
import type { ComfyNodeDef } from "./ComfyNodeDef";
|
||||||
|
|
||||||
export type ComfyPromptRequest = {
|
export type ComfyPromptRequest = {
|
||||||
client_id?: string,
|
client_id?: string,
|
||||||
@@ -30,7 +31,7 @@ export type ComfyAPIQueueResponse = {
|
|||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeID = UUID;
|
export type ComfyNodeID = UUID; // To distinguish from Litegraph NodeID
|
||||||
export type PromptID = UUID; // UUID
|
export type PromptID = UUID; // UUID
|
||||||
|
|
||||||
export type ComfyAPIHistoryItem = [
|
export type ComfyAPIHistoryItem = [
|
||||||
@@ -38,7 +39,7 @@ export type ComfyAPIHistoryItem = [
|
|||||||
PromptID,
|
PromptID,
|
||||||
SerializedPromptInputsAll,
|
SerializedPromptInputsAll,
|
||||||
ComfyBoxPromptExtraData,
|
ComfyBoxPromptExtraData,
|
||||||
NodeID[] // good outputs
|
ComfyNodeID[] // good outputs
|
||||||
]
|
]
|
||||||
|
|
||||||
export type ComfyAPIPromptResponse = {
|
export type ComfyAPIPromptResponse = {
|
||||||
@@ -76,9 +77,10 @@ type ComfyAPIEvents = {
|
|||||||
progress: (progress: Progress) => void,
|
progress: (progress: Progress) => void,
|
||||||
reconnecting: () => void,
|
reconnecting: () => void,
|
||||||
reconnected: () => void,
|
reconnected: () => void,
|
||||||
executing: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
|
executing: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void,
|
||||||
executed: (promptID: PromptID, nodeID: NodeID, output: SerializedPromptOutput) => void,
|
executed: (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutputs) => void,
|
||||||
execution_cached: (promptID: PromptID, nodes: NodeID[]) => void,
|
execution_start: (promptID: PromptID) => void,
|
||||||
|
execution_cached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
||||||
execution_error: (promptID: PromptID, message: string) => void,
|
execution_error: (promptID: PromptID, message: string) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +184,9 @@ export default class ComfyAPI {
|
|||||||
case "executed":
|
case "executed":
|
||||||
this.eventBus.emit("executed", msg.data.prompt_id, msg.data.node, msg.data.output);
|
this.eventBus.emit("executed", msg.data.prompt_id, msg.data.node, msg.data.output);
|
||||||
break;
|
break;
|
||||||
|
case "execution_start":
|
||||||
|
this.eventBus.emit("execution_start", msg.data.prompt_id);
|
||||||
|
break;
|
||||||
case "execution_cached":
|
case "execution_cached":
|
||||||
this.eventBus.emit("execution_cached", msg.data.prompt_id, msg.data.nodes);
|
this.eventBus.emit("execution_cached", msg.data.prompt_id, msg.data.nodes);
|
||||||
break;
|
break;
|
||||||
@@ -226,7 +231,7 @@ export default class ComfyAPI {
|
|||||||
* Loads node object definitions for the graph
|
* Loads node object definitions for the graph
|
||||||
* @returns The node definitions
|
* @returns The node definitions
|
||||||
*/
|
*/
|
||||||
async getNodeDefs(): Promise<any> {
|
async getNodeDefs(): Promise<Record<ComfyNodeID, ComfyNodeDef>> {
|
||||||
return fetch(this.getBackendUrl() + "/object_info", { cache: "no-store" })
|
return fetch(this.getBackendUrl() + "/object_info", { cache: "no-store" })
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
}
|
}
|
||||||
@@ -248,7 +253,7 @@ export default class ComfyAPI {
|
|||||||
postBody = JSON.stringify(body)
|
postBody = JSON.stringify(body)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return Promise.reject({ error })
|
return Promise.reject({ error: error.toString() })
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(this.getBackendUrl() + "/prompt", {
|
return fetch(this.getBackendUrl() + "/prompt", {
|
||||||
@@ -260,12 +265,12 @@ export default class ComfyAPI {
|
|||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.status != 200) {
|
if (res.status != 200) {
|
||||||
throw await res.text()
|
throw await res.json()
|
||||||
}
|
}
|
||||||
return res.json()
|
return res.json()
|
||||||
})
|
})
|
||||||
.then(raw => { return { promptID: raw.prompt_id } })
|
.then(raw => { return { promptID: raw.prompt_id } })
|
||||||
.catch(error => { return { error } })
|
.catch(error => { return error })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Block, BlockTitle } from "@gradio/atoms";
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
import Accordion from "$lib/components/gradio/app/Accordion.svelte";
|
import Accordion from "$lib/components/gradio/app/Accordion.svelte";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import WidgetContainer from "./WidgetContainer.svelte"
|
import WidgetContainer from "./WidgetContainer.svelte"
|
||||||
|
|
||||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
@@ -60,10 +61,11 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container && children}
|
{#if container && Array.isArray(children)}
|
||||||
|
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
|
||||||
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
||||||
class:hide-block={container.attrs.containerVariant === "hidden"}
|
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
class:selected
|
||||||
class:root-container={zIndex === 0}
|
class:root-container={zIndex === 0}
|
||||||
class:is-executing={container.isNodeExecuting}
|
class:is-executing={container.isNodeExecuting}
|
||||||
class:edit={edit}>
|
class:edit={edit}>
|
||||||
@@ -120,6 +122,10 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.container {
|
.container {
|
||||||
|
&.selected {
|
||||||
|
background: var(--comfy-container-selected-background-fill) !important;
|
||||||
|
}
|
||||||
|
|
||||||
> :global(*) {
|
> :global(*) {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Block, BlockTitle } from "@gradio/atoms";
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import WidgetContainer from "./WidgetContainer.svelte"
|
import WidgetContainer from "./WidgetContainer.svelte"
|
||||||
|
|
||||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
@@ -22,14 +23,22 @@
|
|||||||
export let dragDisabled: boolean = false;
|
export let dragDisabled: boolean = false;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|
||||||
let attrsChanged: Writable<boolean> | null = null;
|
let attrsChanged: Writable<number> | null = null;
|
||||||
let children: IDragItem[] | null = null;
|
let children: IDragItem[] | null = null;
|
||||||
const flipDurationMs = 100;
|
const flipDurationMs = 100;
|
||||||
|
|
||||||
$: if (container) {
|
$: if (container) {
|
||||||
|
const entry = $layoutState.allItems[container.id]
|
||||||
|
if (entry) {
|
||||||
children = $layoutState.allItems[container.id].children;
|
children = $layoutState.allItems[container.id].children;
|
||||||
attrsChanged = container.attrsChanged
|
attrsChanged = container.attrsChanged
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
container = null;
|
||||||
|
children = null;
|
||||||
|
attrsChanged = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
children = null;
|
children = null;
|
||||||
attrsChanged = null
|
attrsChanged = null
|
||||||
@@ -44,18 +53,17 @@
|
|||||||
children = layoutState.updateChildren(container, evt.detail.items)
|
children = layoutState.updateChildren(container, evt.detail.items)
|
||||||
// Ensure dragging is stopped on drag finish
|
// Ensure dragging is stopped on drag finish
|
||||||
};
|
};
|
||||||
|
|
||||||
const tt = "asd\nasdlkj"
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container && children}
|
{#if container && Array.isArray(children)}
|
||||||
|
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
|
||||||
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
||||||
class:hide-block={container.attrs.containerVariant === "hidden"}
|
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
class:selected
|
||||||
class:root-container={zIndex === 0}
|
class:root-container={zIndex === 0}
|
||||||
class:is-executing={container.isNodeExecuting}
|
class:is-executing={container.isNodeExecuting}
|
||||||
class:mobile={isMobile}
|
class:mobile={isMobile}
|
||||||
class:edit={edit}>
|
class:edit>
|
||||||
<Block>
|
<Block>
|
||||||
{#if container.attrs.title && container.attrs.title !== ""}
|
{#if container.attrs.title && container.attrs.title !== ""}
|
||||||
<label for={String(container.id)} class={($uiState.uiUnlocked && $uiState.uiEditMode === "widgets") ? "edit-title-label" : ""}>
|
<label for={String(container.id)} class={($uiState.uiUnlocked && $uiState.uiEditMode === "widgets") ? "edit-title-label" : ""}>
|
||||||
@@ -64,7 +72,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="v-pane"
|
<div class="v-pane"
|
||||||
class:empty={children.length === 0}
|
class:empty={children.length === 0}
|
||||||
class:edit={edit}
|
class:edit
|
||||||
use:dndzone="{{
|
use:dndzone="{{
|
||||||
items: children,
|
items: children,
|
||||||
flipDurationMs,
|
flipDurationMs,
|
||||||
@@ -77,8 +85,9 @@
|
|||||||
on:finalize="{handleFinalize}"
|
on:finalize="{handleFinalize}"
|
||||||
>
|
>
|
||||||
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||||
{@const hidden = isHidden(item)}
|
{@const hidden = isHidden(item) && !edit}
|
||||||
<div class="animation-wrapper"
|
<div class="animation-wrapper"
|
||||||
|
class:edit
|
||||||
class:hidden={hidden}
|
class:hidden={hidden}
|
||||||
animate:flip={{duration:flipDurationMs}}
|
animate:flip={{duration:flipDurationMs}}
|
||||||
style={item?.attrs?.style || ""}
|
style={item?.attrs?.style || ""}
|
||||||
@@ -111,17 +120,18 @@
|
|||||||
|
|
||||||
.edit {
|
.edit {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
margin: 0.2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.edit) > .animation-wrapper.hidden {
|
.animation-wrapper.hidden:not(.edit) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
border-width: 3px;
|
border-width: 3px;
|
||||||
border-color: var(--color-grey-400);
|
border-color: var(--comfy-container-empty-border-color);
|
||||||
border-radius: var(--block-radius);
|
border-radius: 0;
|
||||||
background: var(--color-grey-300);
|
background: var(--comfy-container-empty-background-fill);
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
@@ -154,6 +164,10 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: var(--comfy-container-selected-background-fill) !important;
|
||||||
|
}
|
||||||
|
|
||||||
> :global(*) {
|
> :global(*) {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
@@ -234,7 +248,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.handle-hidden {
|
.handle-hidden {
|
||||||
background-color: #40404080;
|
background-color: #303030A0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle-widget:hover {
|
.handle-widget:hover {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Checkbox, TextBox } from "@gradio/form"
|
import { Checkbox, TextBox } from "@gradio/form"
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import layoutState from "$lib/stores/layoutState";
|
import layoutState from "$lib/stores/layoutState";
|
||||||
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import { ImageViewer } from "$lib/ImageViewer";
|
import { ImageViewer } from "$lib/ImageViewer";
|
||||||
import type { ComfyAPIStatus } from "$lib/api";
|
import type { ComfyAPIStatus } from "$lib/api";
|
||||||
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
|
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
|
||||||
@@ -54,12 +55,12 @@
|
|||||||
|
|
||||||
if (!$uiState.uiUnlocked) {
|
if (!$uiState.uiUnlocked) {
|
||||||
app.lCanvas.deselectAllNodes();
|
app.lCanvas.deselectAllNodes();
|
||||||
$layoutState.currentSelectionNodes = []
|
$selectionState.currentSelectionNodes = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($uiState.uiEditMode)
|
$: if ($uiState.uiEditMode)
|
||||||
$layoutState.currentSelection = []
|
$selectionState.currentSelection = []
|
||||||
|
|
||||||
let graphSize = 0;
|
let graphSize = 0;
|
||||||
let graphTransitioning = false;
|
let graphTransitioning = false;
|
||||||
@@ -171,8 +172,6 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await app.setup();
|
await app.setup();
|
||||||
(window as any).app = app;
|
|
||||||
(window as any).appPane = uiPane;
|
|
||||||
|
|
||||||
// await import('../../scss/ux.scss');
|
// await import('../../scss/ux.scss');
|
||||||
|
|
||||||
@@ -184,10 +183,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: if (uiTheme === "gradio-dark") {
|
$: if (uiTheme === "gradio-dark") {
|
||||||
document.getElementById("app").classList.add("dark")
|
document.getElementById("app-root").classList.add("dark")
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
document.getElementById("app").classList.remove("dark")
|
document.getElementById("app-root").classList.remove("dark")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot } from "@litegraph-ts/core";
|
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType, type INodeInputSlot, type NodeID, type NodeTypeSpec, type NodeTypeOpts, type SlotIndex } from "@litegraph-ts/core";
|
||||||
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
|
||||||
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type NodeID, type PromptID } from "$lib/api"
|
import ComfyAPI, { type ComfyAPIStatusResponse, type ComfyBoxPromptExtraData, type ComfyPromptRequest, type ComfyNodeID, type PromptID } from "$lib/api"
|
||||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
@@ -12,11 +12,12 @@ import "@litegraph-ts/nodes-logic"
|
|||||||
import "@litegraph-ts/nodes-math"
|
import "@litegraph-ts/nodes-math"
|
||||||
import "@litegraph-ts/nodes-strings"
|
import "@litegraph-ts/nodes-strings"
|
||||||
import "$lib/nodes/index"
|
import "$lib/nodes/index"
|
||||||
|
import "$lib/nodes/widgets/index"
|
||||||
import * as nodes from "$lib/nodes/index"
|
import * as nodes from "$lib/nodes/index"
|
||||||
|
import * as widgets from "$lib/nodes/widgets/index"
|
||||||
|
|
||||||
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
|
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
|
||||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||||
import * as widgets from "$lib/widgets/index"
|
|
||||||
import queueState from "$lib/stores/queueState";
|
import queueState from "$lib/stores/queueState";
|
||||||
import { type SvelteComponentDev } from "svelte/internal";
|
import { type SvelteComponentDev } from "svelte/internal";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
@@ -28,18 +29,17 @@ import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
|||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { download, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
|
import { download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis } from "$lib/utils";
|
||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
import configState from "$lib/stores/configState";
|
import configState from "$lib/stores/configState";
|
||||||
import { blankGraph } from "$lib/defaultGraph";
|
import { blankGraph } from "$lib/defaultGraph";
|
||||||
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyExecutionResult } from "$lib/utils";
|
||||||
|
import ComfyPromptSerializer, { UpstreamNodeLocator, isActiveBackendNode } from "./ComfyPromptSerializer";
|
||||||
|
import { iterateNodeDefInputs, type ComfyNodeDef, isBackendNodeDefInputType, iterateNodeDefOutputs } from "$lib/ComfyNodeDef";
|
||||||
|
import { ComfyComboNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
|
|
||||||
LiteGraph.catch_exceptions = false;
|
|
||||||
LiteGraph.CANVAS_GRID_SIZE = 32;
|
|
||||||
LiteGraph.default_subgraph_lgraph_factory = () => new ComfyGraph();
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
// Load default visibility
|
// Load default visibility
|
||||||
nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]);
|
nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]);
|
||||||
@@ -56,7 +56,7 @@ export type SerializedAppState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** [link origin, link index] | value */
|
/** [link origin, link index] | value */
|
||||||
export type SerializedPromptInput = [NodeID, number] | any
|
export type SerializedPromptInput = [ComfyNodeID, number] | any
|
||||||
|
|
||||||
export type SerializedPromptInputs = {
|
export type SerializedPromptInputs = {
|
||||||
/* property name -> value or link */
|
/* property name -> value or link */
|
||||||
@@ -64,14 +64,14 @@ export type SerializedPromptInputs = {
|
|||||||
class_type: string
|
class_type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedPromptInputsAll = Record<NodeID, SerializedPromptInputs>
|
export type SerializedPromptInputsAll = Record<ComfyNodeID, SerializedPromptInputs>
|
||||||
|
|
||||||
export type SerializedPrompt = {
|
export type SerializedPrompt = {
|
||||||
workflow: SerializedLGraph,
|
workflow: SerializedLGraph,
|
||||||
output: SerializedPromptInputsAll
|
output: SerializedPromptInputsAll
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedPromptOutputs = Record<NodeID, ComfyExecutionResult>
|
export type SerializedPromptOutputs = Record<ComfyNodeID, ComfyExecutionResult>
|
||||||
|
|
||||||
export type Progress = {
|
export type Progress = {
|
||||||
value: number,
|
value: number,
|
||||||
@@ -79,32 +79,11 @@ export type Progress = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BackendComboNode = {
|
type BackendComboNode = {
|
||||||
comboNode: nodes.ComfyComboNode
|
comboNode: ComfyComboNode,
|
||||||
inputSlot: IComfyInputSlot,
|
comfyInput: IComfyInputSlot,
|
||||||
backendNode: ComfyBackendNode
|
backendNode: ComfyBackendNode
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): boolean {
|
|
||||||
if (!node.isBackendNode)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (tag && !hasTag(node, tag)) {
|
|
||||||
console.debug("Skipping tagged node", tag, node.properties.tags, node)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.mode === NodeMode.NEVER) {
|
|
||||||
// Don't serialize muted nodes
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasTag(node: LGraphNode, tag: string): boolean {
|
|
||||||
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ComfyApp {
|
export default class ComfyApp {
|
||||||
api: ComfyAPI;
|
api: ComfyAPI;
|
||||||
rootEl: HTMLDivElement | null = null;
|
rootEl: HTMLDivElement | null = null;
|
||||||
@@ -115,16 +94,17 @@ export default class ComfyApp {
|
|||||||
dropZone: HTMLElement | null = null;
|
dropZone: HTMLElement | null = null;
|
||||||
nodeOutputs: Record<string, any> = {};
|
nodeOutputs: Record<string, any> = {};
|
||||||
|
|
||||||
dragOverNode: LGraphNode | null = null;
|
|
||||||
shiftDown: boolean = false;
|
shiftDown: boolean = false;
|
||||||
selectedGroupMoving: boolean = false;
|
selectedGroupMoving: boolean = false;
|
||||||
|
|
||||||
private queueItems: QueueItem[] = [];
|
private queueItems: QueueItem[] = [];
|
||||||
private processingQueue: boolean = false;
|
private processingQueue: boolean = false;
|
||||||
private alreadySetup = false;
|
private alreadySetup = false;
|
||||||
|
private promptSerializer: ComfyPromptSerializer;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.api = new ComfyAPI();
|
this.api = new ComfyAPI();
|
||||||
|
this.promptSerializer = new ComfyPromptSerializer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setup(): Promise<void> {
|
async setup(): Promise<void> {
|
||||||
@@ -135,21 +115,16 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
this.setupColorScheme()
|
this.setupColorScheme()
|
||||||
|
|
||||||
this.rootEl = document.getElementById("app") as HTMLDivElement;
|
this.rootEl = document.getElementById("app-root") as HTMLDivElement;
|
||||||
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
|
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
|
||||||
this.lGraph = new ComfyGraph();
|
this.lGraph = new ComfyGraph();
|
||||||
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
|
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
|
||||||
this.canvasCtx = this.canvasEl.getContext("2d");
|
this.canvasCtx = this.canvasEl.getContext("2d");
|
||||||
|
|
||||||
LiteGraph.release_link_on_empty_shows_menu = true;
|
|
||||||
LiteGraph.alt_drag_do_clone_nodes = true;
|
|
||||||
|
|
||||||
const uiUnlocked = get(uiState).uiUnlocked;
|
const uiUnlocked = get(uiState).uiUnlocked;
|
||||||
this.lCanvas.allow_dragnodes = uiUnlocked;
|
this.lCanvas.allow_dragnodes = uiUnlocked;
|
||||||
this.lCanvas.allow_interaction = uiUnlocked;
|
this.lCanvas.allow_interaction = uiUnlocked;
|
||||||
|
|
||||||
(window as any).LiteGraph = LiteGraph;
|
|
||||||
|
|
||||||
// await this.#invokeExtensionsAsync("init");
|
// await this.#invokeExtensionsAsync("init");
|
||||||
await this.registerNodes();
|
await this.registerNodes();
|
||||||
|
|
||||||
@@ -223,15 +198,11 @@ export default class ComfyApp {
|
|||||||
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
||||||
|
|
||||||
private async registerNodes() {
|
private async registerNodes() {
|
||||||
const app = this;
|
|
||||||
|
|
||||||
// Load node definitions from the backend
|
// Load node definitions from the backend
|
||||||
const defs = await this.api.getNodeDefs();
|
const defs = await this.api.getNodeDefs();
|
||||||
|
|
||||||
// Register a node for each definition
|
// Register a node for each definition
|
||||||
for (const nodeId in defs) {
|
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
||||||
const nodeData = defs[nodeId];
|
|
||||||
|
|
||||||
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
||||||
if (typeOverride)
|
if (typeOverride)
|
||||||
console.debug("Attaching custom type to received node:", nodeId, typeOverride)
|
console.debug("Attaching custom type to received node:", nodeId, typeOverride)
|
||||||
@@ -239,19 +210,79 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
const ctor = class extends baseClass {
|
const ctor = class extends baseClass {
|
||||||
constructor(title?: string) {
|
constructor(title?: string) {
|
||||||
super(title, nodeId, nodeData);
|
super(title, nodeId, nodeDef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: LGraphNodeConstructor = {
|
const node: LGraphNodeConstructor = {
|
||||||
class: ctor,
|
class: ctor,
|
||||||
title: nodeData.display_name || nodeData.name,
|
title: nodeDef.display_name || nodeDef.name,
|
||||||
type: nodeId,
|
type: nodeId,
|
||||||
desc: `ComfyNode: ${nodeId}`
|
desc: `ComfyNode: ${nodeId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
LiteGraph.registerNodeType(node);
|
LiteGraph.registerNodeType(node);
|
||||||
node.category = nodeData.category;
|
node.category = nodeDef.category;
|
||||||
|
|
||||||
|
ComfyApp.registerDefaultSlotHandlers(nodeId, nodeDef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static registerDefaultSlotHandlers(nodeId: string, nodeDef: ComfyNodeDef) {
|
||||||
|
const nodeTypeSpec: NodeTypeOpts = {
|
||||||
|
node: nodeId,
|
||||||
|
title: nodeDef.display_name || nodeDef.name,
|
||||||
|
properties: null,
|
||||||
|
inputs: null,
|
||||||
|
outputs: null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [inputName, [inputType, _inputOpts]] of iterateNodeDefInputs(nodeDef)) {
|
||||||
|
if (isBackendNodeDefInputType(inputName, inputType)) {
|
||||||
|
LiteGraph.slot_types_default_out[inputType] ||= ["utils/reroute"]
|
||||||
|
LiteGraph.slot_types_default_out[inputType].push(nodeTypeSpec)
|
||||||
|
|
||||||
|
// Input types have to be stored as lower case
|
||||||
|
// Store each node that can handle this input type
|
||||||
|
const lowerType = inputType.toLocaleLowerCase();
|
||||||
|
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
|
||||||
|
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
|
||||||
|
}
|
||||||
|
LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeId);
|
||||||
|
|
||||||
|
if (!LiteGraph.slot_types_in.includes(lowerType)) {
|
||||||
|
LiteGraph.slot_types_in.push(lowerType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// inputType is an array of combo box entries (["euler", "karras", ...])
|
||||||
|
// or a widget input type ("INT").
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const output of iterateNodeDefOutputs(nodeDef)) {
|
||||||
|
LiteGraph.slot_types_default_in[output.type] ||= ["utils/reroute"]
|
||||||
|
LiteGraph.slot_types_default_in[output.type].push(nodeTypeSpec)
|
||||||
|
|
||||||
|
// Store each node that can handle this output type
|
||||||
|
if (!(output.type in LiteGraph.registered_slot_out_types)) {
|
||||||
|
LiteGraph.registered_slot_out_types[output.type] = { nodes: [] };
|
||||||
|
}
|
||||||
|
LiteGraph.registered_slot_out_types[output.type].nodes.push(nodeId);
|
||||||
|
|
||||||
|
if (!LiteGraph.slot_types_out.includes(output.type)) {
|
||||||
|
LiteGraph.slot_types_out.push(output.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxNodeSuggestions = 5 // TODO config
|
||||||
|
|
||||||
|
// TODO config beforeChanged
|
||||||
|
for (const key of Object.keys(LiteGraph.slot_types_default_in)) {
|
||||||
|
LiteGraph.slot_types_default_in[key] = LiteGraph.slot_types_default_in[key].slice(0, maxNodeSuggestions)
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(LiteGraph.slot_types_default_out)) {
|
||||||
|
LiteGraph.slot_types_default_out[key] = LiteGraph.slot_types_default_out[key].slice(0, maxNodeSuggestions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,21 +373,25 @@ export default class ComfyApp {
|
|||||||
this.lGraph.setDirtyCanvas(true, false);
|
this.lGraph.setDirtyCanvas(true, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: NodeID | null) => {
|
this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => {
|
||||||
queueState.executingUpdated(promptID, nodeID);
|
queueState.executingUpdated(promptID, nodeID);
|
||||||
this.lGraph.setDirtyCanvas(true, false);
|
this.lGraph.setDirtyCanvas(true, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) => {
|
this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => {
|
||||||
this.nodeOutputs[nodeID] = output;
|
this.nodeOutputs[nodeID] = output;
|
||||||
const node = this.lGraph.getNodeById(nodeID) as ComfyGraphNode;
|
const node = this.lGraph.getNodeByIdRecursive(nodeID) as ComfyGraphNode;
|
||||||
if (node?.onExecuted) {
|
if (node?.onExecuted) {
|
||||||
node.onExecuted(output);
|
node.onExecuted(output);
|
||||||
}
|
}
|
||||||
queueState.onExecuted(promptID, nodeID, output)
|
queueState.onExecuted(promptID, nodeID, output)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api.addEventListener("execution_cached", (promptID: PromptID, nodes: NodeID[]) => {
|
this.api.addEventListener("execution_start", (promptID: PromptID) => {
|
||||||
|
queueState.executionStart(promptID)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.addEventListener("execution_cached", (promptID: PromptID, nodes: ComfyNodeID[]) => {
|
||||||
queueState.executionCached(promptID, nodes)
|
queueState.executionCached(promptID, nodes)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -403,7 +438,7 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Distinguish frontend/backend connections
|
// Distinguish frontend/backend connections
|
||||||
const BACKEND_TYPES = ["CLIP", "CLIP_VISION", "CLIP_VISION_OUTPUT", "CONDITIONING", "CONTROL_NET", "LATENT", "MASK", "MODEL", "STYLE_MODEL", "VAE", "UPSCALE_MODEL"]
|
const BACKEND_TYPES = ["CLIP", "CLIP_VISION", "CLIP_VISION_OUTPUT", "CONDITIONING", "CONTROL_NET", "IMAGE", "LATENT", "MASK", "MODEL", "STYLE_MODEL", "VAE", "UPSCALE_MODEL"]
|
||||||
for (const type of BACKEND_TYPES) {
|
for (const type of BACKEND_TYPES) {
|
||||||
setColor(type, "orange")
|
setColor(type, "orange")
|
||||||
}
|
}
|
||||||
@@ -508,6 +543,7 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
layoutState.onStartConfigure();
|
layoutState.onStartConfigure();
|
||||||
|
this.lCanvas.closeAllSubgraphs();
|
||||||
this.lGraph.configure(blankGraph)
|
this.lGraph.configure(blankGraph)
|
||||||
layoutState.initDefaultLayout();
|
layoutState.initDefaultLayout();
|
||||||
uiState.update(s => {
|
uiState.update(s => {
|
||||||
@@ -518,7 +554,7 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runDefaultQueueAction() {
|
runDefaultQueueAction() {
|
||||||
for (const node of this.lGraph.iterateNodesInOrder()) {
|
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||||
if ("onDefaultQueueAction" in node) {
|
if ("onDefaultQueueAction" in node) {
|
||||||
(node as ComfyGraphNode).onDefaultQueueAction()
|
(node as ComfyGraphNode).onDefaultQueueAction()
|
||||||
}
|
}
|
||||||
@@ -559,157 +595,7 @@ export default class ComfyApp {
|
|||||||
* @returns The workflow and node links
|
* @returns The workflow and node links
|
||||||
*/
|
*/
|
||||||
graphToPrompt(tag: string | null = null): SerializedPrompt {
|
graphToPrompt(tag: string | null = null): SerializedPrompt {
|
||||||
// Run frontend-only logic
|
return this.promptSerializer.serialize(this.lGraph, tag)
|
||||||
this.lGraph.runStep(1)
|
|
||||||
|
|
||||||
const workflow = this.lGraph.serialize();
|
|
||||||
|
|
||||||
const output = {};
|
|
||||||
// Process nodes in order of execution
|
|
||||||
for (const node_ of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) {
|
|
||||||
const n = workflow.nodes.find((n) => n.id === node_.id);
|
|
||||||
|
|
||||||
if (!isActiveBackendNode(node_, tag)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = node_ as ComfyBackendNode;
|
|
||||||
|
|
||||||
const inputs = {};
|
|
||||||
|
|
||||||
// Store input values passed by frontend-only nodes
|
|
||||||
if (node.inputs) {
|
|
||||||
for (let i = 0; i < node.inputs.length; i++) {
|
|
||||||
const inp = node.inputs[i];
|
|
||||||
const inputLink = node.getInputLink(i)
|
|
||||||
const inputNode = node.getInputNode(i)
|
|
||||||
|
|
||||||
// We don't check tags for non-backend nodes.
|
|
||||||
// Just check for node inactivity (so you can toggle groups of
|
|
||||||
// tagged frontend nodes on/off)
|
|
||||||
if (inputNode && inputNode.mode === NodeMode.NEVER) {
|
|
||||||
console.debug("Skipping inactive node", inputNode)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inputLink || !inputNode) {
|
|
||||||
if ("config" in inp) {
|
|
||||||
const defaultValue = (inp as IComfyInputSlot).config?.defaultValue
|
|
||||||
if (defaultValue !== null && defaultValue !== undefined)
|
|
||||||
inputs[inp.name] = defaultValue
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let serialize = true;
|
|
||||||
if ("config" in inp)
|
|
||||||
serialize = (inp as IComfyInputSlot).serialize
|
|
||||||
|
|
||||||
let isBackendNode = node.isBackendNode;
|
|
||||||
let isInputBackendNode = false;
|
|
||||||
if ("isBackendNode" in inputNode)
|
|
||||||
isInputBackendNode = (inputNode as ComfyGraphNode).isBackendNode;
|
|
||||||
|
|
||||||
// The reasoning behind this check:
|
|
||||||
// We only want to serialize inputs to nodes with backend equivalents.
|
|
||||||
// And in ComfyBox, the backend nodes in litegraph *never* have widgets, instead they're all inputs.
|
|
||||||
// All values are passed by separate frontend-only nodes,
|
|
||||||
// either UI-bound or something like ConstantInteger.
|
|
||||||
// So we know that any value passed into a backend node *must* come from
|
|
||||||
// a frontend node.
|
|
||||||
// The rest (links between backend nodes) will be serialized after this bit runs.
|
|
||||||
if (serialize && isBackendNode && !isInputBackendNode) {
|
|
||||||
inputs[inp.name] = inputLink.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store links between backend-only and hybrid nodes
|
|
||||||
for (let i = 0; i < node.inputs.length; i++) {
|
|
||||||
let parent: ComfyGraphNode = node.getInputNode(i) as ComfyGraphNode;
|
|
||||||
if (parent) {
|
|
||||||
const seen = {}
|
|
||||||
let link = node.getInputLink(i);
|
|
||||||
|
|
||||||
const isFrontendParent = (parent: ComfyGraphNode) => {
|
|
||||||
if (!parent || parent.isBackendNode)
|
|
||||||
return false;
|
|
||||||
if (tag && !hasTag(parent, tag))
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are frontend-only nodes between us and another
|
|
||||||
// backend node, we have to traverse them first. This
|
|
||||||
// behavior is dependent on the type of node. Reroute nodes
|
|
||||||
// will simply follow their single input, while branching
|
|
||||||
// nodes have conditional logic that determines which link
|
|
||||||
// to follow backwards.
|
|
||||||
while (isFrontendParent(parent)) {
|
|
||||||
if (!("getUpstreamLink" in parent)) {
|
|
||||||
console.warn("[graphToPrompt] Node does not support getUpstreamLink", parent.type)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextLink = parent.getUpstreamLink()
|
|
||||||
if (nextLink == null) {
|
|
||||||
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextLink && !seen[nextLink.id]) {
|
|
||||||
seen[nextLink.id] = true
|
|
||||||
const inputNode = parent.graph.getNodeById(nextLink.origin_id) as ComfyGraphNode;
|
|
||||||
if (inputNode && tag && !hasTag(inputNode, tag)) {
|
|
||||||
console.debug("[graphToPrompt] Skipping tagged intermediate frontend node", tag, node.properties.tags)
|
|
||||||
parent = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.debug("[graphToPrompt] Traverse upstream link", parent.id, inputNode?.id, inputNode?.isBackendNode)
|
|
||||||
link = nextLink;
|
|
||||||
parent = inputNode;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parent = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link && parent && parent.isBackendNode) {
|
|
||||||
if (tag && !hasTag(parent, tag))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
console.debug("[graphToPrompt] final link", parent.id, node.id)
|
|
||||||
const input = node.inputs[i]
|
|
||||||
// TODO can null be a legitimate value in some cases?
|
|
||||||
// Nodes like CLIPLoader will never have a value in the frontend, hence "null".
|
|
||||||
if (!(input.name in inputs))
|
|
||||||
inputs[input.name] = [String(link.origin_id), link.origin_slot];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output[String(node.id)] = {
|
|
||||||
inputs,
|
|
||||||
class_type: node.comfyClass,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove inputs connected to removed nodes
|
|
||||||
for (const nodeId in output) {
|
|
||||||
for (const inputName in output[nodeId].inputs) {
|
|
||||||
if (Array.isArray(output[nodeId].inputs[inputName])
|
|
||||||
&& output[nodeId].inputs[inputName].length === 2
|
|
||||||
&& !output[output[nodeId].inputs[inputName][0]]) {
|
|
||||||
console.debug("Prune removed node link", nodeId, inputName, output[nodeId].inputs[inputName])
|
|
||||||
delete output[nodeId].inputs[inputName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.debug({ workflow, output })
|
|
||||||
// console.debug(promptToGraphVis({ workflow, output }))
|
|
||||||
|
|
||||||
return { workflow, output };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
|
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
|
||||||
@@ -756,6 +642,7 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
const p = this.graphToPrompt(tag);
|
const p = this.graphToPrompt(tag);
|
||||||
const l = layoutState.serialize();
|
const l = layoutState.serialize();
|
||||||
|
console.debug(graphToGraphVis(this.lGraph))
|
||||||
console.debug(promptToGraphVis(p))
|
console.debug(promptToGraphVis(p))
|
||||||
|
|
||||||
const extraData: ComfyBoxPromptExtraData = {
|
const extraData: ComfyBoxPromptExtraData = {
|
||||||
@@ -767,8 +654,8 @@ export default class ComfyApp {
|
|||||||
thumbnails
|
thumbnails
|
||||||
}
|
}
|
||||||
|
|
||||||
let error = null;
|
let error: string | null = null;
|
||||||
let promptID = null;
|
let promptID: PromptID | null = null;
|
||||||
|
|
||||||
const request: ComfyPromptRequest = {
|
const request: ComfyPromptRequest = {
|
||||||
number: num,
|
number: num,
|
||||||
@@ -778,28 +665,26 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.api.queuePrompt(request);
|
const response = await this.api.queuePrompt(request);
|
||||||
|
if (response.error != null) {
|
||||||
// BUG: This can cause race conditions updating frontend state
|
|
||||||
// since we don't have a prompt ID until the backend
|
|
||||||
// returns!
|
|
||||||
promptID = response.promptID;
|
|
||||||
queueState.afterQueued(promptID, num, p.output, extraData)
|
|
||||||
|
|
||||||
error = response.error;
|
error = response.error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
queueState.afterQueued(response.promptID, num, p.output, extraData)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err?.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
const mes = error.response || error.toString()
|
const mes: string = error;
|
||||||
notify(`Error queuing prompt:\n${mes}`, { type: "error" })
|
notify(`Error queuing prompt:\n${mes}`, { type: "error" })
|
||||||
|
console.error(graphToGraphVis(this.lGraph))
|
||||||
console.error(promptToGraphVis(p))
|
console.error(promptToGraphVis(p))
|
||||||
console.error("Error queuing prompt", error, num, p)
|
console.error("Error queuing prompt", error, num, p)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const n of p.workflow.nodes) {
|
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||||
const node = this.lGraph.getNodeById(n.id);
|
|
||||||
if ("afterQueued" in node) {
|
if ("afterQueued" in node) {
|
||||||
(node as ComfyGraphNode).afterQueued(p, tag);
|
(node as ComfyGraphNode).afterQueued(p, tag);
|
||||||
}
|
}
|
||||||
@@ -857,67 +742,77 @@ export default class ComfyApp {
|
|||||||
async refreshComboInNodes(flashUI: boolean = false) {
|
async refreshComboInNodes(flashUI: boolean = false) {
|
||||||
const defs = await this.api.getNodeDefs();
|
const defs = await this.api.getNodeDefs();
|
||||||
|
|
||||||
const toUpdate: BackendComboNode[] = []
|
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
||||||
|
|
||||||
const isComfyComboNode = (node: LGraphNode): boolean => {
|
|
||||||
return node
|
return node
|
||||||
&& node.type === "ui/combo"
|
&& node.type === "ui/combo"
|
||||||
&& "doAutoConfig" in node;
|
&& "doAutoConfig" in node;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isComfyComboInput = (input: INodeInputSlot) => {
|
const isComfyComboInput = (input: INodeInputSlot): input is IComfyInputSlot => {
|
||||||
return "config" in input
|
return "config" in input
|
||||||
&& "widgetNodeType" in input
|
&& "widgetNodeType" in input
|
||||||
&& input.widgetNodeType === "ui/combo";
|
&& input.widgetNodeType === "ui/combo";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node IDs of combo widgets attached to a backend node
|
// Node IDs of combo widgets attached to a backend node
|
||||||
let backendCombos: Set<number> = new Set()
|
const backendUpdatedCombos: Record<NodeID, BackendComboNode> = {}
|
||||||
|
|
||||||
console.debug("[refreshComboInNodes] start")
|
console.debug("[refreshComboInNodes] start")
|
||||||
|
|
||||||
// Figure out which combo nodes to update. They need to be connected to
|
// Figure out which combo nodes to update. They need to be connected to
|
||||||
// an input slot on a backend node with a backend config in the input
|
// an input slot on a backend node with a backend config in the input
|
||||||
// slot connected to.
|
// slot connected to.
|
||||||
for (const node of this.lGraph.iterateNodesInOrder()) {
|
const nodeLocator = new UpstreamNodeLocator(isComfyComboNode)
|
||||||
if (!(node as any).isBackendNode)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const backendNode = (node as ComfyBackendNode)
|
const findComfyInputAndAttachedCombo = (node: LGraphNode, i: SlotIndex): [IComfyInputSlot, ComfyComboNode] | null => {
|
||||||
const found = range(backendNode.inputs.length)
|
const input = node.inputs[i]
|
||||||
.filter(i => {
|
|
||||||
const input = backendNode.inputs[i]
|
|
||||||
const inputNode = backendNode.getInputNode(i)
|
|
||||||
|
|
||||||
// Does this input autocreate a combo box on creation?
|
// Does this input autocreate a combo box on creation?
|
||||||
const isComfyInput = isComfyComboInput(input)
|
const isComfyInput = isComfyComboInput(input)
|
||||||
const isComfyCombo = isComfyComboNode(inputNode)
|
if (!isComfyInput)
|
||||||
|
return null;
|
||||||
|
|
||||||
// console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
|
// Find an attached combo node even if it's inside/outside of a
|
||||||
|
// subgraph, linked after several nodes, etc.
|
||||||
|
const [comboNode, _link] = nodeLocator.locateUpstream(node, i, null);
|
||||||
|
|
||||||
return isComfyCombo && isComfyInput
|
if (comboNode == null)
|
||||||
});
|
return null;
|
||||||
|
|
||||||
for (const inputIndex of found) {
|
const result: [IComfyInputSlot, ComfyComboNode] = [input, comboNode as ComfyComboNode]
|
||||||
const comboNode = backendNode.getInputNode(inputIndex) as nodes.ComfyComboNode
|
return result
|
||||||
const inputSlot = backendNode.inputs[inputIndex] as IComfyInputSlot;
|
}
|
||||||
const def = defs[backendNode.type];
|
|
||||||
|
|
||||||
const hasBackendConfig = def["input"]["required"][inputSlot.name] !== undefined
|
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||||
|
if (!isActiveBackendNode(node))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const found = range(node.inputs.length)
|
||||||
|
.map((i) => findComfyInputAndAttachedCombo(node, i))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
for (const [comfyInput, comboNode] of found) {
|
||||||
|
const def = defs[node.type];
|
||||||
|
|
||||||
|
const hasBackendConfig = def["input"]["required"][comfyInput.name] !== undefined
|
||||||
|
|
||||||
if (hasBackendConfig) {
|
if (hasBackendConfig) {
|
||||||
backendCombos.add(comboNode.id)
|
backendUpdatedCombos[comboNode.id] = { comboNode, comfyInput, backendNode: node }
|
||||||
toUpdate.push({ comboNode, inputSlot, backendNode })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("[refreshComboInNodes] found:", toUpdate.length, toUpdate)
|
console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos)
|
||||||
|
|
||||||
// Mark combo nodes without backend configs as being loaded already.
|
// Mark combo nodes without backend configs as being loaded already.
|
||||||
for (const node of this.lGraph.iterateNodesInOrder()) {
|
for (const node of this.lGraph.iterateNodesOfClassRecursive(ComfyComboNode)) {
|
||||||
if (isComfyComboNode(node) && !backendCombos.has(node.id)) {
|
if (backendUpdatedCombos[node.id] != null) {
|
||||||
const comboNode = node as nodes.ComfyComboNode;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This node isn't connected to a backend node, so it's configured
|
||||||
|
// by the frontend instead.
|
||||||
|
const comboNode = node as ComfyComboNode;
|
||||||
let values = comboNode.properties.values;
|
let values = comboNode.properties.values;
|
||||||
|
|
||||||
// Frontend nodes can declare defaultWidgets which creates a
|
// Frontend nodes can declare defaultWidgets which creates a
|
||||||
@@ -926,29 +821,30 @@ export default class ComfyApp {
|
|||||||
.flatMap(i => node.getInputSlotsConnectedTo(i))
|
.flatMap(i => node.getInputSlotsConnectedTo(i))
|
||||||
.find(inp => "config" in inp && Array.isArray((inp.config as any).values))
|
.find(inp => "config" in inp && Array.isArray((inp.config as any).values))
|
||||||
|
|
||||||
|
let defaultValue = null;
|
||||||
if (foundInput != null) {
|
if (foundInput != null) {
|
||||||
const comfyInput = foundInput as IComfyInputSlot;
|
const comfyInput = foundInput as IComfyInputSlot;
|
||||||
console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values)
|
console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values)
|
||||||
values = comfyInput.config.values;
|
values = comfyInput.config.values;
|
||||||
|
defaultValue = comfyInput.config.defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
comboNode.formatValues(values);
|
comboNode.formatValues(values, defaultValue);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
// Load definitions from the backend.
|
// Load definitions from the backend.
|
||||||
for (const { comboNode, inputSlot, backendNode } of toUpdate) {
|
for (const { comboNode, comfyInput, backendNode } of Object.values(backendUpdatedCombos)) {
|
||||||
const def = defs[backendNode.type];
|
const def = defs[backendNode.type];
|
||||||
const rawValues = def["input"]["required"][inputSlot.name][0];
|
const rawValues = def["input"]["required"][comfyInput.name][0];
|
||||||
|
|
||||||
console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
|
console.debug("[ComfyApp] Reconfiguring combo widget", backendNode.type, "=>", comboNode.type, rawValues.length)
|
||||||
comboNode.doAutoConfig(inputSlot, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
comboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false })
|
||||||
|
|
||||||
comboNode.formatValues(rawValues)
|
comboNode.formatValues(rawValues as string[], true)
|
||||||
if (!rawValues?.includes(get(comboNode.value))) {
|
if (!rawValues?.includes(get(comboNode.value))) {
|
||||||
comboNode.setValue(rawValues[0])
|
comboNode.setValue(rawValues[0], comfyInput.config.defaultValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
input {
|
||||||
|
color: var(--body-text-color);
|
||||||
|
background: var(--input-background-fill);
|
||||||
|
border: var(--input-border-width) solid var(--input-border-color)
|
||||||
|
}
|
||||||
input[disabled] {
|
input[disabled] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|||||||
274
src/lib/components/ComfyPromptSerializer.ts
Normal file
274
src/lib/components/ComfyPromptSerializer.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import type ComfyGraph from "$lib/ComfyGraph";
|
||||||
|
import type { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||||
|
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||||
|
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph, type SlotIndex } from "@litegraph-ts/core";
|
||||||
|
import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputs, SerializedPromptInputsAll } from "./ComfyApp";
|
||||||
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
|
||||||
|
function hasTag(node: LGraphNode, tag: string): boolean {
|
||||||
|
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGraphInputOutput(node: LGraphNode): boolean {
|
||||||
|
return node.is(GraphInput) || node.is(GraphOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isActiveNode(node: LGraphNode, tag: string | null = null): boolean {
|
||||||
|
if (!node)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check tags but not on graph inputs/outputs
|
||||||
|
if (!isGraphInputOutput(node) && (tag && !hasTag(node, tag))) {
|
||||||
|
console.debug("Skipping tagged node", tag, node.properties.tags, node)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.mode !== NodeMode.ALWAYS) {
|
||||||
|
// Don't serialize muted nodes
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isActiveBackendNode(node: LGraphNode, tag: string | null = null): node is ComfyBackendNode {
|
||||||
|
if (!(node as any).isBackendNode)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return isActiveNode(node, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpstreamNodeLocator {
|
||||||
|
constructor(private isTheTargetNode: (node: LGraphNode) => boolean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private followSubgraph(subgraph: Subgraph, link: LLink): [LGraph | null, LLink | null] {
|
||||||
|
if (link.origin_id != subgraph.id)
|
||||||
|
throw new Error("Invalid link and graph output!")
|
||||||
|
|
||||||
|
const innerGraphOutput = subgraph.getInnerGraphOutputByIndex(link.origin_slot)
|
||||||
|
if (innerGraphOutput == null)
|
||||||
|
throw new Error("No inner graph input!")
|
||||||
|
|
||||||
|
const nextLink = innerGraphOutput.getInputLink(0)
|
||||||
|
return [innerGraphOutput.graph, nextLink];
|
||||||
|
}
|
||||||
|
|
||||||
|
private followGraphInput(graphInput: GraphInput, link: LLink): [LGraph | null, LLink | null] {
|
||||||
|
if (link.origin_id != graphInput.id)
|
||||||
|
throw new Error("Invalid link and graph input!")
|
||||||
|
|
||||||
|
const outerSubgraph = graphInput.getParentSubgraph();
|
||||||
|
if (outerSubgraph == null)
|
||||||
|
throw new Error("No outer subgraph!")
|
||||||
|
|
||||||
|
const outerInputIndex = outerSubgraph.inputs.findIndex(i => i.name === graphInput.nameInGraph)
|
||||||
|
if (outerInputIndex == null)
|
||||||
|
throw new Error("No outer input slot!")
|
||||||
|
|
||||||
|
const nextLink = outerSubgraph.getInputLink(outerInputIndex)
|
||||||
|
return [outerSubgraph.graph, nextLink];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUpstreamLink(parent: LGraphNode, currentLink: LLink): [LGraph | null, LLink | null] {
|
||||||
|
if (parent.is(Subgraph)) {
|
||||||
|
console.debug("FollowSubgraph")
|
||||||
|
return this.followSubgraph(parent, currentLink);
|
||||||
|
}
|
||||||
|
else if (parent.is(GraphInput)) {
|
||||||
|
console.debug("FollowGraphInput")
|
||||||
|
return this.followGraphInput(parent, currentLink);
|
||||||
|
}
|
||||||
|
else if ("getUpstreamLink" in parent) {
|
||||||
|
return [parent.graph, (parent as ComfyGraphNode).getUpstreamLink()];
|
||||||
|
}
|
||||||
|
else if (parent.inputs.length === 1) {
|
||||||
|
// Only one input, so assume we can follow it backwards.
|
||||||
|
const link = parent.getInputLink(0);
|
||||||
|
if (link) {
|
||||||
|
return [parent.graph, link]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn("[graphToPrompt] Frontend node does not support getUpstreamLink", parent.type)
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Traverses the graph upstream from outputs towards inputs across
|
||||||
|
* a sequence of nodes dependent on a condition.
|
||||||
|
*
|
||||||
|
* Returns the node and the output link attached to it that leads to the
|
||||||
|
* starting node if any.
|
||||||
|
*/
|
||||||
|
locateUpstream(fromNode: LGraphNode, inputIndex: SlotIndex, tag: string | null): [LGraphNode | null, LLink | null] {
|
||||||
|
let parent = fromNode.getInputNode(inputIndex);
|
||||||
|
if (!parent)
|
||||||
|
return [null, null];
|
||||||
|
|
||||||
|
const seen = {}
|
||||||
|
let currentLink = fromNode.getInputLink(inputIndex);
|
||||||
|
|
||||||
|
const shouldFollowParent = (parent: LGraphNode) => {
|
||||||
|
return isActiveNode(parent, tag) && !this.isTheTargetNode(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are non-target nodes between us and another
|
||||||
|
// backend node, we have to traverse them first. This
|
||||||
|
// behavior is dependent on the type of node. Reroute nodes
|
||||||
|
// will simply follow their single input, while branching
|
||||||
|
// nodes have conditional logic that determines which link
|
||||||
|
// to follow backwards.
|
||||||
|
while (shouldFollowParent(parent)) {
|
||||||
|
const [nextGraph, nextLink] = this.getUpstreamLink(parent, currentLink);
|
||||||
|
|
||||||
|
if (nextLink == null) {
|
||||||
|
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextLink && !seen[nextLink.id]) {
|
||||||
|
seen[nextLink.id] = true
|
||||||
|
const nextParent = nextGraph.getNodeById(nextLink.origin_id);
|
||||||
|
if (!isActiveNode(parent, tag)) {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.debug("[graphToPrompt] Traverse upstream link", parent.id, nextParent?.id, (nextParent as any)?.isBackendNode)
|
||||||
|
currentLink = nextLink;
|
||||||
|
parent = nextParent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isActiveNode(parent, tag) || !this.isTheTargetNode(parent) || currentLink == null)
|
||||||
|
return [null, null];
|
||||||
|
|
||||||
|
return [parent, currentLink]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyPromptSerializer {
|
||||||
|
serializeInputValues(node: ComfyBackendNode): Record<string, SerializedPromptInput> {
|
||||||
|
// Store input values passed by frontend-only nodes
|
||||||
|
if (!node.inputs) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < node.inputs.length; i++) {
|
||||||
|
const inp = node.inputs[i];
|
||||||
|
const inputLink = node.getInputLink(i)
|
||||||
|
const inputNode = node.getInputNode(i)
|
||||||
|
|
||||||
|
// We don't check tags for non-backend nodes.
|
||||||
|
// Just check for node inactivity (so you can toggle groups of
|
||||||
|
// tagged frontend nodes on/off)
|
||||||
|
if (inputNode && inputNode.mode === NodeMode.NEVER) {
|
||||||
|
console.debug("Skipping inactive node", inputNode)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputLink || !inputNode) {
|
||||||
|
if ("config" in inp) {
|
||||||
|
const defaultValue = (inp as IComfyInputSlot).config?.defaultValue
|
||||||
|
if (defaultValue !== null && defaultValue !== undefined)
|
||||||
|
inputs[inp.name] = defaultValue
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialize = true;
|
||||||
|
if ("config" in inp)
|
||||||
|
serialize = (inp as IComfyInputSlot).serialize
|
||||||
|
|
||||||
|
let isBackendNode = node.isBackendNode;
|
||||||
|
let isInputBackendNode = false;
|
||||||
|
if ("isBackendNode" in inputNode)
|
||||||
|
isInputBackendNode = (inputNode as ComfyGraphNode).isBackendNode;
|
||||||
|
|
||||||
|
// The reasoning behind this check:
|
||||||
|
// We only want to serialize inputs to nodes with backend equivalents.
|
||||||
|
// And in ComfyBox, the backend nodes in litegraph *never* have widgets, instead they're all inputs.
|
||||||
|
// All values are passed by separate frontend-only nodes,
|
||||||
|
// either UI-bound or something like ConstantInteger.
|
||||||
|
// So we know that any value passed into a backend node *must* come from
|
||||||
|
// a frontend node.
|
||||||
|
// The rest (links between backend nodes) will be serialized after this bit runs.
|
||||||
|
if (serialize && isBackendNode && !isInputBackendNode) {
|
||||||
|
inputs[inp.name] = inputLink.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeBackendLinks(node: ComfyBackendNode, tag: string | null): Record<string, SerializedPromptInput> {
|
||||||
|
const inputs = {}
|
||||||
|
|
||||||
|
// Find a backend node upstream following before any number of frontend nodes
|
||||||
|
const test = (node: LGraphNode) => (node as any).isBackendNode
|
||||||
|
const nodeLocator = new UpstreamNodeLocator(test)
|
||||||
|
|
||||||
|
// Store links between backend-only and hybrid nodes
|
||||||
|
for (let i = 0; i < node.inputs.length; i++) {
|
||||||
|
const [backendNode, linkLeadingTo] = nodeLocator.locateUpstream(node, i, tag)
|
||||||
|
if (backendNode) {
|
||||||
|
console.debug("[graphToPrompt] final link", backendNode.id, "-->", node.id)
|
||||||
|
const input = node.inputs[i]
|
||||||
|
if (!(input.name in inputs))
|
||||||
|
inputs[input.name] = [String(linkLeadingTo.origin_id), linkLeadingTo.origin_slot];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn("[graphToPrompt] Didn't find upstream link!", node.id, node.type, node.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(graph: ComfyGraph, tag: string | null = null): SerializedPrompt {
|
||||||
|
// Run frontend-only logic
|
||||||
|
graph.runStep(1)
|
||||||
|
|
||||||
|
const workflow = graph.serialize();
|
||||||
|
|
||||||
|
const output: SerializedPromptInputsAll = {};
|
||||||
|
|
||||||
|
// Process nodes in order of execution
|
||||||
|
for (const node of graph.computeExecutionOrderRecursive<ComfyGraphNode>(false, null)) {
|
||||||
|
const n = workflow.nodes.find((n) => n.id === node.id);
|
||||||
|
|
||||||
|
if (!isActiveBackendNode(node, tag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = this.serializeInputValues(node);
|
||||||
|
const links = this.serializeBackendLinks(node, tag);
|
||||||
|
|
||||||
|
output[String(node.id)] = {
|
||||||
|
inputs: { ...inputs, ...links },
|
||||||
|
class_type: node.comfyClass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove inputs connected to removed nodes
|
||||||
|
for (const nodeId in output) {
|
||||||
|
for (const inputName in output[nodeId].inputs) {
|
||||||
|
if (Array.isArray(output[nodeId].inputs[inputName])
|
||||||
|
&& output[nodeId].inputs[inputName].length === 2
|
||||||
|
&& !output[output[nodeId].inputs[inputName][0]]) {
|
||||||
|
console.debug("Prune removed node link", nodeId, inputName, output[nodeId].inputs[inputName])
|
||||||
|
delete output[nodeId].inputs[inputName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug({ workflow, output })
|
||||||
|
// console.debug(promptToGraphVis({ workflow, output }))
|
||||||
|
|
||||||
|
return { workflow, output };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
import { LGraphNode } from "@litegraph-ts/core"
|
import { LGraphNode } from "@litegraph-ts/core"
|
||||||
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||||
import uiState from "$lib/stores/uiState"
|
import uiState from "$lib/stores/uiState"
|
||||||
|
import selectionState from "$lib/stores/selectionState"
|
||||||
import { get, type Writable, writable } from "svelte/store"
|
import { get, type Writable, writable } from "svelte/store"
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
|
||||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||||
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
let target: IDragItem | null = null;
|
let target: IDragItem | null = null;
|
||||||
let node: LGraphNode | null = null;
|
let node: LGraphNode | null = null;
|
||||||
@@ -17,20 +18,21 @@
|
|||||||
|
|
||||||
$: refreshPropsPanel = $layoutState.refreshPropsPanel;
|
$: refreshPropsPanel = $layoutState.refreshPropsPanel;
|
||||||
|
|
||||||
$: if ($layoutState.currentSelection.length > 0) {
|
$: if ($selectionState.currentSelection.length > 0) {
|
||||||
const targetId = $layoutState.currentSelection.slice(-1)[0]
|
node = null;
|
||||||
target = $layoutState.allItems[targetId].dragItem
|
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
||||||
|
const entry = $layoutState.allItems[targetId]
|
||||||
|
if (entry != null) {
|
||||||
|
target = entry.dragItem
|
||||||
attrsChanged = target.attrsChanged;
|
attrsChanged = target.attrsChanged;
|
||||||
if (target.type === "widget") {
|
if (target.type === "widget") {
|
||||||
node = (target as WidgetLayout).node
|
node = (target as WidgetLayout).node
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
node = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ($layoutState.currentSelectionNodes.length > 0) {
|
else if ($selectionState.currentSelectionNodes.length > 0) {
|
||||||
target = null;
|
target = null;
|
||||||
node = $layoutState.currentSelectionNodes[0]
|
node = $selectionState.currentSelectionNodes[0]
|
||||||
attrsChanged = null;
|
attrsChanged = null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -130,7 +132,7 @@
|
|||||||
value = spec.defaultValue
|
value = spec.defaultValue
|
||||||
else if (spec.serialize)
|
else if (spec.serialize)
|
||||||
value = spec.serialize(value)
|
value = spec.serialize(value)
|
||||||
console.debug("[ComfyProperties] getAttribute", spec.name, value, target, spec)
|
// console.debug("[ComfyProperties] getAttribute", spec.name, value, target, spec)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,13 +142,17 @@
|
|||||||
|
|
||||||
const name = spec.name
|
const name = spec.name
|
||||||
|
|
||||||
console.debug("[ComfyProperties] updateAttribute", spec, value, name, node)
|
// console.debug("[ComfyProperties] updateAttribute", spec, value, name, node)
|
||||||
if (spec.deserialize)
|
if (spec.deserialize)
|
||||||
value = spec.deserialize(value)
|
value = spec.deserialize(value)
|
||||||
|
|
||||||
|
const prevValue = target.attrs[name]
|
||||||
target.attrs[name] = value
|
target.attrs[name] = value
|
||||||
target.attrsChanged.set(get(target.attrsChanged) + 1)
|
target.attrsChanged.set(get(target.attrsChanged) + 1)
|
||||||
|
|
||||||
|
if (spec.onChanged)
|
||||||
|
spec.onChanged(target, value, prevValue)
|
||||||
|
|
||||||
if (node && "propsChanged" in node) {
|
if (node && "propsChanged" in node) {
|
||||||
const comfyNode = node as ComfyWidgetNode
|
const comfyNode = node as ComfyWidgetNode
|
||||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||||
@@ -164,7 +170,7 @@
|
|||||||
value = spec.defaultValue
|
value = spec.defaultValue
|
||||||
else if (spec.serialize)
|
else if (spec.serialize)
|
||||||
value = spec.serialize(value)
|
value = spec.serialize(value)
|
||||||
console.debug("[ComfyProperties] getProperty", spec, value, node)
|
// console.debug("[ComfyProperties] getProperty", spec, value, node)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,13 +179,17 @@
|
|||||||
return
|
return
|
||||||
|
|
||||||
const name = spec.name
|
const name = spec.name
|
||||||
console.warn("[ComfyProperties] updateProperty", name, value)
|
// console.warn("[ComfyProperties] updateProperty", name, value)
|
||||||
|
|
||||||
if (spec.deserialize)
|
if (spec.deserialize)
|
||||||
value = spec.deserialize(value)
|
value = spec.deserialize(value)
|
||||||
|
|
||||||
|
const prevValue = node.properties[name]
|
||||||
node.properties[name] = value;
|
node.properties[name] = value;
|
||||||
|
|
||||||
|
if (spec.onChanged)
|
||||||
|
spec.onChanged(node, value, prevValue)
|
||||||
|
|
||||||
if ("propsChanged" in node) {
|
if ("propsChanged" in node) {
|
||||||
const comfyNode = node as ComfyWidgetNode
|
const comfyNode = node as ComfyWidgetNode
|
||||||
comfyNode.notifyPropsChanged();
|
comfyNode.notifyPropsChanged();
|
||||||
@@ -195,7 +205,7 @@
|
|||||||
value = spec.defaultValue
|
value = spec.defaultValue
|
||||||
else if (spec.serialize)
|
else if (spec.serialize)
|
||||||
value = spec.serialize(value)
|
value = spec.serialize(value)
|
||||||
console.debug("[ComfyProperties] getVar", spec, value, node)
|
// console.debug("[ComfyProperties] getVar", spec, value, node)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,12 +215,16 @@
|
|||||||
|
|
||||||
const name = spec.name
|
const name = spec.name
|
||||||
|
|
||||||
console.debug("[ComfyProperties] updateVar", spec, value, name, node)
|
// console.debug("[ComfyProperties] updateVar", spec, value, name, node)
|
||||||
if (spec.deserialize)
|
if (spec.deserialize)
|
||||||
value = spec.deserialize(value)
|
value = spec.deserialize(value)
|
||||||
|
|
||||||
|
const prevValue = node[name]
|
||||||
node[name] = value;
|
node[name] = value;
|
||||||
|
|
||||||
|
if (spec.onChanged)
|
||||||
|
spec.onChanged(node, value, prevValue)
|
||||||
|
|
||||||
if ("propsChanged" in node) {
|
if ("propsChanged" in node) {
|
||||||
const comfyNode = node as ComfyWidgetNode
|
const comfyNode = node as ComfyWidgetNode
|
||||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||||
@@ -227,7 +241,7 @@
|
|||||||
value = spec.defaultValue
|
value = spec.defaultValue
|
||||||
else if (spec.serialize)
|
else if (spec.serialize)
|
||||||
value = spec.serialize(value)
|
value = spec.serialize(value)
|
||||||
console.debug("[ComfyProperties] getWorkflowAttribute", spec.name, value, spec, $layoutState.attrs[spec.name])
|
// console.debug("[ComfyProperties] getWorkflowAttribute", spec.name, value, spec, $layoutState.attrs[spec.name])
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,11 +250,15 @@
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const name = spec.name
|
const name = spec.name
|
||||||
console.warn("updateWorkflowAttribute", name, value)
|
// console.warn("[ComfyProperties] updateWorkflowAttribute", name, value)
|
||||||
|
|
||||||
|
const prevValue = value
|
||||||
$layoutState.attrs[name] = value
|
$layoutState.attrs[name] = value
|
||||||
$layoutState = $layoutState
|
$layoutState = $layoutState
|
||||||
|
|
||||||
|
if (spec.onChanged)
|
||||||
|
spec.onChanged($layoutState, value, prevValue)
|
||||||
|
|
||||||
if (spec.refreshPanelOnChange)
|
if (spec.refreshPanelOnChange)
|
||||||
doRefreshPanel()
|
doRefreshPanel()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,11 @@
|
|||||||
dateStr = formatDate(date);
|
dateStr = formatDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
|
||||||
|
|
||||||
let message = "Prompt";
|
let message = "Prompt";
|
||||||
if (entry.extraData.subgraphs)
|
if (subgraphs?.length > 0)
|
||||||
message = `Prompt: ${entry.extraData.subgraphs.join(', ')}`
|
message = `Prompt: ${subgraphs.join(', ')}`
|
||||||
|
|
||||||
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
||||||
if (Object.keys(entry.outputs).length > 0) {
|
if (Object.keys(entry.outputs).length > 0) {
|
||||||
@@ -156,12 +158,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
|
let expandAll = false;
|
||||||
let selectedPrompt = null;
|
let selectedPrompt = null;
|
||||||
let selectedImages = [];
|
let selectedImages = [];
|
||||||
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
||||||
selectedPrompt = entry.entry.prompt;
|
selectedPrompt = entry.entry.prompt;
|
||||||
selectedImages = entry.images;
|
selectedImages = entry.images;
|
||||||
showModal = true;
|
showModal = true;
|
||||||
|
expandAll = false
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if(!showModal)
|
$: if(!showModal)
|
||||||
@@ -180,8 +184,16 @@
|
|||||||
<h1 style="padding-bottom: 1rem;">Prompt Details</h1>
|
<h1 style="padding-bottom: 1rem;">Prompt Details</h1>
|
||||||
</div>
|
</div>
|
||||||
{#if selectedPrompt}
|
{#if selectedPrompt}
|
||||||
<PromptDisplay prompt={selectedPrompt} images={selectedImages} />
|
<PromptDisplay prompt={selectedPrompt} images={selectedImages} {expandAll} />
|
||||||
{/if}
|
{/if}
|
||||||
|
<div slot="buttons" let:closeDialog>
|
||||||
|
<Button variant="secondary" on:click={closeDialog}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" on:click={() => (expandAll = !expandAll)}>
|
||||||
|
Expand All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div class="queue">
|
<div class="queue">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import WidgetContainer from "./WidgetContainer.svelte";
|
import WidgetContainer from "./WidgetContainer.svelte";
|
||||||
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
|
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import selectionState from "$lib/stores/selectionState";
|
||||||
|
|
||||||
import Menu from './menu/Menu.svelte';
|
import Menu from './menu/Menu.svelte';
|
||||||
import MenuOption from './menu/MenuOption.svelte';
|
import MenuOption from './menu/MenuOption.svelte';
|
||||||
@@ -29,19 +30,72 @@
|
|||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveTo(delta: number | ((cur: number, total: number) => number)) {
|
||||||
|
const dragItemID = $selectionState.currentSelection[0];
|
||||||
|
const entry = $layoutState.allItems[dragItemID];
|
||||||
|
if (!entry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragItem = entry.dragItem;
|
||||||
|
const containing = entry.parent
|
||||||
|
if (containing == null || containing.type !== "container") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const containingEntry = $layoutState.allItems[containing.id];
|
||||||
|
const oldIndex = containingEntry.children.findIndex(c => c.id === dragItem.id)
|
||||||
|
if (oldIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newIndex: number;
|
||||||
|
if (typeof delta === "number")
|
||||||
|
newIndex = oldIndex + delta;
|
||||||
|
else
|
||||||
|
newIndex = delta(oldIndex, containingEntry.children.length);
|
||||||
|
|
||||||
|
layoutState.moveItem(dragItem, containing as ContainerLayout, newIndex)
|
||||||
|
$layoutState = $layoutState
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp() {
|
||||||
|
moveTo(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown() {
|
||||||
|
moveTo(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToTop() {
|
||||||
|
moveTo(() => 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToBottom() {
|
||||||
|
moveTo((cur: number, total: number) => total - 1)
|
||||||
|
}
|
||||||
|
|
||||||
function groupWidgets(horizontal: boolean) {
|
function groupWidgets(horizontal: boolean) {
|
||||||
const items = layoutState.getCurrentSelection()
|
const items = $selectionState.currentSelection
|
||||||
$layoutState.currentSelection = []
|
$selectionState.currentSelection = []
|
||||||
layoutState.groupItems(items, { direction: horizontal ? "horizontal" : "vertical" })
|
layoutState.groupItems(items, { direction: horizontal ? "horizontal" : "vertical" })
|
||||||
}
|
}
|
||||||
|
|
||||||
let canUngroup = false;
|
let canUngroup = false;
|
||||||
let isDeleteGroup = false;
|
let isDeleteGroup = false;
|
||||||
$: canUngroup = $layoutState.currentSelection.length === 1
|
$: {
|
||||||
&& layoutState.getCurrentSelection()[0].type === "container"
|
canUngroup = false;
|
||||||
|
if ($selectionState.currentSelection.length === 1) {
|
||||||
|
const entry = $layoutState.allItems[$selectionState.currentSelection[0]]
|
||||||
|
if (entry != null) {
|
||||||
|
const item = entry.dragItem;
|
||||||
|
canUngroup = item.type === "container"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$: if (canUngroup) {
|
$: if (canUngroup) {
|
||||||
const dragItem = layoutState.getCurrentSelection()[0];
|
const dragItemID = $selectionState.currentSelection[0];
|
||||||
const entry = $layoutState.allItems[dragItem.id];
|
const entry = $layoutState.allItems[dragItemID];
|
||||||
isDeleteGroup = entry.children.length === 0
|
isDeleteGroup = entry.children.length === 0
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -49,11 +103,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ungroup() {
|
function ungroup() {
|
||||||
const item = layoutState.getCurrentSelection()[0]
|
const itemID = $selectionState.currentSelection[0]
|
||||||
if (!item || item.type !== "container")
|
if (itemID == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
$layoutState.currentSelection = []
|
const entry = $layoutState.allItems[$selectionState.currentSelection[0]]
|
||||||
|
if (entry == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
const item = entry.dragItem;
|
||||||
|
if(item.type !== "container")
|
||||||
|
return
|
||||||
|
|
||||||
|
$selectionState.currentSelection = []
|
||||||
layoutState.ungroup(item as ContainerLayout)
|
layoutState.ungroup(item as ContainerLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,11 +156,28 @@
|
|||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||||
<MenuOption
|
<MenuOption
|
||||||
isDisabled={$layoutState.currentSelection.length === 0}
|
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||||
|
on:click={() => moveUp()}
|
||||||
|
text="Move Up" />
|
||||||
|
<MenuOption
|
||||||
|
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||||
|
on:click={() => moveDown()}
|
||||||
|
text="Move Down" />
|
||||||
|
<MenuOption
|
||||||
|
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||||
|
on:click={() => sendToTop()}
|
||||||
|
text="Send to Top" />
|
||||||
|
<MenuOption
|
||||||
|
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||||
|
on:click={() => sendToBottom()}
|
||||||
|
text="Send to Bottom" />
|
||||||
|
<MenuDivider/>
|
||||||
|
<MenuOption
|
||||||
|
isDisabled={$selectionState.currentSelection.length === 0}
|
||||||
on:click={() => groupWidgets(false)}
|
on:click={() => groupWidgets(false)}
|
||||||
text="Group" />
|
text="Group" />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
isDisabled={$layoutState.currentSelection.length === 0}
|
isDisabled={$selectionState.currentSelection.length === 0}
|
||||||
on:click={() => groupWidgets(true)}
|
on:click={() => groupWidgets(true)}
|
||||||
text="Group Horizontally" />
|
text="Group Horizontally" />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Block, BlockTitle } from "@gradio/atoms";
|
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import WidgetContainer from "./WidgetContainer.svelte"
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import BlockContainer from "./BlockContainer.svelte"
|
import BlockContainer from "./BlockContainer.svelte"
|
||||||
import AccordionContainer from "./AccordionContainer.svelte"
|
import AccordionContainer from "./AccordionContainer.svelte"
|
||||||
import TabsContainer from "./TabsContainer.svelte"
|
import TabsContainer from "./TabsContainer.svelte"
|
||||||
|
|
||||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
|
||||||
|
|
||||||
import {fade} from 'svelte/transition';
|
|
||||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||||
import {cubicIn} from 'svelte/easing';
|
import { type ContainerLayout } from "$lib/stores/layoutState";
|
||||||
import { flip } from 'svelte/animate';
|
|
||||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
|
||||||
import { startDrag, stopDrag } from "$lib/utils"
|
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
@@ -22,7 +15,7 @@
|
|||||||
export let classes: string[] = [];
|
export let classes: string[] = [];
|
||||||
export let showHandles: boolean = false;
|
export let showHandles: boolean = false;
|
||||||
export let isMobile: boolean = false
|
export let isMobile: boolean = false
|
||||||
let attrsChanged: Writable<boolean> | null = null;
|
let attrsChanged: Writable<number> | null = null;
|
||||||
|
|
||||||
$: if (container) {
|
$: if (container) {
|
||||||
attrsChanged = container.attrsChanged
|
attrsChanged = container.attrsChanged
|
||||||
@@ -33,8 +26,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container}
|
{#if container}
|
||||||
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
|
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
|
||||||
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked}
|
{@const dragDisabled = zIndex === 0 || $selectionState.currentSelection.length > 2 || !$uiState.uiUnlocked}
|
||||||
{#key $attrsChanged}
|
{#key $attrsChanged}
|
||||||
{#if edit || !isHidden(container)}
|
{#if edit || !isHidden(container)}
|
||||||
{#if container.attrs.variant === "tabs"}
|
{#if container.attrs.variant === "tabs"}
|
||||||
|
|||||||
@@ -42,11 +42,6 @@
|
|||||||
imgElem.src = convertComfyOutputToComfyURL(value[0])
|
imgElem.src = convertComfyOutputToComfyURL(value[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (!(_value && _value.length > 0 && imgElem)) {
|
|
||||||
imgWidth = 1
|
|
||||||
imgHeight = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChange() {
|
function onChange() {
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
}
|
}
|
||||||
@@ -119,6 +114,8 @@
|
|||||||
uploaded = false;
|
uploaded = false;
|
||||||
pending_upload = true;
|
pending_upload = true;
|
||||||
|
|
||||||
|
imgWidth = 0;
|
||||||
|
imgHeight = 0;
|
||||||
old_value = _value;
|
old_value = _value;
|
||||||
|
|
||||||
if (_value == null)
|
if (_value == null)
|
||||||
@@ -177,9 +174,13 @@
|
|||||||
uploaded = true;
|
uploaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: console.warn(imgWidth, imgHeight, "IMGSIZE!!")
|
||||||
|
|
||||||
function handle_clear(_e: CustomEvent<null>) {
|
function handle_clear(_e: CustomEvent<null>) {
|
||||||
_value = null;
|
_value = null;
|
||||||
value = [];
|
value = [];
|
||||||
|
imgWidth = 0;
|
||||||
|
imgHeight = 0;
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
dispatch("clear")
|
dispatch("clear")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function doClose() {
|
function doClose() {
|
||||||
|
showModal = false;
|
||||||
dialog.close();
|
dialog.close();
|
||||||
dispatch("close")
|
dispatch("close")
|
||||||
}
|
}
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
<slot />
|
<slot />
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<slot name="buttons">
|
<slot name="buttons" {closeDialog}>
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<Button variant="secondary" on:click={doClose}>Close</Button>
|
<Button variant="secondary" on:click={doClose}>Close</Button>
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -6,16 +6,20 @@
|
|||||||
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
|
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
|
||||||
import Accordion from "$lib/components/gradio/app/Accordion.svelte";
|
import Accordion from "$lib/components/gradio/app/Accordion.svelte";
|
||||||
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
|
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
|
||||||
|
import { ImageViewer } from "$lib/ImageViewer";
|
||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
|
|
||||||
const splitLength = 50;
|
const splitLength = 50;
|
||||||
|
|
||||||
export let prompt: SerializedPromptInputsAll;
|
export let prompt: SerializedPromptInputsAll;
|
||||||
export let images: string[] = [];
|
export let images: string[] = [];
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
export let expandAll: boolean = false;
|
||||||
|
|
||||||
let galleryStyle: Styles = {
|
let galleryStyle: Styles = {
|
||||||
grid_cols: [2],
|
grid_cols: [2],
|
||||||
object_fit: "cover",
|
object_fit: "cover",
|
||||||
|
height: "var(--size-96)"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInputLink(input: SerializedPromptInput): boolean {
|
function isInputLink(input: SerializedPromptInput): boolean {
|
||||||
@@ -59,6 +63,11 @@
|
|||||||
copyFeedback(nodeID, inputName);
|
copyFeedback(nodeID, inputName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onGalleryImageClicked(e: CustomEvent<HTMLImageElement>) {
|
||||||
|
// TODO dialog renders over it
|
||||||
|
// ImageViewer.instance.showLightbox(e.detail)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="prompt-display">
|
<div class="prompt-display">
|
||||||
@@ -70,7 +79,7 @@
|
|||||||
{#if filtered.length > 0}
|
{#if filtered.length > 0}
|
||||||
<div class="accordion">
|
<div class="accordion">
|
||||||
<Block padding={true}>
|
<Block padding={true}>
|
||||||
<Accordion label="Node {i+1}: {classType}" open={false}>
|
<Accordion label="Node {i+1}: {classType}" open={expandAll}>
|
||||||
{#each filtered as [inputName, input]}
|
{#each filtered as [inputName, input]}
|
||||||
<Block>
|
<Block>
|
||||||
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
|
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
|
||||||
@@ -121,6 +130,7 @@
|
|||||||
style={galleryStyle}
|
style={galleryStyle}
|
||||||
root={""}
|
root={""}
|
||||||
root_url={""}
|
root_url={""}
|
||||||
|
on:clicked={onGalleryImageClicked}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Block, BlockTitle } from "@gradio/atoms";
|
import { Block, BlockTitle } from "@gradio/atoms";
|
||||||
import { Tabs, TabItem } from "@gradio/tabs";
|
import { Tabs, TabItem } from "@gradio/tabs";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import WidgetContainer from "./WidgetContainer.svelte"
|
import WidgetContainer from "./WidgetContainer.svelte"
|
||||||
|
|
||||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
@@ -67,10 +68,11 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if container && children}
|
{#if container && Array.isArray(children)}
|
||||||
|
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(container.id)}
|
||||||
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
|
||||||
class:hide-block={container.attrs.containerVariant === "hidden"}
|
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
class:selected
|
||||||
class:root-container={zIndex === 0}
|
class:root-container={zIndex === 0}
|
||||||
class:is-executing={container.isNodeExecuting}
|
class:is-executing={container.isNodeExecuting}
|
||||||
class:edit={edit}>
|
class:edit={edit}>
|
||||||
@@ -133,6 +135,10 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: var(--comfy-container-selected-background-fill) !important;
|
||||||
|
}
|
||||||
|
|
||||||
> :global(*) {
|
> :global(*) {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
|
|
||||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||||
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import { startDrag, stopDrag } from "$lib/utils"
|
import { startDrag, stopDrag } from "$lib/utils"
|
||||||
import Container from "./Container.svelte"
|
import Container from "./Container.svelte"
|
||||||
import { type Writable } from "svelte/store"
|
import { type Writable } from "svelte/store"
|
||||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import { NodeMode } from "@litegraph-ts/core";
|
|
||||||
import { isHidden } from "$lib/widgets/utils";
|
import { isHidden } from "$lib/widgets/utils";
|
||||||
|
|
||||||
export let dragItem: IDragItem | null = null;
|
export let dragItem: IDragItem | null = null;
|
||||||
@@ -65,14 +65,17 @@
|
|||||||
<Container {container} {classes} {zIndex} {showHandles} {isMobile} />
|
<Container {container} {classes} {zIndex} {showHandles} {isMobile} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else if widget && widget.node}
|
{:else if widget && widget.node}
|
||||||
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
|
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
|
||||||
{@const hidden = isHidden(widget)}
|
{@const hidden = isHidden(widget)}
|
||||||
|
{@const hovered = $uiState.uiUnlocked && $selectionState.currentHovered.has(widget.id)}
|
||||||
|
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(widget.id)}
|
||||||
{#key $attrsChanged}
|
{#key $attrsChanged}
|
||||||
{#key $propsChanged}
|
{#key $propsChanged}
|
||||||
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
|
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
|
||||||
class:edit={edit}
|
class:edit={edit}
|
||||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
|
class:hovered
|
||||||
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeId == widget.node.id}
|
class:selected
|
||||||
|
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
|
||||||
class:hidden={hidden}
|
class:hidden={hidden}
|
||||||
>
|
>
|
||||||
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
|
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
|
||||||
@@ -80,8 +83,8 @@
|
|||||||
{#if hidden && edit}
|
{#if hidden && edit}
|
||||||
<div class="handle handle-hidden" class:hidden={!edit} />
|
<div class="handle handle-hidden" class:hidden={!edit} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if showHandles}
|
{#if showHandles || hovered}
|
||||||
<div class="handle handle-widget" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
<div class="handle handle-widget" class:hovered data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
{/key}
|
{/key}
|
||||||
@@ -92,12 +95,9 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: var(--color-yellow-200);
|
background: var(--comfy-widget-selected-background-fill);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.container.selected {
|
|
||||||
background: var(--color-yellow-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-executing {
|
.is-executing {
|
||||||
border: 3px dashed var(--color-green-600) !important;
|
border: 3px dashed var(--color-green-600) !important;
|
||||||
@@ -123,9 +123,11 @@
|
|||||||
background-color: #40404080;
|
background-color: #40404080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handle-widget:hover {
|
.handle-widget {
|
||||||
|
&:hover, &.hovered {
|
||||||
background-color: #add8e680;
|
background-color: #add8e680;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.node-type {
|
.node-type {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
@@ -134,7 +136,5 @@
|
|||||||
|
|
||||||
.edit {
|
.edit {
|
||||||
border: 2px dashed var(--color-blue-400);
|
border: 2px dashed var(--color-blue-400);
|
||||||
margin: 0.2em;
|
|
||||||
padding: 0.2em;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
|
|
||||||
export let style: Styles = {};
|
export let style: Styles = {};
|
||||||
export let elem_id: string;
|
export let elem_id: string | null;
|
||||||
export let elem_classes: Array<string> = [];
|
export let elem_classes: Array<string> = [];
|
||||||
export let visible: boolean = true;
|
export let visible: boolean = true;
|
||||||
export let variant: "default" | "panel" | "compact" = "default";
|
export let variant: "default" | "panel" | "compact" = "default";
|
||||||
|
|||||||
@@ -21,19 +21,20 @@
|
|||||||
object_fit: "cover",
|
object_fit: "cover",
|
||||||
height: "auto"
|
height: "auto"
|
||||||
};
|
};
|
||||||
export let imageWidth: number = 1;
|
export let imageWidth: number = 0;
|
||||||
export let imageHeight: number = 1;
|
export let imageHeight: number = 0;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
select: SelectData;
|
select: SelectData;
|
||||||
|
clicked: HTMLImageElement
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// tracks whether the value of the gallery was reset
|
// tracks whether the value of the gallery was reset
|
||||||
let was_reset: boolean = true;
|
let was_reset: boolean = true;
|
||||||
|
|
||||||
$: if (selected_image == null || was_reset) {
|
$: if (selected_image == null || was_reset) {
|
||||||
imageWidth = 1;
|
imageWidth = 0;
|
||||||
imageHeight = 1;
|
imageHeight = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: was_reset = value == null || value.length == 0 ? true : was_reset;
|
$: was_reset = value == null || value.length == 0 ? true : was_reset;
|
||||||
@@ -142,6 +143,14 @@
|
|||||||
|
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let window_height = 0;
|
let window_height = 0;
|
||||||
|
|
||||||
|
let imgElem = null;
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
// selected_image = next
|
||||||
|
if (imgElem)
|
||||||
|
dispatch("clicked", imgElem)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerHeight={window_height} />
|
<svelte:window bind:innerHeight={window_height} />
|
||||||
@@ -166,7 +175,8 @@
|
|||||||
<ModifyUpload on:clear={() => (selected_image = null)} />
|
<ModifyUpload on:clear={() => (selected_image = null)} />
|
||||||
|
|
||||||
<img
|
<img
|
||||||
on:click={() => (selected_image = next)}
|
on:click={onClick}
|
||||||
|
bind:this={imgElem}
|
||||||
src={_value[selected_image][0].data}
|
src={_value[selected_image][0].data}
|
||||||
alt={_value[selected_image][1] || ""}
|
alt={_value[selected_image][1] || ""}
|
||||||
title={_value[selected_image][1] || null}
|
title={_value[selected_image][1] || null}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
export let label: string | undefined = undefined;
|
export let label: string | undefined = undefined;
|
||||||
export let show_label: boolean;
|
export let show_label: boolean;
|
||||||
export let selectable: boolean = false;
|
export let selectable: boolean = false;
|
||||||
export let imageWidth: number = 1;
|
export let imageWidth: number = 0;
|
||||||
export let imageHeight: number = 1;
|
export let imageHeight: number = 0;
|
||||||
let imageElem: HTMLImageElement | null = null;
|
let imageElem: HTMLImageElement | null = null;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
$: value && dispatch("change", value);
|
$: value && dispatch("change", value);
|
||||||
|
|
||||||
$: if (value == null || !imageElem) {
|
$: if (value == null || !imageElem) {
|
||||||
imageWidth = 1;
|
imageWidth = 0;
|
||||||
imageHeight = 1;
|
imageHeight = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle_click = (evt: MouseEvent) => {
|
const handle_click = (evt: MouseEvent) => {
|
||||||
|
|||||||
32
src/lib/init.ts
Normal file
32
src/lib/init.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import ComfyGraph from '$lib/ComfyGraph';
|
||||||
|
import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
|
||||||
|
import layoutState from './stores/layoutState';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
export function configureLitegraph(isMobile: boolean = false) {
|
||||||
|
LiteGraph.catch_exceptions = false;
|
||||||
|
|
||||||
|
// Must be enabled, otherwise subgraphs won't work (because of non-unique node/link IDs)
|
||||||
|
LiteGraph.use_uuids = true;
|
||||||
|
|
||||||
|
LiteGraph.search_filter_enabled = true;
|
||||||
|
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||||
|
LiteGraph.alt_drag_do_clone_nodes = true;
|
||||||
|
LiteGraph.middle_click_slot_add_default_node = true;
|
||||||
|
LiteGraph.dialog_close_on_mouse_leave = false;
|
||||||
|
LiteGraph.search_hide_on_mouse_leave = false;
|
||||||
|
LiteGraph.graph_inputs_outputs_use_combo_widget = true;
|
||||||
|
LiteGraph.search_box_refresh_interval_ms = 150;
|
||||||
|
|
||||||
|
LiteGraph.CANVAS_GRID_SIZE = 32;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
LiteGraph.pointerevents_method = "pointer";
|
||||||
|
}
|
||||||
|
|
||||||
|
Subgraph.default_lgraph_factory = () => new ComfyGraph;
|
||||||
|
|
||||||
|
(window as any).LiteGraph = LiteGraph;
|
||||||
|
(window as any).LGraphCanvas = LGraphCanvas;
|
||||||
|
(window as any).layoutState = get(layoutState)
|
||||||
|
}
|
||||||
@@ -5,10 +5,10 @@ import queueState from "$lib/stores/queueState";
|
|||||||
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
||||||
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import type { NotifyOptions } from "$lib/notify";
|
import type { NotifyOptions } from "$lib/notify";
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import { convertComfyOutputToGradio, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
import { type ComfyExecutionResult, type ComfyImageLocation, convertComfyOutputToGradio, type ComfyUploadImageAPIResponse, parseWhateverIntoComfyImageLocations } from "$lib/utils";
|
||||||
|
|
||||||
export class ComfyQueueEvents extends ComfyGraphNode {
|
export class ComfyQueueEvents extends ComfyGraphNode {
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -59,7 +59,7 @@ LiteGraph.registerNodeType({
|
|||||||
class: ComfyQueueEvents,
|
class: ComfyQueueEvents,
|
||||||
title: "Comfy.QueueEvents",
|
title: "Comfy.QueueEvents",
|
||||||
desc: "Triggers a 'bang' event when a prompt is queued.",
|
desc: "Triggers a 'bang' event when a prompt is queued.",
|
||||||
type: "actions/queue_events"
|
type: "events/queue_events"
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
||||||
@@ -177,8 +177,8 @@ export class ComfySwapAction extends ComfyGraphNode {
|
|||||||
override onAction(action: any, param: any) {
|
override onAction(action: any, param: any) {
|
||||||
const a = this.getInputData(0)
|
const a = this.getInputData(0)
|
||||||
const b = this.getInputData(1)
|
const b = this.getInputData(1)
|
||||||
this.triggerSlot(0, a)
|
this.triggerSlot(0, b)
|
||||||
this.triggerSlot(1, b)
|
this.triggerSlot(1, a)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +469,7 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getModeChanges(action: TagAction, enable: boolean, nodeChanges: Record<string, NodeMode>, widgetChanges: Record<DragItemID, boolean>) {
|
private getModeChanges(action: TagAction, enable: boolean, nodeChanges: Record<string, NodeMode>, widgetChanges: Record<DragItemID, boolean>) {
|
||||||
for (const node of this.graph._nodes) {
|
for (const node of this.graph.iterateNodesInOrderRecursive()) {
|
||||||
if ("tags" in node.properties) {
|
if ("tags" in node.properties) {
|
||||||
const comfyNode = node as ComfyGraphNode;
|
const comfyNode = node as ComfyGraphNode;
|
||||||
const hasTag = comfyNode.properties.tags.indexOf(action.tag) != -1;
|
const hasTag = comfyNode.properties.tags.indexOf(action.tag) != -1;
|
||||||
@@ -482,9 +482,6 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
|||||||
newMode = NodeMode.NEVER;
|
newMode = NodeMode.NEVER;
|
||||||
}
|
}
|
||||||
nodeChanges[node.id] = newMode
|
nodeChanges[node.id] = newMode
|
||||||
node.changeMode(newMode);
|
|
||||||
if ("notifyPropsChanged" in node)
|
|
||||||
(node as ComfyWidgetNode).notifyPropsChanged();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,7 +527,7 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [nodeId, newMode] of Object.entries(nodeChanges)) {
|
for (const [nodeId, newMode] of Object.entries(nodeChanges)) {
|
||||||
this.graph.getNodeById(nodeId).changeMode(newMode);
|
this.graph.getNodeByIdRecursive(nodeId).changeMode(newMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const layout = get(layoutState);
|
const layout = get(layoutState);
|
||||||
@@ -601,27 +598,7 @@ export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
|
|||||||
|
|
||||||
override getPromptThumbnails(): ComfyImageLocation[] | null {
|
override getPromptThumbnails(): ComfyImageLocation[] | null {
|
||||||
const data = this.getInputData(0)
|
const data = this.getInputData(0)
|
||||||
|
return parseWhateverIntoComfyImageLocations(data);
|
||||||
const folderType = this.properties.folderType || "input";
|
|
||||||
|
|
||||||
const convertString = (s: string): ComfyImageLocation => {
|
|
||||||
return { filename: data, subfolder: "", type: folderType }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data === "string") {
|
|
||||||
return [convertString(data)]
|
|
||||||
}
|
|
||||||
else if (data != null && typeof data === "object") {
|
|
||||||
if ("filename" in data && "type" in data)
|
|
||||||
return [data as ComfyImageLocation];
|
|
||||||
}
|
|
||||||
else if (Array.isArray(data) && data.length > 0) {
|
|
||||||
if (typeof data[0] === "string")
|
|
||||||
return data.map(convertString)
|
|
||||||
else if (typeof data[0] === "object" && "filename" in data[0] && "type" in data[0])
|
|
||||||
return data as ComfyImageLocation[]
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
import ComfyWidgets from "$lib/widgets"
|
import ComfyWidgets from "$lib/widgets"
|
||||||
import type { ComfyWidgetNode, ComfyExecutionResult } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
|
import { BuiltInSlotShape, BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||||
|
import { iterateNodeDefOutputs, type ComfyNodeDef, iterateNodeDefInputs } from "$lib/ComfyNodeDef";
|
||||||
|
import type { ComfyExecutionResult } from "$lib/utils";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Base class for any node with configuration sent by the backend.
|
* Base class for any node with configuration sent by the backend.
|
||||||
@@ -30,29 +32,26 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
// It just returns a hash like { "ui": { "images": results } } internally.
|
// It just returns a hash like { "ui": { "images": results } } internally.
|
||||||
// So this will need to be hardcoded for now.
|
// So this will need to be hardcoded for now.
|
||||||
if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
|
if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
|
||||||
this.addOutput("onExecuted", BuiltInSlotType.EVENT, { color_off: "rebeccapurple", color_on: "rebeccapurple" });
|
this.addOutput("OUTPUT", BuiltInSlotType.EVENT, { color_off: "rebeccapurple", color_on: "rebeccapurple" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// comfy class -> input name -> input config
|
// comfy class -> input name -> input config
|
||||||
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
|
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
|
||||||
|
|
||||||
private setup(nodeData: any) {
|
private setup(nodeDef: ComfyNodeDef) {
|
||||||
var inputs = nodeData["input"]["required"];
|
|
||||||
if (nodeData["input"]["optional"] != undefined) {
|
|
||||||
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
|
|
||||||
}
|
|
||||||
|
|
||||||
ComfyBackendNode.defaultInputConfigs[this.type] = {}
|
ComfyBackendNode.defaultInputConfigs[this.type] = {}
|
||||||
|
|
||||||
for (const inputName in inputs) {
|
for (const [inputName, inputData] of iterateNodeDefInputs(nodeDef)) {
|
||||||
const config: Partial<IComfyInputSlot> = {};
|
const config: Partial<IComfyInputSlot> = {};
|
||||||
|
|
||||||
const inputData = inputs[inputName];
|
const [type, opts] = inputData;
|
||||||
const type = inputData[0];
|
|
||||||
|
|
||||||
if (inputData[1]?.forceInput) {
|
if (opts?.forceInput) {
|
||||||
this.addInput(inputName, type);
|
if (Array.isArray(type)) {
|
||||||
|
throw new Error(`Can't have forceInput set to true for an enum type! ${type}`)
|
||||||
|
}
|
||||||
|
this.addInput(inputName, type as string);
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(type)) {
|
if (Array.isArray(type)) {
|
||||||
// Enums
|
// Enums
|
||||||
@@ -73,11 +72,9 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
|||||||
ComfyBackendNode.defaultInputConfigs[this.type][inputName] = (config as IComfyInputSlot).config
|
ComfyBackendNode.defaultInputConfigs[this.type][inputName] = (config as IComfyInputSlot).config
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const o in nodeData["output"]) {
|
for (const output of iterateNodeDefOutputs(nodeDef)) {
|
||||||
const output = nodeData["output"][o];
|
const outputShape = output.is_list ? BuiltInSlotShape.GRID_SHAPE : BuiltInSlotShape.CIRCLE_SHAPE;
|
||||||
const outputName = nodeData["output_name"][o] || output;
|
this.addOutput(output.name, output.type, { shape: outputShape });
|
||||||
const outputShape = nodeData["output_is_list"][o] ? BuiltInSlotShape.GRID_SHAPE : BuiltInSlotShape.CIRCLE_SHAPE;
|
|
||||||
this.addOutput(outputName, output, { shape: outputShape });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.serialize_widgets = false;
|
this.serialize_widgets = false;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||||
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
|
||||||
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
|
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||||
|
import type { ComfyExecutionResult, ComfyImageLocation } from "$lib/utils"
|
||||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
|
||||||
import { comfyFileToAnnotatedFilepath, isComfyBoxImageMetadata, parseWhateverIntoImageMetadata } from "$lib/utils";
|
|
||||||
|
|
||||||
export default class ComfyImageToFilepathNode extends ComfyGraphNode {
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "image", type: "COMFYBOX_IMAGES,COMFYBOX_IMAGE" },
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "filepath", type: "string" },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute() {
|
|
||||||
const data = this.getInputData(0)
|
|
||||||
const meta = parseWhateverIntoImageMetadata(data);
|
|
||||||
if (meta == null || meta.length === 0) {
|
|
||||||
this.setOutputData(0, null)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = comfyFileToAnnotatedFilepath(meta[0].comfyUIFile);
|
|
||||||
this.setOutputData(0, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyImageToFilepathNode,
|
|
||||||
title: "Comfy.ImageToFilepath",
|
|
||||||
desc: "Converts ComfyBox image metadata to an annotated filepath like \"image.png[output]\" for use with ComfyUI.",
|
|
||||||
type: "image/file_to_filepath"
|
|
||||||
})
|
|
||||||
@@ -1,35 +1,98 @@
|
|||||||
import { LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
import { LiteGraph, type ITextWidget, type SlotLayout, type INumberWidget } from "@litegraph-ts/core";
|
||||||
import ComfyGraphNode from "./ComfyGraphNode";
|
import ComfyGraphNode from "./ComfyGraphNode";
|
||||||
import { isComfyBoxImageMetadataArray } from "$lib/utils";
|
import { comfyFileToAnnotatedFilepath, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO: This is just a temporary workaround until litegraph can handle typed
|
|
||||||
* array arguments.
|
|
||||||
*/
|
|
||||||
export default class ComfyPickImageNode extends ComfyGraphNode {
|
export default class ComfyPickImageNode extends ComfyGraphNode {
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: "images", type: "COMFYBOX_IMAGES" },
|
{ name: "images", type: "COMFYBOX_IMAGES,COMFYBOX_IMAGE" },
|
||||||
|
{ name: "index", type: "number" },
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
{ name: "image", type: "COMFYBOX_IMAGE" },
|
{ name: "image", type: "COMFYBOX_IMAGE" },
|
||||||
|
{ name: "filename", type: "string" },
|
||||||
|
{ name: "width", type: "number" },
|
||||||
|
{ name: "height", type: "number" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filepathWidget: ITextWidget;
|
||||||
|
folderWidget: ITextWidget;
|
||||||
|
widthWidget: INumberWidget;
|
||||||
|
heightWidget: INumberWidget;
|
||||||
|
|
||||||
|
constructor(title?: string) {
|
||||||
|
super(title)
|
||||||
|
this.filepathWidget = this.addWidget("text", "File", "")
|
||||||
|
this.folderWidget = this.addWidget("text", "Folder", "")
|
||||||
|
this.widthWidget = this.addWidget("number", "Width", 0)
|
||||||
|
this.heightWidget = this.addWidget("number", "Height", 0)
|
||||||
|
for (const widget of this.widgets)
|
||||||
|
widget.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_value: ComfyBoxImageMetadata[] | null = null;
|
||||||
|
_image: ComfyBoxImageMetadata | null = null;
|
||||||
|
_path: string | null = null;
|
||||||
|
_index: number = 0;
|
||||||
|
|
||||||
|
private setValue(value: ComfyBoxImageMetadata[] | ComfyBoxImageMetadata | null, index: number) {
|
||||||
|
if (value != null && !Array.isArray(value)) {
|
||||||
|
value = [value]
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
const changed = this._value != value || this._index != index;
|
||||||
|
this._value = value as ComfyBoxImageMetadata[];
|
||||||
|
this._index = index;
|
||||||
|
if (changed) {
|
||||||
|
if (value && value[this._index] != null) {
|
||||||
|
this._image = value[this._index]
|
||||||
|
this._path = comfyFileToAnnotatedFilepath(this._image.comfyUIFile);
|
||||||
|
this.filepathWidget.value = this._image.comfyUIFile.filename
|
||||||
|
this.folderWidget.value = this._image.comfyUIFile.type
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._image = null;
|
||||||
|
this._path = null;
|
||||||
|
this.filepathWidget.value = "(None)"
|
||||||
|
this.folderWidget.value = ""
|
||||||
|
}
|
||||||
|
console.log("SET", value, this._image, this._path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override onExecute() {
|
override onExecute() {
|
||||||
const data = this.getInputData(0)
|
const data = this.getInputData(0)
|
||||||
if (data == null || !isComfyBoxImageMetadataArray(data)) {
|
const index = this.getInputData(1) || 0
|
||||||
this.setOutputData(0, null)
|
this.setValue(data, index);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setOutputData(0, data[0]);
|
if (this._image == null) {
|
||||||
|
this.setOutputData(0, null)
|
||||||
|
this.setOutputData(1, null)
|
||||||
|
this.setOutputData(2, 0)
|
||||||
|
this.setOutputData(3, 0)
|
||||||
|
|
||||||
|
this.widthWidget.value = 0
|
||||||
|
this.heightWidget.value = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.setOutputData(0, this._image);
|
||||||
|
this.setOutputData(1, this._path);
|
||||||
|
this.setOutputData(2, this._image.width);
|
||||||
|
this.setOutputData(3, this._image.height);
|
||||||
|
|
||||||
|
// XXX: image size doesn't load until the <img> element is ready on
|
||||||
|
// the page so this can come after several frames' worth of
|
||||||
|
// execution
|
||||||
|
this.widthWidget.value = this._image.width
|
||||||
|
this.heightWidget.value = this._image.height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
LiteGraph.registerNodeType({
|
||||||
class: ComfyPickImageNode,
|
class: ComfyPickImageNode,
|
||||||
title: "Comfy.PickImage",
|
title: "Comfy.PickImage",
|
||||||
desc: "Picks out the first image from an array of ComfyBox images.",
|
desc: "Selects an image from an array of ComfyBox images and returns its properties.",
|
||||||
type: "image/pick_image"
|
type: "image/pick_image"
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"
|
|||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties, type DefaultWidgetLayout } from "./ComfyGraphNode";
|
import ComfyGraphNode, { type ComfyGraphNodeProperties, type DefaultWidgetLayout } from "./ComfyGraphNode";
|
||||||
import { clamp } from "$lib/utils";
|
import { clamp } from "$lib/utils";
|
||||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||||
import { ComfyComboNode } from "./ComfyWidgetNodes";
|
import { ComfyComboNode } from "./widgets";
|
||||||
|
|
||||||
export interface ComfyValueControlProperties extends ComfyGraphNodeProperties {
|
export interface ComfyValueControlProperties extends ComfyGraphNodeProperties {
|
||||||
value: any,
|
value: any,
|
||||||
action: "fixed" | "increment" | "decrement" | "randomize",
|
action: "fixed" | "increment" | "decrement" | "randomize",
|
||||||
min: number,
|
min: number,
|
||||||
max: number,
|
max: number,
|
||||||
step: number
|
step: number,
|
||||||
|
ignoreStepWhenRandom: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const INT_MAX = 1125899906842624;
|
const INT_MAX = 1125899906842624;
|
||||||
@@ -18,10 +19,11 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
|||||||
override properties: ComfyValueControlProperties = {
|
override properties: ComfyValueControlProperties = {
|
||||||
tags: [],
|
tags: [],
|
||||||
value: null,
|
value: null,
|
||||||
action: "fixed",
|
action: "randomize",
|
||||||
min: -INT_MAX,
|
min: -INT_MAX,
|
||||||
max: INT_MAX,
|
max: INT_MAX,
|
||||||
step: 1
|
step: 1,
|
||||||
|
ignoreStepWhenRandom: false
|
||||||
}
|
}
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
static slotLayout: SlotLayout = {
|
||||||
@@ -61,15 +63,11 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override onExecute() {
|
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)
|
|
||||||
|
|
||||||
if (this._aboutToChange > 0) {
|
if (this._aboutToChange > 0) {
|
||||||
this._aboutToChange -= 1;
|
this._aboutToChange -= 1;
|
||||||
if (this._aboutToChange <= 0) {
|
if (this._aboutToChange <= 0) {
|
||||||
const value = this._aboutToChangeValue;
|
const value = this._aboutToChangeValue;
|
||||||
|
console.warn("ABOUTTOCHANGE", value)
|
||||||
this._aboutToChange = 0;
|
this._aboutToChange = 0;
|
||||||
this._aboutToChangeValue = null;
|
this._aboutToChangeValue = null;
|
||||||
this.triggerSlot(1, value)
|
this.triggerSlot(1, value)
|
||||||
@@ -82,8 +80,26 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
|||||||
if (typeof v !== "number")
|
if (typeof v !== "number")
|
||||||
return
|
return
|
||||||
|
|
||||||
let min = this.properties.min
|
let action_ = this.getInputData(2);
|
||||||
let max = this.properties.max
|
if (action_ == null)
|
||||||
|
action_ = "fixed"
|
||||||
|
let min = this.getInputData(3);
|
||||||
|
if (min == null)
|
||||||
|
min = -INT_MAX
|
||||||
|
let max = this.getInputData(4);
|
||||||
|
if (max == null)
|
||||||
|
max = INT_MAX
|
||||||
|
let step = this.getInputData(5);
|
||||||
|
if (step == null)
|
||||||
|
step = 1
|
||||||
|
|
||||||
|
this.setProperty("action", action_)
|
||||||
|
this.setProperty("min", min)
|
||||||
|
this.setProperty("max", max)
|
||||||
|
this.setProperty("step", step)
|
||||||
|
|
||||||
|
min = this.properties.min
|
||||||
|
max = this.properties.max
|
||||||
if (min == null) min = -INT_MAX
|
if (min == null) min = -INT_MAX
|
||||||
if (max == null) max = INT_MAX
|
if (max == null) max = INT_MAX
|
||||||
|
|
||||||
@@ -103,7 +119,8 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
|||||||
v -= this.properties.step;
|
v -= this.properties.step;
|
||||||
break;
|
break;
|
||||||
case "randomize":
|
case "randomize":
|
||||||
v = Math.floor(Math.random() * range) * (this.properties.step) + min;
|
const step = this.properties.ignoreStepWhenRandom ? 1 : this.properties.step
|
||||||
|
v = Math.floor(Math.random() * range) * step + min;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,921 +0,0 @@
|
|||||||
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, type PropertyLayout, type IComboWidget, NodeMode, type INumberWidget, type UUID } from "@litegraph-ts/core";
|
|
||||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
|
|
||||||
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, convertComfyOutputToGradio, range, type ComfyUploadImageType, isComfyBoxImageMetadata, filenameToComfyBoxMetadata, type ComfyBoxImageMetadata, isComfyExecutionResult, executionResultToImageMetadata, parseWhateverIntoImageMetadata } from "$lib/utils"
|
|
||||||
import layoutState from "$lib/stores/layoutState";
|
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
|
||||||
import queueState from "$lib/stores/queueState";
|
|
||||||
|
|
||||||
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 CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
|
||||||
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
|
||||||
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
|
||||||
import ImageEditorWidget from "$lib/widgets/ImageEditorWidget.svelte";
|
|
||||||
import type { NodeID } from "$lib/api";
|
|
||||||
|
|
||||||
export type AutoConfigOptions = {
|
|
||||||
includeProperties?: Set<string> | null,
|
|
||||||
setDefaultValue?: boolean
|
|
||||||
setWidgetTitle?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* NOTE: If you want to add a new widget but it has the same input/output type
|
|
||||||
* as another one of the existing widgets, best to create a new "variant" of
|
|
||||||
* that widget instead.
|
|
||||||
*
|
|
||||||
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
|
||||||
* attribute and set `validNodeTypes` to the type of the litegraph node
|
|
||||||
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
|
|
||||||
* - Add an {#if widget.attrs.variant === <...>} statement in the existing Svelte component
|
|
||||||
*
|
|
||||||
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
|
||||||
* You will have to either implement an internal delay on the event triggering
|
|
||||||
* or use an Event Delay node to ensure the output slot data can propagate to
|
|
||||||
* the rest of the graph first (see `delayChangedEvent` for details)
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface ComfyWidgetProperties extends ComfyGraphNodeProperties {
|
|
||||||
defaultValue: any
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A node that is tied to a UI widget in the frontend. When the frontend's
|
|
||||||
* widget is changed, the value of the first output in the node is updated
|
|
||||||
* in the litegraph instance.
|
|
||||||
*/
|
|
||||||
export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|
||||||
abstract properties: ComfyWidgetProperties;
|
|
||||||
|
|
||||||
value: Writable<T>
|
|
||||||
propsChanged: Writable<number> = writable(0) // dummy to indicate if props changed
|
|
||||||
unsubscribe: Unsubscriber;
|
|
||||||
|
|
||||||
/** Svelte class for the frontend logic */
|
|
||||||
abstract svelteComponentType: typeof SvelteComponentDev
|
|
||||||
|
|
||||||
/** If false, user manually set min/max/step, and should not be autoinherited from connected input */
|
|
||||||
autoConfig: boolean = true;
|
|
||||||
|
|
||||||
copyFromInputLink: boolean = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true wait until next frame update to trigger the changed event.
|
|
||||||
* Reason is, if the event is triggered immediately then other stuff that wants to run
|
|
||||||
* their own onExecute on the output value won't have completed yet.
|
|
||||||
*/
|
|
||||||
delayChangedEvent: boolean = true;
|
|
||||||
|
|
||||||
private _aboutToChange: number = 0;
|
|
||||||
private _aboutToChangeValue: any = null;
|
|
||||||
private _noChangedEvent: boolean = false;
|
|
||||||
|
|
||||||
abstract defaultValue: T;
|
|
||||||
|
|
||||||
/** Names of properties to add as inputs */
|
|
||||||
// shownInputProperties: string[] = []
|
|
||||||
|
|
||||||
/** Names of properties to add as outputs */
|
|
||||||
private shownOutputProperties: Record<string, { type: string, index: number }> = {}
|
|
||||||
outputProperties: { name: string, type: string }[] = []
|
|
||||||
|
|
||||||
override isBackendNode = false;
|
|
||||||
override serialize_widgets = true;
|
|
||||||
|
|
||||||
|
|
||||||
// TODO these are bad, create override methods instead
|
|
||||||
// input slots
|
|
||||||
inputIndex: number | null = null;
|
|
||||||
storeActionName: string | null = "store";
|
|
||||||
|
|
||||||
// output slots
|
|
||||||
outputIndex: number | null = 0;
|
|
||||||
changedIndex: number | null = 1;
|
|
||||||
|
|
||||||
|
|
||||||
displayWidget: ITextWidget;
|
|
||||||
|
|
||||||
override size: Vector2 = [60, 40];
|
|
||||||
|
|
||||||
constructor(name: string, value: T) {
|
|
||||||
const color = LGraphCanvas.node_colors["blue"]
|
|
||||||
super(name)
|
|
||||||
this.value = writable(value)
|
|
||||||
this.color ||= color.color
|
|
||||||
this.bgColor ||= color.bgColor
|
|
||||||
this.displayWidget = this.addWidget<ITextWidget>(
|
|
||||||
"text",
|
|
||||||
"Value",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
this.displayWidget.disabled = true; // prevent editing
|
|
||||||
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
addPropertyAsOutput(propertyName: string, type: string) {
|
|
||||||
if (this.shownOutputProperties[propertyName])
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!(propertyName in this.properties)) {
|
|
||||||
throw `No property named ${propertyName} found!`
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shownOutputProperties[propertyName] = { type, index: this.outputs.length }
|
|
||||||
this.addOutput(propertyName, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatValue(value: any): string {
|
|
||||||
return Watch.toString(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override changeMode(modeTo: NodeMode): boolean {
|
|
||||||
const result = super.changeMode(modeTo);
|
|
||||||
this.notifyPropsChanged();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onValueUpdated(value: any) {
|
|
||||||
// console.debug("[Widget] valueUpdated", this, value)
|
|
||||||
this.displayWidget.value = this.formatValue(value)
|
|
||||||
|
|
||||||
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
|
|
||||||
this.setOutputData(this.outputIndex, get(this.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.changedIndex !== null && this.outputs.length >= this.changedIndex && !this._noChangedEvent) {
|
|
||||||
if (!this.delayChangedEvent)
|
|
||||||
this.triggerChangeEvent(get(this.value))
|
|
||||||
else {
|
|
||||||
// console.debug("[Widget] queueChangeEvent", this, value)
|
|
||||||
this._aboutToChange = 2; // wait 1.5-2 frames, in case we're already in the middle of executing the graph
|
|
||||||
this._aboutToChangeValue = get(this.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._noChangedEvent = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private triggerChangeEvent(value: any) {
|
|
||||||
// console.debug("[Widget] trigger changed", this, value)
|
|
||||||
const changedOutput = this.outputs[this.changedIndex]
|
|
||||||
if (changedOutput.type === BuiltInSlotType.EVENT)
|
|
||||||
this.triggerSlot(this.changedIndex, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
parseValue(value: any): T { return value as T };
|
|
||||||
|
|
||||||
getValue(): T {
|
|
||||||
return get(this.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue(value: any, noChangedEvent: boolean = false) {
|
|
||||||
if (noChangedEvent)
|
|
||||||
this._noChangedEvent = true;
|
|
||||||
|
|
||||||
const parsed = this.parseValue(value)
|
|
||||||
this.value.set(parsed)
|
|
||||||
|
|
||||||
// In case value.set() does not trigger onValueUpdated, we need to reset
|
|
||||||
// the counter here also.
|
|
||||||
this._noChangedEvent = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
|
||||||
if (this.shownOutputProperties != null) {
|
|
||||||
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(param: any, options: object) {
|
|
||||||
if (this.inputIndex != null) {
|
|
||||||
if (this.inputs.length >= this.inputIndex) {
|
|
||||||
const data = this.getInputData(this.inputIndex)
|
|
||||||
if (data != null) { // TODO can "null" be a legitimate value here?
|
|
||||||
this.setValue(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.outputIndex != 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])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire a pending change event after one full step of the graph has
|
|
||||||
// finished processing
|
|
||||||
if (this._aboutToChange > 0) {
|
|
||||||
this._aboutToChange -= 1
|
|
||||||
if (this._aboutToChange <= 0) {
|
|
||||||
const value = this._aboutToChangeValue;
|
|
||||||
this._aboutToChange = 0;
|
|
||||||
this._aboutToChangeValue = null;
|
|
||||||
this.triggerChangeEvent(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onAction(action: any, param: any, options: { action_call?: string }) {
|
|
||||||
if (action === this.storeActionName) {
|
|
||||||
let noChangedEvent = false;
|
|
||||||
let value = param;
|
|
||||||
if (param != null && typeof param === "object" && "value" in param) {
|
|
||||||
value = param.value
|
|
||||||
if ("noChangedEvent" in param)
|
|
||||||
noChangedEvent = Boolean(param.noChangedEvent)
|
|
||||||
}
|
|
||||||
this.setValue(value, noChangedEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onConnectOutput(
|
|
||||||
outputIndex: number,
|
|
||||||
inputType: INodeInputSlot["type"],
|
|
||||||
input: INodeInputSlot,
|
|
||||||
inputNode: LGraphNode,
|
|
||||||
inputIndex: number
|
|
||||||
): boolean {
|
|
||||||
const anyConnected = range(this.outputs.length).some(i => this.getOutputLinks(i).length > 0);
|
|
||||||
|
|
||||||
if (this.autoConfig && "config" in input && !anyConnected && (input as IComfyInputSlot).widgetNodeType === this.type) {
|
|
||||||
this.doAutoConfig(input as IComfyInputSlot)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
doAutoConfig(input: IComfyInputSlot, options: AutoConfigOptions = { setDefaultValue: true, setWidgetTitle: true }) {
|
|
||||||
// Copy properties from default config in input slot
|
|
||||||
const comfyInput = input as IComfyInputSlot;
|
|
||||||
for (const key in comfyInput.config) {
|
|
||||||
if (options.includeProperties == null || options.includeProperties.has(key))
|
|
||||||
this.setProperty(key, comfyInput.config[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.setDefaultValue) {
|
|
||||||
if ("defaultValue" in this.properties)
|
|
||||||
this.setValue(this.properties.defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.setWidgetTitle) {
|
|
||||||
const widget = layoutState.findLayoutForNode(this.id as NodeID)
|
|
||||||
if (widget && input.name !== "") {
|
|
||||||
widget.attrs.title = input.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.debug("Property copy", input, this.properties)
|
|
||||||
|
|
||||||
this.setValue(get(this.value))
|
|
||||||
|
|
||||||
this.onAutoConfig(input);
|
|
||||||
|
|
||||||
this.notifyPropsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
onAutoConfig(input: IComfyInputSlot) {
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyPropsChanged() {
|
|
||||||
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as NodeID)
|
|
||||||
if (layoutEntry && layoutEntry.parent) {
|
|
||||||
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
|
||||||
}
|
|
||||||
// console.debug("propsChanged", this)
|
|
||||||
this.propsChanged.set(get(this.propsChanged) + 1)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override onConnectionsChange(
|
|
||||||
type: LConnectionKind,
|
|
||||||
slotIndex: number,
|
|
||||||
isConnected: boolean,
|
|
||||||
link: LLink,
|
|
||||||
ioSlot: (INodeOutputSlot | INodeInputSlot)
|
|
||||||
): void {
|
|
||||||
super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
|
|
||||||
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.notifyPropsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
clampOneConfig(input: IComfyInputSlot) { }
|
|
||||||
|
|
||||||
override onSerialize(o: SerializedLGraphNode) {
|
|
||||||
(o as any).comfyValue = get(this.value);
|
|
||||||
(o as any).shownOutputProperties = this.shownOutputProperties
|
|
||||||
super.onSerialize(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
override onConfigure(o: SerializedLGraphNode) {
|
|
||||||
const value = (o as any).comfyValue || LiteGraph.cloneObject(this.defaultValue);
|
|
||||||
this.value.set(value);
|
|
||||||
this.shownOutputProperties = (o as any).shownOutputProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
override stripUserState(o: SerializedLGraphNode) {
|
|
||||||
super.stripUserState(o);
|
|
||||||
(o as any).comfyValue = this.defaultValue;
|
|
||||||
o.properties.defaultValue = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComfySliderProperties extends ComfyWidgetProperties {
|
|
||||||
min: number,
|
|
||||||
max: number,
|
|
||||||
step: number,
|
|
||||||
precision: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfySliderNode extends ComfyWidgetNode<number> {
|
|
||||||
override properties: ComfySliderProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: 0,
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
step: 1,
|
|
||||||
precision: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = RangeWidget
|
|
||||||
override defaultValue = 0;
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "value", type: "number" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
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 parseValue(value: any): number {
|
|
||||||
if (typeof value !== "number")
|
|
||||||
return this.properties.min;
|
|
||||||
return 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[]
|
|
||||||
|
|
||||||
/* JS Function body that takes a parameter named "value" as a parameter and returns the label for each combo entry */
|
|
||||||
convertValueToLabelCode: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyComboNode extends ComfyWidgetNode<string> {
|
|
||||||
override properties: ComfyComboProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: "A",
|
|
||||||
values: ["A", "B", "C", "D"],
|
|
||||||
convertValueToLabelCode: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = ComboWidget
|
|
||||||
override defaultValue = "A";
|
|
||||||
override saveUserState = false;
|
|
||||||
|
|
||||||
// True if at least one combo box refresh has taken place
|
|
||||||
// Wait until the initial graph load for combo to be valid.
|
|
||||||
firstLoad: Writable<boolean>;
|
|
||||||
valuesForCombo: Writable<any[] | null>; // Changed when the combo box has values.
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, "A")
|
|
||||||
this.firstLoad = writable(false)
|
|
||||||
this.valuesForCombo = writable(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override onPropertyChanged(property: any, value: any) {
|
|
||||||
if (property === "values" || property === "convertValueToLabelCode") {
|
|
||||||
// this.formatValues(this.properties.values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatValues(values: string[]) {
|
|
||||||
if (values == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.properties.values = values;
|
|
||||||
|
|
||||||
let formatter: any;
|
|
||||||
if (this.properties.convertValueToLabelCode)
|
|
||||||
formatter = new Function("value", this.properties.convertValueToLabelCode) as (v: string) => string;
|
|
||||||
else
|
|
||||||
formatter = (value: any) => `${value}`;
|
|
||||||
|
|
||||||
let valuesForCombo = []
|
|
||||||
|
|
||||||
try {
|
|
||||||
valuesForCombo = this.properties.values.map((value, index) => {
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
label: formatter(value),
|
|
||||||
index
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error("Failed formatting!", err)
|
|
||||||
valuesForCombo = this.properties.values.map((value, index) => {
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
label: `${value}`,
|
|
||||||
index
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.firstLoad.set(true)
|
|
||||||
this.valuesForCombo.set(valuesForCombo);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 parseValue(value: any): string {
|
|
||||||
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
|
||||||
return this.properties.values[0]
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
override clampOneConfig(input: IComfyInputSlot) {
|
|
||||||
if (!input.config.values)
|
|
||||||
this.setValue("")
|
|
||||||
else 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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onSerialize(o: SerializedLGraphNode) {
|
|
||||||
super.onSerialize(o);
|
|
||||||
// TODO fix saving combo nodes with huge values lists
|
|
||||||
o.properties.values = []
|
|
||||||
}
|
|
||||||
|
|
||||||
override stripUserState(o: SerializedLGraphNode) {
|
|
||||||
super.stripUserState(o);
|
|
||||||
o.properties.values = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyComboNode,
|
|
||||||
title: "UI.Combo",
|
|
||||||
desc: "Combo box outputting a string value",
|
|
||||||
type: "ui/combo"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyTextProperties extends ComfyWidgetProperties {
|
|
||||||
multiline: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyTextNode extends ComfyWidgetNode<string> {
|
|
||||||
override properties: ComfyTextProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: "",
|
|
||||||
multiline: false
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = TextWidget
|
|
||||||
override defaultValue = "";
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): string {
|
|
||||||
return `${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 interface ComfyExecutionResult {
|
|
||||||
// Technically this response can contain arbitrary data, but "images" is the
|
|
||||||
// most frequently used as it's output by LoadImage and PreviewImage, the
|
|
||||||
// only two output nodes in base ComfyUI.
|
|
||||||
images: ComfyImageLocation[] | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Raw output entry as received from ComfyUI's backend */
|
|
||||||
export type ComfyImageLocation = {
|
|
||||||
/* Filename with extension in the subfolder. */
|
|
||||||
filename: string,
|
|
||||||
/* Subfolder in the containing folder. */
|
|
||||||
subfolder: string,
|
|
||||||
/* Base ComfyUI folder where the image is located. */
|
|
||||||
type: ComfyUploadImageType
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
|
||||||
index: number,
|
|
||||||
updateMode: "replace" | "append",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
|
||||||
override properties: ComfyGalleryProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: [],
|
|
||||||
index: 0,
|
|
||||||
updateMode: "replace",
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "images", type: "OUTPUT" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "images", type: "COMFYBOX_IMAGES" },
|
|
||||||
{ name: "selected_index", type: "number" },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
static propertyLayout: PropertyLayout = [
|
|
||||||
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
|
||||||
]
|
|
||||||
|
|
||||||
override svelteComponentType = GalleryWidget
|
|
||||||
override defaultValue = []
|
|
||||||
override inputIndex = null;
|
|
||||||
override saveUserState = false;
|
|
||||||
override outputIndex = null;
|
|
||||||
override changedIndex = null;
|
|
||||||
|
|
||||||
selectedFilename: string | null = null;
|
|
||||||
|
|
||||||
selectedIndexWidget: ITextWidget;
|
|
||||||
modeWidget: IComboWidget;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, [])
|
|
||||||
this.selectedIndexWidget = this.addWidget("text", "Selected", String(this.properties.index), "index")
|
|
||||||
this.selectedIndexWidget.disabled = true;
|
|
||||||
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
|
||||||
}
|
|
||||||
|
|
||||||
override onPropertyChanged(property: any, value: any) {
|
|
||||||
if (property === "updateMode") {
|
|
||||||
this.modeWidget.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute() {
|
|
||||||
this.setOutputData(0, get(this.value))
|
|
||||||
this.setOutputData(1, this.properties.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override onAction(action: any, param: any, options: { action_call?: string }) {
|
|
||||||
super.onAction(action, param, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
override formatValue(value: ComfyBoxImageMetadata[] | null): string {
|
|
||||||
return `Images: ${value?.length || 0}`
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any): ComfyBoxImageMetadata[] {
|
|
||||||
const meta = parseWhateverIntoImageMetadata(param) || [];
|
|
||||||
|
|
||||||
console.debug("[ComfyGalleryNode] Received output!", param)
|
|
||||||
|
|
||||||
if (this.properties.updateMode === "append") {
|
|
||||||
const currentValue = get(this.value)
|
|
||||||
return currentValue.concat(meta)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override setValue(value: any, noChangedEvent: boolean = false) {
|
|
||||||
super.setValue(value, noChangedEvent)
|
|
||||||
this.setProperty("index", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyGalleryNode,
|
|
||||||
title: "UI.Gallery",
|
|
||||||
desc: "Gallery that shows most recent outputs",
|
|
||||||
type: "ui/gallery"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
|
||||||
param: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
|
||||||
override properties: ComfyButtonProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: false,
|
|
||||||
param: "bang"
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
outputs: [
|
|
||||||
{ name: "clicked", type: BuiltInSlotType.EVENT },
|
|
||||||
{ name: "isClicked", type: "boolean" },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = ButtonWidget;
|
|
||||||
override defaultValue = false;
|
|
||||||
override outputIndex = 1;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any): boolean {
|
|
||||||
return Boolean(param);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick() {
|
|
||||||
this.setValue(true)
|
|
||||||
this.triggerSlot(0, this.properties.param);
|
|
||||||
this.setValue(false) // TODO onRelease
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyButtonNode,
|
|
||||||
title: "UI.Button",
|
|
||||||
desc: "Button that triggers an event when clicked",
|
|
||||||
type: "ui/button"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyCheckboxProperties extends ComfyWidgetProperties {
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
|
||||||
override properties: ComfyCheckboxProperties = {
|
|
||||||
tags: [],
|
|
||||||
defaultValue: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "value", type: "boolean" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "boolean" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = CheckboxWidget;
|
|
||||||
override defaultValue = false;
|
|
||||||
override changedIndex = 1;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any) {
|
|
||||||
return Boolean(param);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyCheckboxNode,
|
|
||||||
title: "UI.Checkbox",
|
|
||||||
desc: "Checkbox that stores a boolean value",
|
|
||||||
type: "ui/checkbox"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyRadioProperties extends ComfyWidgetProperties {
|
|
||||||
choices: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyRadioNode extends ComfyWidgetNode<string> {
|
|
||||||
override properties: ComfyRadioProperties = {
|
|
||||||
tags: [],
|
|
||||||
choices: ["Choice A", "Choice B", "Choice C"],
|
|
||||||
defaultValue: "Choice A",
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "value", type: "string,number" },
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "value", type: "string" },
|
|
||||||
{ name: "index", type: "number" },
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = RadioWidget;
|
|
||||||
override defaultValue = "";
|
|
||||||
override changedIndex = 2;
|
|
||||||
|
|
||||||
indexWidget: INumberWidget;
|
|
||||||
|
|
||||||
index = 0;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, "Choice A")
|
|
||||||
this.indexWidget = this.addWidget("number", "Index", this.index)
|
|
||||||
this.indexWidget.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
override onExecute(param: any, options: object) {
|
|
||||||
super.onExecute(param, options);
|
|
||||||
this.setOutputData(1, this.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override setValue(value: string, noChangedEvent: boolean = false) {
|
|
||||||
super.setValue(value, noChangedEvent)
|
|
||||||
|
|
||||||
value = get(this.value);
|
|
||||||
|
|
||||||
const index = this.properties.choices.indexOf(value)
|
|
||||||
if (index === -1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.index = index;
|
|
||||||
this.indexWidget.value = index;
|
|
||||||
this.setOutputData(1, this.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(param: any): string {
|
|
||||||
if (typeof param === "string") {
|
|
||||||
if (this.properties.choices.indexOf(param) === -1)
|
|
||||||
return this.properties.choices[0]
|
|
||||||
return param
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const index = clamp(parseInt(param), 0, this.properties.choices.length - 1)
|
|
||||||
return this.properties.choices[index] || this.properties.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyRadioNode,
|
|
||||||
title: "UI.Radio",
|
|
||||||
desc: "Radio that outputs a string and index",
|
|
||||||
type: "ui/radio"
|
|
||||||
})
|
|
||||||
|
|
||||||
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComfyImageEditorNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
|
||||||
override properties: ComfyImageEditorNodeProperties = {
|
|
||||||
defaultValue: [],
|
|
||||||
tags: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
static slotLayout: SlotLayout = {
|
|
||||||
inputs: [
|
|
||||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{ name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches
|
|
||||||
{ name: "changed", type: BuiltInSlotType.EVENT },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
override svelteComponentType = ImageEditorWidget;
|
|
||||||
override defaultValue = [];
|
|
||||||
override outputIndex = 0;
|
|
||||||
override inputIndex = null;
|
|
||||||
override changedIndex = 1;
|
|
||||||
override storeActionName = "store";
|
|
||||||
override saveUserState = false;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
override parseValue(value: any): ComfyBoxImageMetadata[] {
|
|
||||||
return parseWhateverIntoImageMetadata(value) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
override formatValue(value: GradioFileData[]): string {
|
|
||||||
return `Images: ${value?.length || 0}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.registerNodeType({
|
|
||||||
class: ComfyImageEditorNode,
|
|
||||||
title: "UI.ImageEditor",
|
|
||||||
desc: "Widget that lets you upload and edit a multi-layered image. Can also act like a standalone image uploader.",
|
|
||||||
type: "ui/image_editor"
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export { default as ComfyReroute } from "./ComfyReroute"
|
export { default as ComfyReroute } from "./ComfyReroute"
|
||||||
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
|
|
||||||
export {
|
export {
|
||||||
ComfyQueueEvents,
|
ComfyQueueEvents,
|
||||||
ComfyCopyAction,
|
ComfyCopyAction,
|
||||||
@@ -17,4 +16,3 @@ export { default as ComfySelector } from "./ComfySelector"
|
|||||||
export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode"
|
export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode"
|
||||||
export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton"
|
export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton"
|
||||||
export { default as ComfyPickImageNode } from "./ComfyPickImageNode"
|
export { default as ComfyPickImageNode } from "./ComfyPickImageNode"
|
||||||
export { default as ComfyImageToFilepathNode } from "./ComfyImageToFilepathNode"
|
|
||||||
|
|||||||
48
src/lib/nodes/widgets/ComfyButtonNode.ts
Normal file
48
src/lib/nodes/widgets/ComfyButtonNode.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
|
||||||
|
import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyButtonProperties extends ComfyWidgetProperties {
|
||||||
|
param: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||||
|
override properties: ComfyButtonProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: false,
|
||||||
|
param: "bang"
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
outputs: [
|
||||||
|
{ name: "clicked", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = ButtonWidget;
|
||||||
|
override defaultValue = false;
|
||||||
|
override outputSlotName = null;
|
||||||
|
override changedEventName = null;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any): boolean {
|
||||||
|
return Boolean(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick() {
|
||||||
|
this.setValue(true)
|
||||||
|
this.triggerSlot(0, this.properties.param);
|
||||||
|
this.setValue(false) // TODO onRelease
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyButtonNode,
|
||||||
|
title: "UI.Button",
|
||||||
|
desc: "Button that triggers an event when clicked",
|
||||||
|
type: "ui/button"
|
||||||
|
})
|
||||||
43
src/lib/nodes/widgets/ComfyCheckboxNode.ts
Normal file
43
src/lib/nodes/widgets/ComfyCheckboxNode.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyCheckboxProperties extends ComfyWidgetProperties {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
|
||||||
|
override properties: ComfyCheckboxProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "boolean" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = CheckboxWidget;
|
||||||
|
override defaultValue = false;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any) {
|
||||||
|
return Boolean(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyCheckboxNode,
|
||||||
|
title: "UI.Checkbox",
|
||||||
|
desc: "Checkbox that stores a boolean value",
|
||||||
|
type: "ui/checkbox"
|
||||||
|
})
|
||||||
173
src/lib/nodes/widgets/ComfyComboNode.ts
Normal file
173
src/lib/nodes/widgets/ComfyComboNode.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type INodeInputSlot, type LGraphNode, type SerializedLGraphNode, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
|
||||||
|
export interface ComfyComboProperties extends ComfyWidgetProperties {
|
||||||
|
values: string[]
|
||||||
|
|
||||||
|
/* JS Function body that takes a parameter named "value" as a parameter and returns the label for each combo entry */
|
||||||
|
convertValueToLabelCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||||
|
override properties: ComfyComboProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: "A",
|
||||||
|
values: ["A", "B", "C", "D"],
|
||||||
|
convertValueToLabelCode: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = ComboWidget
|
||||||
|
override defaultValue = "A";
|
||||||
|
override saveUserState = false;
|
||||||
|
|
||||||
|
// True if at least one combo box refresh has taken place
|
||||||
|
// Wait until the initial graph load for combo to be valid.
|
||||||
|
firstLoad: Writable<boolean>;
|
||||||
|
lightUp: Writable<boolean>;
|
||||||
|
valuesForCombo: Writable<any[] | null>; // Changed when the combo box has values.
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, "A")
|
||||||
|
this.firstLoad = writable(false)
|
||||||
|
this.lightUp = writable(true)
|
||||||
|
this.valuesForCombo = writable(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropertyChanged(property: any, value: any) {
|
||||||
|
if (property === "values" || property === "convertValueToLabelCode") {
|
||||||
|
// this.formatValues(this.properties.values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatValues(values: string[], defaultValue?: string, lightUp: boolean = false) {
|
||||||
|
if (values == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let changed = this.properties.values != values;
|
||||||
|
this.properties.values = values;
|
||||||
|
|
||||||
|
const oldValue = get(this.value)
|
||||||
|
if (this.properties.values.indexOf(oldValue) === -1) {
|
||||||
|
changed = true;
|
||||||
|
this.value.set(defaultValue || this.properties.values[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightUp && get(this.firstLoad) && changed)
|
||||||
|
this.lightUp.set(true)
|
||||||
|
|
||||||
|
let formatter: any;
|
||||||
|
if (this.properties.convertValueToLabelCode)
|
||||||
|
formatter = new Function("value", this.properties.convertValueToLabelCode) as (v: string) => string;
|
||||||
|
else
|
||||||
|
formatter = (value: any) => `${value}`;
|
||||||
|
|
||||||
|
let valuesForCombo = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
valuesForCombo = this.properties.values.map((value, index) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: formatter(value),
|
||||||
|
index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("Failed formatting!", err)
|
||||||
|
valuesForCombo = this.properties.values.map((value, index) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label: `${value}`,
|
||||||
|
index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.firstLoad.set(true)
|
||||||
|
this.valuesForCombo.set(valuesForCombo);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
console.warn("CHECK COMBO CONNECTION", otherProps, thisProps)
|
||||||
|
|
||||||
|
// Ensure combo options match
|
||||||
|
if (!(otherProps.values instanceof Array) || otherProps.values.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
thisProps.values = Array.from(otherProps.values);
|
||||||
|
const value = get(this.value)
|
||||||
|
if (thisProps.values.indexOf(value) === -1)
|
||||||
|
this.setValue(otherProps.defaultValue || thisProps.values[0])
|
||||||
|
|
||||||
|
console.warn("PASSED")
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(value: any): string {
|
||||||
|
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
||||||
|
return this.properties.values[0]
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
override clampOneConfig(input: IComfyInputSlot) {
|
||||||
|
if (!input.config.values)
|
||||||
|
this.setValue("")
|
||||||
|
else 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onSerialize(o: SerializedLGraphNode) {
|
||||||
|
super.onSerialize(o);
|
||||||
|
// TODO fix saving combo nodes with huge values lists
|
||||||
|
o.properties.values = []
|
||||||
|
}
|
||||||
|
|
||||||
|
override stripUserState(o: SerializedLGraphNode) {
|
||||||
|
super.stripUserState(o);
|
||||||
|
o.properties.values = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyComboNode,
|
||||||
|
title: "UI.Combo",
|
||||||
|
desc: "Combo box outputting a string value",
|
||||||
|
type: "ui/combo"
|
||||||
|
})
|
||||||
113
src/lib/nodes/widgets/ComfyGalleryNode.ts
Normal file
113
src/lib/nodes/widgets/ComfyGalleryNode.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
|
||||||
|
index: number | null,
|
||||||
|
updateMode: "replace" | "append",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
||||||
|
override properties: ComfyGalleryProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: [],
|
||||||
|
index: 0,
|
||||||
|
updateMode: "replace",
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "images", type: "OUTPUT" },
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "images", type: "COMFYBOX_IMAGES" },
|
||||||
|
{ name: "selected_index", type: "number" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static propertyLayout: PropertyLayout = [
|
||||||
|
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
|
||||||
|
]
|
||||||
|
|
||||||
|
override svelteComponentType = GalleryWidget
|
||||||
|
override defaultValue = []
|
||||||
|
override saveUserState = false;
|
||||||
|
override outputSlotName = null;
|
||||||
|
override changedEventName = null;
|
||||||
|
|
||||||
|
selectedFilename: string | null = null;
|
||||||
|
|
||||||
|
selectedIndexWidget: ITextWidget;
|
||||||
|
modeWidget: IComboWidget;
|
||||||
|
|
||||||
|
imageWidth: Writable<number> = writable(0);
|
||||||
|
imageHeight: Writable<number> = writable(0);
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, [])
|
||||||
|
this.selectedIndexWidget = this.addWidget("text", "Selected", String(this.properties.index), "index")
|
||||||
|
this.selectedIndexWidget.disabled = true;
|
||||||
|
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropertyChanged(property: any, value: any) {
|
||||||
|
if (property === "updateMode") {
|
||||||
|
this.modeWidget.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute() {
|
||||||
|
const value = get(this.value)
|
||||||
|
this.setOutputData(0, value)
|
||||||
|
this.setOutputData(1, this.properties.index)
|
||||||
|
|
||||||
|
if (this.properties.index != null && value && value[this.properties.index] != null) {
|
||||||
|
const image = value[this.properties.index];
|
||||||
|
image.width = get(this.imageWidth)
|
||||||
|
image.height = get(this.imageHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||||
|
super.onAction(action, param, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
override formatValue(value: ComfyBoxImageMetadata[] | null): string {
|
||||||
|
return `Images: ${value?.length || 0}`
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any): ComfyBoxImageMetadata[] {
|
||||||
|
if (param == null)
|
||||||
|
return []
|
||||||
|
|
||||||
|
const meta = parseWhateverIntoImageMetadata(param) || [];
|
||||||
|
|
||||||
|
console.debug("[ComfyGalleryNode] Received output!", param)
|
||||||
|
|
||||||
|
if (this.properties.updateMode === "append") {
|
||||||
|
const currentValue = get(this.value)
|
||||||
|
return currentValue.concat(meta)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.notifyPropsChanged();
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override setValue(value: any, noChangedEvent: boolean = false) {
|
||||||
|
super.setValue(value, noChangedEvent)
|
||||||
|
this.setProperty("index", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyGalleryNode,
|
||||||
|
title: "UI.Gallery",
|
||||||
|
desc: "Gallery that shows most recent outputs",
|
||||||
|
type: "ui/gallery"
|
||||||
|
})
|
||||||
65
src/lib/nodes/widgets/ComfyImageUploadNode.ts
Normal file
65
src/lib/nodes/widgets/ComfyImageUploadNode.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||||
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export interface ComfyImageUploadNodeProperties extends ComfyWidgetProperties {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyImageUploadNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
|
||||||
|
properties: ComfyImageUploadNodeProperties = {
|
||||||
|
defaultValue: [],
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = ImageUploadWidget;
|
||||||
|
override defaultValue = [];
|
||||||
|
override outputSlotName = "images";
|
||||||
|
override storeActionName = "store";
|
||||||
|
override saveUserState = false;
|
||||||
|
|
||||||
|
imgWidth: Writable<number> = writable(0);
|
||||||
|
imgHeight: Writable<number> = writable(0);
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute() {
|
||||||
|
// TODO better way of getting image size?
|
||||||
|
const value = get(this.value)
|
||||||
|
if (value && value.length > 0) {
|
||||||
|
value[0].width = get(this.imgWidth)
|
||||||
|
value[0].height = get(this.imgHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(value: any): ComfyBoxImageMetadata[] {
|
||||||
|
return parseWhateverIntoImageMetadata(value) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
override formatValue(value: GradioFileData[]): string {
|
||||||
|
return `Images: ${value?.length || 0}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyImageUploadNode,
|
||||||
|
title: "UI.ImageUpload",
|
||||||
|
desc: "Widget that lets you upload and edit a multi-layered image. Can also act like a standalone image uploader.",
|
||||||
|
type: "ui/image_upload"
|
||||||
|
})
|
||||||
69
src/lib/nodes/widgets/ComfyNumberNode.ts
Normal file
69
src/lib/nodes/widgets/ComfyNumberNode.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
import { clamp } from "$lib/utils";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import NumberWidget from "$lib/widgets/NumberWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyNumberProperties extends ComfyWidgetProperties {
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
step: number,
|
||||||
|
precision: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyNumberNode extends ComfyWidgetNode<number> {
|
||||||
|
override properties: ComfyNumberProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
precision: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = NumberWidget
|
||||||
|
override defaultValue = 0;
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
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 parseValue(value: any): number {
|
||||||
|
if (typeof value !== "number")
|
||||||
|
return this.properties.min;
|
||||||
|
return 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: ComfyNumberNode,
|
||||||
|
title: "UI.Number",
|
||||||
|
desc: "Displays a number, by default in a slider format.",
|
||||||
|
type: "ui/number"
|
||||||
|
})
|
||||||
82
src/lib/nodes/widgets/ComfyRadioNode.ts
Normal file
82
src/lib/nodes/widgets/ComfyRadioNode.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { clamp } from "$lib/utils";
|
||||||
|
import { BuiltInSlotType, LiteGraph, type INumberWidget, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
|
||||||
|
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
import ComfyWidgetNode from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
|
||||||
|
export interface ComfyRadioProperties extends ComfyWidgetProperties {
|
||||||
|
choices: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyRadioNode extends ComfyWidgetNode<string> {
|
||||||
|
override properties: ComfyRadioProperties = {
|
||||||
|
tags: [],
|
||||||
|
choices: ["Choice A", "Choice B", "Choice C"],
|
||||||
|
defaultValue: "Choice A",
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "index", type: "number" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override svelteComponentType = RadioWidget;
|
||||||
|
override defaultValue = "";
|
||||||
|
|
||||||
|
indexWidget: INumberWidget;
|
||||||
|
|
||||||
|
index = 0;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, "Choice A")
|
||||||
|
this.indexWidget = this.addWidget("number", "Index", this.index)
|
||||||
|
this.indexWidget.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onExecute(param: any, options: object) {
|
||||||
|
super.onExecute(param, options);
|
||||||
|
this.setOutputData(1, this.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override setValue(value: string, noChangedEvent: boolean = false) {
|
||||||
|
super.setValue(value, noChangedEvent)
|
||||||
|
|
||||||
|
value = get(this.value);
|
||||||
|
|
||||||
|
const index = this.properties.choices.indexOf(value)
|
||||||
|
if (index === -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.index = index;
|
||||||
|
this.indexWidget.value = index;
|
||||||
|
this.setOutputData(1, this.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(param: any): string {
|
||||||
|
if (typeof param === "string") {
|
||||||
|
if (this.properties.choices.indexOf(param) === -1)
|
||||||
|
return this.properties.choices[0]
|
||||||
|
return param
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const index = clamp(parseInt(param), 0, this.properties.choices.length - 1)
|
||||||
|
return this.properties.choices[index] || this.properties.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyRadioNode,
|
||||||
|
title: "UI.Radio",
|
||||||
|
desc: "Radio that outputs a string and index",
|
||||||
|
type: "ui/radio"
|
||||||
|
})
|
||||||
46
src/lib/nodes/widgets/ComfyTextNode.ts
Normal file
46
src/lib/nodes/widgets/ComfyTextNode.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||||
|
|
||||||
|
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||||
|
import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||||
|
|
||||||
|
export interface ComfyTextProperties extends ComfyWidgetProperties {
|
||||||
|
multiline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||||
|
override properties: ComfyTextProperties = {
|
||||||
|
tags: [],
|
||||||
|
defaultValue: "",
|
||||||
|
multiline: false
|
||||||
|
}
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "value", type: "string" },
|
||||||
|
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override inputSlotName = "value";
|
||||||
|
override svelteComponentType = TextWidget
|
||||||
|
override defaultValue = "";
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override parseValue(value: any): string {
|
||||||
|
return `${value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: ComfyTextNode,
|
||||||
|
title: "UI.Text",
|
||||||
|
desc: "Textbox outputting a string value",
|
||||||
|
type: "ui/text"
|
||||||
|
})
|
||||||
349
src/lib/nodes/widgets/ComfyWidgetNode.ts
Normal file
349
src/lib/nodes/widgets/ComfyWidgetNode.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||||
|
import layoutState from "$lib/stores/layoutState";
|
||||||
|
import { range } from "$lib/utils";
|
||||||
|
import { LConnectionKind, LGraphCanvas, LLink, LiteGraph, NodeMode, type INodeInputSlot, type INodeOutputSlot, type ITextWidget, type LGraphNode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
|
||||||
|
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||||
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
|
import { get, writable, type Unsubscriber, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
import type { ComfyNodeID } from "$lib/api";
|
||||||
|
import type { ComfyGraphNodeProperties } from "../ComfyGraphNode";
|
||||||
|
import ComfyGraphNode from "../ComfyGraphNode";
|
||||||
|
|
||||||
|
export type AutoConfigOptions = {
|
||||||
|
includeProperties?: Set<string> | null,
|
||||||
|
setDefaultValue?: boolean
|
||||||
|
setWidgetTitle?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE: If you want to add a new widget but it has the same input/output type
|
||||||
|
* as another one of the existing widgets, best to create a new "variant" of
|
||||||
|
* that widget instead.
|
||||||
|
*
|
||||||
|
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
|
||||||
|
* attribute and set `validNodeTypes` to the type of the litegraph node
|
||||||
|
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfyNumberWidget
|
||||||
|
* - Add an {#if widget.attrs.variant === <...>} statement in the existing Svelte component
|
||||||
|
*
|
||||||
|
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
|
||||||
|
* You will have to either implement an internal delay on the event triggering
|
||||||
|
* or use an Event Delay node to ensure the output slot data can propagate to
|
||||||
|
* the rest of the graph first (see `delayChangedEvent` for details)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ComfyWidgetProperties extends ComfyGraphNodeProperties {
|
||||||
|
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 default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||||
|
abstract properties: ComfyWidgetProperties;
|
||||||
|
|
||||||
|
value: Writable<T>
|
||||||
|
propsChanged: Writable<number> = writable(0) // dummy to indicate if props changed
|
||||||
|
unsubscribe: Unsubscriber;
|
||||||
|
|
||||||
|
/** Svelte class for the frontend logic */
|
||||||
|
abstract svelteComponentType: typeof SvelteComponentDev
|
||||||
|
|
||||||
|
/** If false, user manually set min/max/step, and should not be autoinherited from connected input */
|
||||||
|
autoConfig: boolean = true;
|
||||||
|
|
||||||
|
copyFromInputLink: boolean = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true wait until next frame update to trigger the changed event.
|
||||||
|
* Reason is, if the event is triggered immediately then other stuff that wants to run
|
||||||
|
* their own onExecute on the output value won't have completed yet.
|
||||||
|
*/
|
||||||
|
delayChangedEvent: boolean = true;
|
||||||
|
|
||||||
|
private _aboutToChange: number = 0;
|
||||||
|
private _aboutToChangeValue: any = null;
|
||||||
|
private _noChangedEvent: boolean = false;
|
||||||
|
|
||||||
|
abstract defaultValue: T;
|
||||||
|
|
||||||
|
/** Names of properties to add as inputs */
|
||||||
|
// shownInputProperties: string[] = []
|
||||||
|
|
||||||
|
/** Names of properties to add as outputs */
|
||||||
|
private shownOutputProperties: Record<string, { type: string, outputName: string }> = {}
|
||||||
|
outputProperties: { name: string, type: string }[] = []
|
||||||
|
|
||||||
|
override isBackendNode = false;
|
||||||
|
override serialize_widgets = true;
|
||||||
|
|
||||||
|
// input slots
|
||||||
|
inputSlotName: string | null = "value";
|
||||||
|
storeActionName: string | null = "store";
|
||||||
|
|
||||||
|
// output slots
|
||||||
|
outputSlotName: string | null = "value";
|
||||||
|
changedEventName: string | null = "changed";
|
||||||
|
|
||||||
|
displayWidget: ITextWidget;
|
||||||
|
|
||||||
|
override size: Vector2 = [60, 40];
|
||||||
|
|
||||||
|
constructor(name: string, value: T) {
|
||||||
|
const color = LGraphCanvas.node_colors["blue"]
|
||||||
|
super(name)
|
||||||
|
this.value = writable(value)
|
||||||
|
this.color ||= color.color
|
||||||
|
this.bgColor ||= color.bgColor
|
||||||
|
this.displayWidget = this.addWidget<ITextWidget>(
|
||||||
|
"text",
|
||||||
|
"Value",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
this.displayWidget.disabled = true; // prevent editing
|
||||||
|
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
addPropertyAsOutput(propertyName: string, type: string) {
|
||||||
|
if (this.shownOutputProperties[propertyName])
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!(propertyName in this.properties)) {
|
||||||
|
throw `No property named ${propertyName} found!`
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputName = "@" + propertyName;
|
||||||
|
this.shownOutputProperties[propertyName] = { type, outputName }
|
||||||
|
this.addOutput(outputName, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatValue(value: any): string {
|
||||||
|
return Watch.toString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override changeMode(modeTo: NodeMode): boolean {
|
||||||
|
const result = super.changeMode(modeTo);
|
||||||
|
this.notifyPropsChanged();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onValueUpdated(value: any) {
|
||||||
|
// console.debug("[Widget] valueUpdated", this, value)
|
||||||
|
this.displayWidget.value = this.formatValue(value)
|
||||||
|
|
||||||
|
if (this.outputSlotName !== null) {
|
||||||
|
const outputIndex = this.findOutputSlotIndexByName(this.outputSlotName)
|
||||||
|
if (outputIndex !== -1)
|
||||||
|
this.setOutputData(outputIndex, get(this.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changedEventName !== null && !this._noChangedEvent) {
|
||||||
|
if (!this.delayChangedEvent)
|
||||||
|
this.triggerChangeEvent(get(this.value))
|
||||||
|
else {
|
||||||
|
// console.debug("[Widget] queueChangeEvent", this, value)
|
||||||
|
this._aboutToChange = 2; // wait 1.5-2 frames, in case we're already in the middle of executing the graph
|
||||||
|
this._aboutToChangeValue = get(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._noChangedEvent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerChangeEvent(value: any) {
|
||||||
|
if (this.changedEventName == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// console.debug("[Widget] trigger changed", this, value)
|
||||||
|
this.trigger(this.changedEventName, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseValue(value: any): T { return value as T };
|
||||||
|
|
||||||
|
getValue(): T {
|
||||||
|
return get(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value: any, noChangedEvent: boolean = false) {
|
||||||
|
if (noChangedEvent)
|
||||||
|
this._noChangedEvent = true;
|
||||||
|
|
||||||
|
const parsed = this.parseValue(value)
|
||||||
|
this.value.set(parsed)
|
||||||
|
|
||||||
|
// In case value.set() does not trigger onValueUpdated, we need to reset
|
||||||
|
// the counter here also.
|
||||||
|
this._noChangedEvent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPropertyChanged(property: string, value: any, prevValue?: any) {
|
||||||
|
if (this.shownOutputProperties != null) {
|
||||||
|
const data = this.shownOutputProperties[property]
|
||||||
|
if (data) {
|
||||||
|
const index = this.findOutputSlotIndexByName(data.outputName)
|
||||||
|
if (index !== -1)
|
||||||
|
this.setOutputData(index, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Logic to run if this widget can be treated as output (slider, combo, text)
|
||||||
|
*/
|
||||||
|
override onExecute(param: any, options: object) {
|
||||||
|
if (this.inputSlotName != null) {
|
||||||
|
const inputIndex = this.findInputSlotIndexByName(this.inputSlotName)
|
||||||
|
if (inputIndex !== -1) {
|
||||||
|
const data = this.getInputData(inputIndex)
|
||||||
|
if (data != null) { // TODO can "null" be a legitimate value here?
|
||||||
|
this.setValue(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.outputSlotName != null) {
|
||||||
|
const outputIndex = this.findOutputSlotIndexByName(this.outputSlotName)
|
||||||
|
if (outputIndex !== -1)
|
||||||
|
this.setOutputData(outputIndex, get(this.value))
|
||||||
|
}
|
||||||
|
for (const propName in this.shownOutputProperties) {
|
||||||
|
const data = this.shownOutputProperties[propName]
|
||||||
|
const index = this.findOutputSlotIndexByName(data.outputName)
|
||||||
|
if (index !== -1)
|
||||||
|
this.setOutputData(index, this.properties[propName])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire a pending change event after one full step of the graph has
|
||||||
|
// finished processing
|
||||||
|
if (this._aboutToChange > 0) {
|
||||||
|
this._aboutToChange -= 1
|
||||||
|
if (this._aboutToChange <= 0) {
|
||||||
|
const value = this._aboutToChangeValue;
|
||||||
|
this._aboutToChange = 0;
|
||||||
|
this._aboutToChangeValue = null;
|
||||||
|
this.triggerChangeEvent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAction(action: any, param: any, options: { action_call?: string }) {
|
||||||
|
if (action === this.storeActionName) {
|
||||||
|
let noChangedEvent = false;
|
||||||
|
let value = param;
|
||||||
|
if (param != null && typeof param === "object" && "value" in param) {
|
||||||
|
value = param.value
|
||||||
|
if ("noChangedEvent" in param)
|
||||||
|
noChangedEvent = Boolean(param.noChangedEvent)
|
||||||
|
}
|
||||||
|
this.setValue(value, noChangedEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectOutput(
|
||||||
|
outputIndex: number,
|
||||||
|
inputType: INodeInputSlot["type"],
|
||||||
|
input: INodeInputSlot,
|
||||||
|
inputNode: LGraphNode,
|
||||||
|
inputIndex: number
|
||||||
|
): boolean {
|
||||||
|
const anyConnected = range(this.outputs.length).some(i => this.getOutputLinks(i).length > 0);
|
||||||
|
|
||||||
|
if (this.autoConfig && "config" in input && !anyConnected && (input as IComfyInputSlot).widgetNodeType === this.type) {
|
||||||
|
this.doAutoConfig(input as IComfyInputSlot)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
doAutoConfig(input: IComfyInputSlot, options: AutoConfigOptions = { setDefaultValue: true, setWidgetTitle: true }) {
|
||||||
|
// Copy properties from default config in input slot
|
||||||
|
const comfyInput = input as IComfyInputSlot;
|
||||||
|
for (const key in comfyInput.config) {
|
||||||
|
if (options.includeProperties == null || options.includeProperties.has(key))
|
||||||
|
this.setProperty(key, comfyInput.config[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.setDefaultValue) {
|
||||||
|
if ("defaultValue" in this.properties)
|
||||||
|
this.setValue(this.properties.defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.setWidgetTitle) {
|
||||||
|
const widget = layoutState.findLayoutForNode(this.id as ComfyNodeID)
|
||||||
|
if (widget && input.name !== "") {
|
||||||
|
widget.attrs.title = input.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug("Property copy", input, this.properties)
|
||||||
|
|
||||||
|
this.setValue(get(this.value))
|
||||||
|
|
||||||
|
this.onAutoConfig(input);
|
||||||
|
|
||||||
|
this.notifyPropsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAutoConfig(input: IComfyInputSlot) {
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyPropsChanged() {
|
||||||
|
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
||||||
|
if (layoutEntry && layoutEntry.parent) {
|
||||||
|
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
||||||
|
}
|
||||||
|
// console.debug("propsChanged", this)
|
||||||
|
this.propsChanged.set(get(this.propsChanged) + 1)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override onConnectionsChange(
|
||||||
|
type: LConnectionKind,
|
||||||
|
slotIndex: number,
|
||||||
|
isConnected: boolean,
|
||||||
|
link: LLink,
|
||||||
|
ioSlot: (INodeOutputSlot | INodeInputSlot)
|
||||||
|
): void {
|
||||||
|
super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
|
||||||
|
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.notifyPropsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
clampOneConfig(input: IComfyInputSlot) { }
|
||||||
|
|
||||||
|
override onSerialize(o: SerializedLGraphNode) {
|
||||||
|
(o as any).comfyValue = get(this.value);
|
||||||
|
(o as any).shownOutputProperties = this.shownOutputProperties
|
||||||
|
super.onSerialize(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onConfigure(o: SerializedLGraphNode) {
|
||||||
|
const value = (o as any).comfyValue || LiteGraph.cloneObject(this.defaultValue);
|
||||||
|
this.value.set(value);
|
||||||
|
this.shownOutputProperties = (o as any).shownOutputProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
override stripUserState(o: SerializedLGraphNode) {
|
||||||
|
super.stripUserState(o);
|
||||||
|
(o as any).comfyValue = this.defaultValue;
|
||||||
|
o.properties.defaultValue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/lib/nodes/widgets/index.ts
Normal file
9
src/lib/nodes/widgets/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { default as ComfyWidgetNode } from "./ComfyWidgetNode"
|
||||||
|
export { default as ComfyButtonNode } from "./ComfyButtonNode"
|
||||||
|
export { default as ComfyCheckboxNode } from "./ComfyCheckboxNode"
|
||||||
|
export { default as ComfyComboNode } from "./ComfyComboNode"
|
||||||
|
export { default as ComfyGalleryNode } from "./ComfyGalleryNode"
|
||||||
|
export { default as ComfyImageUploadNode } from "./ComfyImageUploadNode"
|
||||||
|
export { default as ComfyRadioNode } from "./ComfyRadioNode"
|
||||||
|
export { default as ComfyNumberNode } from "./ComfyNumberNode"
|
||||||
|
export { default as ComfyTextNode } from "./ComfyTextNode"
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import type ComfyApp from "$lib/components/ComfyApp"
|
import type ComfyApp from "$lib/components/ComfyApp"
|
||||||
import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions, type UUID } from "@litegraph-ts/core"
|
import { type LGraphNode, type IWidget, type LGraph, NodeMode, type LGraphRemoveNodeOptions, type LGraphAddNodeOptions, type UUID, type NodeID, LiteGraph, type GraphIDMapping } from "@litegraph-ts/core"
|
||||||
import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||||
import type { ComfyWidgetNode } from '$lib/nodes';
|
import type { ComfyNodeID } from '$lib/api';
|
||||||
import type { NodeID } from '$lib/api';
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import type { ComfyWidgetNode } from '$lib/nodes/widgets';
|
||||||
|
|
||||||
|
function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode {
|
||||||
|
return "svelteComponentType" in node
|
||||||
|
}
|
||||||
|
|
||||||
type DragItemEntry = {
|
type DragItemEntry = {
|
||||||
/*
|
/*
|
||||||
@@ -60,17 +64,7 @@ export type LayoutState = {
|
|||||||
* Items indexed by the litegraph node they're bound to
|
* Items indexed by the litegraph node they're bound to
|
||||||
* Only contains drag items of type "widget"
|
* Only contains drag items of type "widget"
|
||||||
*/
|
*/
|
||||||
allItemsByNode: Record<NodeID, DragItemEntry>,
|
allItemsByNode: Record<ComfyNodeID, DragItemEntry>,
|
||||||
|
|
||||||
/*
|
|
||||||
* Selected drag items.
|
|
||||||
*/
|
|
||||||
currentSelection: DragItemID[],
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Selected LGraphNodes inside the litegraph canvas.
|
|
||||||
*/
|
|
||||||
currentSelectionNodes: LGraphNode[],
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If true, a saved workflow is being deserialized, so ignore any
|
* If true, a saved workflow is being deserialized, so ignore any
|
||||||
@@ -194,7 +188,7 @@ export type AttributesSpec = {
|
|||||||
location: "widget" | "nodeProps" | "nodeVars" | "workflow"
|
location: "widget" | "nodeProps" | "nodeVars" | "workflow"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Can this attribute be edited in the properties pane.
|
* Can this attribute be edited in the properties pane?
|
||||||
*/
|
*/
|
||||||
editable: boolean,
|
editable: boolean,
|
||||||
|
|
||||||
@@ -259,7 +253,12 @@ export type AttributesSpec = {
|
|||||||
* This should be used if there's a canShow dependent on this property so
|
* This should be used if there's a canShow dependent on this property so
|
||||||
* the pane can be updated with the new list of valid properties.
|
* the pane can be updated with the new list of valid properties.
|
||||||
*/
|
*/
|
||||||
refreshPanelOnChange?: boolean
|
refreshPanelOnChange?: boolean,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback run when this value is changed.
|
||||||
|
*/
|
||||||
|
onChanged?: (arg: IDragItem | LGraphNode | LayoutState, value: any, prevValue: any) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -283,6 +282,24 @@ const deserializeStringArray = (arg: string) => {
|
|||||||
return arg.split(",").map(s => s.trim())
|
return arg.split(",").map(s => s.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setNodeTitle = (arg: IDragItem, value: any) => {
|
||||||
|
if (arg.type !== "widget")
|
||||||
|
return
|
||||||
|
|
||||||
|
const widget = arg as WidgetLayout;
|
||||||
|
if (widget.node == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const reg = LiteGraph.registered_node_types[widget.node.type];
|
||||||
|
if (reg == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (value && value !== reg.title)
|
||||||
|
widget.node.title = `${reg.title} (${value})`
|
||||||
|
else
|
||||||
|
widget.node.title = reg.title
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Attributes that will show up in the properties panel.
|
* Attributes that will show up in the properties panel.
|
||||||
* Their order in the list is the order they'll appear in the panel.
|
* Their order in the list is the order they'll appear in the panel.
|
||||||
@@ -297,6 +314,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
location: "widget",
|
location: "widget",
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
editable: true,
|
editable: true,
|
||||||
|
// onChanged: setNodeTitle
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "hidden",
|
name: "hidden",
|
||||||
@@ -341,7 +359,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
location: "widget",
|
location: "widget",
|
||||||
editable: true,
|
editable: true,
|
||||||
values: ["visible", "disabled", "hidden"],
|
values: ["visible", "disabled", "hidden"],
|
||||||
defaultValue: "disabled",
|
defaultValue: "hidden",
|
||||||
canShow: (di: IDragItem) => di.type === "widget"
|
canShow: (di: IDragItem) => di.type === "widget"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -362,7 +380,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
location: "widget",
|
location: "widget",
|
||||||
editable: true,
|
editable: true,
|
||||||
values: ["block", "hidden"],
|
values: ["block", "hidden"],
|
||||||
defaultValue: "block",
|
defaultValue: "hidden",
|
||||||
canShow: (di: IDragItem) => di.type === "container"
|
canShow: (di: IDragItem) => di.type === "container"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -493,7 +511,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
min: -2 ^ 16,
|
min: -2 ^ 16,
|
||||||
max: 2 ^ 16,
|
max: 2 ^ 16,
|
||||||
validNodeTypes: ["ui/slider"],
|
validNodeTypes: ["ui/number"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "max",
|
name: "max",
|
||||||
@@ -503,7 +521,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
defaultValue: 10,
|
defaultValue: 10,
|
||||||
min: -2 ^ 16,
|
min: -2 ^ 16,
|
||||||
max: 2 ^ 16,
|
max: 2 ^ 16,
|
||||||
validNodeTypes: ["ui/slider"],
|
validNodeTypes: ["ui/number"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "step",
|
name: "step",
|
||||||
@@ -513,7 +531,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
|||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
min: -2 ^ 16,
|
min: -2 ^ 16,
|
||||||
max: 2 ^ 16,
|
max: 2 ^ 16,
|
||||||
validNodeTypes: ["ui/slider"],
|
validNodeTypes: ["ui/number"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Button
|
// Button
|
||||||
@@ -579,6 +597,7 @@ for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
|||||||
|
|
||||||
export { ALL_ATTRIBUTES };
|
export { ALL_ATTRIBUTES };
|
||||||
|
|
||||||
|
// TODO Should be nested by category for name uniqueness?
|
||||||
const defaultWidgetAttributes: Attributes = {} as any
|
const defaultWidgetAttributes: Attributes = {} as any
|
||||||
const defaultWorkflowAttributes: LayoutAttributes = {} as any
|
const defaultWorkflowAttributes: LayoutAttributes = {} as any
|
||||||
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||||
@@ -660,11 +679,11 @@ type LayoutStateOps = {
|
|||||||
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
||||||
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void,
|
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void,
|
||||||
nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void,
|
nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void,
|
||||||
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout,
|
moveItem: (target: IDragItem, to: ContainerLayout, index?: number) => void,
|
||||||
|
groupItems: (dragItemIDs: DragItemID[], attrs?: Partial<Attributes>) => ContainerLayout,
|
||||||
ungroup: (container: ContainerLayout) => void,
|
ungroup: (container: ContainerLayout) => void,
|
||||||
getCurrentSelection: () => IDragItem[],
|
findLayoutEntryForNode: (nodeId: ComfyNodeID) => DragItemEntry | null,
|
||||||
findLayoutEntryForNode: (nodeId: NodeID) => DragItemEntry | null,
|
findLayoutForNode: (nodeId: ComfyNodeID) => IDragItem | null,
|
||||||
findLayoutForNode: (nodeId: NodeID) => IDragItem | null,
|
|
||||||
serialize: () => SerializedLayoutState,
|
serialize: () => SerializedLayoutState,
|
||||||
deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
|
deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
|
||||||
initDefaultLayout: () => void,
|
initDefaultLayout: () => void,
|
||||||
@@ -676,8 +695,6 @@ const store: Writable<LayoutState> = writable({
|
|||||||
root: null,
|
root: null,
|
||||||
allItems: {},
|
allItems: {},
|
||||||
allItemsByNode: {},
|
allItemsByNode: {},
|
||||||
currentSelection: [],
|
|
||||||
currentSelectionNodes: [],
|
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
isConfiguring: true,
|
isConfiguring: true,
|
||||||
refreshPropsPanel: writable(0),
|
refreshPropsPanel: writable(0),
|
||||||
@@ -686,6 +703,20 @@ const store: Writable<LayoutState> = writable({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
store.set({
|
||||||
|
root: null,
|
||||||
|
allItems: {},
|
||||||
|
allItemsByNode: {},
|
||||||
|
isMenuOpen: false,
|
||||||
|
isConfiguring: true,
|
||||||
|
refreshPropsPanel: writable(0),
|
||||||
|
attrs: {
|
||||||
|
...defaultWorkflowAttributes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function findDefaultContainerForInsertion(): ContainerLayout | null {
|
function findDefaultContainerForInsertion(): ContainerLayout | null {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
|
|
||||||
@@ -706,6 +737,16 @@ function findDefaultContainerForInsertion(): ContainerLayout | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runOnChangedForWidgetDefaults(dragItem: IDragItem) {
|
||||||
|
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||||
|
for (const spec of Object.values(cat.specs)) {
|
||||||
|
if (defaultWidgetAttributes[spec.name] !== undefined && spec.onChanged != null) {
|
||||||
|
spec.onChanged(dragItem, dragItem.attrs[spec.name], dragItem.attrs[spec.name])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index?: number): ContainerLayout {
|
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index?: number): ContainerLayout {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
const dragItem: ContainerLayout = {
|
const dragItem: ContainerLayout = {
|
||||||
@@ -718,19 +759,26 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
|||||||
...attrs
|
...attrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
||||||
|
|
||||||
|
if (state.allItemsByNode[dragItem.id] != null)
|
||||||
|
throw new Error(`Container with ID ${dragItem.id} already registered!!!`)
|
||||||
state.allItems[dragItem.id] = entry;
|
state.allItems[dragItem.id] = entry;
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
moveItem(dragItem, parent, index)
|
moveItem(dragItem, parent, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("[layoutState] addContainer", state)
|
console.debug("[layoutState] addContainer", state)
|
||||||
store.set(state)
|
store.set(state)
|
||||||
|
// runOnChangedForWidgetDefaults(dragItem)
|
||||||
return dragItem;
|
return dragItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index?: number): WidgetLayout {
|
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index?: number): WidgetLayout {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
const widgetName = "Widget"
|
const widgetName = node.title || "Widget"
|
||||||
const dragItem: WidgetLayout = {
|
const dragItem: WidgetLayout = {
|
||||||
type: "widget",
|
type: "widget",
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
@@ -739,16 +787,23 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
|||||||
attrs: {
|
attrs: {
|
||||||
...defaultWidgetAttributes,
|
...defaultWidgetAttributes,
|
||||||
title: widgetName,
|
title: widgetName,
|
||||||
nodeDisabledState: "disabled",
|
|
||||||
...attrs
|
...attrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const parentEntry = state.allItems[parent.id]
|
|
||||||
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
const entry: DragItemEntry = { dragItem, children: [], parent: null };
|
||||||
|
|
||||||
|
if (state.allItems[dragItem.id] != null)
|
||||||
|
throw new Error(`Widget with ID ${dragItem.id} already registered!!!`)
|
||||||
state.allItems[dragItem.id] = entry;
|
state.allItems[dragItem.id] = entry;
|
||||||
|
|
||||||
|
if (state.allItemsByNode[node.id] != null)
|
||||||
|
throw new Error(`Widget's node with ID ${node.id} already registered!!!`)
|
||||||
state.allItemsByNode[node.id] = entry;
|
state.allItemsByNode[node.id] = entry;
|
||||||
|
|
||||||
console.debug("[layoutState] addWidget", state)
|
console.debug("[layoutState] addWidget", state)
|
||||||
moveItem(dragItem, parent, index)
|
moveItem(dragItem, parent, index)
|
||||||
|
// runOnChangedForWidgetDefaults(dragItem)
|
||||||
return dragItem;
|
return dragItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,24 +839,62 @@ function removeEntry(state: LayoutState, id: DragItemID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||||
|
// Only concern ourselves with widget nodes
|
||||||
|
if (!isComfyWidgetNode(node))
|
||||||
|
return;
|
||||||
|
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
if (state.isConfiguring)
|
if (state.isConfiguring)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
let attrs: Partial<Attributes> = {}
|
||||||
|
|
||||||
if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
|
if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
|
||||||
// All we need to do is update the nodeID linked to this node.
|
// All we need to do is update the nodeID linked to this node.
|
||||||
const item = state.allItemsByNode[options.prevNodeId]
|
const item = state.allItemsByNode[options.prevNodeID]
|
||||||
delete state.allItemsByNode[options.prevNodeId]
|
delete state.allItemsByNode[options.prevNodeID]
|
||||||
state.allItemsByNode[node.id] = item
|
state.allItemsByNode[node.id] = item
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if ((options.addedBy === "cloneSelection" || options.addedBy === "paste") && options.prevNodeID != null) {
|
||||||
|
console.warn("WASCLONED", options.addedBy, options.prevNodeID, Object.keys(state.allItemsByNode), options.prevNode, options.subgraphs)
|
||||||
|
// Grab layout state information and clone it too.
|
||||||
|
|
||||||
|
let prevWidget = state.allItemsByNode[node.id]
|
||||||
|
if (prevWidget == null) {
|
||||||
|
// If a subgraph was cloned, try looking for the original widget node corresponding to the new widget node being added.
|
||||||
|
// `node` is the new ComfyWidgetNode instance to copy attrs to.
|
||||||
|
// `options.cloneData` should contain the results of Subgraph.clone(), called "subgraphNewIDMapping".
|
||||||
|
// `options.cloneData` is attached to the onNodeAdded options if a node is added to a graph after being
|
||||||
|
// selection-cloned or pasted, as they both call clone() internally.
|
||||||
|
const cloneData = options.cloneData.forNode[options.prevNodeID]
|
||||||
|
if (cloneData && cloneData.subgraphNewIDMapping != null) {
|
||||||
|
// At this point we know options.prevNodeID points to a subgraph.
|
||||||
|
const mapping = cloneData.subgraphNewIDMapping as GraphIDMapping
|
||||||
|
|
||||||
|
// This mapping is two-way, so oldID -> newID *and* newID -> oldID are supported.
|
||||||
|
// Take the cloned node's ID and look up what the original node's ID was.
|
||||||
|
const nodeIDInLayoutState = mapping.nodeIDs[node.id];
|
||||||
|
|
||||||
|
if (nodeIDInLayoutState) {
|
||||||
|
// Gottem.
|
||||||
|
prevWidget = state.allItemsByNode[nodeIDInLayoutState]
|
||||||
|
console.warn("FOUND CLONED SUBGRAPH NODE", node.id, "=>", nodeIDInLayoutState, prevWidget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevWidget) {
|
||||||
|
console.warn("FOUND", prevWidget.dragItem.attrs)
|
||||||
|
// XXX: Will this work for most properties?
|
||||||
|
attrs = structuredClone(prevWidget.dragItem.attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parent = findDefaultContainerForInsertion();
|
const parent = findDefaultContainerForInsertion();
|
||||||
|
|
||||||
console.debug("[layoutState] nodeAdded", node)
|
console.debug("[layoutState] nodeAdded", node.id)
|
||||||
if ("svelteComponentType" in node) {
|
addWidget(parent, node, attrs);
|
||||||
addWidget(parent, node as ComfyWidgetNode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||||
@@ -830,7 +923,7 @@ function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
|||||||
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const entry = state.allItems[target.id]
|
const entry = state.allItems[target.id]
|
||||||
if (entry.parent && entry.parent.id === to.id)
|
if (!entry || (entry.parent && entry.parent.id === to.id && entry.children.indexOf(target) === index))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (entry.parent) {
|
if (entry.parent) {
|
||||||
@@ -858,33 +951,27 @@ function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
|||||||
store.set(state)
|
store.set(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentSelection(): IDragItem[] {
|
function groupItems(dragItemIDs: DragItemID[], attrs: Partial<Attributes> = {}): ContainerLayout {
|
||||||
const state = get(store)
|
if (dragItemIDs.length === 0)
|
||||||
return state.currentSelection.map(id => state.allItems[id].dragItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupItems(dragItems: IDragItem[], attrs: Partial<Attributes> = {}): ContainerLayout {
|
|
||||||
if (dragItems.length === 0)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const parent = state.allItems[dragItems[0].id].parent || findDefaultContainerForInsertion();
|
const parent = state.allItems[dragItemIDs[0]].parent || findDefaultContainerForInsertion();
|
||||||
|
|
||||||
if (parent === null || parent.type !== "container")
|
if (parent === null || parent.type !== "container")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let index = undefined;
|
let index = undefined;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const indexFound = state.allItems[parent.id].children.findIndex(c => c.id === dragItems[0].id)
|
const indexFound = state.allItems[parent.id].children.findIndex(c => c.id === dragItemIDs[0])
|
||||||
if (indexFound !== -1)
|
if (indexFound !== -1)
|
||||||
index = indexFound
|
index = indexFound
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = dragItems.length <= 1 ? "" : "Group";
|
const container = addContainer(parent as ContainerLayout, { title: "", containerVariant: "block", ...attrs }, index)
|
||||||
|
|
||||||
const container = addContainer(parent as ContainerLayout, { title, ...attrs }, index)
|
for (const itemID of dragItemIDs) {
|
||||||
|
const item = state.allItems[itemID].dragItem;
|
||||||
for (const item of dragItems) {
|
|
||||||
moveItem(item, container)
|
moveItem(item, container)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -924,7 +1011,7 @@ function ungroup(container: ContainerLayout) {
|
|||||||
store.set(state)
|
store.set(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLayoutEntryForNode(nodeId: NodeID): DragItemEntry | null {
|
function findLayoutEntryForNode(nodeId: ComfyNodeID): DragItemEntry | null {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const found = Object.entries(state.allItems).find(pair =>
|
const found = Object.entries(state.allItems).find(pair =>
|
||||||
pair[1].dragItem.type === "widget"
|
pair[1].dragItem.type === "widget"
|
||||||
@@ -934,7 +1021,7 @@ function findLayoutEntryForNode(nodeId: NodeID): DragItemEntry | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLayoutForNode(nodeId: NodeID): WidgetLayout | null {
|
function findLayoutForNode(nodeId: ComfyNodeID): WidgetLayout | null {
|
||||||
const found = findLayoutEntryForNode(nodeId);
|
const found = findLayoutEntryForNode(nodeId);
|
||||||
if (!found)
|
if (!found)
|
||||||
return null;
|
return null;
|
||||||
@@ -946,8 +1033,6 @@ function initDefaultLayout() {
|
|||||||
root: null,
|
root: null,
|
||||||
allItems: {},
|
allItems: {},
|
||||||
allItemsByNode: {},
|
allItemsByNode: {},
|
||||||
currentSelection: [],
|
|
||||||
currentSelectionNodes: [],
|
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
isConfiguring: false,
|
isConfiguring: false,
|
||||||
refreshPropsPanel: writable(0),
|
refreshPropsPanel: writable(0),
|
||||||
@@ -1034,7 +1119,9 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
|||||||
|
|
||||||
if (dragItem.type === "widget") {
|
if (dragItem.type === "widget") {
|
||||||
const widget = dragItem as WidgetLayout;
|
const widget = dragItem as WidgetLayout;
|
||||||
widget.node = graph.getNodeById(entry.dragItem.nodeId) as ComfyWidgetNode
|
widget.node = graph.getNodeByIdRecursive(entry.dragItem.nodeId) as ComfyWidgetNode
|
||||||
|
if (widget.node == null)
|
||||||
|
throw (`Node in litegraph not found! ${entry.dragItem.nodeId}`)
|
||||||
allItemsByNode[entry.dragItem.nodeId] = dragEntry
|
allItemsByNode[entry.dragItem.nodeId] = dragEntry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1059,8 +1146,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
|||||||
root,
|
root,
|
||||||
allItems,
|
allItems,
|
||||||
allItemsByNode,
|
allItemsByNode,
|
||||||
currentSelection: [],
|
|
||||||
currentSelectionNodes: [],
|
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
isConfiguring: false,
|
isConfiguring: false,
|
||||||
refreshPropsPanel: writable(0),
|
refreshPropsPanel: writable(0),
|
||||||
@@ -1091,7 +1176,7 @@ const layoutStateStore: WritableLayoutStateStore =
|
|||||||
updateChildren,
|
updateChildren,
|
||||||
nodeAdded,
|
nodeAdded,
|
||||||
nodeRemoved,
|
nodeRemoved,
|
||||||
getCurrentSelection,
|
moveItem,
|
||||||
groupItems,
|
groupItems,
|
||||||
findLayoutEntryForNode,
|
findLayoutEntryForNode,
|
||||||
findLayoutForNode,
|
findLayoutForNode,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, NodeID, PromptID } from "$lib/api";
|
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, ComfyNodeID, PromptID } from "$lib/api";
|
||||||
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
|
||||||
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
||||||
import notify from "$lib/notify";
|
import notify from "$lib/notify";
|
||||||
@@ -14,12 +14,13 @@ type QueueStateOps = {
|
|||||||
queueUpdated: (resp: ComfyAPIQueueResponse) => void,
|
queueUpdated: (resp: ComfyAPIQueueResponse) => void,
|
||||||
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
|
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
|
||||||
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
|
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
|
||||||
executingUpdated: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
|
executionStart: (promptID: PromptID) => void,
|
||||||
executionCached: (promptID: PromptID, nodes: NodeID[]) => void,
|
executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void,
|
||||||
|
executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
||||||
executionError: (promptID: PromptID, message: string) => void,
|
executionError: (promptID: PromptID, message: string) => void,
|
||||||
progressUpdated: (progress: Progress) => void
|
progressUpdated: (progress: Progress) => void
|
||||||
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
|
||||||
onExecuted: (promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) => void
|
onExecuted: (promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueueEntry = {
|
export type QueueEntry = {
|
||||||
@@ -30,7 +31,7 @@ export type QueueEntry = {
|
|||||||
promptID: PromptID,
|
promptID: PromptID,
|
||||||
prompt: SerializedPromptInputsAll,
|
prompt: SerializedPromptInputsAll,
|
||||||
extraData: ComfyBoxPromptExtraData,
|
extraData: ComfyBoxPromptExtraData,
|
||||||
goodOutputs: NodeID[],
|
goodOutputs: ComfyNodeID[],
|
||||||
|
|
||||||
/* Data not sent by ComfyUI's API, lost on page refresh */
|
/* Data not sent by ComfyUI's API, lost on page refresh */
|
||||||
|
|
||||||
@@ -38,8 +39,8 @@ export type QueueEntry = {
|
|||||||
outputs: SerializedPromptOutputs,
|
outputs: SerializedPromptOutputs,
|
||||||
|
|
||||||
/* Nodes in of the workflow that have finished running so far. */
|
/* Nodes in of the workflow that have finished running so far. */
|
||||||
nodesRan: Set<NodeID>,
|
nodesRan: Set<ComfyNodeID>,
|
||||||
cachedNodes: Set<NodeID>
|
cachedNodes: Set<ComfyNodeID>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompletedQueueEntry = {
|
export type CompletedQueueEntry = {
|
||||||
@@ -54,7 +55,7 @@ export type QueueState = {
|
|||||||
queuePending: Writable<QueueEntry[]>,
|
queuePending: Writable<QueueEntry[]>,
|
||||||
queueCompleted: Writable<CompletedQueueEntry[]>,
|
queueCompleted: Writable<CompletedQueueEntry[]>,
|
||||||
queueRemaining: number | "X" | null;
|
queueRemaining: number | "X" | null;
|
||||||
runningNodeID: NodeID | null;
|
runningNodeID: ComfyNodeID | null;
|
||||||
progress: Progress | null,
|
progress: Progress | null,
|
||||||
isInterrupting: boolean
|
isInterrupting: boolean
|
||||||
}
|
}
|
||||||
@@ -161,7 +162,7 @@ function moveToCompleted(index: number, queue: Writable<QueueEntry[]>, status: Q
|
|||||||
store.set(state)
|
store.set(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function executingUpdated(promptID: PromptID, runningNodeID: NodeID | null) {
|
function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) {
|
||||||
console.debug("[queueState] executingUpdated", promptID, runningNodeID)
|
console.debug("[queueState] executingUpdated", promptID, runningNodeID)
|
||||||
store.update((s) => {
|
store.update((s) => {
|
||||||
s.progress = null;
|
s.progress = null;
|
||||||
@@ -199,7 +200,7 @@ function executingUpdated(promptID: PromptID, runningNodeID: NodeID | null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function executionCached(promptID: PromptID, nodes: NodeID[]) {
|
function executionCached(promptID: PromptID, nodes: ComfyNodeID[]) {
|
||||||
console.debug("[queueState] executionCached", promptID, nodes)
|
console.debug("[queueState] executionCached", promptID, nodes)
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const [index, entry, queue] = findEntryInPending(promptID);
|
const [index, entry, queue] = findEntryInPending(promptID);
|
||||||
@@ -235,10 +236,8 @@ function executionError(promptID: PromptID, message: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) {
|
function createNewQueueEntry(promptID: PromptID, number: number = -1, prompt: SerializedPromptInputsAll = {}, extraData: any = {}): QueueEntry {
|
||||||
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt))
|
return {
|
||||||
store.update(s => {
|
|
||||||
const entry: QueueEntry = {
|
|
||||||
number,
|
number,
|
||||||
queuedAt: new Date(), // Now
|
queuedAt: new Date(), // Now
|
||||||
finishedAt: null,
|
finishedAt: null,
|
||||||
@@ -250,14 +249,44 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp
|
|||||||
nodesRan: new Set(),
|
nodesRan: new Set(),
|
||||||
cachedNodes: new Set()
|
cachedNodes: new Set()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executionStart(promptID: PromptID) {
|
||||||
|
console.debug("[queueState] executionStart", promptID)
|
||||||
|
store.update(s => {
|
||||||
|
const [index, entry, queue] = findEntryInPending(promptID);
|
||||||
|
if (entry == null) {
|
||||||
|
const entry = createNewQueueEntry(promptID);
|
||||||
s.queuePending.update(qp => { qp.push(entry); return qp })
|
s.queuePending.update(qp => { qp.push(entry); return qp })
|
||||||
console.debug("[queueState] ADD PROMPT", promptID)
|
console.debug("[queueState] ADD PROMPT", promptID)
|
||||||
|
}
|
||||||
s.isInterrupting = false;
|
s.isInterrupting = false;
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExecuted(promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) {
|
function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) {
|
||||||
|
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt))
|
||||||
|
store.update(s => {
|
||||||
|
const [index, entry, queue] = findEntryInPending(promptID);
|
||||||
|
if (entry == null) {
|
||||||
|
const entry = createNewQueueEntry(promptID, number, prompt, extraData);
|
||||||
|
s.queuePending.update(qp => { qp.push(entry); return qp })
|
||||||
|
console.debug("[queueState] ADD PROMPT", promptID)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entry.number = number;
|
||||||
|
entry.prompt = prompt
|
||||||
|
entry.extraData = extraData
|
||||||
|
queue.set(get(queue))
|
||||||
|
console.warn("[queueState] UPDATE PROMPT", promptID)
|
||||||
|
}
|
||||||
|
s.isInterrupting = false;
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExecuted(promptID: PromptID, nodeID: ComfyNodeID, output: ComfyExecutionResult) {
|
||||||
console.debug("[queueState] onExecuted", promptID, nodeID, output)
|
console.debug("[queueState] onExecuted", promptID, nodeID, output)
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const [index, entry, queue] = findEntryInPending(promptID)
|
const [index, entry, queue] = findEntryInPending(promptID)
|
||||||
@@ -279,6 +308,7 @@ const queueStateStore: WritableQueueStateStore =
|
|||||||
historyUpdated,
|
historyUpdated,
|
||||||
statusUpdated,
|
statusUpdated,
|
||||||
progressUpdated,
|
progressUpdated,
|
||||||
|
executionStart,
|
||||||
executingUpdated,
|
executingUpdated,
|
||||||
executionCached,
|
executionCached,
|
||||||
executionError,
|
executionError,
|
||||||
|
|||||||
57
src/lib/stores/selectionState.ts
Normal file
57
src/lib/stores/selectionState.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
|
import type { DragItemID, IDragItem } from './layoutState';
|
||||||
|
import type { LGraphNode, NodeID } from '@litegraph-ts/core';
|
||||||
|
|
||||||
|
export type SelectionState = {
|
||||||
|
/*
|
||||||
|
* Selected drag items.
|
||||||
|
* NOTE: Order is important, for node grouping actions.
|
||||||
|
*/
|
||||||
|
currentSelection: DragItemID[],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hovered drag items.
|
||||||
|
*/
|
||||||
|
currentHovered: Set<DragItemID>,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Selected LGraphNodes inside the litegraph canvas.
|
||||||
|
* NOTE: Order is important, for node grouping actions.
|
||||||
|
*/
|
||||||
|
currentSelectionNodes: LGraphNode[],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Currently hovered nodes.
|
||||||
|
*/
|
||||||
|
currentHoveredNodes: Set<NodeID>
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectionStateOps = {
|
||||||
|
clear: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WritableSelectionStateStore = Writable<SelectionState> & SelectionStateOps;
|
||||||
|
const store: Writable<SelectionState> = writable(
|
||||||
|
{
|
||||||
|
currentSelection: [],
|
||||||
|
currentSelectionNodes: [],
|
||||||
|
currentHovered: new Set(),
|
||||||
|
currentHoveredNodes: new Set(),
|
||||||
|
})
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
store.set({
|
||||||
|
currentSelection: [],
|
||||||
|
currentSelectionNodes: [],
|
||||||
|
currentHovered: new Set(),
|
||||||
|
currentHoveredNodes: new Set(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiStateStore: WritableSelectionStateStore =
|
||||||
|
{
|
||||||
|
...store,
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
export default uiStateStore;
|
||||||
177
src/lib/utils.ts
177
src/lib/utils.ts
@@ -1,13 +1,10 @@
|
|||||||
import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
|
import layoutState, { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
import selectionState from "$lib/stores/selectionState";
|
||||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
|
||||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
import layoutState from "$lib/stores/layoutState"
|
|
||||||
import type { SvelteComponentDev } from "svelte/internal";
|
|
||||||
import type { SerializedLGraph } from "@litegraph-ts/core";
|
|
||||||
import type { FileNameOrGalleryData, ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
|
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
|
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID } from "@litegraph-ts/core";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import type { ComfyNodeID } from "./api";
|
||||||
|
import { type SerializedPrompt } from "./components/ComfyApp";
|
||||||
|
|
||||||
export function clamp(n: number, min: number, max: number): number {
|
export function clamp(n: number, min: number, max: number): number {
|
||||||
return Math.min(Math.max(n, min), max)
|
return Math.min(Math.max(n, min), max)
|
||||||
@@ -21,6 +18,13 @@ export function range(size: number, startAt: number = 0): ReadonlyArray<number>
|
|||||||
return [...Array(size).keys()].map(i => i + startAt);
|
return [...Array(size).keys()].map(i => i + startAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* enumerate<T>(iterable: Iterable<T>): Iterable<[number, T]> {
|
||||||
|
let index = 0;
|
||||||
|
for (const value of iterable) {
|
||||||
|
yield [index++, value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function download(filename: string, text: string, type: string = "text/plain") {
|
export function download(filename: string, text: string, type: string = "text/plain") {
|
||||||
const blob = new Blob([text], { type: type });
|
const blob = new Blob([text], { type: type });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -37,11 +41,12 @@ export function download(filename: string, text: string, type: string = "text/pl
|
|||||||
|
|
||||||
export function startDrag(evt: MouseEvent) {
|
export function startDrag(evt: MouseEvent) {
|
||||||
const dragItemId: string = evt.target.dataset["dragItemId"];
|
const dragItemId: string = evt.target.dataset["dragItemId"];
|
||||||
|
const ss = get(selectionState)
|
||||||
const ls = get(layoutState)
|
const ls = get(layoutState)
|
||||||
|
|
||||||
if (evt.button !== 0) {
|
if (evt.button !== 0) {
|
||||||
if (ls.currentSelection.length <= 1 && !ls.isMenuOpen)
|
if (ss.currentSelection.length <= 1 && !ls.isMenuOpen)
|
||||||
ls.currentSelection = [dragItemId]
|
ss.currentSelection = [dragItemId]
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,24 +55,110 @@ export function startDrag(evt: MouseEvent) {
|
|||||||
console.debug("startDrag", item)
|
console.debug("startDrag", item)
|
||||||
|
|
||||||
if (evt.ctrlKey) {
|
if (evt.ctrlKey) {
|
||||||
const index = ls.currentSelection.indexOf(item.id)
|
const index = ss.currentSelection.indexOf(item.id)
|
||||||
if (index === -1)
|
if (index === -1)
|
||||||
ls.currentSelection.push(item.id);
|
ss.currentSelection.push(item.id);
|
||||||
else
|
else
|
||||||
ls.currentSelection.splice(index, 1);
|
ss.currentSelection.splice(index, 1);
|
||||||
ls.currentSelection = ls.currentSelection;
|
ss.currentSelection = ss.currentSelection;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ls.currentSelection = [item.id]
|
ss.currentSelection = [item.id]
|
||||||
|
}
|
||||||
|
ss.currentSelectionNodes = [];
|
||||||
|
for (const id of ss.currentSelection) {
|
||||||
|
const item = ls.allItems[id].dragItem
|
||||||
|
if (item.type === "widget") {
|
||||||
|
const node = (item as WidgetLayout).node;
|
||||||
|
if (node) {
|
||||||
|
ss.currentSelectionNodes.push(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ls.currentSelectionNodes = [];
|
|
||||||
|
|
||||||
layoutState.set(ls)
|
layoutState.set(ls)
|
||||||
|
selectionState.set(ss)
|
||||||
};
|
};
|
||||||
|
|
||||||
export function stopDrag(evt: MouseEvent) {
|
export function stopDrag(evt: MouseEvent) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function graphToGraphVis(graph: LGraph): string {
|
||||||
|
let links: string[] = []
|
||||||
|
let seenLinks = new Set()
|
||||||
|
let subgraphs: Record<string, [Subgraph, string[]]> = {}
|
||||||
|
let subgraphNodes: Record<number | UUID, Subgraph> = {}
|
||||||
|
let idToInt: Record<number | UUID, number> = {}
|
||||||
|
let curId = 0;
|
||||||
|
|
||||||
|
const convId = (id: number | UUID): number => {
|
||||||
|
if (idToInt[id] == null) {
|
||||||
|
idToInt[id] = curId++;
|
||||||
|
}
|
||||||
|
return idToInt[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLink = (node: LGraphNode, link: LLink): string => {
|
||||||
|
const nodeA = node.graph.getNodeById(link.origin_id)
|
||||||
|
const nodeB = node.graph.getNodeById(link.target_id);
|
||||||
|
seenLinks.add(link.id)
|
||||||
|
return ` "${convId(nodeA.id)}_${nodeA.title}" -> "${convId(nodeB.id)}_${nodeB.title}";\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of graph.iterateNodesInOrderRecursive()) {
|
||||||
|
for (let [index, input] of enumerate(node.iterateInputInfo())) {
|
||||||
|
const link = node.getInputLink(index);
|
||||||
|
if (link && !seenLinks.has(link.id)) {
|
||||||
|
const linkText = addLink(node, link)
|
||||||
|
if (node.graph != graph) {
|
||||||
|
subgraphs[node.graph._subgraph_node.id] ||= [node.graph._subgraph_node, []]
|
||||||
|
subgraphs[node.graph._subgraph_node.id][1].push(linkText)
|
||||||
|
subgraphNodes[node.graph._subgraph_node.id] = node.graph._subgraph_node
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
links.push(linkText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let [index, output] of enumerate(node.iterateOutputInfo())) {
|
||||||
|
for (const link of node.getOutputLinks(index)) {
|
||||||
|
if (!seenLinks.has(link.id)) {
|
||||||
|
const linkText = addLink(node, link)
|
||||||
|
if (node.graph != graph) {
|
||||||
|
subgraphs[node.graph._subgraph_node.id] ||= [node.graph._subgraph_node, []]
|
||||||
|
subgraphs[node.graph._subgraph_node.id][1].push(linkText)
|
||||||
|
subgraphNodes[node.graph._subgraph_node.id] = node.graph._subgraph_node
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
links.push(linkText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = "digraph {\n"
|
||||||
|
out += ' fontname="Helvetica,Arial,sans-serif"\n'
|
||||||
|
out += ' node [fontname="Helvetica,Arial,sans-serif"]\n'
|
||||||
|
out += ' edge [fontname="Helvetica,Arial,sans-serif"]\n'
|
||||||
|
out += ' node [shape=box style=filled fillcolor="#DDDDDD"]\n'
|
||||||
|
|
||||||
|
for (const [subgraph, links] of Object.values(subgraphs)) {
|
||||||
|
// Subgraph name has to be prefixed with "cluster" to show up as a cluster...
|
||||||
|
out += ` subgraph cluster_subgraph_${convId(subgraph.id)} {\n`
|
||||||
|
out += ` label="${convId(subgraph.id)}_${subgraph.title}";\n`;
|
||||||
|
out += " color=red;\n";
|
||||||
|
// out += " style=grey;\n";
|
||||||
|
out += " " + links.join(" ")
|
||||||
|
out += " }\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
out += links.join("")
|
||||||
|
|
||||||
|
out += "}"
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
export function workflowToGraphVis(workflow: SerializedLGraph): string {
|
export function workflowToGraphVis(workflow: SerializedLGraph): string {
|
||||||
let out = "digraph {\n"
|
let out = "digraph {\n"
|
||||||
|
|
||||||
@@ -87,32 +178,38 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
|
|||||||
for (const pair of Object.entries(prompt.output)) {
|
for (const pair of Object.entries(prompt.output)) {
|
||||||
const [id, o] = pair;
|
const [id, o] = pair;
|
||||||
const outNode = prompt.workflow.nodes.find(n => n.id == id)
|
const outNode = prompt.workflow.nodes.find(n => n.id == id)
|
||||||
|
if (outNode) {
|
||||||
for (const pair2 of Object.entries(o.inputs)) {
|
for (const pair2 of Object.entries(o.inputs)) {
|
||||||
const [inpName, i] = pair2;
|
const [inpName, i] = pair2;
|
||||||
|
|
||||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||||
// Link
|
// Link
|
||||||
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
||||||
|
if (inpNode) {
|
||||||
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// Value
|
// Value
|
||||||
out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n`
|
out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out += "}"
|
out += "}"
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeInfo(nodeId: NodeID): string {
|
export function getNodeInfo(nodeId: ComfyNodeID): string {
|
||||||
let app = (window as any).app;
|
let app = (window as any).app;
|
||||||
if (!app || !app.lGraph)
|
if (!app || !app.lGraph)
|
||||||
return String(nodeId);
|
return String(nodeId);
|
||||||
|
|
||||||
const title = app.lGraph.getNodeById(nodeId)?.title || String(nodeId);
|
const displayNodeID = nodeId ? (nodeId.split("-")[0]) : String(nodeId);
|
||||||
return title + " (" + nodeId + ")"
|
|
||||||
|
const title = app.lGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId);
|
||||||
|
return title + " (" + displayNodeID + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const debounce = (callback: Function, wait = 250) => {
|
export const debounce = (callback: Function, wait = 250) => {
|
||||||
@@ -231,6 +328,24 @@ export async function uploadImageToComfyUI(blob: Blob, filename: string, type: C
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Raw output as received from ComfyUI's backend */
|
||||||
|
export interface ComfyExecutionResult {
|
||||||
|
// Technically this response can contain arbitrary data, but "images" is the
|
||||||
|
// most frequently used as it's output by LoadImage and PreviewImage, the
|
||||||
|
// only two output nodes in base ComfyUI.
|
||||||
|
images: ComfyImageLocation[] | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raw output entry as received from ComfyUI's backend */
|
||||||
|
export type ComfyImageLocation = {
|
||||||
|
/* Filename with extension in the subfolder. */
|
||||||
|
filename: string,
|
||||||
|
/* Subfolder in the containing folder. */
|
||||||
|
subfolder: string,
|
||||||
|
/* Base ComfyUI folder where the image is located. */
|
||||||
|
type: ComfyUploadImageType
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convenient type for passing around image filepaths and their metadata with
|
* Convenient type for passing around image filepaths and their metadata with
|
||||||
* wires. Needs to be converted to a filename for use with LoadImage.
|
* wires. Needs to be converted to a filename for use with LoadImage.
|
||||||
@@ -304,6 +419,12 @@ export function executionResultToImageMetadata(result: ComfyExecutionResult): Co
|
|||||||
return result.images.map(comfyFileToComfyBoxMetadata)
|
return result.images.map(comfyFileToComfyBoxMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isComfyImageLocation(param: any): param is ComfyImageLocation {
|
||||||
|
return param != null && typeof param === "object"
|
||||||
|
&& typeof param.filename === "string"
|
||||||
|
&& typeof param.type === "string"
|
||||||
|
}
|
||||||
|
|
||||||
export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadata[] | null {
|
export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadata[] | null {
|
||||||
let meta: ComfyBoxImageMetadata[] | null = null
|
let meta: ComfyBoxImageMetadata[] | null = null
|
||||||
|
|
||||||
@@ -314,12 +435,26 @@ export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadat
|
|||||||
meta = param
|
meta = param
|
||||||
}
|
}
|
||||||
else if (isComfyExecutionResult(param)) {
|
else if (isComfyExecutionResult(param)) {
|
||||||
meta = executionResultToImageMetadata(param)
|
meta = executionResultToImageMetadata(param);
|
||||||
|
}
|
||||||
|
else if (isComfyImageLocation(param)) {
|
||||||
|
meta = [comfyFileToComfyBoxMetadata(param)]
|
||||||
|
}
|
||||||
|
else if (Array.isArray(param) && param.every(isComfyImageLocation)) {
|
||||||
|
meta = param.map(comfyFileToComfyBoxMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseWhateverIntoComfyImageLocations(param: any): ComfyImageLocation[] | null {
|
||||||
|
const meta = parseWhateverIntoImageMetadata(param);
|
||||||
|
if (!Array.isArray(meta))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return meta.map(m => m.comfyUIFile);
|
||||||
|
}
|
||||||
|
|
||||||
export function comfyBoxImageToComfyFile(image: ComfyBoxImageMetadata): ComfyImageLocation {
|
export function comfyBoxImageToComfyFile(image: ComfyBoxImageMetadata): ComfyImageLocation {
|
||||||
return image.comfyUIFile
|
return image.comfyUIFile
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { IWidget, LGraphNode } from "@litegraph-js/core";
|
import { LGraphNode, LiteGraph } from "@litegraph-ts/core";
|
||||||
import ComfyValueControlWidget from "./widgets/ComfyValueControlWidget";
|
|
||||||
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
|
||||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||||
import { BuiltInSlotShape, LiteGraph } from "@litegraph-ts/core";
|
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
||||||
import { ComfyComboNode, ComfySliderNode, ComfyTextNode } from "./nodes";
|
import { ComfyComboNode, ComfyNumberNode, ComfyTextNode } from "./nodes/widgets";
|
||||||
|
|
||||||
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot;
|
type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot;
|
||||||
|
|
||||||
@@ -37,12 +35,12 @@ function addComfyInput(node: LGraphNode, inputName: string, extraInfo: Partial<I
|
|||||||
|
|
||||||
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||||
const config = getNumberDefaults(inputData, 0.5);
|
const config = getNumberDefaults(inputData, 0.5);
|
||||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
|
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
||||||
}
|
}
|
||||||
|
|
||||||
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||||
const config = getNumberDefaults(inputData, 1);
|
const config = getNumberDefaults(inputData, 1);
|
||||||
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
|
return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfyNumberNode })
|
||||||
};
|
};
|
||||||
|
|
||||||
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyButtonNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Button } from "@gradio/button";
|
import { Button } from "@gradio/button";
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
|
import type { ComfyButtonNode } from "$lib/nodes/widgets";
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
let node: ComfyButtonNode | null = null;
|
let node: ComfyButtonNode | null = null;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Block } from "@gradio/atoms";
|
import { Block } from "@gradio/atoms";
|
||||||
import { Checkbox } from "@gradio/form";
|
import { Checkbox } from "@gradio/form";
|
||||||
import { get, type Writable, writable } from "svelte/store";
|
import { get, type Writable, writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
import type { SelectData } from "@gradio/utils";
|
import type { SelectData } from "@gradio/utils";
|
||||||
|
import type { ComfyCheckboxNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import Select from 'svelte-select';
|
import Select from 'svelte-select';
|
||||||
// import VirtualList from '$lib/components/VirtualList.svelte';
|
// import VirtualList from '$lib/components/VirtualList.svelte';
|
||||||
import VirtualList from 'svelte-tiny-virtual-list';
|
import VirtualList from 'svelte-tiny-virtual-list';
|
||||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
import type { ComfyComboNode } from "$lib/nodes/widgets";
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
let node: ComfyComboNode | null = null;
|
let node: ComfyComboNode | null = null;
|
||||||
let nodeValue: Writable<string> | null = null;
|
let nodeValue: Writable<string> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
|
let lightUp: Writable<boolean> = writable(false);
|
||||||
let valuesForCombo: Writable<any[]> | null = null;
|
let valuesForCombo: Writable<any[]> | null = null;
|
||||||
let lastConfigured: any = null;
|
let lastConfigured: any = null;
|
||||||
let option: any = null;
|
let option: any = null;
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
node = widget.node as ComfyComboNode
|
node = widget.node as ComfyComboNode
|
||||||
nodeValue = node.value;
|
nodeValue = node.value;
|
||||||
propsChanged = node.propsChanged;
|
propsChanged = node.propsChanged;
|
||||||
|
lightUp = node.lightUp;
|
||||||
valuesForCombo = node.valuesForCombo;
|
valuesForCombo = node.valuesForCombo;
|
||||||
lastConfigured = $valuesForCombo
|
lastConfigured = $valuesForCombo
|
||||||
}
|
}
|
||||||
@@ -52,16 +54,8 @@
|
|||||||
activeIndex = values.findIndex(v => v.value === value);
|
activeIndex = values.findIndex(v => v.value === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: $valuesForCombo != lastConfigured && flashOnRefreshed();
|
$: if ($lightUp)
|
||||||
let lightUp = false;
|
setTimeout(() => ($lightUp = false), 1000);
|
||||||
|
|
||||||
function flashOnRefreshed() {
|
|
||||||
lastConfigured = $valuesForCombo
|
|
||||||
if (lastConfigured != null) {
|
|
||||||
lightUp = true;
|
|
||||||
setTimeout(() => (lightUp = false), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinkValue() {
|
function getLinkValue() {
|
||||||
if (!node)
|
if (!node)
|
||||||
@@ -137,7 +131,7 @@
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper comfy-combo" class:mobile={isMobile} class:updated={lightUp}>
|
<div class="wrapper comfy-combo" class:mobile={isMobile} class:updated={$lightUp}>
|
||||||
{#key $valuesForCombo}
|
{#key $valuesForCombo}
|
||||||
{#if node !== null && nodeValue !== null}
|
{#if node !== null && nodeValue !== null}
|
||||||
{#if $valuesForCombo == null}
|
{#if $valuesForCombo == null}
|
||||||
@@ -172,10 +166,11 @@
|
|||||||
<div class="comfy-select-list" slot="list" let:filteredItems>
|
<div class="comfy-select-list" slot="list" let:filteredItems>
|
||||||
{#if filteredItems.length > 0}
|
{#if filteredItems.length > 0}
|
||||||
{@const itemSize = isMobile ? 50 : 25}
|
{@const itemSize = isMobile ? 50 : 25}
|
||||||
|
{@const itemsToShow = isMobile ? 10 : 30}
|
||||||
<VirtualList
|
<VirtualList
|
||||||
items={filteredItems}
|
items={filteredItems}
|
||||||
width="100%"
|
width="100%"
|
||||||
height={Math.min(filteredItems.length, 10) * itemSize}
|
height={Math.min(filteredItems.length, itemsToShow) * itemSize}
|
||||||
itemCount={filteredItems.length}
|
itemCount={filteredItems.length}
|
||||||
{itemSize}
|
{itemSize}
|
||||||
overscanCount={5}
|
overscanCount={5}
|
||||||
@@ -252,7 +247,7 @@
|
|||||||
--chevron-color: var(--body-text-color);
|
--chevron-color: var(--body-text-color);
|
||||||
--border: 1px solid var(--input-border-color);
|
--border: 1px solid var(--input-border-color);
|
||||||
--border-hover: 1px solid var(--input-border-color-hover);
|
--border-hover: 1px solid var(--input-border-color-hover);
|
||||||
--border-focused: 1px solid var(--input-border-color-focus);
|
--border-focused: 1px solid var(--neutral-400);
|
||||||
--border-radius-focused: 0px;
|
--border-radius-focused: 0px;
|
||||||
--border-radius: 0px;
|
--border-radius: 0px;
|
||||||
--list-background: var(--comfy-dropdown-list-background);
|
--list-background: var(--comfy-dropdown-list-background);
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
|
||||||
import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core";
|
|
||||||
|
|
||||||
export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T, V> {
|
|
||||||
name: string;
|
|
||||||
value: V;
|
|
||||||
node: ComfyGraphNode;
|
|
||||||
|
|
||||||
constructor(name: string, value: V, node: ComfyGraphNode) {
|
|
||||||
this.name = name;
|
|
||||||
this.value = value
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
isVirtual?: boolean;
|
|
||||||
options?: T;
|
|
||||||
type?: WidgetTypes | string | any;
|
|
||||||
y?: number;
|
|
||||||
property?: string;
|
|
||||||
last_y?: number;
|
|
||||||
width?: number;
|
|
||||||
clicked?: boolean;
|
|
||||||
marker?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
callback?: WidgetCallback<this>;
|
|
||||||
|
|
||||||
setValue(value: V) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number): void;
|
|
||||||
|
|
||||||
mouse?(event: MouseEvent, pos: Vector2, node: LGraphNode): boolean;
|
|
||||||
|
|
||||||
computeSize?(width: number): [number, number];
|
|
||||||
|
|
||||||
afterQueued?(): void;
|
|
||||||
|
|
||||||
serializeValue?(serialized: SerializedLGraphNode<LGraphNode>, slot: number): Promise<any>;
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
import { StaticImage } from "$lib/components/gradio/image";
|
import { StaticImage } from "$lib/components/gradio/image";
|
||||||
import type { Styles } from "@gradio/utils";
|
import type { Styles } from "@gradio/utils";
|
||||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
import type { WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import type { Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||||
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||||
import { f7 } from "framework7-svelte";
|
import { f7 } from "framework7-svelte";
|
||||||
|
import type { ComfyGalleryNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
let option: number | null = null;
|
let option: number | null = null;
|
||||||
let imageWidth: number = 1;
|
let imageWidth: Writable<number> = writable(0);
|
||||||
let imageHeight: number = 1;
|
let imageHeight: Writable<number> = writable(0);
|
||||||
let selected_image: number | null = null;
|
let selected_image: number | null = null;
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
$: widget && setNodeValue(widget);
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
node = widget.node as ComfyGalleryNode
|
node = widget.node as ComfyGalleryNode
|
||||||
nodeValue = node.value;
|
nodeValue = node.value;
|
||||||
propsChanged = node.propsChanged;
|
propsChanged = node.propsChanged;
|
||||||
|
imageWidth = node.imageWidth
|
||||||
|
imageHeight = node.imageHeight
|
||||||
|
|
||||||
if ($nodeValue != null) {
|
if ($nodeValue != null) {
|
||||||
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
||||||
@@ -45,18 +47,9 @@
|
|||||||
}
|
}
|
||||||
let element: HTMLDivElement;
|
let element: HTMLDivElement;
|
||||||
|
|
||||||
$: if (node) {
|
|
||||||
if (imageWidth > 1 || imageHeight > 1) {
|
|
||||||
node.imageSize = [imageWidth, imageHeight]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
node.imageSize = [1, 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mobileLightbox = null;
|
let mobileLightbox = null;
|
||||||
|
|
||||||
function showMobileLightbox(event: Event) {
|
function showMobileLightbox(source: HTMLImageElement) {
|
||||||
if (!f7)
|
if (!f7)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -65,16 +58,14 @@
|
|||||||
mobileLightbox = null;
|
mobileLightbox = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = (event.target || event.srcElement) as HTMLImageElement;
|
|
||||||
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
||||||
console.debug("[ImageViewer] showModal", event, source, galleryElem);
|
console.debug("[ImageViewer] showModal", source, galleryElem);
|
||||||
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
|
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
|
||||||
console.error("No buttons found on gallery element!", galleryElem)
|
console.error("No buttons found on gallery element!", galleryElem)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allGalleryButtons = ImageViewer.all_gallery_buttons(galleryElem);
|
const allGalleryButtons = ImageViewer.all_gallery_buttons(galleryElem);
|
||||||
const selectedSource = source.src
|
|
||||||
|
|
||||||
const images = allGalleryButtons.map(button => {
|
const images = allGalleryButtons.map(button => {
|
||||||
return {
|
return {
|
||||||
@@ -91,56 +82,29 @@
|
|||||||
type: 'popup',
|
type: 'popup',
|
||||||
});
|
});
|
||||||
mobileLightbox.open(selected_image)
|
mobileLightbox.open(selected_image)
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupImageForMobileLightbox(e: HTMLImageElement) {
|
function onClicked(e: CustomEvent<HTMLImageElement>) {
|
||||||
if (e.dataset.modded === "true")
|
if (isMobile) {
|
||||||
return;
|
showMobileLightbox(e.detail)
|
||||||
|
}
|
||||||
e.dataset.modded = "true";
|
else {
|
||||||
e.style.cursor = "pointer";
|
ImageViewer.instance.showLightbox(e.detail)
|
||||||
e.style.userSelect = "none";
|
}
|
||||||
|
|
||||||
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
|
|
||||||
|
|
||||||
// For Firefox, listening on click first switched to next image then shows the lightbox.
|
|
||||||
// If you know how to fix this without switching to mousedown event, please.
|
|
||||||
// For other browsers the event is click to make it possiblr to drag picture.
|
|
||||||
var event = isFirefox ? 'mousedown' : 'click'
|
|
||||||
|
|
||||||
e.addEventListener(event, (evt) => {
|
|
||||||
evt.preventDefault()
|
|
||||||
showMobileLightbox(evt)
|
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelect(e: CustomEvent<GradioSelectData>) {
|
function onSelect(e: CustomEvent<GradioSelectData>) {
|
||||||
// Setup lightbox
|
|
||||||
// Wait for gradio gallery to show the large preview image, if no timeout then
|
|
||||||
// the event might fire too early
|
|
||||||
|
|
||||||
const callback = isMobile ? setupImageForMobileLightbox
|
|
||||||
: ImageViewer.instance.setupGalleryImageForLightbox.bind(ImageViewer.instance)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const images = element.querySelectorAll<HTMLImageElement>('div.block div > img')
|
|
||||||
if (images != null) {
|
|
||||||
images.forEach(callback);
|
|
||||||
}
|
|
||||||
ImageViewer.instance.refreshImages();
|
|
||||||
}, 200)
|
|
||||||
|
|
||||||
// Update index
|
// Update index
|
||||||
node.setProperty("index", e.detail.index as number)
|
node.setProperty("index", e.detail.index as number)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($propsChanged > -1 && widget && $nodeValue) {
|
$: if ($propsChanged > -1 && widget && $nodeValue) {
|
||||||
if (widget.attrs.variant === "image") {
|
if (widget.attrs.variant === "image") {
|
||||||
selected_image = $nodeValue.length - 1
|
|
||||||
node.setProperty("index", selected_image)
|
node.setProperty("index", selected_image)
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
node.setProperty("index", $nodeValue.length > 0 ? 0 : null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
node.setProperty("index", null)
|
node.setProperty("index", null)
|
||||||
@@ -160,8 +124,8 @@
|
|||||||
value={url}
|
value={url}
|
||||||
show_label={widget.attrs.title != ""}
|
show_label={widget.attrs.title != ""}
|
||||||
label={widget.attrs.title}
|
label={widget.attrs.title}
|
||||||
bind:imageWidth
|
bind:imageWidth={$imageWidth}
|
||||||
bind:imageHeight
|
bind:imageHeight={$imageHeight}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Empty size="large" unpadded_box={true}><Image /></Empty>
|
<Empty size="large" unpadded_box={true}><Image /></Empty>
|
||||||
@@ -181,8 +145,9 @@
|
|||||||
root={""}
|
root={""}
|
||||||
root_url={""}
|
root_url={""}
|
||||||
on:select={onSelect}
|
on:select={onSelect}
|
||||||
bind:imageWidth
|
on:clicked={onClicked}
|
||||||
bind:imageHeight
|
bind:imageWidth={$imageWidth}
|
||||||
|
bind:imageHeight={$imageHeight}
|
||||||
bind:selected_image
|
bind:selected_image
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,343 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
|
||||||
import { Block } from "@gradio/atoms";
|
|
||||||
import { TextBox } from "@gradio/form";
|
|
||||||
import Row from "$lib/components/gradio/app/Row.svelte";
|
|
||||||
import { get, type Writable } from "svelte/store";
|
|
||||||
import Modal from "$lib/components/Modal.svelte";
|
|
||||||
import { Button } from "@gradio/button";
|
|
||||||
import type { ComfyImageEditorNode, ComfyImageLocation } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import { Embed as Klecks } from "klecks";
|
|
||||||
|
|
||||||
import "klecks/style/style.scss";
|
|
||||||
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
|
||||||
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType } from "$lib/utils";
|
|
||||||
import notify from "$lib/notify";
|
|
||||||
import NumberInput from "$lib/components/NumberInput.svelte";
|
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
|
||||||
export let isMobile: boolean = false;
|
|
||||||
let node: ComfyImageEditorNode | null = null;
|
|
||||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
|
||||||
let attrsChanged: Writable<number> | null = null;
|
|
||||||
|
|
||||||
let imgWidth: number = 0;
|
|
||||||
let imgHeight: number = 0;
|
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
|
||||||
|
|
||||||
$: if ($nodeValue && $nodeValue.length > 0) {
|
|
||||||
// TODO improve
|
|
||||||
if (imgWidth > 0 && imgHeight > 0) {
|
|
||||||
$nodeValue[0].width = imgWidth
|
|
||||||
$nodeValue[0].height = imgHeight
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$nodeValue[0].width = 0
|
|
||||||
$nodeValue[0].height = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
|
||||||
if (widget) {
|
|
||||||
node = widget.node as ComfyImageEditorNode
|
|
||||||
nodeValue = node.value;
|
|
||||||
attrsChanged = widget.attrsChanged;
|
|
||||||
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let editorRoot: HTMLDivElement | null = null;
|
|
||||||
let showModal = false;
|
|
||||||
let kl: Klecks | null = null;
|
|
||||||
|
|
||||||
function disposeEditor() {
|
|
||||||
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
|
||||||
|
|
||||||
if (editorRoot) {
|
|
||||||
while (editorRoot.firstChild) {
|
|
||||||
editorRoot.removeChild(editorRoot.firstChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kl = null;
|
|
||||||
showModal = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.save();
|
|
||||||
ctx.fillStyle = fill,
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
ctx.restore();
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const e = new Image();
|
|
||||||
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
|
|
||||||
e.addEventListener("load", () => { resolve(e); });
|
|
||||||
e.src = imageURL;
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
|
|
||||||
const image = await loadImage(imageURL);
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = image.width;
|
|
||||||
canvas.height = image.height;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.save();
|
|
||||||
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
ctx.drawImage(image, 0, 0);
|
|
||||||
ctx.restore();
|
|
||||||
return [canvas, image.width, image.height];
|
|
||||||
}
|
|
||||||
|
|
||||||
const FILENAME: string = "ComfyUITemp.png";
|
|
||||||
const SUBFOLDER: string = "ComfyBox_Editor";
|
|
||||||
const DIRECTORY: ComfyUploadImageType = "input";
|
|
||||||
|
|
||||||
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
|
||||||
const blob = kl.getPNG();
|
|
||||||
|
|
||||||
status = "uploading"
|
|
||||||
|
|
||||||
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
|
|
||||||
.then((entry: ComfyImageLocation) => {
|
|
||||||
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
|
|
||||||
$nodeValue = [meta] // TODO more than one image
|
|
||||||
status = "uploaded"
|
|
||||||
notify("Saved image to ComfyUI!", { type: "success" })
|
|
||||||
onSuccess();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
|
|
||||||
status = "error"
|
|
||||||
uploadError = err;
|
|
||||||
$nodeValue = []
|
|
||||||
onError();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let closeDialog = null;
|
|
||||||
|
|
||||||
async function saveAndClose() {
|
|
||||||
console.log(closeDialog, kl)
|
|
||||||
if (!closeDialog || !kl)
|
|
||||||
return;
|
|
||||||
|
|
||||||
submitKlecksToComfyUI(() => {}, () => {});
|
|
||||||
closeDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
let blankImageWidth = 512;
|
|
||||||
let blankImageHeight = 512;
|
|
||||||
|
|
||||||
async function openImageEditor() {
|
|
||||||
if (!editorRoot)
|
|
||||||
return;
|
|
||||||
|
|
||||||
showModal = true;
|
|
||||||
|
|
||||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
|
||||||
|
|
||||||
kl = new Klecks({
|
|
||||||
embedUrl: url,
|
|
||||||
onSubmit: submitKlecksToComfyUI,
|
|
||||||
targetEl: editorRoot,
|
|
||||||
warnOnPageClose: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
|
|
||||||
|
|
||||||
let canvas = null;
|
|
||||||
let width = blankImageWidth;
|
|
||||||
let height = blankImageHeight;
|
|
||||||
|
|
||||||
if ($nodeValue && $nodeValue.length > 0) {
|
|
||||||
const comfyImage = $nodeValue[0];
|
|
||||||
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
|
|
||||||
[canvas, width, height] = await generateImageCanvas(comfyURL);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
canvas = generateBlankCanvas(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
kl.openProject({
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
layers: [{
|
|
||||||
name: 'Image',
|
|
||||||
opacity: 1,
|
|
||||||
mixModeStr: 'source-over',
|
|
||||||
image: canvas
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
kl.klApp?.out("yo");
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = "empty";
|
|
||||||
let uploadError = null;
|
|
||||||
|
|
||||||
function onUploading() {
|
|
||||||
console.warn("UPLOADING!!!")
|
|
||||||
uploadError = null;
|
|
||||||
status = "uploading"
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
|
|
||||||
console.warn("UPLOADED!!!")
|
|
||||||
uploadError = null;
|
|
||||||
status = "uploaded"
|
|
||||||
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClear() {
|
|
||||||
console.warn("CLEAR!!!")
|
|
||||||
uploadError = null;
|
|
||||||
status = "empty"
|
|
||||||
$nodeValue = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUploadError(e: CustomEvent<any>) {
|
|
||||||
console.warn("ERROR!!!")
|
|
||||||
status = "error"
|
|
||||||
uploadError = e.detail
|
|
||||||
$nodeValue = []
|
|
||||||
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
|
|
||||||
}
|
|
||||||
|
|
||||||
let _value: ComfyImageLocation[] = []
|
|
||||||
$: if ($nodeValue)
|
|
||||||
_value = $nodeValue.map(comfyBoxImageToComfyFile)
|
|
||||||
else
|
|
||||||
_value = []
|
|
||||||
|
|
||||||
$: canEdit = status === "empty" || status === "uploaded";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper comfy-image-editor">
|
|
||||||
{#if widget.attrs.variant === "fileUpload" || isMobile}
|
|
||||||
<ImageUpload value={_value}
|
|
||||||
bind:imgWidth
|
|
||||||
bind:imgHeight
|
|
||||||
fileCount={"single"}
|
|
||||||
elem_classes={[]}
|
|
||||||
style={""}
|
|
||||||
label={widget.attrs.title}
|
|
||||||
on:uploading={onUploading}
|
|
||||||
on:uploaded={onUploaded}
|
|
||||||
on:upload_error={onUploadError}
|
|
||||||
on:clear={onClear}
|
|
||||||
on:change={onChange}
|
|
||||||
on:image_clicked={openImageEditor}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="comfy-image-editor-panel">
|
|
||||||
<ImageUpload value={_value}
|
|
||||||
bind:imgWidth
|
|
||||||
bind:imgHeight
|
|
||||||
fileCount={"single"}
|
|
||||||
elem_classes={[]}
|
|
||||||
style={""}
|
|
||||||
label={widget.attrs.title}
|
|
||||||
on:uploading={onUploading}
|
|
||||||
on:uploaded={onUploaded}
|
|
||||||
on:upload_error={onUploadError}
|
|
||||||
on:clear={onClear}
|
|
||||||
on:change={onChange}
|
|
||||||
on:image_clicked={openImageEditor}
|
|
||||||
/>
|
|
||||||
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
|
|
||||||
<div>
|
|
||||||
<div id="klecks-loading-screen">
|
|
||||||
<span id="klecks-loading-screen-text"></span>
|
|
||||||
</div>
|
|
||||||
<div class="image-editor-root" bind:this={editorRoot} />
|
|
||||||
</div>
|
|
||||||
<div slot="buttons">
|
|
||||||
<Button variant="primary" on:click={saveAndClose}>
|
|
||||||
Save and Close
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" on:click={closeDialog}>
|
|
||||||
Discard Edits
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
<Block>
|
|
||||||
{#if !$nodeValue || $nodeValue.length === 0}
|
|
||||||
<Row>
|
|
||||||
<Row>
|
|
||||||
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
|
||||||
Create Image
|
|
||||||
</Button>
|
|
||||||
<div>
|
|
||||||
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
|
||||||
</div>
|
|
||||||
{#if uploadError}
|
|
||||||
<div>
|
|
||||||
Upload error: {uploadError}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
|
|
||||||
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
{:else}
|
|
||||||
<Row>
|
|
||||||
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
|
||||||
Edit Image
|
|
||||||
</Button>
|
|
||||||
<div>
|
|
||||||
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
|
||||||
</div>
|
|
||||||
{#if uploadError}
|
|
||||||
<div>
|
|
||||||
Upload error: {uploadError}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Row>
|
|
||||||
{/if}
|
|
||||||
</Block>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.image-editor-root {
|
|
||||||
width: 75vw;
|
|
||||||
height: 75vh;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
:global(> .g-root) {
|
|
||||||
height: calc(100% - 59px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comfy-image-editor {
|
|
||||||
:global(> dialog) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.kl-popup) {
|
|
||||||
z-index: 999999999999;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
|
import { Block } from "@gradio/atoms";
|
||||||
|
import { TextBox } from "@gradio/form";
|
||||||
|
import Row from "$lib/components/gradio/app/Row.svelte";
|
||||||
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import { Button } from "@gradio/button";
|
||||||
|
import { Embed as Klecks } from "klecks";
|
||||||
|
|
||||||
|
import "klecks/style/style.scss";
|
||||||
|
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
||||||
|
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType, type ComfyImageLocation } from "$lib/utils";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
import NumberInput from "$lib/components/NumberInput.svelte";
|
||||||
|
import type { ComfyImageEditorNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
|
export let widget: WidgetLayout | null = null;
|
||||||
|
export let isMobile: boolean = false;
|
||||||
|
let node: ComfyImageEditorNode | null = null;
|
||||||
|
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||||
|
let attrsChanged: Writable<number> | null = null;
|
||||||
|
|
||||||
|
let imgWidth: Writable<number> = writable(0);
|
||||||
|
let imgHeight: Writable<number> = writable(0);
|
||||||
|
|
||||||
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
|
if (widget) {
|
||||||
|
node = widget.node as ComfyImageEditorNode
|
||||||
|
nodeValue = node.value;
|
||||||
|
attrsChanged = widget.attrsChanged;
|
||||||
|
imgWidth = node.imgWidth
|
||||||
|
imgHeight = node.imgHeight
|
||||||
|
status = $nodeValue && $nodeValue.length > 0 ? "uploaded" : "empty"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let editorRoot: HTMLDivElement | null = null;
|
||||||
|
let showModal = false;
|
||||||
|
let kl: Klecks | null = null;
|
||||||
|
|
||||||
|
function disposeEditor() {
|
||||||
|
console.warn("[ImageEditorWidget] CLOSING", widget, $nodeValue)
|
||||||
|
|
||||||
|
if (editorRoot) {
|
||||||
|
while (editorRoot.firstChild) {
|
||||||
|
editorRoot.removeChild(editorRoot.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kl = null;
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBlankCanvas(width: number, height: number, fill: string = "#fff"): HTMLCanvasElement {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = fill,
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.restore();
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImage(imageURL: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const e = new Image();
|
||||||
|
e.setAttribute('crossorigin', 'anonymous'); // Don't taint the canvas from loading files on-disk
|
||||||
|
e.addEventListener("load", () => { resolve(e); });
|
||||||
|
e.src = imageURL;
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateImageCanvas(imageURL: string): Promise<[HTMLCanvasElement, number, number]> {
|
||||||
|
const image = await loadImage(imageURL);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = "rgba(255, 255, 255, 0.0)";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
ctx.restore();
|
||||||
|
return [canvas, image.width, image.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILENAME: string = "ComfyUITemp.png";
|
||||||
|
const SUBFOLDER: string = "ComfyBox_Editor";
|
||||||
|
const DIRECTORY: ComfyUploadImageType = "input";
|
||||||
|
|
||||||
|
async function submitKlecksToComfyUI(onSuccess: () => void, onError: () => void) {
|
||||||
|
const blob = kl.getPNG();
|
||||||
|
|
||||||
|
status = "uploading"
|
||||||
|
|
||||||
|
await uploadImageToComfyUI(blob, FILENAME, DIRECTORY, SUBFOLDER)
|
||||||
|
.then((entry: ComfyImageLocation) => {
|
||||||
|
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
|
||||||
|
$nodeValue = [meta] // TODO more than one image
|
||||||
|
status = "uploaded"
|
||||||
|
notify("Saved image to ComfyUI!", { type: "success" })
|
||||||
|
onSuccess();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notify(`Failed to upload image from editor: ${err}`, { type: "error", timeout: 10000 })
|
||||||
|
status = "error"
|
||||||
|
uploadError = err;
|
||||||
|
$nodeValue = []
|
||||||
|
onError();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let closeDialog = null;
|
||||||
|
|
||||||
|
async function saveAndClose() {
|
||||||
|
console.log(closeDialog, kl)
|
||||||
|
if (!closeDialog || !kl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
submitKlecksToComfyUI(() => {}, () => {});
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
let blankImageWidth = 512;
|
||||||
|
let blankImageHeight = 512;
|
||||||
|
|
||||||
|
async function openImageEditor() {
|
||||||
|
if (!editorRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
showModal = true;
|
||||||
|
|
||||||
|
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||||
|
|
||||||
|
kl = new Klecks({
|
||||||
|
embedUrl: url,
|
||||||
|
onSubmit: submitKlecksToComfyUI,
|
||||||
|
targetEl: editorRoot,
|
||||||
|
warnOnPageClose: false
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn("[ImageEditorWidget] OPENING", widget, $nodeValue)
|
||||||
|
|
||||||
|
let canvas = null;
|
||||||
|
let width = blankImageWidth;
|
||||||
|
let height = blankImageHeight;
|
||||||
|
|
||||||
|
if ($nodeValue && $nodeValue.length > 0) {
|
||||||
|
const comfyImage = $nodeValue[0];
|
||||||
|
const comfyURL = comfyBoxImageToComfyURL(comfyImage);
|
||||||
|
[canvas, width, height] = await generateImageCanvas(comfyURL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canvas = generateBlankCanvas(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
kl.openProject({
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
layers: [{
|
||||||
|
name: 'Image',
|
||||||
|
opacity: 1,
|
||||||
|
mixModeStr: 'source-over',
|
||||||
|
image: canvas
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
kl?.klApp?.out("yo");
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = "empty";
|
||||||
|
let uploadError = null;
|
||||||
|
|
||||||
|
function onUploading() {
|
||||||
|
console.warn("UPLOADING!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploading"
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
console.warn("UPLOADED!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "uploaded"
|
||||||
|
$nodeValue = e.detail.map(comfyFileToComfyBoxMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() {
|
||||||
|
console.warn("CLEAR!!!")
|
||||||
|
uploadError = null;
|
||||||
|
status = "empty"
|
||||||
|
$nodeValue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploadError(e: CustomEvent<any>) {
|
||||||
|
console.warn("ERROR!!!")
|
||||||
|
status = "error"
|
||||||
|
uploadError = e.detail
|
||||||
|
$nodeValue = []
|
||||||
|
notify(`Failed to upload image to ComfyUI: ${uploadError}`, { type: "error", timeout: 10000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
let _value: ComfyImageLocation[] = []
|
||||||
|
$: if ($nodeValue)
|
||||||
|
_value = $nodeValue.map(comfyBoxImageToComfyFile)
|
||||||
|
else
|
||||||
|
_value = []
|
||||||
|
|
||||||
|
$: canEdit = status === "empty" || status === "uploaded";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper comfy-image-editor">
|
||||||
|
{#if widget.attrs.variant === "fileUpload" || isMobile}
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth={$imgWidth}
|
||||||
|
bind:imgHeight={$imgHeight}
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="comfy-image-editor-panel">
|
||||||
|
<ImageUpload value={_value}
|
||||||
|
bind:imgWidth={$imgWidth}
|
||||||
|
bind:imgHeight={$imgHeight}
|
||||||
|
fileCount={"single"}
|
||||||
|
elem_classes={[]}
|
||||||
|
style={""}
|
||||||
|
label={widget.attrs.title}
|
||||||
|
on:uploading={onUploading}
|
||||||
|
on:uploaded={onUploaded}
|
||||||
|
on:upload_error={onUploadError}
|
||||||
|
on:clear={onClear}
|
||||||
|
on:change={onChange}
|
||||||
|
on:image_clicked={openImageEditor}
|
||||||
|
/>
|
||||||
|
<Modal bind:showModal closeOnClick={false} on:close={disposeEditor} bind:closeDialog>
|
||||||
|
<div>
|
||||||
|
<div id="klecks-loading-screen">
|
||||||
|
<span id="klecks-loading-screen-text"></span>
|
||||||
|
</div>
|
||||||
|
<div class="image-editor-root" bind:this={editorRoot} />
|
||||||
|
</div>
|
||||||
|
<div slot="buttons">
|
||||||
|
<Button variant="primary" on:click={saveAndClose}>
|
||||||
|
Save and Close
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" on:click={closeDialog}>
|
||||||
|
Discard Edits
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Block>
|
||||||
|
{#if !$nodeValue || $nodeValue.length === 0}
|
||||||
|
<Row>
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Create Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox show_label={false} disabled={true} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<NumberInput label={"Width"} min={64} max={2048} step={64} bind:value={blankImageWidth} />
|
||||||
|
<NumberInput label={"Height"} min={64} max={2048} step={64} bind:value={blankImageHeight} />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
<Row>
|
||||||
|
<Button variant="secondary" disabled={!canEdit} on:click={openImageEditor}>
|
||||||
|
Edit Image
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<TextBox label={""} show_label={false} disabled={true} lines={1} max_lines={1} value="Status: {status}"/>
|
||||||
|
</div>
|
||||||
|
{#if uploadError}
|
||||||
|
<div>
|
||||||
|
Upload error: {uploadError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
{/if}
|
||||||
|
</Block>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.image-editor-root {
|
||||||
|
width: 75vw;
|
||||||
|
height: 75vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
:global(> .g-root) {
|
||||||
|
height: calc(100% - 59px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comfy-image-editor {
|
||||||
|
:global(> dialog) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.kl-popup) {
|
||||||
|
z-index: 999999999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
import type { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Range } from "$lib/components/gradio/form";
|
import { Range } from "$lib/components/gradio/form";
|
||||||
import { get, type Writable } from "svelte/store";
|
import { get, type Writable } from "svelte/store";
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
let node: ComfySliderNode | null = null;
|
let node: ComfyNumberNode | null = null;
|
||||||
let nodeValue: Writable<number> | null = null;
|
let nodeValue: Writable<number> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
let option: number | null = null;
|
let option: number | null = null;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
if (widget) {
|
if (widget) {
|
||||||
node = widget.node as ComfySliderNode
|
node = widget.node as ComfyNumberNode
|
||||||
nodeValue = node.value;
|
nodeValue = node.value;
|
||||||
propsChanged = node.propsChanged;
|
propsChanged = node.propsChanged;
|
||||||
setOption($nodeValue); // don't react on option
|
setOption($nodeValue); // don't react on option
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComfyRadioNode } from "$lib/nodes/ComfyWidgetNodes";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { Block } from "@gradio/atoms";
|
import { Block } from "@gradio/atoms";
|
||||||
import { Radio } from "@gradio/form";
|
import { Radio } from "@gradio/form";
|
||||||
@@ -7,6 +6,7 @@
|
|||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
import type { SelectData } from "@gradio/utils";
|
import type { SelectData } from "@gradio/utils";
|
||||||
import { clamp } from "$lib/utils";
|
import { clamp } from "$lib/utils";
|
||||||
|
import type { ComfyRadioNode } from "$lib/nodes/widgets";
|
||||||
|
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TextBox } from "@gradio/form";
|
import { TextBox } from "@gradio/form";
|
||||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
|
||||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||||
import { get, type Writable } from "svelte/store";
|
import { type Writable } from "svelte/store";
|
||||||
import { isDisabled } from "./utils"
|
import { isDisabled } from "./utils"
|
||||||
|
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||||
export let widget: WidgetLayout | null = null;
|
export let widget: WidgetLayout | null = null;
|
||||||
export let isMobile: boolean = false;
|
export let isMobile: boolean = false;
|
||||||
let node: ComfyComboNode | null = null;
|
|
||||||
|
let node: ComfyTextNode | null = null;
|
||||||
let nodeValue: Writable<string> | null = null;
|
let nodeValue: Writable<string> | null = null;
|
||||||
let propsChanged: Writable<number> | null = null;
|
let propsChanged: Writable<number> | null = null;
|
||||||
let itemValue: WidgetUIStateStore | null = null;
|
|
||||||
|
|
||||||
$: widget && setNodeValue(widget);
|
$: widget && setNodeValue(widget);
|
||||||
|
|
||||||
function setNodeValue(widget: WidgetLayout) {
|
function setNodeValue(widget: WidgetLayout) {
|
||||||
if (widget) {
|
if (widget) {
|
||||||
node = widget.node as ComfySliderNode
|
node = widget.node as ComfyTextNode
|
||||||
nodeValue = node.value;
|
nodeValue = node.value;
|
||||||
propsChanged = node.propsChanged;
|
propsChanged = node.propsChanged;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default as ComfyGalleryWidget } from "./ComfyGalleryWidget"
|
|
||||||
export { default as ComfyGalleryWidget_Svelte } from "./ComfyGalleryWidget.svelte"
|
|
||||||
@@ -1,14 +1,27 @@
|
|||||||
import type { IDragItem } from "$lib/stores/layoutState";
|
import type { IDragItem } from "$lib/stores/layoutState";
|
||||||
import layoutState from "$lib/stores/layoutState";
|
import layoutState from "$lib/stores/layoutState";
|
||||||
import { NodeMode } from "@litegraph-ts/core";
|
import { LGraphNode, NodeMode } from "@litegraph-ts/core";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
export function isNodeDisabled(node: LGraphNode): boolean {
|
||||||
|
while (node != null) {
|
||||||
|
if (node.mode !== NodeMode.ALWAYS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node.graph == null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
node = node.graph._subgraph_node;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function isDisabled(widget: IDragItem) {
|
export function isDisabled(widget: IDragItem) {
|
||||||
if (widget.attrs.disabled)
|
if (widget.attrs.disabled)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (widget.type === "widget") {
|
if (widget.type === "widget") {
|
||||||
return widget.attrs.nodeDisabledState === "disabled" && widget.node.mode === NodeMode.NEVER
|
return widget.attrs.nodeDisabledState === "disabled" && isNodeDisabled(widget.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -19,7 +32,7 @@ export function isHidden(widget: IDragItem) {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (widget.type === "widget") {
|
if (widget.type === "widget") {
|
||||||
return widget.attrs.nodeDisabledState === "hidden" && widget.node.mode === NodeMode.NEVER
|
return widget.attrs.nodeDisabledState === "hidden" && isNodeDisabled(widget.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { LiteGraph } from '@litegraph-ts/core';
|
import ComfyApp from '$lib/components/ComfyApp';
|
||||||
|
import { configureLitegraph } from '$lib/init';
|
||||||
import App from './App.svelte';
|
import App from './App.svelte';
|
||||||
|
|
||||||
LiteGraph.use_uuids = true;
|
configureLitegraph()
|
||||||
|
|
||||||
|
const comfyApp = new ComfyApp();
|
||||||
|
(window as any).app = comfyApp;
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.getElementById('app'),
|
target: document.getElementById("app-root"),
|
||||||
|
props: { app: comfyApp }
|
||||||
})
|
})
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -6,20 +6,17 @@ import ComfyApp from '$lib/components/ComfyApp';
|
|||||||
import uiState from '$lib/stores/uiState';
|
import uiState from '$lib/stores/uiState';
|
||||||
import { LiteGraph } from '@litegraph-ts/core';
|
import { LiteGraph } from '@litegraph-ts/core';
|
||||||
import ComfyGraph from '$lib/ComfyGraph';
|
import ComfyGraph from '$lib/ComfyGraph';
|
||||||
|
import { configureLitegraph } from '$lib/init';
|
||||||
|
|
||||||
Framework7.use(Framework7Svelte);
|
Framework7.use(Framework7Svelte);
|
||||||
|
|
||||||
LiteGraph.use_uuids = true;
|
configureLitegraph(true);
|
||||||
LiteGraph.dialog_close_on_mouse_leave = false;
|
|
||||||
LiteGraph.search_hide_on_mouse_leave = false;
|
|
||||||
LiteGraph.pointerevents_method = "pointer";
|
|
||||||
|
|
||||||
const comfyApp = new ComfyApp();
|
const comfyApp = new ComfyApp();
|
||||||
|
(window as any).app = comfyApp;
|
||||||
uiState.update(s => { s.app = comfyApp; return s; })
|
|
||||||
|
|
||||||
const app = new AppMobile({
|
const app = new AppMobile({
|
||||||
target: document.getElementById('app'),
|
target: document.getElementById("app-root"),
|
||||||
props: { app: comfyApp }
|
props: { app: comfyApp }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "gradio";
|
@import "gradio";
|
||||||
|
@import "litegraph";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -7,14 +8,22 @@ body {
|
|||||||
overscroll-behavior-y: contain;
|
overscroll-behavior-y: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app-root {
|
||||||
background: var(--body-background-fill);
|
background: var(--body-background-fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-blue-500: #3985f5;
|
--color-blue-500: #3985f5;
|
||||||
|
|
||||||
|
--input-border-color-focus: var(--neutral-400);
|
||||||
|
|
||||||
--comfy-accent-soft: var(--neutral-300);
|
--comfy-accent-soft: var(--neutral-300);
|
||||||
|
--comfy-widget-selected-background-fill: var(--color-yellow-100);
|
||||||
|
--comfy-widget-hovered-background-fill: var(--secondary-200);
|
||||||
|
--comfy-container-selected-background-fill: var(--color-yellow-300);
|
||||||
|
--comfy-container-hovered-background-fill: var(--secondary-300);
|
||||||
|
--comfy-container-empty-background-fill: var(--color-grey-300);
|
||||||
|
--comfy-container-empty-border-color: var(--color-grey-400);
|
||||||
--comfy-disabled-label-color: var(--neutral-400);
|
--comfy-disabled-label-color: var(--neutral-400);
|
||||||
--comfy-disabled-textbox-background-fill: var(--neutral-200);
|
--comfy-disabled-textbox-background-fill: var(--neutral-200);
|
||||||
--comfy-disabled-textbox-border-color: var(--neutral-300);
|
--comfy-disabled-textbox-border-color: var(--neutral-300);
|
||||||
@@ -37,7 +46,15 @@ body {
|
|||||||
.dark {
|
.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--input-border-color-focus: var(--neutral-500);
|
||||||
|
|
||||||
--comfy-accent-soft: var(--neutral-600);
|
--comfy-accent-soft: var(--neutral-600);
|
||||||
|
--comfy-widget-selected-background-fill: var(--primary-500);
|
||||||
|
--comfy-widget-hovered-background-fill: var(--neutral-600);
|
||||||
|
--comfy-container-selected-background-fill: var(--primary-700);
|
||||||
|
--comfy-container-hovered-background-fill: var(--neutral-500);
|
||||||
|
--comfy-container-empty-background-fill: var(--color-grey-800);
|
||||||
|
--comfy-container-empty-border-color: var(--color-grey-600);
|
||||||
--comfy-disabled-label-color: var(--neutral-500);
|
--comfy-disabled-label-color: var(--neutral-500);
|
||||||
--comfy-disabled-textbox-background-fill: var(--neutral-800);
|
--comfy-disabled-textbox-background-fill: var(--neutral-800);
|
||||||
--comfy-disabled-textbox-border-color: var(--neutral-700);
|
--comfy-disabled-textbox-border-color: var(--neutral-700);
|
||||||
@@ -90,9 +107,16 @@ hr {
|
|||||||
color: var(--panel-border-color);
|
color: var(--panel-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
input, textarea {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(.litegraph) {
|
||||||
|
select {
|
||||||
color: var(--body-text-color);
|
color: var(--body-text-color);
|
||||||
background: var(--block-background-fill);
|
background: var(--input-background-fill);
|
||||||
|
border: var(--input-border-width) solid var(--input-border-color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -125,17 +149,6 @@ select {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// button {
|
|
||||||
// filter: none;
|
|
||||||
// &.primary:active {
|
|
||||||
// filter: brightness(80%)
|
|
||||||
// }
|
|
||||||
// &.secondary:active {
|
|
||||||
// filter: brightness(80%)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
&.primary:active {
|
&.primary:active {
|
||||||
border-color: var(--button-primary-border-color-active) !important;
|
border-color: var(--button-primary-border-color-active) !important;
|
||||||
|
|||||||
108
src/scss/litegraph.scss
Normal file
108
src/scss/litegraph.scss
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Improvements to litegraph's css.
|
||||||
|
// Taken from ComfyUI.
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--litegraph-fg-color: #000;
|
||||||
|
--litegraph-bg-color: #fff;
|
||||||
|
--litegraph-comfy-menu-bg: #353535;
|
||||||
|
--litegraph-comfy-input-bg: #222;
|
||||||
|
--litegraph-input-text: #ddd;
|
||||||
|
--litegraph-descrip-text: #999;
|
||||||
|
--litegraph-drag-text: #ccc;
|
||||||
|
--litegraph-error-text: #ff4444;
|
||||||
|
--litegraph-border-color: #4e4e4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.litegraph {
|
||||||
|
/* Input popup */
|
||||||
|
&.graphdialog {
|
||||||
|
min-height: 1em;
|
||||||
|
background-color: var(--litegraph-comfy-menu-bg);
|
||||||
|
.name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: var(--litegraph-descrip-text);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-top: unset;
|
||||||
|
vertical-align: unset;
|
||||||
|
height: 1.6em;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphdialog input, .graphdialog textarea, .graphdialog select {
|
||||||
|
background-color: var(--litegraph-comfy-input-bg);
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: var(--litegraph-border-color);
|
||||||
|
color: var(--litegraph-input-text);
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context Menu */
|
||||||
|
.litemenu-entry {
|
||||||
|
&.has_submenu {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.litecontextmenu {
|
||||||
|
.litemenu-entry {
|
||||||
|
&:hover {
|
||||||
|
&:not(.disabled):not(.separator) {
|
||||||
|
background-color: var(--litegraph-comfy-menu-bg) !important;
|
||||||
|
filter: brightness(155%);
|
||||||
|
color: var(--litegraph-input-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
background-color: var(--litegraph-comfy-input-bg) !important;
|
||||||
|
color: var(--litegraph-input-text) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.litesearchbox {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
background-color: var(--litegraph-comfy-menu-bg) !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
&.lite-search-item {
|
||||||
|
color: var(--litegraph-input-text);
|
||||||
|
background-color: var(--litegraph-comfy-input-bg);
|
||||||
|
filter: brightness(80%);
|
||||||
|
padding-left: 0.2em;
|
||||||
|
&.generic_type {
|
||||||
|
color: var(--litegraph-input-text);
|
||||||
|
filter: brightness(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.litemenu-entry {
|
||||||
|
&.has_submenu {
|
||||||
|
&::after {
|
||||||
|
content: ">";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.litegraph.litecontextmenu,
|
||||||
|
.litegraph.litecontextmenu.dark {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
background-color: var(--litegraph-comfy-menu-bg) !important;
|
||||||
|
filter: brightness(95%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.litegraph.litecontextmenu .litemenu-entry.submenu,
|
||||||
|
.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
|
||||||
|
background-color: var(--litegraph-comfy-menu-bg) !important;
|
||||||
|
color: var(--litegraph-input-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.litegraph.litesearchbox input,
|
||||||
|
.litegraph.litesearchbox select {
|
||||||
|
background-color: var(--litegraph-comfy-input-bg) !important;
|
||||||
|
color: var(--litegraph-input-text);
|
||||||
|
}
|
||||||
94
src/tests/ComfyGraphTests.ts
Normal file
94
src/tests/ComfyGraphTests.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { LGraph, LiteGraph, Subgraph, type SlotLayout } from "@litegraph-ts/core"
|
||||||
|
import { Watch } from "@litegraph-ts/nodes-basic"
|
||||||
|
import { expect } from 'vitest'
|
||||||
|
import UnitTest from "./UnitTest"
|
||||||
|
import ComfyGraph from "$lib/ComfyGraph";
|
||||||
|
import ComfyPromptSerializer from "$lib/components/ComfyPromptSerializer";
|
||||||
|
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||||
|
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||||
|
import { graphToGraphVis } from "$lib/utils";
|
||||||
|
import layoutState from "$lib/stores/layoutState";
|
||||||
|
import { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
export default class ComfyGraphTests extends UnitTest {
|
||||||
|
test__onNodeAdded__updatesLayoutState() {
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
layoutState.initDefaultLayout() // adds 3 containers
|
||||||
|
|
||||||
|
const state = get(layoutState)
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(3)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(0)
|
||||||
|
|
||||||
|
const widget = LiteGraph.createNode(ComfyNumberNode);
|
||||||
|
graph.add(widget)
|
||||||
|
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(4)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(1)
|
||||||
|
expect(state.allItemsByNode[widget.id]).toBeTruthy();
|
||||||
|
|
||||||
|
graph.add(widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
test__correctSubgraphFactory() {
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
const subgraph = LiteGraph.createNode(Subgraph);
|
||||||
|
graph.add(subgraph)
|
||||||
|
expect(subgraph.graph).toBeInstanceOf(ComfyGraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
test__onNodeAdded__handlesNodesAddedInSubgraphs() {
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
layoutState.initDefaultLayout()
|
||||||
|
|
||||||
|
const subgraph = LiteGraph.createNode(Subgraph);
|
||||||
|
graph.add(subgraph)
|
||||||
|
|
||||||
|
const state = get(layoutState)
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(3)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(0)
|
||||||
|
|
||||||
|
const widget = LiteGraph.createNode(ComfyNumberNode);
|
||||||
|
subgraph.subgraph.add(widget)
|
||||||
|
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(4)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(1)
|
||||||
|
expect(state.allItemsByNode[widget.id]).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
test__onNodeAdded__handlesSubgraphsWithNodes() {
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
layoutState.initDefaultLayout()
|
||||||
|
|
||||||
|
const state = get(layoutState)
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(3)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(0)
|
||||||
|
|
||||||
|
const subgraph = LiteGraph.createNode(Subgraph);
|
||||||
|
const widget = LiteGraph.createNode(ComfyNumberNode);
|
||||||
|
subgraph.subgraph.add(widget)
|
||||||
|
graph.add(subgraph)
|
||||||
|
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(4)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(1)
|
||||||
|
expect(state.allItemsByNode[widget.id]).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
test__onNodeRemoved__updatesLayoutState() {
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
layoutState.initDefaultLayout()
|
||||||
|
|
||||||
|
const widget = LiteGraph.createNode(ComfyNumberNode);
|
||||||
|
graph.add(widget)
|
||||||
|
|
||||||
|
const state = get(layoutState)
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(4)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(1)
|
||||||
|
expect(state.allItemsByNode[widget.id]).toBeTruthy();
|
||||||
|
|
||||||
|
graph.remove(widget)
|
||||||
|
|
||||||
|
expect(Object.keys(state.allItems)).toHaveLength(3)
|
||||||
|
expect(Object.keys(state.allItemsByNode)).toHaveLength(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/tests/ComfyPromptSerializerTests.ts
Normal file
181
src/tests/ComfyPromptSerializerTests.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { LGraph, LiteGraph, Subgraph, type SlotLayout } from "@litegraph-ts/core"
|
||||||
|
import { Watch } from "@litegraph-ts/nodes-basic"
|
||||||
|
import { expect } from 'vitest'
|
||||||
|
import UnitTest from "./UnitTest"
|
||||||
|
import ComfyGraph from "$lib/ComfyGraph";
|
||||||
|
import ComfyPromptSerializer from "$lib/components/ComfyPromptSerializer";
|
||||||
|
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||||
|
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||||
|
import { graphToGraphVis } from "$lib/utils";
|
||||||
|
|
||||||
|
class MockBackendInput extends ComfyGraphNode {
|
||||||
|
override isBackendNode = true;
|
||||||
|
comfyClass: string = "MockBackendInput";
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "in", type: "*" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: MockBackendInput,
|
||||||
|
title: "Test.MockBackendInput",
|
||||||
|
desc: "one input",
|
||||||
|
type: "test/input"
|
||||||
|
})
|
||||||
|
|
||||||
|
class MockBackendLink extends ComfyGraphNode {
|
||||||
|
override isBackendNode = true;
|
||||||
|
comfyClass: string = "MockBackendLink";
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
inputs: [
|
||||||
|
{ name: "in", type: "*" },
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{ name: "out", type: "*" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: MockBackendLink,
|
||||||
|
title: "Test.MockBackendLink",
|
||||||
|
desc: "one input, one output",
|
||||||
|
type: "test/link"
|
||||||
|
})
|
||||||
|
|
||||||
|
class MockBackendOutput extends ComfyGraphNode {
|
||||||
|
override isBackendNode = true;
|
||||||
|
comfyClass: string = "MockBackendOutput";
|
||||||
|
|
||||||
|
static slotLayout: SlotLayout = {
|
||||||
|
outputs: [
|
||||||
|
{ name: "out", type: "*" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType({
|
||||||
|
class: MockBackendOutput,
|
||||||
|
title: "Test.MockBackendOutput",
|
||||||
|
desc: "one output",
|
||||||
|
type: "test/output"
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class ComfyPromptSerializerTests extends UnitTest {
|
||||||
|
test__serialize__shouldIgnoreFrontend() {
|
||||||
|
const ser = new ComfyPromptSerializer();
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
|
||||||
|
const nodeA = LiteGraph.createNode(Watch)
|
||||||
|
const nodeB = LiteGraph.createNode(Watch)
|
||||||
|
|
||||||
|
graph.add(nodeA)
|
||||||
|
graph.add(nodeB)
|
||||||
|
|
||||||
|
const result = ser.serialize(graph)
|
||||||
|
|
||||||
|
expect(result.output).toEqual({})
|
||||||
|
}
|
||||||
|
|
||||||
|
test__serialize__shouldSerializeBackendNodes() {
|
||||||
|
const ser = new ComfyPromptSerializer();
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
|
||||||
|
const input = LiteGraph.createNode(MockBackendInput)
|
||||||
|
const link = LiteGraph.createNode(MockBackendLink)
|
||||||
|
const output = LiteGraph.createNode(MockBackendOutput)
|
||||||
|
|
||||||
|
graph.add(input)
|
||||||
|
graph.add(link)
|
||||||
|
graph.add(output)
|
||||||
|
|
||||||
|
output.connect(0, link, 0)
|
||||||
|
link.connect(0, input, 0)
|
||||||
|
|
||||||
|
const result = ser.serialize(graph)
|
||||||
|
|
||||||
|
console.warn(result.output)
|
||||||
|
expect(Object.keys(result.output)).toHaveLength(3);
|
||||||
|
expect(result.output[input.id].inputs["in"]).toBeInstanceOf(Array)
|
||||||
|
expect(result.output[input.id].inputs["in"][0]).toEqual(link.id)
|
||||||
|
expect(result.output[link.id].inputs["in"]).toBeInstanceOf(Array)
|
||||||
|
expect(result.output[link.id].inputs["in"][0]).toEqual(output.id)
|
||||||
|
expect(Object.keys(result.output[output.id].inputs)).toHaveLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test__serialize__shouldFollowSubgraphs() {
|
||||||
|
const ser = new ComfyPromptSerializer();
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
|
||||||
|
const output = LiteGraph.createNode(MockBackendOutput)
|
||||||
|
const link = LiteGraph.createNode(MockBackendLink)
|
||||||
|
const input = LiteGraph.createNode(MockBackendInput)
|
||||||
|
|
||||||
|
const subgraph = LiteGraph.createNode(Subgraph)
|
||||||
|
const graphInput = subgraph.addGraphInput("testIn", "number")
|
||||||
|
const graphOutput = subgraph.addGraphOutput("testOut", "number")
|
||||||
|
|
||||||
|
graph.add(subgraph)
|
||||||
|
graph.add(output)
|
||||||
|
subgraph.subgraph.add(link)
|
||||||
|
graph.add(input)
|
||||||
|
|
||||||
|
output.connect(0, subgraph, 0)
|
||||||
|
graphInput.innerNode.connect(0, link, 0)
|
||||||
|
link.connect(0, graphOutput.innerNode, 0)
|
||||||
|
subgraph.connect(0, input, 0)
|
||||||
|
|
||||||
|
const result = ser.serialize(graph)
|
||||||
|
|
||||||
|
expect(Object.keys(result.output)).toHaveLength(3);
|
||||||
|
expect(result.output[input.id].inputs["in"]).toBeInstanceOf(Array)
|
||||||
|
expect(result.output[input.id].inputs["in"][0]).toEqual(link.id)
|
||||||
|
expect(result.output[link.id].inputs["in"]).toBeInstanceOf(Array)
|
||||||
|
expect(result.output[link.id].inputs["in"][0]).toEqual(output.id)
|
||||||
|
expect(result.output[output.id].inputs).toEqual({})
|
||||||
|
}
|
||||||
|
|
||||||
|
test__serialize__shouldFollowSubgraphsRecursively() {
|
||||||
|
const ser = new ComfyPromptSerializer();
|
||||||
|
const graph = new ComfyGraph();
|
||||||
|
|
||||||
|
const output = LiteGraph.createNode(MockBackendOutput)
|
||||||
|
const link = LiteGraph.createNode(MockBackendLink)
|
||||||
|
const input = LiteGraph.createNode(MockBackendInput)
|
||||||
|
|
||||||
|
const subgraphA = LiteGraph.createNode(Subgraph)
|
||||||
|
const subgraphB = LiteGraph.createNode(Subgraph)
|
||||||
|
const graphInputA = subgraphA.addGraphInput("testIn", "number")
|
||||||
|
const graphOutputA = subgraphA.addGraphOutput("testOut", "number")
|
||||||
|
const graphInputB = subgraphB.addGraphInput("testIn", "number")
|
||||||
|
const graphOutputB = subgraphB.addGraphOutput("testOut", "number")
|
||||||
|
|
||||||
|
graph.add(subgraphA)
|
||||||
|
subgraphA.subgraph.add(subgraphB)
|
||||||
|
graph.add(output)
|
||||||
|
subgraphB.subgraph.add(link)
|
||||||
|
graph.add(input)
|
||||||
|
|
||||||
|
output.connect(0, subgraphA, 0)
|
||||||
|
graphInputA.innerNode.connect(0, subgraphB, 0)
|
||||||
|
graphInputB.innerNode.connect(0, link, 0)
|
||||||
|
link.connect(0, graphOutputB.innerNode, 0)
|
||||||
|
subgraphB.connect(0, graphOutputA.innerNode, 0)
|
||||||
|
subgraphA.connect(0, input, 0)
|
||||||
|
|
||||||
|
const result = ser.serialize(graph)
|
||||||
|
|
||||||
|
console.warn(graphToGraphVis(graph))
|
||||||
|
console.warn(result.output)
|
||||||
|
expect(Object.keys(result.output)).toHaveLength(3);
|
||||||
|
expect(result.output[input.id].inputs["in"]).toBeInstanceOf(Array)
|
||||||
|
expect(result.output[input.id].inputs["in"][0]).toEqual(link.id)
|
||||||
|
expect(result.output[link.id].inputs["in"]).toBeInstanceOf(Array)
|
||||||
|
expect(result.output[link.id].inputs["in"][0]).toEqual(output.id)
|
||||||
|
expect(result.output[output.id].inputs).toEqual({})
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/tests/UnitTest.ts
Normal file
4
src/tests/UnitTest.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default abstract class UnitTest {
|
||||||
|
setUp() { }
|
||||||
|
tearDown() { }
|
||||||
|
}
|
||||||
81
src/tests/main.ts
Normal file
81
src/tests/main.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
console.debug = (...msg) => {
|
||||||
|
}
|
||||||
|
|
||||||
|
import { vi, describe, it } from "vitest"
|
||||||
|
import UnitTest from "./UnitTest"
|
||||||
|
import * as testSuite from "./testSuite"
|
||||||
|
|
||||||
|
import { LiteGraph } from "@litegraph-ts/core"
|
||||||
|
import "@litegraph-ts/core"
|
||||||
|
import "@litegraph-ts/nodes-basic"
|
||||||
|
|
||||||
|
LiteGraph.use_uuids = true;
|
||||||
|
|
||||||
|
// I don't like BDD syntax...
|
||||||
|
// Emulate minitest instead...
|
||||||
|
function runTests<T extends UnitTest>(ctor: new () => T) {
|
||||||
|
const instance = new ctor()
|
||||||
|
const ctorName = instance.constructor.name
|
||||||
|
const idx = ctorName.indexOf("Tests")
|
||||||
|
if (idx === -1) {
|
||||||
|
throw `Invalid test name ${ctorName}, must end with "Tests"`
|
||||||
|
}
|
||||||
|
const classCategory = ctorName.substring(0, idx)
|
||||||
|
describe(classCategory, () => {
|
||||||
|
const allTopLevelTests: [string, Function][] = []
|
||||||
|
const allTests: Record<string, [string, Function][]> = {}
|
||||||
|
for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(instance))) {
|
||||||
|
if (key.startsWith("test")) {
|
||||||
|
const keys = key.split("__")
|
||||||
|
let _ = null;
|
||||||
|
let category = null;
|
||||||
|
let testName = null;
|
||||||
|
if (keys.length == 2) {
|
||||||
|
[_, testName] = keys
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[_, category, testName] = keys
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = instance[key]
|
||||||
|
if (typeof value === "function") {
|
||||||
|
const testFn = () => {
|
||||||
|
instance.setUp()
|
||||||
|
value.apply(instance)
|
||||||
|
instance.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category != null) {
|
||||||
|
allTests[category] ||= []
|
||||||
|
allTests[category].push([testName, testFn])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
allTopLevelTests.push([testName, testFn])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, testFn] of allTopLevelTests) {
|
||||||
|
const should = name.split(/\.?(?=[A-Z])/).join(' ').toLowerCase();
|
||||||
|
it(should, testFn.bind(instance))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [category, tests] of Object.entries(allTests)) {
|
||||||
|
describe(category, () => {
|
||||||
|
for (const [name, testFn] of tests) {
|
||||||
|
const should = name.split(/\.?(?=[A-Z])/).join(' ').toLowerCase();
|
||||||
|
it(should, testFn.bind(instance))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTestSuite() {
|
||||||
|
for (const ctor of Object.values(testSuite)) {
|
||||||
|
runTests(ctor as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTestSuite();
|
||||||
2
src/tests/testSuite.ts
Normal file
2
src/tests/testSuite.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ComfyPromptSerializerTests } from "./ComfyPromptSerializerTests"
|
||||||
|
export { default as ComfyGraphTests } from "./ComfyGraphTests"
|
||||||
@@ -72,6 +72,13 @@ export default defineConfig({
|
|||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
include: ['litegraph/packages/tests/src/main.ts']
|
environment: 'jsdom',
|
||||||
|
deps: {
|
||||||
|
inline: [/^svelte/, /^@floating-ui/, /dist/, "skeleton-elements", "mdn-polyfills", "loupe"]
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
'litegraph/packages/tests/src/main.ts',
|
||||||
|
'src/tests/main.ts'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user