@@ -19,7 +19,7 @@
|
||||
}
|
||||
</script>
|
||||
<body>
|
||||
<div id="app"/>
|
||||
<div id="app-root"/>
|
||||
<script type="module" src='/src/main-desktop.ts'></script>
|
||||
</body>
|
||||
</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">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="mobile">
|
||||
<div id="app-root" class="mobile">
|
||||
<script type="module" src='/src/main-mobile.ts'></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:inspect": "vitest --inspect-brk --single-thread",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@floating-ui/core": "^1.2.6",
|
||||
"@floating-ui/dom": "^1.2.8",
|
||||
"@zerodevx/svelte-toast": "^0.9.3",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"happy-dom": "^9.18.3",
|
||||
"jsdom": "^22.0.0",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-svelte": "^2.10.0",
|
||||
"sass": "^1.61.0",
|
||||
@@ -33,7 +38,7 @@
|
||||
"vite-plugin-static-copy": "^0.14.0",
|
||||
"vite-plugin-svelte-console-remover": "^1.0.10",
|
||||
"vite-tsconfig-paths": "^4.0.8",
|
||||
"vitest": "^0.25.8"
|
||||
"vitest": "^0.27.3"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
331
pnpm-lock.yaml
generated
331
pnpm-lock.yaml
generated
@@ -122,6 +122,12 @@ importers:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(vite@4.3.1)
|
||||
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':
|
||||
specifier: ^0.9.3
|
||||
version: 0.9.3(svelte@3.58.0)
|
||||
@@ -134,6 +140,12 @@ importers:
|
||||
eslint-plugin-svelte3:
|
||||
specifier: ^4.0.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:
|
||||
specifier: ^2.8.7
|
||||
version: 2.8.7
|
||||
@@ -171,8 +183,8 @@ importers:
|
||||
specifier: ^4.0.8
|
||||
version: 4.0.8(typescript@5.0.3)(vite@4.3.1)
|
||||
vitest:
|
||||
specifier: ^0.25.8
|
||||
version: 0.25.8(sass@1.61.0)
|
||||
specifier: ^0.27.3
|
||||
version: 0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0)
|
||||
|
||||
gradio/client/js:
|
||||
dependencies:
|
||||
@@ -1677,13 +1689,11 @@ packages:
|
||||
|
||||
/@floating-ui/core@1.2.6:
|
||||
resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==}
|
||||
dev: false
|
||||
|
||||
/@floating-ui/dom@1.2.6:
|
||||
resolution: {integrity: sha512-02vxFDuvuVPs22iJICacezYJyf7zwwOCWkPNkWNBr1U0Qt1cKFYzWvxts0AmqcOQGwt/3KJWcWIgtbUU38keyw==}
|
||||
/@floating-ui/dom@1.2.8:
|
||||
resolution: {integrity: sha512-XLwhYV90MxiHDq6S0rzFZj00fnDM+A1R9jhSioZoMsa7G0Q0i+Q4x40ajR8FHSdYDE1bgjG45mIWe6jtv9UPmg==}
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.2.6
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.8:
|
||||
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
|
||||
@@ -3053,6 +3063,11 @@ packages:
|
||||
tslib: 2.5.0
|
||||
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:
|
||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -3361,6 +3376,10 @@ packages:
|
||||
svelte: 3.58.0
|
||||
dev: true
|
||||
|
||||
/abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
dev: true
|
||||
|
||||
/abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
dev: false
|
||||
@@ -3395,6 +3414,15 @@ packages:
|
||||
pako: 2.1.0
|
||||
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:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
dependencies:
|
||||
@@ -3476,7 +3504,6 @@ packages:
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: false
|
||||
|
||||
/automation-events@6.0.0:
|
||||
resolution: {integrity: sha512-mSOnckbtuJso8cczB+broNsfcuDIQ+J4GFhlW/V3iD+LAXbS4XGHhdjYvtPnCKclKcvs9cVLtUMUkNncOzUQPg==}
|
||||
@@ -3698,7 +3725,6 @@ packages:
|
||||
/cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/call-bind@1.0.2:
|
||||
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
||||
@@ -3896,7 +3922,6 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: false
|
||||
|
||||
/commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
@@ -4053,6 +4078,10 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: false
|
||||
|
||||
/css.escape@1.5.1:
|
||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||
dev: true
|
||||
|
||||
/cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4065,6 +4094,13 @@ packages:
|
||||
css-tree: 1.1.3
|
||||
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:
|
||||
resolution: {integrity: sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4227,6 +4263,15 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
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:
|
||||
resolution: {integrity: sha512-9rcwGd3zhvmJChyLzL5jjZ6UEtWO0SKa9Ycy6RVoQxSW43TSOBRbizj/Zn8UonfpBjCikHEQrJyE72Xw5eCY5A==}
|
||||
dependencies:
|
||||
@@ -4257,6 +4302,10 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
/decimal.js@10.4.3:
|
||||
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||
dev: true
|
||||
|
||||
/dedent@0.7.0:
|
||||
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
|
||||
dev: true
|
||||
@@ -4284,7 +4333,6 @@ packages:
|
||||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/dequal@2.0.2:
|
||||
resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==}
|
||||
@@ -4353,6 +4401,13 @@ packages:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -4415,6 +4470,11 @@ packages:
|
||||
engines: {node: '>=0.12'}
|
||||
dev: false
|
||||
|
||||
/entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
dev: true
|
||||
|
||||
/error-ex@1.3.2:
|
||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||
dependencies:
|
||||
@@ -5045,6 +5105,15 @@ packages:
|
||||
mime-types: 2.1.34
|
||||
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:
|
||||
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
|
||||
dev: true
|
||||
@@ -5328,6 +5397,17 @@ packages:
|
||||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -5365,6 +5445,13 @@ packages:
|
||||
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
dev: true
|
||||
@@ -5423,6 +5510,17 @@ packages:
|
||||
parse-cache-control: 1.0.1
|
||||
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:
|
||||
resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==}
|
||||
dependencies:
|
||||
@@ -5438,6 +5536,16 @@ packages:
|
||||
sshpk: 1.17.0
|
||||
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:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -5448,7 +5556,6 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
@@ -5565,6 +5672,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
dev: true
|
||||
|
||||
/is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6136,6 +6247,44 @@ packages:
|
||||
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -6171,7 +6320,6 @@ packages:
|
||||
|
||||
/jsonc-parser@3.2.0:
|
||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||
dev: false
|
||||
|
||||
/jsonfile@4.0.0:
|
||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||
@@ -6515,14 +6663,12 @@ packages:
|
||||
/mime-db@1.51.0:
|
||||
resolution: {integrity: sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/mime-types@2.1.34:
|
||||
resolution: {integrity: sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.51.0
|
||||
dev: false
|
||||
|
||||
/mime@3.0.0:
|
||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||
@@ -6573,7 +6719,6 @@ packages:
|
||||
pathe: 1.1.0
|
||||
pkg-types: 1.0.3
|
||||
ufo: 1.1.2
|
||||
dev: false
|
||||
|
||||
/mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
@@ -6715,6 +6860,10 @@ packages:
|
||||
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
|
||||
dev: false
|
||||
|
||||
/nwsapi@2.2.4:
|
||||
resolution: {integrity: sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==}
|
||||
dev: true
|
||||
|
||||
/oauth-sign@0.9.0:
|
||||
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
||||
dev: false
|
||||
@@ -6860,6 +7009,12 @@ packages:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
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:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
dev: false
|
||||
@@ -6889,9 +7044,12 @@ packages:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
/pathe@0.2.0:
|
||||
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
|
||||
dev: true
|
||||
|
||||
/pathe@1.1.0:
|
||||
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
|
||||
dev: false
|
||||
|
||||
/pathval@1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
@@ -6928,7 +7086,6 @@ packages:
|
||||
jsonc-parser: 3.2.0
|
||||
mlly: 1.2.1
|
||||
pathe: 1.1.0
|
||||
dev: false
|
||||
|
||||
/plotly.js-dist-min@2.10.1:
|
||||
resolution: {integrity: sha512-H0ls1C2uu2U+qWw76djo4/zOGtUKfMILwFhu7tCOaG/wH5ypujrYGCH03N9SQVf1SXcctTfW57USf8LmagSiPQ==}
|
||||
@@ -7114,7 +7271,6 @@ packages:
|
||||
|
||||
/psl@1.9.0:
|
||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||
dev: false
|
||||
|
||||
/punycode@2.3.0:
|
||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||
@@ -7136,6 +7292,10 @@ packages:
|
||||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: true
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -7252,6 +7412,10 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: true
|
||||
|
||||
/resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
dev: false
|
||||
@@ -7332,6 +7496,10 @@ packages:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
/rrweb-cssom@0.6.0:
|
||||
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
|
||||
dev: true
|
||||
|
||||
/run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
dependencies:
|
||||
@@ -7370,7 +7538,6 @@ packages:
|
||||
|
||||
/safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
|
||||
/sander@0.5.1:
|
||||
resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
|
||||
@@ -7389,6 +7556,13 @@ packages:
|
||||
immutable: 4.3.0
|
||||
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:
|
||||
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
|
||||
hasBin: true
|
||||
@@ -7452,7 +7626,6 @@ packages:
|
||||
|
||||
/siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
dev: false
|
||||
|
||||
/sigmund@1.0.1:
|
||||
resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==}
|
||||
@@ -7519,7 +7692,6 @@ packages:
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
source-map: 0.6.1
|
||||
dev: false
|
||||
|
||||
/source-map@0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
@@ -7577,7 +7749,6 @@ packages:
|
||||
|
||||
/stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
dev: false
|
||||
|
||||
/standardized-audio-context@25.3.45:
|
||||
resolution: {integrity: sha512-d1UVvbz0mDmEqNehvoTKlpSevRJ3YiVZ6kdboeaWX+8cl94H1w8x7c5RNdg0nqxiE049LMeF4tFPuDl5Vm78Kg==}
|
||||
@@ -7589,7 +7760,6 @@ packages:
|
||||
|
||||
/std-env@3.3.3:
|
||||
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
|
||||
dev: false
|
||||
|
||||
/streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
@@ -7782,7 +7952,7 @@ packages:
|
||||
resolution: {integrity: sha512-8Ifi5CD2Ui7FX7NjJRmutFtXjrB8T/FMNoS2H8P81t5LHK4I9G4NIs007rLWG/nRl7y+zJUXa3tWuTjYXw/O5A==}
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.2.6
|
||||
'@floating-ui/dom': 1.2.6
|
||||
'@floating-ui/dom': 1.2.8
|
||||
dev: false
|
||||
|
||||
/svelte-hmr@0.15.1(svelte@3.58.0):
|
||||
@@ -7953,7 +8123,7 @@ packages:
|
||||
/svelte-select@5.5.3:
|
||||
resolution: {integrity: sha512-0QIiEmyon3bqoenFtR/BhamMpMmzOWWg1ctE7xxwP7nEnZhAGGoTra1HPPfEyT6C6gVaOFgCpdBaZoM8DEHlEw==}
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.2.6
|
||||
'@floating-ui/dom': 1.2.8
|
||||
svelte-floating-ui: 1.2.8
|
||||
dev: false
|
||||
|
||||
@@ -8007,6 +8177,10 @@ packages:
|
||||
ssr-window: 4.0.2
|
||||
dev: false
|
||||
|
||||
/symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
dev: true
|
||||
|
||||
/sync-request@6.1.0:
|
||||
resolution: {integrity: sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -8195,10 +8369,27 @@ packages:
|
||||
punycode: 2.3.0
|
||||
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:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
@@ -8292,7 +8483,6 @@ packages:
|
||||
|
||||
/ufo@1.1.2:
|
||||
resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
|
||||
dev: false
|
||||
|
||||
/undici@5.20.0:
|
||||
resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==}
|
||||
@@ -8306,6 +8496,11 @@ packages:
|
||||
engines: {node: '>= 4.0.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -8325,6 +8520,13 @@ packages:
|
||||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -8761,6 +8963,29 @@ packages:
|
||||
extsprintf: 1.3.0
|
||||
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):
|
||||
resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==}
|
||||
engines: {node: '>=v14.18.0'}
|
||||
@@ -9154,8 +9379,8 @@ packages:
|
||||
vite: 4.3.1(sass@1.61.0)
|
||||
dev: false
|
||||
|
||||
/vitest@0.25.8(sass@1.61.0):
|
||||
resolution: {integrity: sha512-X75TApG2wZTJn299E/TIYevr4E9/nBo1sUtZzn0Ci5oK8qnpZAZyhwg0qCeMSakGIWtc6oRwcQFyFfW14aOFWg==}
|
||||
/vitest@0.27.3(happy-dom@9.18.3)(jsdom@22.0.0)(sass@1.61.0):
|
||||
resolution: {integrity: sha512-Ld3UVgRVhJUtqvQ3dW89GxiApFAgBsWJZBCWzK+gA3w2yG68csXlGZZ4WDJURf+8ecNfgrScga6xY+8YSOpiMg==}
|
||||
engines: {node: '>=v14.16.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -9181,15 +9406,22 @@ packages:
|
||||
'@types/node': 18.16.0
|
||||
acorn: 8.8.2
|
||||
acorn-walk: 8.2.0
|
||||
cac: 6.7.14
|
||||
chai: 4.3.7
|
||||
debug: 4.3.4
|
||||
happy-dom: 9.18.3
|
||||
jsdom: 22.0.0
|
||||
local-pkg: 0.4.3
|
||||
picocolors: 1.0.0
|
||||
source-map: 0.6.1
|
||||
std-env: 3.3.3
|
||||
strip-literal: 1.0.1
|
||||
tinybench: 2.4.0
|
||||
tinypool: 0.3.1
|
||||
tinyspy: 1.1.1
|
||||
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:
|
||||
- less
|
||||
- sass
|
||||
@@ -9308,6 +9540,13 @@ packages:
|
||||
resolution: {integrity: sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
||||
dependencies:
|
||||
@@ -9322,11 +9561,36 @@ packages:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==}
|
||||
engines: {node: '>=6'}
|
||||
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:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
dependencies:
|
||||
@@ -9349,7 +9613,6 @@ packages:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
dev: false
|
||||
|
||||
/word-wrap@1.2.3:
|
||||
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
|
||||
@@ -9405,7 +9668,15 @@ packages:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
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:
|
||||
resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const app = new ComfyAppState();
|
||||
export let app: ComfyAppState;
|
||||
</script>
|
||||
|
||||
<ComfyApp {app}/>
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
|
||||
onMount(async () => {
|
||||
await app.setup();
|
||||
(window as any).app = app;
|
||||
window.addEventListener("backbutton", 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 EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
@@ -8,7 +8,8 @@ import { get } from "svelte/store";
|
||||
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
|
||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||
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 = {
|
||||
configured: (graph: LGraph) => void
|
||||
@@ -26,29 +27,48 @@ export default class ComfyGraph extends LGraph {
|
||||
|
||||
override onConfigure() {
|
||||
console.debug("Configured");
|
||||
this.eventBus.emit("configured", this);
|
||||
}
|
||||
|
||||
override onBeforeChange(graph: LGraph, info: any) {
|
||||
console.debug("BeforeChange", info);
|
||||
this.eventBus.emit("beforeChange", graph, info);
|
||||
}
|
||||
|
||||
override onAfterChange(graph: LGraph, info: any) {
|
||||
console.debug("AfterChange", info);
|
||||
this.eventBus.emit("afterChange", graph, info);
|
||||
}
|
||||
|
||||
override onAfterExecute() {
|
||||
this.eventBus.emit("afterExecute");
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
// 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)
|
||||
|
||||
// All nodes whether they come from base litegraph or ComfyBox should
|
||||
// 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)
|
||||
node.properties.tags = []
|
||||
|
||||
@@ -88,7 +108,7 @@ export default class ComfyGraph extends LGraph {
|
||||
if (!("svelteComponentType" in node) && options.addedBy == null) {
|
||||
console.debug("[ComfyGraph] AutoAdd UI")
|
||||
const comfyNode = node as ComfyGraphNode;
|
||||
const widgetNodesAdded = []
|
||||
const widgetNodesAdded: ComfyWidgetNode[] = []
|
||||
for (let index = 0; index < comfyNode.inputs.length; index++) {
|
||||
const input = comfyNode.inputs[index];
|
||||
if ("config" in input) {
|
||||
@@ -96,7 +116,7 @@ export default class ComfyGraph extends LGraph {
|
||||
if (comfyInput.defaultWidgetNode) {
|
||||
const widgetNode = LiteGraph.createNode(comfyInput.defaultWidgetNode)
|
||||
const inputPos = comfyNode.getConnectionPos(true, index);
|
||||
this.add(widgetNode)
|
||||
node.graph.add(widgetNode)
|
||||
widgetNode.connect(0, comfyNode, index);
|
||||
widgetNode.collapse();
|
||||
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)
|
||||
console.debug("[ComfyGraph] Group new widgets", dragItems)
|
||||
const dragItemIDs = widgetNodesAdded.map(wn => get(layoutState).allItemsByNode[wn.id]?.dragItem?.id).filter(Boolean)
|
||||
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);
|
||||
this.eventBus.emit("nodeAdded", node);
|
||||
// Handle nodes in subgraphs being attached to this graph indirectly
|
||||
// ************** 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) {
|
||||
selectionState.clear(); // safest option
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 queueState from "./stores/queueState";
|
||||
import { get } from "svelte/store";
|
||||
import { get, type Unsubscriber } from "svelte/store";
|
||||
import uiState from "./stores/uiState";
|
||||
import layoutState from "./stores/layoutState";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
import { ComfyReroute } from "./nodes";
|
||||
import type { Progress } from "./components/ComfyApp";
|
||||
import selectionState from "./stores/selectionState";
|
||||
|
||||
export type SerializedGraphCanvasState = {
|
||||
offset: Vector2,
|
||||
@@ -14,6 +16,7 @@ export type SerializedGraphCanvasState = {
|
||||
|
||||
export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
app: ComfyApp | null;
|
||||
private _unsubscribe: Unsubscriber;
|
||||
|
||||
constructor(
|
||||
app: ComfyApp,
|
||||
@@ -27,13 +30,33 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
) {
|
||||
super(canvas, app.lGraph, options);
|
||||
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 {
|
||||
return {
|
||||
offset: this.ds.offset,
|
||||
scale: this.ds.scale
|
||||
let offset = this.ds.offset;
|
||||
let 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) {
|
||||
@@ -58,52 +81,63 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
super.drawNodeShape(node, ctx, size, fgColor, bgColor, selected, mouseOver);
|
||||
|
||||
let state = get(queueState);
|
||||
let ss = get(selectionState);
|
||||
const isRunningNode = node.id === state.runningNodeID
|
||||
|
||||
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";
|
||||
// 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) {
|
||||
const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.beginPath();
|
||||
if (shape == BuiltInSlotShape.BOX_SHAPE)
|
||||
ctx.rect(-6, -6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
|
||||
else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed))
|
||||
ctx.roundRect(
|
||||
-6,
|
||||
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
||||
12 + size[0] + 1,
|
||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
this.round_radius * 2
|
||||
);
|
||||
else if (shape == BuiltInSlotShape.CARD_SHAPE)
|
||||
ctx.roundRect(
|
||||
-6,
|
||||
-6 + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
12 + size[0] + 1,
|
||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
this.round_radius * 2,
|
||||
2
|
||||
);
|
||||
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.strokeStyle = color;
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = fgColor;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
ctx.lineWidth = outlineThickness;
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.beginPath();
|
||||
if (shape == BuiltInSlotShape.BOX_SHAPE)
|
||||
ctx.rect(-6, -6 + LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT);
|
||||
else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed))
|
||||
ctx.roundRect(
|
||||
-6,
|
||||
-6 - LiteGraph.NODE_TITLE_HEIGHT,
|
||||
12 + size[0] + 1,
|
||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
this.round_radius * 2
|
||||
);
|
||||
else if (shape == BuiltInSlotShape.CARD_SHAPE)
|
||||
ctx.roundRect(
|
||||
-6,
|
||||
-6 + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
12 + size[0] + 1,
|
||||
12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
this.round_radius * 2,
|
||||
2
|
||||
);
|
||||
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.strokeStyle = outlineColor;
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = fgColor;
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
private alignToGrid(node: LGraphNode, ctx: CanvasRenderingContext2D) {
|
||||
@@ -235,10 +269,43 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
}
|
||||
|
||||
override onSelectionChange(nodes: Record<number, LGraphNode>) {
|
||||
const ls = get(layoutState)
|
||||
ls.currentSelectionNodes = Object.values(nodes)
|
||||
ls.currentSelection = []
|
||||
layoutState.set(ls)
|
||||
selectionState.update(ss => {
|
||||
ss.currentSelectionNodes = Object.values(nodes)
|
||||
ss.currentSelection = []
|
||||
const ls = get(layoutState)
|
||||
for (const node of ss.currentSelectionNodes) {
|
||||
const widget = ls.allItemsByNode[node.id]
|
||||
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) {
|
||||
|
||||
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,45 +137,22 @@ export class ImageViewer {
|
||||
}
|
||||
}
|
||||
|
||||
setupGalleryImageForLightbox(e: HTMLImageElement) {
|
||||
if (e.dataset.modded === "true")
|
||||
showLightbox(source: HTMLImageElement) {
|
||||
const initiallyZoomed = true
|
||||
this.modalZoomSet(this.modalImage, initiallyZoomed)
|
||||
|
||||
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
||||
console.debug("[ImageViewer] showModal", event, source, galleryElem);
|
||||
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
|
||||
console.error("No buttons found on gallery element!", galleryElem)
|
||||
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
|
||||
this.modalZoomSet(this.modalImage, initiallyZoomed)
|
||||
evt.preventDefault()
|
||||
|
||||
const source = evt.target as HTMLImageElement;
|
||||
|
||||
const galleryElem = source.closest<HTMLDivElement>("div.block")
|
||||
console.debug("[ImageViewer] showModal", event, source, galleryElem);
|
||||
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
|
||||
console.error("No buttons found on gallery element!", galleryElem)
|
||||
return;
|
||||
}
|
||||
|
||||
let urls = ImageViewer.get_gallery_urls(galleryElem)
|
||||
const [_currentButton, index] = ImageViewer.selected_gallery_button(galleryElem)
|
||||
console.warn("Gallery!", index, urls, galleryElem)
|
||||
|
||||
this.showModal(urls, index, galleryElem)
|
||||
evt.stopPropagation();
|
||||
}, true);
|
||||
let urls = ImageViewer.get_gallery_urls(galleryElem)
|
||||
const [_currentButton, index] = ImageViewer.selected_gallery_button(galleryElem)
|
||||
console.warn("Gallery!", index, urls, galleryElem)
|
||||
|
||||
this.showModal(urls, index, galleryElem)
|
||||
}
|
||||
|
||||
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 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 { SerializedLayoutState } from "./stores/layoutState";
|
||||
import type { ComfyNodeDef } from "./ComfyNodeDef";
|
||||
|
||||
export type ComfyPromptRequest = {
|
||||
client_id?: string,
|
||||
@@ -30,7 +31,7 @@ export type ComfyAPIQueueResponse = {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type NodeID = UUID;
|
||||
export type ComfyNodeID = UUID; // To distinguish from Litegraph NodeID
|
||||
export type PromptID = UUID; // UUID
|
||||
|
||||
export type ComfyAPIHistoryItem = [
|
||||
@@ -38,7 +39,7 @@ export type ComfyAPIHistoryItem = [
|
||||
PromptID,
|
||||
SerializedPromptInputsAll,
|
||||
ComfyBoxPromptExtraData,
|
||||
NodeID[] // good outputs
|
||||
ComfyNodeID[] // good outputs
|
||||
]
|
||||
|
||||
export type ComfyAPIPromptResponse = {
|
||||
@@ -76,9 +77,10 @@ type ComfyAPIEvents = {
|
||||
progress: (progress: Progress) => void,
|
||||
reconnecting: () => void,
|
||||
reconnected: () => void,
|
||||
executing: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
|
||||
executed: (promptID: PromptID, nodeID: NodeID, output: SerializedPromptOutput) => void,
|
||||
execution_cached: (promptID: PromptID, nodes: NodeID[]) => void,
|
||||
executing: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void,
|
||||
executed: (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutputs) => void,
|
||||
execution_start: (promptID: PromptID) => void,
|
||||
execution_cached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
||||
execution_error: (promptID: PromptID, message: string) => void,
|
||||
}
|
||||
|
||||
@@ -182,6 +184,9 @@ export default class ComfyAPI {
|
||||
case "executed":
|
||||
this.eventBus.emit("executed", msg.data.prompt_id, msg.data.node, msg.data.output);
|
||||
break;
|
||||
case "execution_start":
|
||||
this.eventBus.emit("execution_start", msg.data.prompt_id);
|
||||
break;
|
||||
case "execution_cached":
|
||||
this.eventBus.emit("execution_cached", msg.data.prompt_id, msg.data.nodes);
|
||||
break;
|
||||
@@ -226,7 +231,7 @@ export default class ComfyAPI {
|
||||
* Loads node object definitions for the graph
|
||||
* @returns The node definitions
|
||||
*/
|
||||
async getNodeDefs(): Promise<any> {
|
||||
async getNodeDefs(): Promise<Record<ComfyNodeID, ComfyNodeDef>> {
|
||||
return fetch(this.getBackendUrl() + "/object_info", { cache: "no-store" })
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
@@ -248,7 +253,7 @@ export default class ComfyAPI {
|
||||
postBody = JSON.stringify(body)
|
||||
}
|
||||
catch (error) {
|
||||
return Promise.reject({ error })
|
||||
return Promise.reject({ error: error.toString() })
|
||||
}
|
||||
|
||||
return fetch(this.getBackendUrl() + "/prompt", {
|
||||
@@ -260,12 +265,12 @@ export default class ComfyAPI {
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.status != 200) {
|
||||
throw await res.text()
|
||||
throw await res.json()
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
.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 Accordion from "$lib/components/gradio/app/Accordion.svelte";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import WidgetContainer from "./WidgetContainer.svelte"
|
||||
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
@@ -60,10 +61,11 @@
|
||||
}
|
||||
</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}"
|
||||
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
||||
class:selected
|
||||
class:root-container={zIndex === 0}
|
||||
class:is-executing={container.isNodeExecuting}
|
||||
class:edit={edit}>
|
||||
@@ -120,6 +122,10 @@
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
&.selected {
|
||||
background: var(--comfy-container-selected-background-fill) !important;
|
||||
}
|
||||
|
||||
> :global(*) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import WidgetContainer from "./WidgetContainer.svelte"
|
||||
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
@@ -22,13 +23,21 @@
|
||||
export let dragDisabled: boolean = false;
|
||||
export let isMobile: boolean = false;
|
||||
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
let attrsChanged: Writable<number> | null = null;
|
||||
let children: IDragItem[] | null = null;
|
||||
const flipDurationMs = 100;
|
||||
|
||||
$: if (container) {
|
||||
children = $layoutState.allItems[container.id].children;
|
||||
attrsChanged = container.attrsChanged
|
||||
const entry = $layoutState.allItems[container.id]
|
||||
if (entry) {
|
||||
children = $layoutState.allItems[container.id].children;
|
||||
attrsChanged = container.attrsChanged
|
||||
}
|
||||
else {
|
||||
container = null;
|
||||
children = null;
|
||||
attrsChanged = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
children = null;
|
||||
@@ -44,18 +53,17 @@
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
};
|
||||
|
||||
const tt = "asd\nasdlkj"
|
||||
</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}"
|
||||
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
||||
class:selected
|
||||
class:root-container={zIndex === 0}
|
||||
class:is-executing={container.isNodeExecuting}
|
||||
class:mobile={isMobile}
|
||||
class:edit={edit}>
|
||||
class:edit>
|
||||
<Block>
|
||||
{#if container.attrs.title && container.attrs.title !== ""}
|
||||
<label for={String(container.id)} class={($uiState.uiUnlocked && $uiState.uiEditMode === "widgets") ? "edit-title-label" : ""}>
|
||||
@@ -64,7 +72,7 @@
|
||||
{/if}
|
||||
<div class="v-pane"
|
||||
class:empty={children.length === 0}
|
||||
class:edit={edit}
|
||||
class:edit
|
||||
use:dndzone="{{
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
@@ -77,8 +85,9 @@
|
||||
on:finalize="{handleFinalize}"
|
||||
>
|
||||
{#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"
|
||||
class:edit
|
||||
class:hidden={hidden}
|
||||
animate:flip={{duration:flipDurationMs}}
|
||||
style={item?.attrs?.style || ""}
|
||||
@@ -111,17 +120,18 @@
|
||||
|
||||
.edit {
|
||||
min-width: 200px;
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
|
||||
&:not(.edit) > .animation-wrapper.hidden {
|
||||
.animation-wrapper.hidden:not(.edit) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
border-width: 3px;
|
||||
border-color: var(--color-grey-400);
|
||||
border-radius: var(--block-radius);
|
||||
background: var(--color-grey-300);
|
||||
border-color: var(--comfy-container-empty-border-color);
|
||||
border-radius: 0;
|
||||
background: var(--comfy-container-empty-background-fill);
|
||||
min-height: 100px;
|
||||
border-style: dashed;
|
||||
}
|
||||
@@ -154,6 +164,10 @@
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
&.selected {
|
||||
background: var(--comfy-container-selected-background-fill) !important;
|
||||
}
|
||||
|
||||
> :global(*) {
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -234,7 +248,7 @@
|
||||
}
|
||||
|
||||
.handle-hidden {
|
||||
background-color: #40404080;
|
||||
background-color: #303030A0;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { Checkbox, TextBox } from "@gradio/form"
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import layoutState from "$lib/stores/layoutState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import type { ComfyAPIStatus } from "$lib/api";
|
||||
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
|
||||
@@ -54,12 +55,12 @@
|
||||
|
||||
if (!$uiState.uiUnlocked) {
|
||||
app.lCanvas.deselectAllNodes();
|
||||
$layoutState.currentSelectionNodes = []
|
||||
$selectionState.currentSelectionNodes = []
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($uiState.uiEditMode)
|
||||
$layoutState.currentSelection = []
|
||||
$selectionState.currentSelection = []
|
||||
|
||||
let graphSize = 0;
|
||||
let graphTransitioning = false;
|
||||
@@ -171,8 +172,6 @@
|
||||
|
||||
onMount(async () => {
|
||||
await app.setup();
|
||||
(window as any).app = app;
|
||||
(window as any).appPane = uiPane;
|
||||
|
||||
// await import('../../scss/ux.scss');
|
||||
|
||||
@@ -184,10 +183,10 @@
|
||||
}
|
||||
|
||||
$: if (uiTheme === "gradio-dark") {
|
||||
document.getElementById("app").classList.add("dark")
|
||||
document.getElementById("app-root").classList.add("dark")
|
||||
}
|
||||
else {
|
||||
document.getElementById("app").classList.remove("dark")
|
||||
document.getElementById("app-root").classList.remove("dark")
|
||||
}
|
||||
</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 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 EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
@@ -12,11 +12,12 @@ import "@litegraph-ts/nodes-logic"
|
||||
import "@litegraph-ts/nodes-math"
|
||||
import "@litegraph-ts/nodes-strings"
|
||||
import "$lib/nodes/index"
|
||||
import "$lib/nodes/widgets/index"
|
||||
import * as nodes from "$lib/nodes/index"
|
||||
import * as widgets from "$lib/nodes/widgets/index"
|
||||
|
||||
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
|
||||
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import * as widgets from "$lib/widgets/index"
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import { type SvelteComponentDev } from "svelte/internal";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
@@ -28,18 +29,17 @@ import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
import { get } from "svelte/store";
|
||||
import { tick } from "svelte";
|
||||
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 configState from "$lib/stores/configState";
|
||||
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;
|
||||
|
||||
LiteGraph.catch_exceptions = false;
|
||||
LiteGraph.CANVAS_GRID_SIZE = 32;
|
||||
LiteGraph.default_subgraph_lgraph_factory = () => new ComfyGraph();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
// Load default visibility
|
||||
nodes.ComfyReroute.setDefaultTextVisibility(!!localStorage["Comfy.ComfyReroute.DefaultVisibility"]);
|
||||
@@ -56,7 +56,7 @@ export type SerializedAppState = {
|
||||
}
|
||||
|
||||
/** [link origin, link index] | value */
|
||||
export type SerializedPromptInput = [NodeID, number] | any
|
||||
export type SerializedPromptInput = [ComfyNodeID, number] | any
|
||||
|
||||
export type SerializedPromptInputs = {
|
||||
/* property name -> value or link */
|
||||
@@ -64,14 +64,14 @@ export type SerializedPromptInputs = {
|
||||
class_type: string
|
||||
}
|
||||
|
||||
export type SerializedPromptInputsAll = Record<NodeID, SerializedPromptInputs>
|
||||
export type SerializedPromptInputsAll = Record<ComfyNodeID, SerializedPromptInputs>
|
||||
|
||||
export type SerializedPrompt = {
|
||||
workflow: SerializedLGraph,
|
||||
output: SerializedPromptInputsAll
|
||||
}
|
||||
|
||||
export type SerializedPromptOutputs = Record<NodeID, ComfyExecutionResult>
|
||||
export type SerializedPromptOutputs = Record<ComfyNodeID, ComfyExecutionResult>
|
||||
|
||||
export type Progress = {
|
||||
value: number,
|
||||
@@ -79,32 +79,11 @@ export type Progress = {
|
||||
}
|
||||
|
||||
type BackendComboNode = {
|
||||
comboNode: nodes.ComfyComboNode
|
||||
inputSlot: IComfyInputSlot,
|
||||
comboNode: ComfyComboNode,
|
||||
comfyInput: IComfyInputSlot,
|
||||
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 {
|
||||
api: ComfyAPI;
|
||||
rootEl: HTMLDivElement | null = null;
|
||||
@@ -115,16 +94,17 @@ export default class ComfyApp {
|
||||
dropZone: HTMLElement | null = null;
|
||||
nodeOutputs: Record<string, any> = {};
|
||||
|
||||
dragOverNode: LGraphNode | null = null;
|
||||
shiftDown: boolean = false;
|
||||
selectedGroupMoving: boolean = false;
|
||||
|
||||
private queueItems: QueueItem[] = [];
|
||||
private processingQueue: boolean = false;
|
||||
private alreadySetup = false;
|
||||
private promptSerializer: ComfyPromptSerializer;
|
||||
|
||||
constructor() {
|
||||
this.api = new ComfyAPI();
|
||||
this.promptSerializer = new ComfyPromptSerializer();
|
||||
}
|
||||
|
||||
async setup(): Promise<void> {
|
||||
@@ -135,21 +115,16 @@ export default class ComfyApp {
|
||||
|
||||
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.lGraph = new ComfyGraph();
|
||||
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
|
||||
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;
|
||||
this.lCanvas.allow_dragnodes = uiUnlocked;
|
||||
this.lCanvas.allow_interaction = uiUnlocked;
|
||||
|
||||
(window as any).LiteGraph = LiteGraph;
|
||||
|
||||
// await this.#invokeExtensionsAsync("init");
|
||||
await this.registerNodes();
|
||||
|
||||
@@ -223,15 +198,11 @@ export default class ComfyApp {
|
||||
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
||||
|
||||
private async registerNodes() {
|
||||
const app = this;
|
||||
|
||||
// Load node definitions from the backend
|
||||
const defs = await this.api.getNodeDefs();
|
||||
|
||||
// Register a node for each definition
|
||||
for (const nodeId in defs) {
|
||||
const nodeData = defs[nodeId];
|
||||
|
||||
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
||||
const typeOverride = ComfyApp.node_type_overrides[nodeId]
|
||||
if (typeOverride)
|
||||
console.debug("Attaching custom type to received node:", nodeId, typeOverride)
|
||||
@@ -239,19 +210,79 @@ export default class ComfyApp {
|
||||
|
||||
const ctor = class extends baseClass {
|
||||
constructor(title?: string) {
|
||||
super(title, nodeId, nodeData);
|
||||
super(title, nodeId, nodeDef);
|
||||
}
|
||||
}
|
||||
|
||||
const node: LGraphNodeConstructor = {
|
||||
class: ctor,
|
||||
title: nodeData.display_name || nodeData.name,
|
||||
title: nodeDef.display_name || nodeDef.name,
|
||||
type: nodeId,
|
||||
desc: `ComfyNode: ${nodeId}`
|
||||
}
|
||||
|
||||
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.api.addEventListener("executing", (promptID: PromptID | null, nodeID: NodeID | null) => {
|
||||
this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => {
|
||||
queueState.executingUpdated(promptID, nodeID);
|
||||
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;
|
||||
const node = this.lGraph.getNodeById(nodeID) as ComfyGraphNode;
|
||||
const node = this.lGraph.getNodeByIdRecursive(nodeID) as ComfyGraphNode;
|
||||
if (node?.onExecuted) {
|
||||
node.onExecuted(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)
|
||||
});
|
||||
|
||||
@@ -403,7 +438,7 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
setColor(type, "orange")
|
||||
}
|
||||
@@ -508,6 +543,7 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
layoutState.onStartConfigure();
|
||||
this.lCanvas.closeAllSubgraphs();
|
||||
this.lGraph.configure(blankGraph)
|
||||
layoutState.initDefaultLayout();
|
||||
uiState.update(s => {
|
||||
@@ -518,7 +554,7 @@ export default class ComfyApp {
|
||||
}
|
||||
|
||||
runDefaultQueueAction() {
|
||||
for (const node of this.lGraph.iterateNodesInOrder()) {
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
if ("onDefaultQueueAction" in node) {
|
||||
(node as ComfyGraphNode).onDefaultQueueAction()
|
||||
}
|
||||
@@ -559,157 +595,7 @@ export default class ComfyApp {
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
graphToPrompt(tag: string | null = null): SerializedPrompt {
|
||||
// Run frontend-only logic
|
||||
this.lGraph.runStep(1)
|
||||
|
||||
const workflow = this.lGraph.serialize();
|
||||
|
||||
const output = {};
|
||||
// Process nodes in order of execution
|
||||
for (const node_ of this.lGraph.computeExecutionOrder<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 };
|
||||
return this.promptSerializer.serialize(this.lGraph, tag)
|
||||
}
|
||||
|
||||
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 l = layoutState.serialize();
|
||||
console.debug(graphToGraphVis(this.lGraph))
|
||||
console.debug(promptToGraphVis(p))
|
||||
|
||||
const extraData: ComfyBoxPromptExtraData = {
|
||||
@@ -767,8 +654,8 @@ export default class ComfyApp {
|
||||
thumbnails
|
||||
}
|
||||
|
||||
let error = null;
|
||||
let promptID = null;
|
||||
let error: string | null = null;
|
||||
let promptID: PromptID | null = null;
|
||||
|
||||
const request: ComfyPromptRequest = {
|
||||
number: num,
|
||||
@@ -778,28 +665,26 @@ export default class ComfyApp {
|
||||
|
||||
try {
|
||||
const response = await this.api.queuePrompt(request);
|
||||
|
||||
// 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;
|
||||
if (response.error != null) {
|
||||
error = response.error;
|
||||
}
|
||||
else {
|
||||
queueState.afterQueued(response.promptID, num, p.output, extraData)
|
||||
}
|
||||
} catch (err) {
|
||||
error = err
|
||||
error = err?.toString();
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
const mes = error.response || error.toString()
|
||||
const mes: string = error;
|
||||
notify(`Error queuing prompt:\n${mes}`, { type: "error" })
|
||||
console.error(graphToGraphVis(this.lGraph))
|
||||
console.error(promptToGraphVis(p))
|
||||
console.error("Error queuing prompt", error, num, p)
|
||||
break;
|
||||
}
|
||||
|
||||
for (const n of p.workflow.nodes) {
|
||||
const node = this.lGraph.getNodeById(n.id);
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
if ("afterQueued" in node) {
|
||||
(node as ComfyGraphNode).afterQueued(p, tag);
|
||||
}
|
||||
@@ -857,98 +742,109 @@ export default class ComfyApp {
|
||||
async refreshComboInNodes(flashUI: boolean = false) {
|
||||
const defs = await this.api.getNodeDefs();
|
||||
|
||||
const toUpdate: BackendComboNode[] = []
|
||||
|
||||
const isComfyComboNode = (node: LGraphNode): boolean => {
|
||||
const isComfyComboNode = (node: LGraphNode): node is ComfyComboNode => {
|
||||
return node
|
||||
&& node.type === "ui/combo"
|
||||
&& "doAutoConfig" in node;
|
||||
}
|
||||
|
||||
const isComfyComboInput = (input: INodeInputSlot) => {
|
||||
const isComfyComboInput = (input: INodeInputSlot): input is IComfyInputSlot => {
|
||||
return "config" in input
|
||||
&& "widgetNodeType" in input
|
||||
&& input.widgetNodeType === "ui/combo";
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
// 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
|
||||
// slot connected to.
|
||||
for (const node of this.lGraph.iterateNodesInOrder()) {
|
||||
if (!(node as any).isBackendNode)
|
||||
const nodeLocator = new UpstreamNodeLocator(isComfyComboNode)
|
||||
|
||||
const findComfyInputAndAttachedCombo = (node: LGraphNode, i: SlotIndex): [IComfyInputSlot, ComfyComboNode] | null => {
|
||||
const input = node.inputs[i]
|
||||
|
||||
// Does this input autocreate a combo box on creation?
|
||||
const isComfyInput = isComfyComboInput(input)
|
||||
if (!isComfyInput)
|
||||
return null;
|
||||
|
||||
// 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);
|
||||
|
||||
if (comboNode == null)
|
||||
return null;
|
||||
|
||||
const result: [IComfyInputSlot, ComfyComboNode] = [input, comboNode as ComfyComboNode]
|
||||
return result
|
||||
}
|
||||
|
||||
for (const node of this.lGraph.iterateNodesInOrderRecursive()) {
|
||||
if (!isActiveBackendNode(node))
|
||||
continue;
|
||||
|
||||
const backendNode = (node as ComfyBackendNode)
|
||||
const found = range(backendNode.inputs.length)
|
||||
.filter(i => {
|
||||
const input = backendNode.inputs[i]
|
||||
const inputNode = backendNode.getInputNode(i)
|
||||
const found = range(node.inputs.length)
|
||||
.map((i) => findComfyInputAndAttachedCombo(node, i))
|
||||
.filter(Boolean);
|
||||
|
||||
// Does this input autocreate a combo box on creation?
|
||||
const isComfyInput = isComfyComboInput(input)
|
||||
const isComfyCombo = isComfyComboNode(inputNode)
|
||||
for (const [comfyInput, comboNode] of found) {
|
||||
const def = defs[node.type];
|
||||
|
||||
// console.debug("[refreshComboInNodes] CHECK", backendNode.type, input.name, "isComfyCombo", isComfyCombo, "isComfyInput", isComfyInput)
|
||||
|
||||
return isComfyCombo && isComfyInput
|
||||
});
|
||||
|
||||
for (const inputIndex of found) {
|
||||
const comboNode = backendNode.getInputNode(inputIndex) as nodes.ComfyComboNode
|
||||
const inputSlot = backendNode.inputs[inputIndex] as IComfyInputSlot;
|
||||
const def = defs[backendNode.type];
|
||||
|
||||
const hasBackendConfig = def["input"]["required"][inputSlot.name] !== undefined
|
||||
const hasBackendConfig = def["input"]["required"][comfyInput.name] !== undefined
|
||||
|
||||
if (hasBackendConfig) {
|
||||
backendCombos.add(comboNode.id)
|
||||
toUpdate.push({ comboNode, inputSlot, backendNode })
|
||||
backendUpdatedCombos[comboNode.id] = { comboNode, comfyInput, backendNode: node }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("[refreshComboInNodes] found:", toUpdate.length, toUpdate)
|
||||
console.debug("[refreshComboInNodes] found:", backendUpdatedCombos.length, backendUpdatedCombos)
|
||||
|
||||
// Mark combo nodes without backend configs as being loaded already.
|
||||
for (const node of this.lGraph.iterateNodesInOrder()) {
|
||||
if (isComfyComboNode(node) && !backendCombos.has(node.id)) {
|
||||
const comboNode = node as nodes.ComfyComboNode;
|
||||
let values = comboNode.properties.values;
|
||||
|
||||
// Frontend nodes can declare defaultWidgets which creates a
|
||||
// config inside their own inputs slots too.
|
||||
const foundInput = range(node.outputs.length)
|
||||
.flatMap(i => node.getInputSlotsConnectedTo(i))
|
||||
.find(inp => "config" in inp && Array.isArray((inp.config as any).values))
|
||||
|
||||
if (foundInput != null) {
|
||||
const comfyInput = foundInput as IComfyInputSlot;
|
||||
console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values)
|
||||
values = comfyInput.config.values;
|
||||
}
|
||||
|
||||
comboNode.formatValues(values);
|
||||
for (const node of this.lGraph.iterateNodesOfClassRecursive(ComfyComboNode)) {
|
||||
if (backendUpdatedCombos[node.id] != null) {
|
||||
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;
|
||||
|
||||
// Frontend nodes can declare defaultWidgets which creates a
|
||||
// config inside their own inputs slots too.
|
||||
const foundInput = range(node.outputs.length)
|
||||
.flatMap(i => node.getInputSlotsConnectedTo(i))
|
||||
.find(inp => "config" in inp && Array.isArray((inp.config as any).values))
|
||||
|
||||
let defaultValue = null;
|
||||
if (foundInput != null) {
|
||||
const comfyInput = foundInput as IComfyInputSlot;
|
||||
console.warn("[refreshComboInNodes] found frontend config:", node.title, node.type, comfyInput.config.values)
|
||||
values = comfyInput.config.values;
|
||||
defaultValue = comfyInput.config.defaultValue;
|
||||
}
|
||||
|
||||
comboNode.formatValues(values, defaultValue);
|
||||
}
|
||||
|
||||
await tick();
|
||||
|
||||
// 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 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)
|
||||
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))) {
|
||||
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] {
|
||||
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 layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
|
||||
import uiState from "$lib/stores/uiState"
|
||||
import selectionState from "$lib/stores/selectionState"
|
||||
import { get, type Writable, writable } from "svelte/store"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||
|
||||
let target: IDragItem | null = null;
|
||||
let node: LGraphNode | null = null;
|
||||
@@ -17,20 +18,21 @@
|
||||
|
||||
$: refreshPropsPanel = $layoutState.refreshPropsPanel;
|
||||
|
||||
$: if ($layoutState.currentSelection.length > 0) {
|
||||
const targetId = $layoutState.currentSelection.slice(-1)[0]
|
||||
target = $layoutState.allItems[targetId].dragItem
|
||||
attrsChanged = target.attrsChanged;
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
}
|
||||
else {
|
||||
node = null;
|
||||
$: if ($selectionState.currentSelection.length > 0) {
|
||||
node = null;
|
||||
const targetId = $selectionState.currentSelection.slice(-1)[0]
|
||||
const entry = $layoutState.allItems[targetId]
|
||||
if (entry != null) {
|
||||
target = entry.dragItem
|
||||
attrsChanged = target.attrsChanged;
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($layoutState.currentSelectionNodes.length > 0) {
|
||||
else if ($selectionState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $layoutState.currentSelectionNodes[0]
|
||||
node = $selectionState.currentSelectionNodes[0]
|
||||
attrsChanged = null;
|
||||
}
|
||||
else {
|
||||
@@ -130,7 +132,7 @@
|
||||
value = spec.defaultValue
|
||||
else if (spec.serialize)
|
||||
value = spec.serialize(value)
|
||||
console.debug("[ComfyProperties] getAttribute", spec.name, value, target, spec)
|
||||
// console.debug("[ComfyProperties] getAttribute", spec.name, value, target, spec)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -140,13 +142,17 @@
|
||||
|
||||
const name = spec.name
|
||||
|
||||
console.debug("[ComfyProperties] updateAttribute", spec, value, name, node)
|
||||
// console.debug("[ComfyProperties] updateAttribute", spec, value, name, node)
|
||||
if (spec.deserialize)
|
||||
value = spec.deserialize(value)
|
||||
|
||||
const prevValue = target.attrs[name]
|
||||
target.attrs[name] = value
|
||||
target.attrsChanged.set(get(target.attrsChanged) + 1)
|
||||
|
||||
if (spec.onChanged)
|
||||
spec.onChanged(target, value, prevValue)
|
||||
|
||||
if (node && "propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
@@ -164,7 +170,7 @@
|
||||
value = spec.defaultValue
|
||||
else if (spec.serialize)
|
||||
value = spec.serialize(value)
|
||||
console.debug("[ComfyProperties] getProperty", spec, value, node)
|
||||
// console.debug("[ComfyProperties] getProperty", spec, value, node)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -173,13 +179,17 @@
|
||||
return
|
||||
|
||||
const name = spec.name
|
||||
console.warn("[ComfyProperties] updateProperty", name, value)
|
||||
// console.warn("[ComfyProperties] updateProperty", name, value)
|
||||
|
||||
if (spec.deserialize)
|
||||
value = spec.deserialize(value)
|
||||
|
||||
const prevValue = node.properties[name]
|
||||
node.properties[name] = value;
|
||||
|
||||
if (spec.onChanged)
|
||||
spec.onChanged(node, value, prevValue)
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.notifyPropsChanged();
|
||||
@@ -195,7 +205,7 @@
|
||||
value = spec.defaultValue
|
||||
else if (spec.serialize)
|
||||
value = spec.serialize(value)
|
||||
console.debug("[ComfyProperties] getVar", spec, value, node)
|
||||
// console.debug("[ComfyProperties] getVar", spec, value, node)
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -205,12 +215,16 @@
|
||||
|
||||
const name = spec.name
|
||||
|
||||
console.debug("[ComfyProperties] updateVar", spec, value, name, node)
|
||||
// console.debug("[ComfyProperties] updateVar", spec, value, name, node)
|
||||
if (spec.deserialize)
|
||||
value = spec.deserialize(value)
|
||||
|
||||
const prevValue = node[name]
|
||||
node[name] = value;
|
||||
|
||||
if (spec.onChanged)
|
||||
spec.onChanged(node, value, prevValue)
|
||||
|
||||
if ("propsChanged" in node) {
|
||||
const comfyNode = node as ComfyWidgetNode
|
||||
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
|
||||
@@ -227,7 +241,7 @@
|
||||
value = spec.defaultValue
|
||||
else if (spec.serialize)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -236,11 +250,15 @@
|
||||
return;
|
||||
|
||||
const name = spec.name
|
||||
console.warn("updateWorkflowAttribute", name, value)
|
||||
// console.warn("[ComfyProperties] updateWorkflowAttribute", name, value)
|
||||
|
||||
const prevValue = value
|
||||
$layoutState.attrs[name] = value
|
||||
$layoutState = $layoutState
|
||||
|
||||
if (spec.onChanged)
|
||||
spec.onChanged($layoutState, value, prevValue)
|
||||
|
||||
if (spec.refreshPanelOnChange)
|
||||
doRefreshPanel()
|
||||
}
|
||||
|
||||
@@ -65,9 +65,11 @@
|
||||
dateStr = formatDate(date);
|
||||
}
|
||||
|
||||
const subgraphs: string[] | null = entry.extraData?.extra_pnginfo?.comfyBoxSubgraphs;
|
||||
|
||||
let message = "Prompt";
|
||||
if (entry.extraData.subgraphs)
|
||||
message = `Prompt: ${entry.extraData.subgraphs.join(', ')}`
|
||||
if (subgraphs?.length > 0)
|
||||
message = `Prompt: ${subgraphs.join(', ')}`
|
||||
|
||||
let submessage = `Nodes: ${Object.keys(entry.prompt).length}`
|
||||
if (Object.keys(entry.outputs).length > 0) {
|
||||
@@ -156,12 +158,14 @@
|
||||
}
|
||||
|
||||
let showModal = false;
|
||||
let expandAll = false;
|
||||
let selectedPrompt = null;
|
||||
let selectedImages = [];
|
||||
function showPrompt(entry: QueueUIEntry, e: MouseEvent) {
|
||||
selectedPrompt = entry.entry.prompt;
|
||||
selectedImages = entry.images;
|
||||
showModal = true;
|
||||
expandAll = false
|
||||
}
|
||||
|
||||
$: if(!showModal)
|
||||
@@ -180,8 +184,16 @@
|
||||
<h1 style="padding-bottom: 1rem;">Prompt Details</h1>
|
||||
</div>
|
||||
{#if selectedPrompt}
|
||||
<PromptDisplay prompt={selectedPrompt} images={selectedImages} />
|
||||
<PromptDisplay prompt={selectedPrompt} images={selectedImages} {expandAll} />
|
||||
{/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>
|
||||
|
||||
<div class="queue">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import WidgetContainer from "./WidgetContainer.svelte";
|
||||
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
|
||||
import Menu from './menu/Menu.svelte';
|
||||
import MenuOption from './menu/MenuOption.svelte';
|
||||
@@ -29,19 +30,72 @@
|
||||
// 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) {
|
||||
const items = layoutState.getCurrentSelection()
|
||||
$layoutState.currentSelection = []
|
||||
const items = $selectionState.currentSelection
|
||||
$selectionState.currentSelection = []
|
||||
layoutState.groupItems(items, { direction: horizontal ? "horizontal" : "vertical" })
|
||||
}
|
||||
|
||||
let canUngroup = 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) {
|
||||
const dragItem = layoutState.getCurrentSelection()[0];
|
||||
const entry = $layoutState.allItems[dragItem.id];
|
||||
const dragItemID = $selectionState.currentSelection[0];
|
||||
const entry = $layoutState.allItems[dragItemID];
|
||||
isDeleteGroup = entry.children.length === 0
|
||||
}
|
||||
else {
|
||||
@@ -49,11 +103,19 @@
|
||||
}
|
||||
|
||||
function ungroup() {
|
||||
const item = layoutState.getCurrentSelection()[0]
|
||||
if (!item || item.type !== "container")
|
||||
const itemID = $selectionState.currentSelection[0]
|
||||
if (itemID == null)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -94,11 +156,28 @@
|
||||
{#if showMenu}
|
||||
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||
<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)}
|
||||
text="Group" />
|
||||
<MenuOption
|
||||
isDisabled={$layoutState.currentSelection.length === 0}
|
||||
isDisabled={$selectionState.currentSelection.length === 0}
|
||||
on:click={() => groupWidgets(true)}
|
||||
text="Group Horizontally" />
|
||||
<MenuOption
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import WidgetContainer from "./WidgetContainer.svelte"
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import BlockContainer from "./BlockContainer.svelte"
|
||||
import AccordionContainer from "./AccordionContainer.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)
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { flip } from 'svelte/animate';
|
||||
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
import { type ContainerLayout } from "$lib/stores/layoutState";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
|
||||
export let container: ContainerLayout | null = null;
|
||||
export let zIndex: number = 0;
|
||||
export let classes: string[] = [];
|
||||
export let showHandles: boolean = false;
|
||||
export let isMobile: boolean = false
|
||||
let attrsChanged: Writable<boolean> | null = null;
|
||||
let attrsChanged: Writable<number> | null = null;
|
||||
|
||||
$: if (container) {
|
||||
attrsChanged = container.attrsChanged
|
||||
@@ -33,8 +26,8 @@
|
||||
</script>
|
||||
|
||||
{#if container}
|
||||
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
|
||||
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked}
|
||||
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
|
||||
{@const dragDisabled = zIndex === 0 || $selectionState.currentSelection.length > 2 || !$uiState.uiUnlocked}
|
||||
{#key $attrsChanged}
|
||||
{#if edit || !isHidden(container)}
|
||||
{#if container.attrs.variant === "tabs"}
|
||||
|
||||
@@ -42,11 +42,6 @@
|
||||
imgElem.src = convertComfyOutputToComfyURL(value[0])
|
||||
}
|
||||
|
||||
$: if (!(_value && _value.length > 0 && imgElem)) {
|
||||
imgWidth = 1
|
||||
imgHeight = 1
|
||||
}
|
||||
|
||||
function onChange() {
|
||||
dispatch("change", value)
|
||||
}
|
||||
@@ -119,6 +114,8 @@
|
||||
uploaded = false;
|
||||
pending_upload = true;
|
||||
|
||||
imgWidth = 0;
|
||||
imgHeight = 0;
|
||||
old_value = _value;
|
||||
|
||||
if (_value == null)
|
||||
@@ -177,9 +174,13 @@
|
||||
uploaded = true;
|
||||
}
|
||||
|
||||
$: console.warn(imgWidth, imgHeight, "IMGSIZE!!")
|
||||
|
||||
function handle_clear(_e: CustomEvent<null>) {
|
||||
_value = null;
|
||||
value = [];
|
||||
imgWidth = 0;
|
||||
imgHeight = 0;
|
||||
dispatch("change", value)
|
||||
dispatch("clear")
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
}
|
||||
|
||||
function doClose() {
|
||||
showModal = false;
|
||||
dialog.close();
|
||||
dispatch("close")
|
||||
}
|
||||
@@ -41,7 +42,7 @@
|
||||
<slot name="header" />
|
||||
<slot />
|
||||
<div class="button-row">
|
||||
<slot name="buttons">
|
||||
<slot name="buttons" {closeDialog}>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<Button variant="secondary" on:click={doClose}>Close</Button>
|
||||
</slot>
|
||||
|
||||
@@ -6,16 +6,20 @@
|
||||
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
|
||||
import Accordion from "$lib/components/gradio/app/Accordion.svelte";
|
||||
import Gallery from "$lib/components/gradio/gallery/Gallery.svelte";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
|
||||
const splitLength = 50;
|
||||
|
||||
export let prompt: SerializedPromptInputsAll;
|
||||
export let images: string[] = [];
|
||||
export let isMobile: boolean = false;
|
||||
export let expandAll: boolean = false;
|
||||
|
||||
let galleryStyle: Styles = {
|
||||
grid_cols: [2],
|
||||
object_fit: "cover",
|
||||
height: "var(--size-96)"
|
||||
}
|
||||
|
||||
function isInputLink(input: SerializedPromptInput): boolean {
|
||||
@@ -59,6 +63,11 @@
|
||||
copyFeedback(nodeID, inputName);
|
||||
}
|
||||
}
|
||||
|
||||
function onGalleryImageClicked(e: CustomEvent<HTMLImageElement>) {
|
||||
// TODO dialog renders over it
|
||||
// ImageViewer.instance.showLightbox(e.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="prompt-display">
|
||||
@@ -70,7 +79,7 @@
|
||||
{#if filtered.length > 0}
|
||||
<div class="accordion">
|
||||
<Block padding={true}>
|
||||
<Accordion label="Node {i+1}: {classType}" open={false}>
|
||||
<Accordion label="Node {i+1}: {classType}" open={expandAll}>
|
||||
{#each filtered as [inputName, input]}
|
||||
<Block>
|
||||
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
|
||||
@@ -121,6 +130,7 @@
|
||||
style={galleryStyle}
|
||||
root={""}
|
||||
root_url={""}
|
||||
on:clicked={onGalleryImageClicked}
|
||||
/>
|
||||
</Block>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { Tabs, TabItem } from "@gradio/tabs";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import WidgetContainer from "./WidgetContainer.svelte"
|
||||
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
@@ -67,10 +68,11 @@
|
||||
}
|
||||
</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}"
|
||||
class:hide-block={container.attrs.containerVariant === "hidden"}
|
||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
|
||||
class:selected
|
||||
class:root-container={zIndex === 0}
|
||||
class:is-executing={container.isNodeExecuting}
|
||||
class:edit={edit}>
|
||||
@@ -133,6 +135,10 @@
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
&.selected {
|
||||
background: var(--comfy-container-selected-background-fill) !important;
|
||||
}
|
||||
|
||||
> :global(*) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import uiState from "$lib/stores/uiState";
|
||||
|
||||
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 Container from "./Container.svelte"
|
||||
import { type Writable } from "svelte/store"
|
||||
import type { ComfyWidgetNode } from "$lib/nodes";
|
||||
import { NodeMode } from "@litegraph-ts/core";
|
||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
|
||||
export let dragItem: IDragItem | null = null;
|
||||
@@ -65,23 +65,26 @@
|
||||
<Container {container} {classes} {zIndex} {showHandles} {isMobile} />
|
||||
{/key}
|
||||
{: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 hovered = $uiState.uiUnlocked && $selectionState.currentHovered.has(widget.id)}
|
||||
{@const selected = $uiState.uiUnlocked && $selectionState.currentSelection.includes(widget.id)}
|
||||
{#key $attrsChanged}
|
||||
{#key $propsChanged}
|
||||
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
|
||||
class:edit={edit}
|
||||
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
|
||||
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeId == widget.node.id}
|
||||
class:hidden={hidden}
|
||||
>
|
||||
class:hovered
|
||||
class:selected
|
||||
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
|
||||
class:hidden={hidden}
|
||||
>
|
||||
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
|
||||
</div>
|
||||
{#if hidden && edit}
|
||||
<div class="handle handle-hidden" class:hidden={!edit} />
|
||||
{/if}
|
||||
{#if showHandles}
|
||||
<div class="handle handle-widget" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
|
||||
{#if showHandles || hovered}
|
||||
<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}
|
||||
{/key}
|
||||
{/key}
|
||||
@@ -92,12 +95,9 @@
|
||||
height: 100%;
|
||||
|
||||
&.selected {
|
||||
background: var(--color-yellow-200);
|
||||
background: var(--comfy-widget-selected-background-fill);
|
||||
}
|
||||
}
|
||||
.container.selected {
|
||||
background: var(--color-yellow-400);
|
||||
}
|
||||
|
||||
.is-executing {
|
||||
border: 3px dashed var(--color-green-600) !important;
|
||||
@@ -123,8 +123,10 @@
|
||||
background-color: #40404080;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
background-color: #add8e680;
|
||||
.handle-widget {
|
||||
&:hover, &.hovered {
|
||||
background-color: #add8e680;
|
||||
}
|
||||
}
|
||||
|
||||
.node-type {
|
||||
@@ -134,7 +136,5 @@
|
||||
|
||||
.edit {
|
||||
border: 2px dashed var(--color-blue-400);
|
||||
margin: 0.2em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { Styles } from "@gradio/utils";
|
||||
|
||||
export let style: Styles = {};
|
||||
export let elem_id: string;
|
||||
export let elem_id: string | null;
|
||||
export let elem_classes: Array<string> = [];
|
||||
export let visible: boolean = true;
|
||||
export let variant: "default" | "panel" | "compact" = "default";
|
||||
|
||||
@@ -21,19 +21,20 @@
|
||||
object_fit: "cover",
|
||||
height: "auto"
|
||||
};
|
||||
export let imageWidth: number = 1;
|
||||
export let imageHeight: number = 1;
|
||||
export let imageWidth: number = 0;
|
||||
export let imageHeight: number = 0;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
select: SelectData;
|
||||
clicked: HTMLImageElement
|
||||
}>();
|
||||
|
||||
// tracks whether the value of the gallery was reset
|
||||
let was_reset: boolean = true;
|
||||
|
||||
$: if (selected_image == null || was_reset) {
|
||||
imageWidth = 1;
|
||||
imageHeight = 1;
|
||||
imageWidth = 0;
|
||||
imageHeight = 0;
|
||||
}
|
||||
|
||||
$: was_reset = value == null || value.length == 0 ? true : was_reset;
|
||||
@@ -142,6 +143,14 @@
|
||||
|
||||
let height = 0;
|
||||
let window_height = 0;
|
||||
|
||||
let imgElem = null;
|
||||
|
||||
function onClick() {
|
||||
// selected_image = next
|
||||
if (imgElem)
|
||||
dispatch("clicked", imgElem)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={window_height} />
|
||||
@@ -166,7 +175,8 @@
|
||||
<ModifyUpload on:clear={() => (selected_image = null)} />
|
||||
|
||||
<img
|
||||
on:click={() => (selected_image = next)}
|
||||
on:click={onClick}
|
||||
bind:this={imgElem}
|
||||
src={_value[selected_image][0].data}
|
||||
alt={_value[selected_image][1] || ""}
|
||||
title={_value[selected_image][1] || null}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
export let label: string | undefined = undefined;
|
||||
export let show_label: boolean;
|
||||
export let selectable: boolean = false;
|
||||
export let imageWidth: number = 1;
|
||||
export let imageHeight: number = 1;
|
||||
export let imageWidth: number = 0;
|
||||
export let imageHeight: number = 0;
|
||||
let imageElem: HTMLImageElement | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -23,8 +23,8 @@
|
||||
$: value && dispatch("change", value);
|
||||
|
||||
$: if (value == null || !imageElem) {
|
||||
imageWidth = 1;
|
||||
imageHeight = 1;
|
||||
imageWidth = 0;
|
||||
imageHeight = 0;
|
||||
}
|
||||
|
||||
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 { get } from "svelte/store";
|
||||
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 { 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 {
|
||||
static slotLayout: SlotLayout = {
|
||||
@@ -59,7 +59,7 @@ LiteGraph.registerNodeType({
|
||||
class: ComfyQueueEvents,
|
||||
title: "Comfy.QueueEvents",
|
||||
desc: "Triggers a 'bang' event when a prompt is queued.",
|
||||
type: "actions/queue_events"
|
||||
type: "events/queue_events"
|
||||
})
|
||||
|
||||
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
|
||||
@@ -177,8 +177,8 @@ export class ComfySwapAction extends ComfyGraphNode {
|
||||
override onAction(action: any, param: any) {
|
||||
const a = this.getInputData(0)
|
||||
const b = this.getInputData(1)
|
||||
this.triggerSlot(0, a)
|
||||
this.triggerSlot(1, b)
|
||||
this.triggerSlot(0, 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>) {
|
||||
for (const node of this.graph._nodes) {
|
||||
for (const node of this.graph.iterateNodesInOrderRecursive()) {
|
||||
if ("tags" in node.properties) {
|
||||
const comfyNode = node as ComfyGraphNode;
|
||||
const hasTag = comfyNode.properties.tags.indexOf(action.tag) != -1;
|
||||
@@ -482,9 +482,6 @@ export class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
||||
newMode = NodeMode.NEVER;
|
||||
}
|
||||
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)) {
|
||||
this.graph.getNodeById(nodeId).changeMode(newMode);
|
||||
this.graph.getNodeByIdRecursive(nodeId).changeMode(newMode);
|
||||
}
|
||||
|
||||
const layout = get(layoutState);
|
||||
@@ -601,27 +598,7 @@ export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
|
||||
|
||||
override getPromptThumbnails(): ComfyImageLocation[] | null {
|
||||
const data = this.getInputData(0)
|
||||
|
||||
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;
|
||||
return parseWhateverIntoComfyImageLocations(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
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 type IComfyInputSlot 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.
|
||||
@@ -30,29 +32,26 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
||||
// It just returns a hash like { "ui": { "images": results } } internally.
|
||||
// So this will need to be hardcoded for now.
|
||||
if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
|
||||
this.addOutput("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
|
||||
private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
|
||||
|
||||
private setup(nodeData: any) {
|
||||
var inputs = nodeData["input"]["required"];
|
||||
if (nodeData["input"]["optional"] != undefined) {
|
||||
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
|
||||
}
|
||||
|
||||
private setup(nodeDef: ComfyNodeDef) {
|
||||
ComfyBackendNode.defaultInputConfigs[this.type] = {}
|
||||
|
||||
for (const inputName in inputs) {
|
||||
for (const [inputName, inputData] of iterateNodeDefInputs(nodeDef)) {
|
||||
const config: Partial<IComfyInputSlot> = {};
|
||||
|
||||
const inputData = inputs[inputName];
|
||||
const type = inputData[0];
|
||||
const [type, opts] = inputData;
|
||||
|
||||
if (inputData[1]?.forceInput) {
|
||||
this.addInput(inputName, type);
|
||||
if (opts?.forceInput) {
|
||||
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 {
|
||||
if (Array.isArray(type)) {
|
||||
// Enums
|
||||
@@ -73,11 +72,9 @@ export class ComfyBackendNode extends ComfyGraphNode {
|
||||
ComfyBackendNode.defaultInputConfigs[this.type][inputName] = (config as IComfyInputSlot).config
|
||||
}
|
||||
|
||||
for (const o in nodeData["output"]) {
|
||||
const output = nodeData["output"][o];
|
||||
const outputName = nodeData["output_name"][o] || output;
|
||||
const outputShape = nodeData["output_is_list"][o] ? BuiltInSlotShape.GRID_SHAPE : BuiltInSlotShape.CIRCLE_SHAPE;
|
||||
this.addOutput(outputName, output, { shape: outputShape });
|
||||
for (const output of iterateNodeDefOutputs(nodeDef)) {
|
||||
const outputShape = output.is_list ? BuiltInSlotShape.GRID_SHAPE : BuiltInSlotShape.CIRCLE_SHAPE;
|
||||
this.addOutput(output.name, output.type, { shape: outputShape });
|
||||
}
|
||||
|
||||
this.serialize_widgets = false;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||
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 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 uiState from "$lib/stores/uiState";
|
||||
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 { 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 {
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "COMFYBOX_IMAGES" },
|
||||
{ name: "images", type: "COMFYBOX_IMAGES,COMFYBOX_IMAGE" },
|
||||
{ name: "index", type: "number" },
|
||||
],
|
||||
outputs: [
|
||||
{ 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() {
|
||||
const data = this.getInputData(0)
|
||||
if (data == null || !isComfyBoxImageMetadataArray(data)) {
|
||||
this.setOutputData(0, null)
|
||||
return;
|
||||
}
|
||||
const index = this.getInputData(1) || 0
|
||||
this.setValue(data, index);
|
||||
|
||||
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({
|
||||
class: ComfyPickImageNode,
|
||||
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"
|
||||
})
|
||||
|
||||
@@ -2,14 +2,15 @@ import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"
|
||||
import ComfyGraphNode, { type ComfyGraphNodeProperties, type DefaultWidgetLayout } from "./ComfyGraphNode";
|
||||
import { clamp } from "$lib/utils";
|
||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import { ComfyComboNode } from "./ComfyWidgetNodes";
|
||||
import { ComfyComboNode } from "./widgets";
|
||||
|
||||
export interface ComfyValueControlProperties extends ComfyGraphNodeProperties {
|
||||
value: any,
|
||||
action: "fixed" | "increment" | "decrement" | "randomize",
|
||||
min: number,
|
||||
max: number,
|
||||
step: number
|
||||
step: number,
|
||||
ignoreStepWhenRandom: boolean
|
||||
}
|
||||
|
||||
const INT_MAX = 1125899906842624;
|
||||
@@ -18,10 +19,11 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
||||
override properties: ComfyValueControlProperties = {
|
||||
tags: [],
|
||||
value: null,
|
||||
action: "fixed",
|
||||
action: "randomize",
|
||||
min: -INT_MAX,
|
||||
max: INT_MAX,
|
||||
step: 1
|
||||
step: 1,
|
||||
ignoreStepWhenRandom: false
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
@@ -61,15 +63,11 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
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) {
|
||||
this._aboutToChange -= 1;
|
||||
if (this._aboutToChange <= 0) {
|
||||
const value = this._aboutToChangeValue;
|
||||
console.warn("ABOUTTOCHANGE", value)
|
||||
this._aboutToChange = 0;
|
||||
this._aboutToChangeValue = null;
|
||||
this.triggerSlot(1, value)
|
||||
@@ -82,8 +80,26 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
||||
if (typeof v !== "number")
|
||||
return
|
||||
|
||||
let min = this.properties.min
|
||||
let max = this.properties.max
|
||||
let action_ = this.getInputData(2);
|
||||
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 (max == null) max = INT_MAX
|
||||
|
||||
@@ -103,7 +119,8 @@ export default class ComfyValueControl extends ComfyGraphNode {
|
||||
v -= this.properties.step;
|
||||
break;
|
||||
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:
|
||||
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 { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
|
||||
export {
|
||||
ComfyQueueEvents,
|
||||
ComfyCopyAction,
|
||||
@@ -17,4 +16,3 @@ export { default as ComfySelector } from "./ComfySelector"
|
||||
export { default as ComfyTriggerNewEventNode } from "./ComfyTriggerNewEventNode"
|
||||
export { default as ComfyConfigureQueuePromptButton } from "./ComfyConfigureQueuePromptButton"
|
||||
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 type { Writable } from 'svelte/store';
|
||||
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 type { ComfyWidgetNode } from '$lib/nodes';
|
||||
import type { NodeID } from '$lib/api';
|
||||
import type { ComfyNodeID } from '$lib/api';
|
||||
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 = {
|
||||
/*
|
||||
@@ -60,17 +64,7 @@ export type LayoutState = {
|
||||
* Items indexed by the litegraph node they're bound to
|
||||
* Only contains drag items of type "widget"
|
||||
*/
|
||||
allItemsByNode: Record<NodeID, DragItemEntry>,
|
||||
|
||||
/*
|
||||
* Selected drag items.
|
||||
*/
|
||||
currentSelection: DragItemID[],
|
||||
|
||||
/*
|
||||
* Selected LGraphNodes inside the litegraph canvas.
|
||||
*/
|
||||
currentSelectionNodes: LGraphNode[],
|
||||
allItemsByNode: Record<ComfyNodeID, DragItemEntry>,
|
||||
|
||||
/*
|
||||
* If true, a saved workflow is being deserialized, so ignore any
|
||||
@@ -194,7 +188,7 @@ export type AttributesSpec = {
|
||||
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,
|
||||
|
||||
@@ -259,7 +253,12 @@ export type AttributesSpec = {
|
||||
* 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.
|
||||
*/
|
||||
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())
|
||||
}
|
||||
|
||||
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.
|
||||
* Their order in the list is the order they'll appear in the panel.
|
||||
@@ -297,6 +314,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
location: "widget",
|
||||
defaultValue: "",
|
||||
editable: true,
|
||||
// onChanged: setNodeTitle
|
||||
},
|
||||
{
|
||||
name: "hidden",
|
||||
@@ -341,7 +359,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
location: "widget",
|
||||
editable: true,
|
||||
values: ["visible", "disabled", "hidden"],
|
||||
defaultValue: "disabled",
|
||||
defaultValue: "hidden",
|
||||
canShow: (di: IDragItem) => di.type === "widget"
|
||||
},
|
||||
|
||||
@@ -362,7 +380,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
location: "widget",
|
||||
editable: true,
|
||||
values: ["block", "hidden"],
|
||||
defaultValue: "block",
|
||||
defaultValue: "hidden",
|
||||
canShow: (di: IDragItem) => di.type === "container"
|
||||
},
|
||||
|
||||
@@ -493,7 +511,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
defaultValue: 0,
|
||||
min: -2 ^ 16,
|
||||
max: 2 ^ 16,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
validNodeTypes: ["ui/number"],
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
@@ -503,7 +521,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
defaultValue: 10,
|
||||
min: -2 ^ 16,
|
||||
max: 2 ^ 16,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
validNodeTypes: ["ui/number"],
|
||||
},
|
||||
{
|
||||
name: "step",
|
||||
@@ -513,7 +531,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
defaultValue: 1,
|
||||
min: -2 ^ 16,
|
||||
max: 2 ^ 16,
|
||||
validNodeTypes: ["ui/slider"],
|
||||
validNodeTypes: ["ui/number"],
|
||||
},
|
||||
|
||||
// Button
|
||||
@@ -579,6 +597,7 @@ for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||
|
||||
export { ALL_ATTRIBUTES };
|
||||
|
||||
// TODO Should be nested by category for name uniqueness?
|
||||
const defaultWidgetAttributes: Attributes = {} as any
|
||||
const defaultWorkflowAttributes: LayoutAttributes = {} as any
|
||||
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||
@@ -660,11 +679,11 @@ type LayoutStateOps = {
|
||||
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
||||
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => 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,
|
||||
getCurrentSelection: () => IDragItem[],
|
||||
findLayoutEntryForNode: (nodeId: NodeID) => DragItemEntry | null,
|
||||
findLayoutForNode: (nodeId: NodeID) => IDragItem | null,
|
||||
findLayoutEntryForNode: (nodeId: ComfyNodeID) => DragItemEntry | null,
|
||||
findLayoutForNode: (nodeId: ComfyNodeID) => IDragItem | null,
|
||||
serialize: () => SerializedLayoutState,
|
||||
deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
|
||||
initDefaultLayout: () => void,
|
||||
@@ -676,8 +695,6 @@ const store: Writable<LayoutState> = writable({
|
||||
root: null,
|
||||
allItems: {},
|
||||
allItemsByNode: {},
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: true,
|
||||
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 {
|
||||
const state = get(store);
|
||||
|
||||
@@ -706,6 +737,16 @@ function findDefaultContainerForInsertion(): ContainerLayout | 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 {
|
||||
const state = get(store);
|
||||
const dragItem: ContainerLayout = {
|
||||
@@ -718,19 +759,26 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
||||
...attrs
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (parent) {
|
||||
moveItem(dragItem, parent, index)
|
||||
}
|
||||
|
||||
console.debug("[layoutState] addContainer", state)
|
||||
store.set(state)
|
||||
// runOnChangedForWidgetDefaults(dragItem)
|
||||
return dragItem;
|
||||
}
|
||||
|
||||
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index?: number): WidgetLayout {
|
||||
const state = get(store);
|
||||
const widgetName = "Widget"
|
||||
const widgetName = node.title || "Widget"
|
||||
const dragItem: WidgetLayout = {
|
||||
type: "widget",
|
||||
id: uuidv4(),
|
||||
@@ -739,16 +787,23 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
||||
attrs: {
|
||||
...defaultWidgetAttributes,
|
||||
title: widgetName,
|
||||
nodeDisabledState: "disabled",
|
||||
...attrs
|
||||
}
|
||||
}
|
||||
const parentEntry = state.allItems[parent.id]
|
||||
|
||||
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;
|
||||
|
||||
if (state.allItemsByNode[node.id] != null)
|
||||
throw new Error(`Widget's node with ID ${node.id} already registered!!!`)
|
||||
state.allItemsByNode[node.id] = entry;
|
||||
|
||||
console.debug("[layoutState] addWidget", state)
|
||||
moveItem(dragItem, parent, index)
|
||||
// runOnChangedForWidgetDefaults(dragItem)
|
||||
return dragItem;
|
||||
}
|
||||
|
||||
@@ -784,24 +839,62 @@ function removeEntry(state: LayoutState, id: DragItemID) {
|
||||
}
|
||||
|
||||
function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||
// Only concern ourselves with widget nodes
|
||||
if (!isComfyWidgetNode(node))
|
||||
return;
|
||||
|
||||
const state = get(store)
|
||||
if (state.isConfiguring)
|
||||
return;
|
||||
|
||||
let attrs: Partial<Attributes> = {}
|
||||
|
||||
if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
|
||||
// All we need to do is update the nodeID linked to this node.
|
||||
const item = state.allItemsByNode[options.prevNodeId]
|
||||
delete state.allItemsByNode[options.prevNodeId]
|
||||
const item = state.allItemsByNode[options.prevNodeID]
|
||||
delete state.allItemsByNode[options.prevNodeID]
|
||||
state.allItemsByNode[node.id] = item
|
||||
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();
|
||||
|
||||
console.debug("[layoutState] nodeAdded", node)
|
||||
if ("svelteComponentType" in node) {
|
||||
addWidget(parent, node as ComfyWidgetNode);
|
||||
}
|
||||
console.debug("[layoutState] nodeAdded", node.id)
|
||||
addWidget(parent, node, attrs);
|
||||
}
|
||||
|
||||
function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||
@@ -830,7 +923,7 @@ function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||
const state = get(store)
|
||||
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;
|
||||
|
||||
if (entry.parent) {
|
||||
@@ -858,33 +951,27 @@ function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function getCurrentSelection(): IDragItem[] {
|
||||
const state = get(store)
|
||||
return state.currentSelection.map(id => state.allItems[id].dragItem)
|
||||
}
|
||||
|
||||
function groupItems(dragItems: IDragItem[], attrs: Partial<Attributes> = {}): ContainerLayout {
|
||||
if (dragItems.length === 0)
|
||||
function groupItems(dragItemIDs: DragItemID[], attrs: Partial<Attributes> = {}): ContainerLayout {
|
||||
if (dragItemIDs.length === 0)
|
||||
return;
|
||||
|
||||
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")
|
||||
return;
|
||||
|
||||
let index = undefined;
|
||||
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)
|
||||
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 item of dragItems) {
|
||||
for (const itemID of dragItemIDs) {
|
||||
const item = state.allItems[itemID].dragItem;
|
||||
moveItem(item, container)
|
||||
}
|
||||
|
||||
@@ -924,7 +1011,7 @@ function ungroup(container: ContainerLayout) {
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function findLayoutEntryForNode(nodeId: NodeID): DragItemEntry | null {
|
||||
function findLayoutEntryForNode(nodeId: ComfyNodeID): DragItemEntry | null {
|
||||
const state = get(store)
|
||||
const found = Object.entries(state.allItems).find(pair =>
|
||||
pair[1].dragItem.type === "widget"
|
||||
@@ -934,7 +1021,7 @@ function findLayoutEntryForNode(nodeId: NodeID): DragItemEntry | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLayoutForNode(nodeId: NodeID): WidgetLayout | null {
|
||||
function findLayoutForNode(nodeId: ComfyNodeID): WidgetLayout | null {
|
||||
const found = findLayoutEntryForNode(nodeId);
|
||||
if (!found)
|
||||
return null;
|
||||
@@ -946,8 +1033,6 @@ function initDefaultLayout() {
|
||||
root: null,
|
||||
allItems: {},
|
||||
allItemsByNode: {},
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false,
|
||||
refreshPropsPanel: writable(0),
|
||||
@@ -1034,7 +1119,9 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
|
||||
if (dragItem.type === "widget") {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1059,8 +1146,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
root,
|
||||
allItems,
|
||||
allItemsByNode,
|
||||
currentSelection: [],
|
||||
currentSelectionNodes: [],
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false,
|
||||
refreshPropsPanel: writable(0),
|
||||
@@ -1091,7 +1176,7 @@ const layoutStateStore: WritableLayoutStateStore =
|
||||
updateChildren,
|
||||
nodeAdded,
|
||||
nodeRemoved,
|
||||
getCurrentSelection,
|
||||
moveItem,
|
||||
groupItems,
|
||||
findLayoutEntryForNode,
|
||||
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 { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import notify from "$lib/notify";
|
||||
@@ -14,12 +14,13 @@ type QueueStateOps = {
|
||||
queueUpdated: (resp: ComfyAPIQueueResponse) => void,
|
||||
historyUpdated: (resp: ComfyAPIHistoryResponse) => void,
|
||||
statusUpdated: (status: ComfyAPIStatusResponse | null) => void,
|
||||
executingUpdated: (promptID: PromptID | null, runningNodeID: NodeID | null) => void,
|
||||
executionCached: (promptID: PromptID, nodes: NodeID[]) => void,
|
||||
executionStart: (promptID: PromptID) => void,
|
||||
executingUpdated: (promptID: PromptID | null, runningNodeID: ComfyNodeID | null) => void,
|
||||
executionCached: (promptID: PromptID, nodes: ComfyNodeID[]) => void,
|
||||
executionError: (promptID: PromptID, message: string) => void,
|
||||
progressUpdated: (progress: Progress) => 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 = {
|
||||
@@ -30,7 +31,7 @@ export type QueueEntry = {
|
||||
promptID: PromptID,
|
||||
prompt: SerializedPromptInputsAll,
|
||||
extraData: ComfyBoxPromptExtraData,
|
||||
goodOutputs: NodeID[],
|
||||
goodOutputs: ComfyNodeID[],
|
||||
|
||||
/* Data not sent by ComfyUI's API, lost on page refresh */
|
||||
|
||||
@@ -38,8 +39,8 @@ export type QueueEntry = {
|
||||
outputs: SerializedPromptOutputs,
|
||||
|
||||
/* Nodes in of the workflow that have finished running so far. */
|
||||
nodesRan: Set<NodeID>,
|
||||
cachedNodes: Set<NodeID>
|
||||
nodesRan: Set<ComfyNodeID>,
|
||||
cachedNodes: Set<ComfyNodeID>
|
||||
}
|
||||
|
||||
export type CompletedQueueEntry = {
|
||||
@@ -54,7 +55,7 @@ export type QueueState = {
|
||||
queuePending: Writable<QueueEntry[]>,
|
||||
queueCompleted: Writable<CompletedQueueEntry[]>,
|
||||
queueRemaining: number | "X" | null;
|
||||
runningNodeID: NodeID | null;
|
||||
runningNodeID: ComfyNodeID | null;
|
||||
progress: Progress | null,
|
||||
isInterrupting: boolean
|
||||
}
|
||||
@@ -161,7 +162,7 @@ function moveToCompleted(index: number, queue: Writable<QueueEntry[]>, status: Q
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
function executingUpdated(promptID: PromptID, runningNodeID: NodeID | null) {
|
||||
function executingUpdated(promptID: PromptID, runningNodeID: ComfyNodeID | null) {
|
||||
console.debug("[queueState] executingUpdated", promptID, runningNodeID)
|
||||
store.update((s) => {
|
||||
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)
|
||||
store.update(s => {
|
||||
const [index, entry, queue] = findEntryInPending(promptID);
|
||||
@@ -235,29 +236,57 @@ function executionError(promptID: PromptID, message: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) {
|
||||
console.debug("[queueState] afterQueued", promptID, Object.keys(prompt))
|
||||
function createNewQueueEntry(promptID: PromptID, number: number = -1, prompt: SerializedPromptInputsAll = {}, extraData: any = {}): QueueEntry {
|
||||
return {
|
||||
number,
|
||||
queuedAt: new Date(), // Now
|
||||
finishedAt: null,
|
||||
promptID,
|
||||
prompt,
|
||||
extraData,
|
||||
goodOutputs: [],
|
||||
outputs: {},
|
||||
nodesRan: new Set(),
|
||||
cachedNodes: new Set()
|
||||
}
|
||||
}
|
||||
|
||||
function executionStart(promptID: PromptID) {
|
||||
console.debug("[queueState] executionStart", promptID)
|
||||
store.update(s => {
|
||||
const entry: QueueEntry = {
|
||||
number,
|
||||
queuedAt: new Date(), // Now
|
||||
finishedAt: null,
|
||||
promptID,
|
||||
prompt,
|
||||
extraData,
|
||||
goodOutputs: [],
|
||||
outputs: {},
|
||||
nodesRan: new Set(),
|
||||
cachedNodes: new Set()
|
||||
const [index, entry, queue] = findEntryInPending(promptID);
|
||||
if (entry == null) {
|
||||
const entry = createNewQueueEntry(promptID);
|
||||
s.queuePending.update(qp => { qp.push(entry); return qp })
|
||||
console.debug("[queueState] ADD PROMPT", promptID)
|
||||
}
|
||||
s.queuePending.update(qp => { qp.push(entry); return qp })
|
||||
console.debug("[queueState] ADD PROMPT", promptID)
|
||||
s.isInterrupting = false;
|
||||
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)
|
||||
store.update(s => {
|
||||
const [index, entry, queue] = findEntryInPending(promptID)
|
||||
@@ -279,6 +308,7 @@ const queueStateStore: WritableQueueStateStore =
|
||||
historyUpdated,
|
||||
statusUpdated,
|
||||
progressUpdated,
|
||||
executionStart,
|
||||
executingUpdated,
|
||||
executionCached,
|
||||
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;
|
||||
197
src/lib/utils.ts
197
src/lib/utils.ts
@@ -1,13 +1,10 @@
|
||||
import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
|
||||
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
|
||||
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
|
||||
import TextWidget from "$lib/widgets/TextWidget.svelte";
|
||||
import { 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 layoutState, { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
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") {
|
||||
const blob = new Blob([text], { type: type });
|
||||
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) {
|
||||
const dragItemId: string = evt.target.dataset["dragItemId"];
|
||||
const ss = get(selectionState)
|
||||
const ls = get(layoutState)
|
||||
|
||||
if (evt.button !== 0) {
|
||||
if (ls.currentSelection.length <= 1 && !ls.isMenuOpen)
|
||||
ls.currentSelection = [dragItemId]
|
||||
if (ss.currentSelection.length <= 1 && !ls.isMenuOpen)
|
||||
ss.currentSelection = [dragItemId]
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,24 +55,110 @@ export function startDrag(evt: MouseEvent) {
|
||||
console.debug("startDrag", item)
|
||||
|
||||
if (evt.ctrlKey) {
|
||||
const index = ls.currentSelection.indexOf(item.id)
|
||||
const index = ss.currentSelection.indexOf(item.id)
|
||||
if (index === -1)
|
||||
ls.currentSelection.push(item.id);
|
||||
ss.currentSelection.push(item.id);
|
||||
else
|
||||
ls.currentSelection.splice(index, 1);
|
||||
ls.currentSelection = ls.currentSelection;
|
||||
ss.currentSelection.splice(index, 1);
|
||||
ss.currentSelection = ss.currentSelection;
|
||||
}
|
||||
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)
|
||||
selectionState.set(ss)
|
||||
};
|
||||
|
||||
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 {
|
||||
let out = "digraph {\n"
|
||||
|
||||
@@ -87,17 +178,21 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||
for (const pair of Object.entries(prompt.output)) {
|
||||
const [id, o] = pair;
|
||||
const outNode = prompt.workflow.nodes.find(n => n.id == id)
|
||||
for (const pair2 of Object.entries(o.inputs)) {
|
||||
const [inpName, i] = pair2;
|
||||
if (outNode) {
|
||||
for (const pair2 of Object.entries(o.inputs)) {
|
||||
const [inpName, i] = pair2;
|
||||
|
||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||
// Link
|
||||
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
||||
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
||||
}
|
||||
else {
|
||||
// Value
|
||||
out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n`
|
||||
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
|
||||
// Link
|
||||
const inpNode = prompt.workflow.nodes.find(n => n.id == i[0])
|
||||
if (inpNode) {
|
||||
out += `"${inpNode.title}" -> "${outNode.title}"\n`
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Value
|
||||
out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,13 +201,15 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
|
||||
return out
|
||||
}
|
||||
|
||||
export function getNodeInfo(nodeId: NodeID): string {
|
||||
export function getNodeInfo(nodeId: ComfyNodeID): string {
|
||||
let app = (window as any).app;
|
||||
if (!app || !app.lGraph)
|
||||
return String(nodeId);
|
||||
|
||||
const title = app.lGraph.getNodeById(nodeId)?.title || String(nodeId);
|
||||
return title + " (" + nodeId + ")"
|
||||
const displayNodeID = nodeId ? (nodeId.split("-")[0]) : String(nodeId);
|
||||
|
||||
const title = app.lGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId);
|
||||
return title + " (" + displayNodeID + ")"
|
||||
}
|
||||
|
||||
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
|
||||
* 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
let meta: ComfyBoxImageMetadata[] | null = null
|
||||
|
||||
@@ -314,12 +435,26 @@ export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadat
|
||||
meta = 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
return image.comfyUIFile
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { IWidget, LGraphNode } from "@litegraph-js/core";
|
||||
import ComfyValueControlWidget from "./widgets/ComfyValueControlWidget";
|
||||
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
||||
import { LGraphNode, LiteGraph } from "@litegraph-ts/core";
|
||||
import type IComfyInputSlot from "./IComfyInputSlot";
|
||||
import { BuiltInSlotShape, LiteGraph } from "@litegraph-ts/core";
|
||||
import { ComfyComboNode, ComfySliderNode, ComfyTextNode } from "./nodes";
|
||||
import type { ComfyInputConfig } from "./IComfyInputSlot";
|
||||
import { ComfyComboNode, ComfyNumberNode, ComfyTextNode } from "./nodes/widgets";
|
||||
|
||||
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 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 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 => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyButtonNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import type { ComfySliderNode } from "$lib/nodes/index";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Button } from "@gradio/button";
|
||||
import { get, type Writable, writable } from "svelte/store";
|
||||
import { isDisabled } from "./utils"
|
||||
import type { ComfyButtonNode } from "$lib/nodes/widgets";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyButtonNode | null = null;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Checkbox } from "@gradio/form";
|
||||
import { get, type Writable, writable } from "svelte/store";
|
||||
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 isMobile: boolean = false;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Select from 'svelte-select';
|
||||
// import VirtualList from '$lib/components/VirtualList.svelte';
|
||||
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 { get, writable, type Writable } from "svelte/store";
|
||||
import { isDisabled } from "./utils"
|
||||
@@ -13,6 +13,7 @@
|
||||
let node: ComfyComboNode | null = null;
|
||||
let nodeValue: Writable<string> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let lightUp: Writable<boolean> = writable(false);
|
||||
let valuesForCombo: Writable<any[]> | null = null;
|
||||
let lastConfigured: any = null;
|
||||
let option: any = null;
|
||||
@@ -40,6 +41,7 @@
|
||||
node = widget.node as ComfyComboNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
lightUp = node.lightUp;
|
||||
valuesForCombo = node.valuesForCombo;
|
||||
lastConfigured = $valuesForCombo
|
||||
}
|
||||
@@ -52,16 +54,8 @@
|
||||
activeIndex = values.findIndex(v => v.value === value);
|
||||
}
|
||||
|
||||
$: $valuesForCombo != lastConfigured && flashOnRefreshed();
|
||||
let lightUp = false;
|
||||
|
||||
function flashOnRefreshed() {
|
||||
lastConfigured = $valuesForCombo
|
||||
if (lastConfigured != null) {
|
||||
lightUp = true;
|
||||
setTimeout(() => (lightUp = false), 1000);
|
||||
}
|
||||
}
|
||||
$: if ($lightUp)
|
||||
setTimeout(() => ($lightUp = false), 1000);
|
||||
|
||||
function getLinkValue() {
|
||||
if (!node)
|
||||
@@ -137,7 +131,7 @@
|
||||
|
||||
</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}
|
||||
{#if node !== null && nodeValue !== null}
|
||||
{#if $valuesForCombo == null}
|
||||
@@ -172,10 +166,11 @@
|
||||
<div class="comfy-select-list" slot="list" let:filteredItems>
|
||||
{#if filteredItems.length > 0}
|
||||
{@const itemSize = isMobile ? 50 : 25}
|
||||
{@const itemsToShow = isMobile ? 10 : 30}
|
||||
<VirtualList
|
||||
items={filteredItems}
|
||||
width="100%"
|
||||
height={Math.min(filteredItems.length, 10) * itemSize}
|
||||
height={Math.min(filteredItems.length, itemsToShow) * itemSize}
|
||||
itemCount={filteredItems.length}
|
||||
{itemSize}
|
||||
overscanCount={5}
|
||||
@@ -252,7 +247,7 @@
|
||||
--chevron-color: var(--body-text-color);
|
||||
--border: 1px solid var(--input-border-color);
|
||||
--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: 0px;
|
||||
--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 type { Styles } from "@gradio/utils";
|
||||
import type { WidgetLayout } from "$lib/stores/layoutState";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import type { SelectData as GradioSelectData } from "@gradio/utils";
|
||||
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||
import { f7 } from "framework7-svelte";
|
||||
import type { ComfyGalleryNode } from "$lib/nodes/widgets";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
@@ -19,8 +19,8 @@
|
||||
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
let imageWidth: number = 1;
|
||||
let imageHeight: number = 1;
|
||||
let imageWidth: Writable<number> = writable(0);
|
||||
let imageHeight: Writable<number> = writable(0);
|
||||
let selected_image: number | null = null;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
@@ -30,6 +30,8 @@
|
||||
node = widget.node as ComfyGalleryNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
imageWidth = node.imageWidth
|
||||
imageHeight = node.imageHeight
|
||||
|
||||
if ($nodeValue != null) {
|
||||
if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
|
||||
@@ -45,18 +47,9 @@
|
||||
}
|
||||
let element: HTMLDivElement;
|
||||
|
||||
$: if (node) {
|
||||
if (imageWidth > 1 || imageHeight > 1) {
|
||||
node.imageSize = [imageWidth, imageHeight]
|
||||
}
|
||||
else {
|
||||
node.imageSize = [1, 1]
|
||||
}
|
||||
}
|
||||
|
||||
let mobileLightbox = null;
|
||||
|
||||
function showMobileLightbox(event: Event) {
|
||||
function showMobileLightbox(source: HTMLImageElement) {
|
||||
if (!f7)
|
||||
return
|
||||
|
||||
@@ -65,16 +58,14 @@
|
||||
mobileLightbox = null;
|
||||
}
|
||||
|
||||
const source = (event.target || event.srcElement) as HTMLImageElement;
|
||||
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) {
|
||||
console.error("No buttons found on gallery element!", galleryElem)
|
||||
return;
|
||||
}
|
||||
|
||||
const allGalleryButtons = ImageViewer.all_gallery_buttons(galleryElem);
|
||||
const selectedSource = source.src
|
||||
|
||||
const images = allGalleryButtons.map(button => {
|
||||
return {
|
||||
@@ -91,56 +82,29 @@
|
||||
type: 'popup',
|
||||
});
|
||||
mobileLightbox.open(selected_image)
|
||||
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function setupImageForMobileLightbox(e: 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) => {
|
||||
evt.preventDefault()
|
||||
showMobileLightbox(evt)
|
||||
}, true);
|
||||
function onClicked(e: CustomEvent<HTMLImageElement>) {
|
||||
if (isMobile) {
|
||||
showMobileLightbox(e.detail)
|
||||
}
|
||||
else {
|
||||
ImageViewer.instance.showLightbox(e.detail)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
node.setProperty("index", e.detail.index as number)
|
||||
}
|
||||
|
||||
$: if ($propsChanged > -1 && widget && $nodeValue) {
|
||||
if (widget.attrs.variant === "image") {
|
||||
selected_image = $nodeValue.length - 1
|
||||
node.setProperty("index", selected_image)
|
||||
}
|
||||
else {
|
||||
node.setProperty("index", $nodeValue.length > 0 ? 0 : null)
|
||||
}
|
||||
}
|
||||
else {
|
||||
node.setProperty("index", null)
|
||||
@@ -160,8 +124,8 @@
|
||||
value={url}
|
||||
show_label={widget.attrs.title != ""}
|
||||
label={widget.attrs.title}
|
||||
bind:imageWidth
|
||||
bind:imageHeight
|
||||
bind:imageWidth={$imageWidth}
|
||||
bind:imageHeight={$imageHeight}
|
||||
/>
|
||||
{:else}
|
||||
<Empty size="large" unpadded_box={true}><Image /></Empty>
|
||||
@@ -181,8 +145,9 @@
|
||||
root={""}
|
||||
root_url={""}
|
||||
on:select={onSelect}
|
||||
bind:imageWidth
|
||||
bind:imageHeight
|
||||
on:clicked={onClicked}
|
||||
bind:imageWidth={$imageWidth}
|
||||
bind:imageHeight={$imageHeight}
|
||||
bind:selected_image
|
||||
/>
|
||||
</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,14 +1,14 @@
|
||||
<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 { Range } from "$lib/components/gradio/form";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { debounce } from "$lib/utils";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
import { debounce } from "$lib/utils";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
import { isDisabled } from "./utils"
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfySliderNode | null = null;
|
||||
let node: ComfyNumberNode | null = null;
|
||||
let nodeValue: Writable<number> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let option: number | null = null;
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfySliderNode
|
||||
node = widget.node as ComfyNumberNode
|
||||
nodeValue = node.value;
|
||||
propsChanged = node.propsChanged;
|
||||
setOption($nodeValue); // don't react on option
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyRadioNode } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Radio } from "@gradio/form";
|
||||
@@ -7,6 +6,7 @@
|
||||
import { isDisabled } from "./utils"
|
||||
import type { SelectData } from "@gradio/utils";
|
||||
import { clamp } from "$lib/utils";
|
||||
import type { ComfyRadioNode } from "$lib/nodes/widgets";
|
||||
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { TextBox } from "@gradio/form";
|
||||
import type { ComfyComboNode } from "$lib/nodes/index";
|
||||
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 type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
let node: ComfyComboNode | null = null;
|
||||
|
||||
let node: ComfyTextNode | null = null;
|
||||
let nodeValue: Writable<string> | null = null;
|
||||
let propsChanged: Writable<number> | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
|
||||
$: widget && setNodeValue(widget);
|
||||
|
||||
function setNodeValue(widget: WidgetLayout) {
|
||||
if (widget) {
|
||||
node = widget.node as ComfySliderNode
|
||||
node = widget.node as ComfyTextNode
|
||||
nodeValue = node.value;
|
||||
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 layoutState from "$lib/stores/layoutState";
|
||||
import { NodeMode } from "@litegraph-ts/core";
|
||||
import { LGraphNode, NodeMode } from "@litegraph-ts/core";
|
||||
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) {
|
||||
if (widget.attrs.disabled)
|
||||
return true;
|
||||
|
||||
if (widget.type === "widget") {
|
||||
return widget.attrs.nodeDisabledState === "disabled" && widget.node.mode === NodeMode.NEVER
|
||||
return widget.attrs.nodeDisabledState === "disabled" && isNodeDisabled(widget.node)
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -19,7 +32,7 @@ export function isHidden(widget: IDragItem) {
|
||||
return true;
|
||||
|
||||
if (widget.type === "widget") {
|
||||
return widget.attrs.nodeDisabledState === "hidden" && widget.node.mode === NodeMode.NEVER
|
||||
return widget.attrs.nodeDisabledState === "hidden" && isNodeDisabled(widget.node)
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
LiteGraph.use_uuids = true;
|
||||
configureLitegraph()
|
||||
|
||||
const comfyApp = new ComfyApp();
|
||||
(window as any).app = comfyApp;
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
target: document.getElementById("app-root"),
|
||||
props: { app: comfyApp }
|
||||
})
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -6,20 +6,17 @@ import ComfyApp from '$lib/components/ComfyApp';
|
||||
import uiState from '$lib/stores/uiState';
|
||||
import { LiteGraph } from '@litegraph-ts/core';
|
||||
import ComfyGraph from '$lib/ComfyGraph';
|
||||
import { configureLitegraph } from '$lib/init';
|
||||
|
||||
Framework7.use(Framework7Svelte);
|
||||
|
||||
LiteGraph.use_uuids = true;
|
||||
LiteGraph.dialog_close_on_mouse_leave = false;
|
||||
LiteGraph.search_hide_on_mouse_leave = false;
|
||||
LiteGraph.pointerevents_method = "pointer";
|
||||
configureLitegraph(true);
|
||||
|
||||
const comfyApp = new ComfyApp();
|
||||
|
||||
uiState.update(s => { s.app = comfyApp; return s; })
|
||||
(window as any).app = comfyApp;
|
||||
|
||||
const app = new AppMobile({
|
||||
target: document.getElementById('app'),
|
||||
target: document.getElementById("app-root"),
|
||||
props: { app: comfyApp }
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "gradio";
|
||||
@import "litegraph";
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
@@ -7,14 +8,22 @@ body {
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
#app {
|
||||
#app-root {
|
||||
background: var(--body-background-fill);
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-blue-500: #3985f5;
|
||||
|
||||
--input-border-color-focus: var(--neutral-400);
|
||||
|
||||
--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-textbox-background-fill: var(--neutral-200);
|
||||
--comfy-disabled-textbox-border-color: var(--neutral-300);
|
||||
@@ -37,7 +46,15 @@ body {
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
--input-border-color-focus: var(--neutral-500);
|
||||
|
||||
--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-textbox-background-fill: var(--neutral-800);
|
||||
--comfy-disabled-textbox-border-color: var(--neutral-700);
|
||||
@@ -90,9 +107,16 @@ hr {
|
||||
color: var(--panel-border-color);
|
||||
}
|
||||
|
||||
select {
|
||||
color: var(--body-text-color);
|
||||
background: var(--block-background-fill);
|
||||
input, textarea {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
:not(.litegraph) {
|
||||
select {
|
||||
color: var(--body-text-color);
|
||||
background: var(--input-background-fill);
|
||||
border: var(--input-border-width) solid var(--input-border-color)
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -125,17 +149,6 @@ select {
|
||||
// }
|
||||
}
|
||||
|
||||
// button {
|
||||
// filter: none;
|
||||
// &.primary:active {
|
||||
// filter: brightness(80%)
|
||||
// }
|
||||
// &.secondary:active {
|
||||
// filter: brightness(80%)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
button {
|
||||
&.primary:active {
|
||||
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: {
|
||||
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