Format all code / Add pre-commit format hook (#81)

* Add format-guard

* Format code
This commit is contained in:
Chenlei Hu
2024-07-02 13:22:37 -04:00
committed by GitHub
parent 4fb7fa9db1
commit acdaa6a594
62 changed files with 28225 additions and 25406 deletions

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"semi": true,
"trailingComma": "es5"
}

664
package-lock.json generated
View File

@@ -20,9 +20,12 @@
"@types/node": "^20.14.8",
"babel-plugin-transform-import-meta": "^2.2.1",
"babel-plugin-transform-rename-import": "^2.3.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.7",
"prettier": "^3.3.2",
"ts-jest": "^29.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.15.6",
@@ -3884,6 +3887,87 @@
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
"dev": true
},
"node_modules/cli-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
"integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
"dev": true,
"dependencies": {
"restore-cursor": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
"dev": true,
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/cli-truncate/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
"dev": true
},
"node_modules/cli-truncate/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -3929,6 +4013,12 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3941,6 +4031,15 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"engines": {
"node": ">=18"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4380,6 +4479,12 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -4577,6 +4682,18 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
"integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -4739,6 +4856,21 @@
"node": ">=10.17.0"
}
},
"node_modules/husky": {
"version": "9.0.11",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz",
"integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==",
"dev": true,
"bin": {
"husky": "bin.mjs"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -6758,12 +6890,293 @@
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
"node_modules/lint-staged": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.7.tgz",
"integrity": "sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==",
"dev": true,
"dependencies": {
"chalk": "~5.3.0",
"commander": "~12.1.0",
"debug": "~4.3.4",
"execa": "~8.0.1",
"lilconfig": "~3.1.1",
"listr2": "~8.2.1",
"micromatch": "~4.0.7",
"pidtree": "~0.6.0",
"string-argv": "~0.3.2",
"yaml": "~2.4.2"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": ">=18.12.0"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/lint-staged/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/lint-staged/node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^8.0.1",
"human-signals": "^5.0.0",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^3.0.0"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/lint-staged/node_modules/get-stream": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
"dev": true,
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lint-staged/node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
"dev": true,
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/lint-staged/node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lint-staged/node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lint-staged/node_modules/npm-run-path": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
"dev": true,
"dependencies": {
"path-key": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lint-staged/node_modules/onetime": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
"dev": true,
"dependencies": {
"mimic-fn": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lint-staged/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lint-staged/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/lint-staged/node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/listr2": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.3.tgz",
"integrity": "sha512-Lllokma2mtoniUOS94CcOErHWAug5iu7HOmDrvWgpw8jyQH2fomgB+7lZS4HWZxytUuQwkGOwe49FvwVaA85Xw==",
"dev": true,
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
"log-update": "^6.0.0",
"rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/listr2/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/listr2/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/listr2/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
"dev": true
},
"node_modules/listr2/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/listr2/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/listr2/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -6788,6 +7201,147 @@
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true
},
"node_modules/log-update": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz",
"integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==",
"dev": true,
"dependencies": {
"ansi-escapes": "^6.2.0",
"cli-cursor": "^4.0.0",
"slice-ansi": "^7.0.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-escapes": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz",
"integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==",
"dev": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/log-update/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/log-update/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
"dev": true
},
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
"integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
"dev": true,
"dependencies": {
"get-east-asian-width": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
"integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -7140,6 +7694,18 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pidtree": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dev": true,
"bin": {
"pidtree": "bin/pidtree.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -7233,6 +7799,21 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -7510,6 +8091,22 @@
"node": ">=10"
}
},
"node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
"integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
"dev": true,
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -7520,6 +8117,12 @@
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true
},
"node_modules/rollup": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
@@ -7659,6 +8262,46 @@
"node": ">=8"
}
},
"node_modules/slice-ansi": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -7723,6 +8366,15 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -8850,6 +9502,18 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"node_modules/yaml": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",

View File

@@ -8,6 +8,7 @@
"build": "npm run typecheck && vite build",
"zipdist": "node scripts/zipdist.js",
"typecheck": "tsc --noEmit",
"format": "prettier --write 'src/**/*.{js,ts,tsx}'",
"test": "npm run build && jest",
"test:generate": "npx tsx tests-ui/setup",
"test:browser": "npx playwright test",
@@ -21,9 +22,12 @@
"@types/node": "^20.14.8",
"babel-plugin-transform-import-meta": "^2.2.1",
"babel-plugin-transform-rename-import": "^2.3.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.7",
"prettier": "^3.3.2",
"ts-jest": "^29.1.4",
"ts-node": "^10.9.2",
"tsx": "^4.15.6",
@@ -36,5 +40,11 @@
"dotenv": "^16.4.5",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
},
"lint-staged": {
"src/**/*.{js,ts,tsx}": [
"prettier --write",
"git add"
]
}
}
}

View File

@@ -3,164 +3,182 @@ import { ComfyDialog, $el } from "../../scripts/ui";
import { ComfyApp } from "../../scripts/app";
export class ClipspaceDialog extends ComfyDialog {
static items = [];
static instance = null;
static items = [];
static instance = null;
static registerButton(name, contextPredicate, callback) {
const item =
$el("button", {
type: "button",
textContent: name,
contextPredicate: contextPredicate,
onclick: callback
})
static registerButton(name, contextPredicate, callback) {
const item = $el("button", {
type: "button",
textContent: name,
contextPredicate: contextPredicate,
onclick: callback,
});
ClipspaceDialog.items.push(item);
}
ClipspaceDialog.items.push(item);
}
static invalidatePreview() {
if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) {
const img_preview = document.getElementById("clipspace_preview") as HTMLImageElement;
if(img_preview) {
img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
img_preview.style.maxHeight = "100%";
img_preview.style.maxWidth = "100%";
}
}
}
static invalidatePreview() {
if (
ComfyApp.clipspace &&
ComfyApp.clipspace.imgs &&
ComfyApp.clipspace.imgs.length > 0
) {
const img_preview = document.getElementById(
"clipspace_preview"
) as HTMLImageElement;
if (img_preview) {
img_preview.src =
ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src;
img_preview.style.maxHeight = "100%";
img_preview.style.maxWidth = "100%";
}
}
}
static invalidate() {
if(ClipspaceDialog.instance) {
const self = ClipspaceDialog.instance;
// allow reconstruct controls when copying from non-image to image content.
const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]);
static invalidate() {
if (ClipspaceDialog.instance) {
const self = ClipspaceDialog.instance;
// allow reconstruct controls when copying from non-image to image content.
const children = $el("div.comfy-modal-content", [
self.createImgSettings(),
...self.createButtons(),
]);
if(self.element) {
// update
self.element.removeChild(self.element.firstChild);
self.element.appendChild(children);
}
else {
// new
self.element = $el("div.comfy-modal", { parent: document.body }, [children,]);
}
if (self.element) {
// update
self.element.removeChild(self.element.firstChild);
self.element.appendChild(children);
} else {
// new
self.element = $el("div.comfy-modal", { parent: document.body }, [
children,
]);
}
if(self.element.children[0].children.length <= 1) {
self.element.children[0].appendChild($el("p", {}, ["Unable to find the features to edit content of a format stored in the current Clipspace."]));
}
if (self.element.children[0].children.length <= 1) {
self.element.children[0].appendChild(
$el("p", {}, [
"Unable to find the features to edit content of a format stored in the current Clipspace.",
])
);
}
ClipspaceDialog.invalidatePreview();
}
}
ClipspaceDialog.invalidatePreview();
}
}
constructor() {
super();
}
constructor() {
super();
}
createButtons() {
const buttons = [];
createButtons() {
const buttons = [];
for(let idx in ClipspaceDialog.items) {
const item = ClipspaceDialog.items[idx];
if(!item.contextPredicate || item.contextPredicate())
buttons.push(ClipspaceDialog.items[idx]);
}
for (let idx in ClipspaceDialog.items) {
const item = ClipspaceDialog.items[idx];
if (!item.contextPredicate || item.contextPredicate())
buttons.push(ClipspaceDialog.items[idx]);
}
buttons.push(
$el("button", {
type: "button",
textContent: "Close",
onclick: () => { this.close(); }
})
);
buttons.push(
$el("button", {
type: "button",
textContent: "Close",
onclick: () => {
this.close();
},
})
);
return buttons;
}
return buttons;
}
createImgSettings() {
if(ComfyApp.clipspace.imgs) {
const combo_items = [];
const imgs = ComfyApp.clipspace.imgs;
createImgSettings() {
if (ComfyApp.clipspace.imgs) {
const combo_items = [];
const imgs = ComfyApp.clipspace.imgs;
for(let i=0; i < imgs.length; i++) {
combo_items.push($el("option", {value:i}, [`${i}`]));
}
for (let i = 0; i < imgs.length; i++) {
combo_items.push($el("option", { value: i }, [`${i}`]));
}
const combo1 = $el("select",
{id:"clipspace_img_selector", onchange:(event) => {
ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex;
ClipspaceDialog.invalidatePreview();
} }, combo_items);
const combo1 = $el(
"select",
{
id: "clipspace_img_selector",
onchange: (event) => {
ComfyApp.clipspace["selectedIndex"] = event.target.selectedIndex;
ClipspaceDialog.invalidatePreview();
},
},
combo_items
);
const row1 =
$el("tr", {},
[
$el("td", {}, [$el("font", {color:"white"}, ["Select Image"])]),
$el("td", {}, [combo1])
]);
const row1 = $el("tr", {}, [
$el("td", {}, [$el("font", { color: "white" }, ["Select Image"])]),
$el("td", {}, [combo1]),
]);
const combo2 = $el(
"select",
{
id: "clipspace_img_paste_mode",
onchange: (event) => {
ComfyApp.clipspace["img_paste_mode"] = event.target.value;
},
},
[
$el("option", { value: "selected" }, "selected"),
$el("option", { value: "all" }, "all"),
]
) as HTMLSelectElement;
combo2.value = ComfyApp.clipspace["img_paste_mode"];
const combo2 = $el("select",
{id:"clipspace_img_paste_mode", onchange:(event) => {
ComfyApp.clipspace['img_paste_mode'] = event.target.value;
} },
[
$el("option", {value:'selected'}, 'selected'),
$el("option", {value:'all'}, 'all')
]) as HTMLSelectElement;
combo2.value = ComfyApp.clipspace['img_paste_mode'];
const row2 = $el("tr", {}, [
$el("td", {}, [$el("font", { color: "white" }, ["Paste Mode"])]),
$el("td", {}, [combo2]),
]);
const row2 =
$el("tr", {},
[
$el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]),
$el("td", {}, [combo2])
]);
const td = $el(
"td",
{ align: "center", width: "100px", height: "100px", colSpan: "2" },
[$el("img", { id: "clipspace_preview", ondragstart: () => false }, [])]
);
const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'},
[ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]);
const row3 = $el("tr", {}, [td]);
const row3 =
$el("tr", {}, [td]);
return $el("table", {}, [row1, row2, row3]);
} else {
return [];
}
}
return $el("table", {}, [row1, row2, row3]);
}
else {
return [];
}
}
createImgPreview() {
if (ComfyApp.clipspace.imgs) {
return $el("img", { id: "clipspace_preview", ondragstart: () => false });
} else return [];
}
createImgPreview() {
if(ComfyApp.clipspace.imgs) {
return $el("img",{id:"clipspace_preview", ondragstart:() => false});
}
else
return [];
}
show() {
const img_preview = document.getElementById("clipspace_preview");
ClipspaceDialog.invalidate();
show() {
const img_preview = document.getElementById("clipspace_preview");
ClipspaceDialog.invalidate();
this.element.style.display = "block";
}
this.element.style.display = "block";
}
}
app.registerExtension({
name: "Comfy.Clipspace",
init(app) {
app.openClipspace =
function () {
if(!ClipspaceDialog.instance) {
ClipspaceDialog.instance = new ClipspaceDialog();
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
}
name: "Comfy.Clipspace",
init(app) {
app.openClipspace = function () {
if (!ClipspaceDialog.instance) {
ClipspaceDialog.instance = new ClipspaceDialog();
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
}
if(ComfyApp.clipspace) {
ClipspaceDialog.instance.show();
}
else
app.ui.dialog.show("Clipspace is Empty!");
};
}
});
if (ComfyApp.clipspace) {
ClipspaceDialog.instance.show();
} else app.ui.dialog.show("Clipspace is Empty!");
};
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,150 +1,171 @@
import {app} from "../../scripts/app";
import { app } from "../../scripts/app";
// Adds filtering to combo context menus
const ext = {
name: "Comfy.ContextMenuFilter",
init() {
const ctxMenu = LiteGraph.ContextMenu;
// @ts-ignore
// TODO Very hacky way to modify Litegraph behaviour. Fix this later.
LiteGraph.ContextMenu = function (values, options) {
const ctx = ctxMenu.call(this, values, options);
name: "Comfy.ContextMenuFilter",
init() {
const ctxMenu = LiteGraph.ContextMenu;
// @ts-ignore
// TODO Very hacky way to modify Litegraph behaviour. Fix this later.
LiteGraph.ContextMenu = function (values, options) {
const ctx = ctxMenu.call(this, values, options);
// If we are a dark menu (only used for combo boxes) then add a filter input
if (options?.className === "dark" && values?.length > 10) {
const filter = document.createElement("input");
filter.classList.add("comfy-context-menu-filter");
filter.placeholder = "Filter list";
this.root.prepend(filter);
// If we are a dark menu (only used for combo boxes) then add a filter input
if (options?.className === "dark" && values?.length > 10) {
const filter = document.createElement("input");
filter.classList.add("comfy-context-menu-filter");
filter.placeholder = "Filter list";
this.root.prepend(filter);
const items = Array.from(this.root.querySelectorAll(".litemenu-entry")) as HTMLElement[];
let displayedItems = [...items];
let itemCount = displayedItems.length;
const items = Array.from(
this.root.querySelectorAll(".litemenu-entry")
) as HTMLElement[];
let displayedItems = [...items];
let itemCount = displayedItems.length;
// We must request an animation frame for the current node of the active canvas to update.
requestAnimationFrame(() => {
// @ts-ignore
const currentNode = LGraphCanvas.active_canvas.current_node;
const clickedComboValue = currentNode.widgets
?.filter(w => w.type === "combo" && w.options.values.length === values.length)
.find(w => w.options.values.every((v, i) => v === values[i]))
?.value;
// We must request an animation frame for the current node of the active canvas to update.
requestAnimationFrame(() => {
// @ts-ignore
const currentNode = LGraphCanvas.active_canvas.current_node;
const clickedComboValue = currentNode.widgets
?.filter(
(w) =>
w.type === "combo" && w.options.values.length === values.length
)
.find((w) =>
w.options.values.every((v, i) => v === values[i])
)?.value;
let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0;
if (selectedIndex < 0) {
selectedIndex = 0;
}
let selectedItem = displayedItems[selectedIndex];
updateSelected();
let selectedIndex = clickedComboValue
? values.findIndex((v) => v === clickedComboValue)
: 0;
if (selectedIndex < 0) {
selectedIndex = 0;
}
let selectedItem = displayedItems[selectedIndex];
updateSelected();
// Apply highlighting to the selected item
function updateSelected() {
selectedItem?.style.setProperty("background-color", "");
selectedItem?.style.setProperty("color", "");
selectedItem = displayedItems[selectedIndex];
selectedItem?.style.setProperty("background-color", "#ccc", "important");
selectedItem?.style.setProperty("color", "#000", "important");
}
// Apply highlighting to the selected item
function updateSelected() {
selectedItem?.style.setProperty("background-color", "");
selectedItem?.style.setProperty("color", "");
selectedItem = displayedItems[selectedIndex];
selectedItem?.style.setProperty(
"background-color",
"#ccc",
"important"
);
selectedItem?.style.setProperty("color", "#000", "important");
}
const positionList = () => {
const rect = this.root.getBoundingClientRect();
const positionList = () => {
const rect = this.root.getBoundingClientRect();
// If the top is off-screen then shift the element with scaling applied
if (rect.top < 0) {
const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight;
const shift = (this.root.clientHeight * scale) / 2;
this.root.style.top = -shift + "px";
}
}
// If the top is off-screen then shift the element with scaling applied
if (rect.top < 0) {
const scale =
1 -
this.root.getBoundingClientRect().height /
this.root.clientHeight;
const shift = (this.root.clientHeight * scale) / 2;
this.root.style.top = -shift + "px";
}
};
// Arrow up/down to select items
filter.addEventListener("keydown", (event) => {
switch (event.key) {
case "ArrowUp":
event.preventDefault();
if (selectedIndex === 0) {
selectedIndex = itemCount - 1;
} else {
selectedIndex--;
}
updateSelected();
break;
case "ArrowRight":
event.preventDefault();
selectedIndex = itemCount - 1;
updateSelected();
break;
case "ArrowDown":
event.preventDefault();
if (selectedIndex === itemCount - 1) {
selectedIndex = 0;
} else {
selectedIndex++;
}
updateSelected();
break;
case "ArrowLeft":
event.preventDefault();
selectedIndex = 0;
updateSelected();
break;
case "Enter":
selectedItem?.click();
break;
case "Escape":
this.close();
break;
}
});
// Arrow up/down to select items
filter.addEventListener("keydown", (event) => {
switch (event.key) {
case "ArrowUp":
event.preventDefault();
if (selectedIndex === 0) {
selectedIndex = itemCount - 1;
} else {
selectedIndex--;
}
updateSelected();
break;
case "ArrowRight":
event.preventDefault();
selectedIndex = itemCount - 1;
updateSelected();
break;
case "ArrowDown":
event.preventDefault();
if (selectedIndex === itemCount - 1) {
selectedIndex = 0;
} else {
selectedIndex++;
}
updateSelected();
break;
case "ArrowLeft":
event.preventDefault();
selectedIndex = 0;
updateSelected();
break;
case "Enter":
selectedItem?.click();
break;
case "Escape":
this.close();
break;
}
});
filter.addEventListener("input", () => {
// Hide all items that don't match our filter
const term = filter.value.toLocaleLowerCase();
// When filtering, recompute which items are visible for arrow up/down and maintain selection.
displayedItems = items.filter(item => {
const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term);
item.style.display = isVisible ? "block" : "none";
return isVisible;
});
filter.addEventListener("input", () => {
// Hide all items that don't match our filter
const term = filter.value.toLocaleLowerCase();
// When filtering, recompute which items are visible for arrow up/down and maintain selection.
displayedItems = items.filter((item) => {
const isVisible =
!term || item.textContent.toLocaleLowerCase().includes(term);
item.style.display = isVisible ? "block" : "none";
return isVisible;
});
selectedIndex = 0;
if (displayedItems.includes(selectedItem)) {
selectedIndex = displayedItems.findIndex(d => d === selectedItem);
}
itemCount = displayedItems.length;
selectedIndex = 0;
if (displayedItems.includes(selectedItem)) {
selectedIndex = displayedItems.findIndex(
(d) => d === selectedItem
);
}
itemCount = displayedItems.length;
updateSelected();
updateSelected();
// If we have an event then we can try and position the list under the source
if (options.event) {
let top = options.event.clientY - 10;
// If we have an event then we can try and position the list under the source
if (options.event) {
let top = options.event.clientY - 10;
const bodyRect = document.body.getBoundingClientRect();
const rootRect = this.root.getBoundingClientRect();
if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) {
top = Math.max(0, bodyRect.height - rootRect.height - 10);
}
const bodyRect = document.body.getBoundingClientRect();
const rootRect = this.root.getBoundingClientRect();
if (
bodyRect.height &&
top > bodyRect.height - rootRect.height - 10
) {
top = Math.max(0, bodyRect.height - rootRect.height - 10);
}
this.root.style.top = top + "px";
positionList();
}
});
this.root.style.top = top + "px";
positionList();
}
});
requestAnimationFrame(() => {
// Focus the filter box when opening
filter.focus();
requestAnimationFrame(() => {
// Focus the filter box when opening
filter.focus();
positionList();
});
})
}
positionList();
});
});
}
return ctx;
};
return ctx;
};
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
},
}
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
},
};
app.registerExtension(ext);

View File

@@ -7,42 +7,46 @@ import { app } from "../../scripts/app";
* Strips C-style line and block comments from a string
*/
function stripComments(str) {
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,'');
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
}
app.registerExtension({
name: "Comfy.DynamicPrompts",
nodeCreated(node) {
if (node.widgets) {
// Locate dynamic prompt text widgets
// Include any widgets with dynamicPrompts set to true, and customtext
const widgets = node.widgets.filter(
(n) => n.dynamicPrompts
);
for (const widget of widgets) {
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
widget.serializeValue = (workflowNode, widgetIndex) => {
let prompt = stripComments(widget.value);
while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) {
const startIndex = prompt.replace("\\{", "00").indexOf("{");
const endIndex = prompt.replace("\\}", "00").indexOf("}");
name: "Comfy.DynamicPrompts",
nodeCreated(node) {
if (node.widgets) {
// Locate dynamic prompt text widgets
// Include any widgets with dynamicPrompts set to true, and customtext
const widgets = node.widgets.filter((n) => n.dynamicPrompts);
for (const widget of widgets) {
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
widget.serializeValue = (workflowNode, widgetIndex) => {
let prompt = stripComments(widget.value);
while (
prompt.replace("\\{", "").includes("{") &&
prompt.replace("\\}", "").includes("}")
) {
const startIndex = prompt.replace("\\{", "00").indexOf("{");
const endIndex = prompt.replace("\\}", "00").indexOf("}");
const optionsString = prompt.substring(startIndex + 1, endIndex);
const options = optionsString.split("|");
const optionsString = prompt.substring(startIndex + 1, endIndex);
const options = optionsString.split("|");
const randomIndex = Math.floor(Math.random() * options.length);
const randomOption = options[randomIndex];
const randomIndex = Math.floor(Math.random() * options.length);
const randomOption = options[randomIndex];
prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1);
}
prompt =
prompt.substring(0, startIndex) +
randomOption +
prompt.substring(endIndex + 1);
}
// Overwrite the value in the serialized workflow pnginfo
if (workflowNode?.widgets_values)
workflowNode.widgets_values[widgetIndex] = prompt;
// Overwrite the value in the serialized workflow pnginfo
if (workflowNode?.widgets_values)
workflowNode.widgets_values[widgetIndex] = prompt;
return prompt;
};
}
}
},
return prompt;
};
}
}
},
});

View File

@@ -3,142 +3,159 @@ import { app } from "../../scripts/app";
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
app.registerExtension({
name: "Comfy.EditAttention",
init() {
const editAttentionDelta = app.ui.settings.addSetting({
id: "Comfy.EditAttention.Delta",
name: "Ctrl+up/down precision",
type: "slider",
attrs: {
min: 0.01,
max: 0.5,
step: 0.01,
},
defaultValue: 0.05,
});
name: "Comfy.EditAttention",
init() {
const editAttentionDelta = app.ui.settings.addSetting({
id: "Comfy.EditAttention.Delta",
name: "Ctrl+up/down precision",
type: "slider",
attrs: {
min: 0.01,
max: 0.5,
step: 0.01,
},
defaultValue: 0.05,
});
function incrementWeight(weight, delta) {
const floatWeight = parseFloat(weight);
if (isNaN(floatWeight)) return weight;
const newWeight = floatWeight + delta;
if (newWeight < 0) return "0";
return String(Number(newWeight.toFixed(10)));
function incrementWeight(weight, delta) {
const floatWeight = parseFloat(weight);
if (isNaN(floatWeight)) return weight;
const newWeight = floatWeight + delta;
if (newWeight < 0) return "0";
return String(Number(newWeight.toFixed(10)));
}
function findNearestEnclosure(text, cursorPos) {
let start = cursorPos,
end = cursorPos;
let openCount = 0,
closeCount = 0;
// Find opening parenthesis before cursor
while (start >= 0) {
start--;
if (text[start] === "(" && openCount === closeCount) break;
if (text[start] === "(") openCount++;
if (text[start] === ")") closeCount++;
}
if (start < 0) return false;
openCount = 0;
closeCount = 0;
// Find closing parenthesis after cursor
while (end < text.length) {
if (text[end] === ")" && openCount === closeCount) break;
if (text[end] === "(") openCount++;
if (text[end] === ")") closeCount++;
end++;
}
if (end === text.length) return false;
return { start: start + 1, end: end };
}
function addWeightToParentheses(text) {
const parenRegex = /^\((.*)\)$/;
const parenMatch = text.match(parenRegex);
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
const floatMatch = text.match(floatRegex);
if (parenMatch && !floatMatch) {
return `(${parenMatch[1]}:1.0)`;
} else {
return text;
}
}
function editAttention(event) {
const inputField = event.composedPath()[0];
const delta = parseFloat(editAttentionDelta.value);
if (inputField.tagName !== "TEXTAREA") return;
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
if (!event.ctrlKey && !event.metaKey) return;
event.preventDefault();
let start = inputField.selectionStart;
let end = inputField.selectionEnd;
let selectedText = inputField.value.substring(start, end);
// If there is no selection, attempt to find the nearest enclosure, or select the current word
if (!selectedText) {
const nearestEnclosure = findNearestEnclosure(inputField.value, start);
if (nearestEnclosure) {
start = nearestEnclosure.start;
end = nearestEnclosure.end;
selectedText = inputField.value.substring(start, end);
} else {
// Select the current word, find the start and end of the word
const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t";
while (
!delimiters.includes(inputField.value[start - 1]) &&
start > 0
) {
start--;
}
while (
!delimiters.includes(inputField.value[end]) &&
end < inputField.value.length
) {
end++;
}
selectedText = inputField.value.substring(start, end);
if (!selectedText) return;
}
}
function findNearestEnclosure(text, cursorPos) {
let start = cursorPos, end = cursorPos;
let openCount = 0, closeCount = 0;
// If the selection ends with a space, remove it
if (selectedText[selectedText.length - 1] === " ") {
selectedText = selectedText.substring(0, selectedText.length - 1);
end -= 1;
}
// Find opening parenthesis before cursor
while (start >= 0) {
start--;
if (text[start] === "(" && openCount === closeCount) break;
if (text[start] === "(") openCount++;
if (text[start] === ")") closeCount++;
}
if (start < 0) return false;
// If there are parentheses left and right of the selection, select them
if (
inputField.value[start - 1] === "(" &&
inputField.value[end] === ")"
) {
start -= 1;
end += 1;
selectedText = inputField.value.substring(start, end);
}
openCount = 0;
closeCount = 0;
// If the selection is not enclosed in parentheses, add them
if (
selectedText[0] !== "(" ||
selectedText[selectedText.length - 1] !== ")"
) {
selectedText = `(${selectedText})`;
}
// Find closing parenthesis after cursor
while (end < text.length) {
if (text[end] === ")" && openCount === closeCount) break;
if (text[end] === "(") openCount++;
if (text[end] === ")") closeCount++;
end++;
}
if (end === text.length) return false;
// If the selection does not have a weight, add a weight of 1.0
selectedText = addWeightToParentheses(selectedText);
return { start: start + 1, end: end };
// Increment the weight
const weightDelta = event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(
/\((.*):(\d+(?:\.\d+)?)\)/,
(match, text, weight) => {
weight = incrementWeight(weight, weightDelta);
if (weight == 1) {
return text;
} else {
return `(${text}:${weight})`;
}
}
);
function addWeightToParentheses(text) {
const parenRegex = /^\((.*)\)$/;
const parenMatch = text.match(parenRegex);
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
const floatMatch = text.match(floatRegex);
if (parenMatch && !floatMatch) {
return `(${parenMatch[1]}:1.0)`;
} else {
return text;
}
};
function editAttention(event) {
const inputField = event.composedPath()[0];
const delta = parseFloat(editAttentionDelta.value);
if (inputField.tagName !== "TEXTAREA") return;
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
if (!event.ctrlKey && !event.metaKey) return;
event.preventDefault();
let start = inputField.selectionStart;
let end = inputField.selectionEnd;
let selectedText = inputField.value.substring(start, end);
// If there is no selection, attempt to find the nearest enclosure, or select the current word
if (!selectedText) {
const nearestEnclosure = findNearestEnclosure(inputField.value, start);
if (nearestEnclosure) {
start = nearestEnclosure.start;
end = nearestEnclosure.end;
selectedText = inputField.value.substring(start, end);
} else {
// Select the current word, find the start and end of the word
const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t";
while (!delimiters.includes(inputField.value[start - 1]) && start > 0) {
start--;
}
while (!delimiters.includes(inputField.value[end]) && end < inputField.value.length) {
end++;
}
selectedText = inputField.value.substring(start, end);
if (!selectedText) return;
}
}
// If the selection ends with a space, remove it
if (selectedText[selectedText.length - 1] === " ") {
selectedText = selectedText.substring(0, selectedText.length - 1);
end -= 1;
}
// If there are parentheses left and right of the selection, select them
if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") {
start -= 1;
end += 1;
selectedText = inputField.value.substring(start, end);
}
// If the selection is not enclosed in parentheses, add them
if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") {
selectedText = `(${selectedText})`;
}
// If the selection does not have a weight, add a weight of 1.0
selectedText = addWeightToParentheses(selectedText);
// Increment the weight
const weightDelta = event.key === "ArrowUp" ? delta : -delta;
const updatedText = selectedText.replace(/\((.*):(\d+(?:\.\d+)?)\)/, (match, text, weight) => {
weight = incrementWeight(weight, weightDelta);
if (weight == 1) {
return text;
} else {
return `(${text}:${weight})`;
}
});
inputField.setRangeText(updatedText, start, end, "select");
}
window.addEventListener("keydown", editAttention);
},
inputField.setRangeText(updatedText, start, end, "select");
}
window.addEventListener("keydown", editAttention);
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import "./groupNodeManage.css";
import { app, type ComfyApp } from "../../scripts/app";
import type { LGraphNode, LGraphNodeConstructor } from "/types/litegraph";
const ORDER: symbol = Symbol();
function merge(target, source) {
@@ -26,11 +25,23 @@ function merge(target, source) {
}
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
tabs: Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}>;
tabs: Record<
"Inputs" | "Outputs" | "Widgets",
{ tab: HTMLAnchorElement; page: HTMLElement }
>;
selectedNodeIndex: number | null | undefined;
selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs";
selectedGroup: string | undefined;
modifications: Record<string, Record<string, Record<string, { name?: string | undefined, visible?: boolean | undefined }>>> = {};
modifications: Record<
string,
Record<
string,
Record<
string,
{ name?: string | undefined; visible?: boolean | undefined }
>
>
> = {};
nodeItems: any[];
app: ComfyApp;
groupNodeType: LGraphNodeConstructor<LGraphNode>;
@@ -86,7 +97,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}
getGroupData() {
this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
this.groupNodeType =
LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
this.groupNodeDef = this.groupNodeType.nodeData;
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
}
@@ -131,24 +143,39 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
this.changeNode(0);
} else {
const items = this.draggable.getAllItems();
let index = items.findIndex(item => item.classList.contains("selected"));
if(index === -1) index = this.selectedNodeIndex;
let index = items.findIndex((item) =>
item.classList.contains("selected")
);
if (index === -1) index = this.selectedNodeIndex;
this.changeNode(index, true);
}
const ordered = [...nodes];
this.draggable?.dispose();
this.draggable = new DraggableList(this.innerNodesList, "li");
this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => {
if (oldPosition === newPosition) return;
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
for (let i = 0; i < ordered.length; i++) {
this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i });
this.draggable.addEventListener(
"dragend",
({ detail: { oldPosition, newPosition } }) => {
if (oldPosition === newPosition) return;
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
for (let i = 0; i < ordered.length; i++) {
this.storeModification({
nodeIndex: ordered[i].index,
section: ORDER,
prop: "order",
value: i,
});
}
}
});
);
}
storeModification(props: { nodeIndex?: number; section: symbol; prop: string; value: any }) {
storeModification(props: {
nodeIndex?: number;
section: symbol;
prop: string;
value: any;
}) {
const { nodeIndex, section, prop, value } = props;
const groupMod = (this.modifications[this.selectedGroup] ??= {});
const nodesMod = (groupMod.nodes ??= {});
@@ -165,7 +192,10 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
if (value === placeholder) value = "";
const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop];
const mods =
this.modifications[this.selectedGroup]?.nodes?.[
this.selectedNodeInnerIndex
]?.[section]?.[prop];
if (mods) {
if (mods.name != null) {
value = mods.name;
@@ -181,7 +211,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
placeholder,
type: "text",
onchange: (e) => {
this.storeModification({ section, prop, value: { name: e.target.value } });
this.storeModification({
section,
prop,
value: { name: e.target.value },
});
},
}),
$el("label", { textContent: "Visible" }, [
@@ -190,7 +224,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
checked,
disabled: !checkable,
onchange: (e) => {
this.storeModification({ section, prop, value: { visible: !!e.target.checked } });
this.storeModification({
section,
prop,
value: { visible: !!e.target.checked },
});
},
}),
]),
@@ -198,13 +236,20 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}
buildWidgetsPage() {
const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
const widgets =
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
const items = Object.keys(widgets ?? {});
const type = app.graph.extra.groupNodes[this.selectedGroup];
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
this.widgetsPage.replaceChildren(
...items.map((oldName) => {
return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false);
return this.getEditElement(
"input",
oldName,
widgets[oldName],
oldName,
config?.[oldName]?.visible !== false
);
})
);
return !!items.length;
@@ -223,7 +268,13 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
return;
}
return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false);
return this.getEditElement(
"input",
oldName,
value,
oldName,
config?.[oldName]?.visible !== false
);
})
.filter(Boolean)
);
@@ -232,9 +283,12 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
buildOutputsPage() {
const nodes = this.groupData.nodeData.nodes;
const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]);
const innerNodeDef = this.groupData.getNodeDef(
nodes[this.selectedNodeInnerIndex]
);
const outputs = innerNodeDef?.output ?? [];
const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
const groupOutputs =
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
const type = app.graph.extra.groupNodes[this.selectedGroup];
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
@@ -250,7 +304,14 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
if (!value || value === oldName) {
value = "";
}
return this.getEditElement("output", slot, value, oldName, visible, checkable);
return this.getEditElement(
"output",
slot,
value,
oldName,
visible,
checkable
);
})
.filter(Boolean)
);
@@ -258,13 +319,21 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}
show(type?) {
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b));
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort(
(a, b) => a.localeCompare(b)
);
this.innerNodesList = $el("ul.comfy-group-manage-list-items") as HTMLUListElement;
this.innerNodesList = $el(
"ul.comfy-group-manage-list-items"
) as HTMLUListElement;
this.widgetsPage = $el("section.comfy-group-manage-node-page");
this.inputsPage = $el("section.comfy-group-manage-node-page");
this.outputsPage = $el("section.comfy-group-manage-node-page");
const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]);
const pages = $el("div", [
this.widgetsPage,
this.inputsPage,
this.outputsPage,
]);
this.tabs = [
["Inputs", this.inputsPage],
@@ -318,12 +387,20 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
{
onclick: (e) => {
// @ts-ignore
const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup);
const node = app.graph._nodes.find(
(n) => n.type === "workflow/" + this.selectedGroup
);
if (node) {
alert("This group node is in use in the current workflow, please first remove these.");
alert(
"This group node is in use in the current workflow, please first remove these."
);
return;
}
if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) {
if (
confirm(
`Are you sure you want to remove the node: "${this.selectedGroup}"`
)
) {
delete app.graph.extra.groupNodes[this.selectedGroup];
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
}
@@ -416,16 +493,22 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
},
"Save"
),
$el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"),
$el(
"button.comfy-btn",
{ onclick: () => this.element.close() },
"Close"
),
]),
]);
this.element.replaceChildren(outer);
this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]);
this.changeGroup(
type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]
);
this.element.showModal();
this.element.addEventListener("close", () => {
this.draggable?.dispose();
});
}
}
}

View File

@@ -1,262 +1,265 @@
import {app} from "../../scripts/app";
import { app } from "../../scripts/app";
function setNodeMode(node, mode) {
node.mode = mode;
node.graph.change();
node.mode = mode;
node.graph.change();
}
function addNodesToGroup(group, nodes=[]) {
var x1, y1, x2, y2;
var nx1, ny1, nx2, ny2;
var node;
function addNodesToGroup(group, nodes = []) {
var x1, y1, x2, y2;
var nx1, ny1, nx2, ny2;
var node;
x1 = y1 = x2 = y2 = -1;
nx1 = ny1 = nx2 = ny2 = -1;
x1 = y1 = x2 = y2 = -1;
nx1 = ny1 = nx2 = ny2 = -1;
for (var n of [group._nodes, nodes]) {
for (var i in n) {
node = n[i]
for (var n of [group._nodes, nodes]) {
for (var i in n) {
node = n[i];
nx1 = node.pos[0]
ny1 = node.pos[1]
nx2 = node.pos[0] + node.size[0]
ny2 = node.pos[1] + node.size[1]
nx1 = node.pos[0];
ny1 = node.pos[1];
nx2 = node.pos[0] + node.size[0];
ny2 = node.pos[1] + node.size[1];
if (node.type != "Reroute") {
ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
}
if (node.type != "Reroute") {
ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
}
if (node.flags?.collapsed) {
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
if (node.flags?.collapsed) {
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
if (node?._collapsed_width) {
nx2 = nx1 + Math.round(node._collapsed_width);
}
}
if (x1 == -1 || nx1 < x1) {
x1 = nx1;
}
if (y1 == -1 || ny1 < y1) {
y1 = ny1;
}
if (x2 == -1 || nx2 > x2) {
x2 = nx2;
}
if (y2 == -1 || ny2 > y2) {
y2 = ny2;
}
if (node?._collapsed_width) {
nx2 = nx1 + Math.round(node._collapsed_width);
}
}
if (x1 == -1 || nx1 < x1) {
x1 = nx1;
}
if (y1 == -1 || ny1 < y1) {
y1 = ny1;
}
if (x2 == -1 || nx2 > x2) {
x2 = nx2;
}
if (y2 == -1 || ny2 > y2) {
y2 = ny2;
}
}
}
var padding = 10;
var padding = 10;
y1 = y1 - Math.round(group.font_size * 1.4);
y1 = y1 - Math.round(group.font_size * 1.4);
group.pos = [x1 - padding, y1 - padding];
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
group.pos = [x1 - padding, y1 - padding];
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
}
app.registerExtension({
name: "Comfy.GroupOptions",
setup() {
// @ts-ignore
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
// graph_mouse
// @ts-ignore
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
const group = this.graph.getGroupOnPos(this.graph_mouse[0], this.graph_mouse[1]);
if (!group) {
options.push({
content: "Add Group For Selected Nodes",
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => {
// @ts-ignore
var group = new LiteGraph.LGraphGroup();
addNodesToGroup(group, this.selected_nodes)
app.canvas.graph.add(group);
this.graph.change();
}
});
name: "Comfy.GroupOptions",
setup() {
// @ts-ignore
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
// graph_mouse
// @ts-ignore
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
const group = this.graph.getGroupOnPos(
this.graph_mouse[0],
this.graph_mouse[1]
);
if (!group) {
options.push({
content: "Add Group For Selected Nodes",
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => {
// @ts-ignore
var group = new LiteGraph.LGraphGroup();
addNodesToGroup(group, this.selected_nodes);
app.canvas.graph.add(group);
this.graph.change();
},
});
return options;
}
return options;
}
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
group.recomputeInsideNodes();
const nodesInGroup = group._nodes;
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
group.recomputeInsideNodes();
const nodesInGroup = group._nodes;
options.push({
content: "Add Selected Nodes To Group",
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => {
addNodesToGroup(group, this.selected_nodes)
this.graph.change();
}
});
options.push({
content: "Add Selected Nodes To Group",
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => {
addNodesToGroup(group, this.selected_nodes);
this.graph.change();
},
});
// No nodes in group, return default options
if (nodesInGroup.length === 0) {
return options;
} else {
// Add a separator between the default options and the group options
options.push(null);
}
// No nodes in group, return default options
if (nodesInGroup.length === 0) {
return options;
} else {
// Add a separator between the default options and the group options
options.push(null);
}
// Check if all nodes are the same mode
let allNodesAreSameMode = true;
for (let i = 1; i < nodesInGroup.length; i++) {
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
allNodesAreSameMode = false;
break;
}
}
options.push({
content: "Fit Group To Nodes",
callback: () => {
addNodesToGroup(group)
this.graph.change();
}
});
options.push({
content: "Select Nodes",
callback: () => {
this.selectNodes(nodesInGroup);
this.graph.change();
this.canvas.focus();
}
});
// Modes
// 0: Always
// 1: On Event
// 2: Never
// 3: On Trigger
// 4: Bypass
// If all nodes are the same mode, add a menu option to change the mode
if (allNodesAreSameMode) {
const mode = nodesInGroup[0].mode;
switch (mode) {
case 0:
// All nodes are always, option to disable, and bypass
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
}
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
}
});
break;
case 2:
// All nodes are never, option to enable, and bypass
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
}
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
}
});
break;
case 4:
// All nodes are bypass, option to enable, and disable
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
}
});
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
}
});
break;
default:
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
}
});
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
}
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
}
});
break;
}
} else {
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
}
});
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
}
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
}
});
}
return options
// Check if all nodes are the same mode
let allNodesAreSameMode = true;
for (let i = 1; i < nodesInGroup.length; i++) {
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
allNodesAreSameMode = false;
break;
}
}
}
options.push({
content: "Fit Group To Nodes",
callback: () => {
addNodesToGroup(group);
this.graph.change();
},
});
options.push({
content: "Select Nodes",
callback: () => {
this.selectNodes(nodesInGroup);
this.graph.change();
this.canvas.focus();
},
});
// Modes
// 0: Always
// 1: On Event
// 2: Never
// 3: On Trigger
// 4: Bypass
// If all nodes are the same mode, add a menu option to change the mode
if (allNodesAreSameMode) {
const mode = nodesInGroup[0].mode;
switch (mode) {
case 0:
// All nodes are always, option to disable, and bypass
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
},
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
},
});
break;
case 2:
// All nodes are never, option to enable, and bypass
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
},
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
},
});
break;
case 4:
// All nodes are bypass, option to enable, and disable
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
},
});
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
},
});
break;
default:
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
},
});
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
},
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
},
});
break;
}
} else {
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
options.push({
content: "Set Group Nodes to Always",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 0);
}
},
});
options.push({
content: "Set Group Nodes to Never",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 2);
}
},
});
options.push({
content: "Bypass Group Nodes",
callback: () => {
for (const node of nodesInGroup) {
setNodeMode(node, 4);
}
},
});
}
return options;
};
},
});

View File

@@ -4,34 +4,34 @@ import { app } from "../../scripts/app";
const id = "Comfy.InvertMenuScrolling";
app.registerExtension({
name: id,
init() {
const ctxMenu = LiteGraph.ContextMenu;
const replace = () => {
// @ts-ignore
LiteGraph.ContextMenu = function (values, options) {
options = options || {};
if (options.scroll_speed) {
options.scroll_speed *= -1;
} else {
options.scroll_speed = -0.1;
}
return ctxMenu.call(this, values, options);
};
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
};
app.ui.settings.addSetting({
id,
name: "Invert Menu Scrolling",
type: "boolean",
defaultValue: false,
onChange(value) {
if (value) {
replace();
} else {
LiteGraph.ContextMenu = ctxMenu;
}
},
});
},
name: id,
init() {
const ctxMenu = LiteGraph.ContextMenu;
const replace = () => {
// @ts-ignore
LiteGraph.ContextMenu = function (values, options) {
options = options || {};
if (options.scroll_speed) {
options.scroll_speed *= -1;
} else {
options.scroll_speed = -0.1;
}
return ctxMenu.call(this, values, options);
};
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
};
app.ui.settings.addSetting({
id,
name: "Invert Menu Scrolling",
type: "boolean",
defaultValue: false,
onChange(value) {
if (value) {
replace();
} else {
LiteGraph.ContextMenu = ctxMenu;
}
},
});
},
});

View File

@@ -1,69 +1,73 @@
import {app} from "../../scripts/app";
import { app } from "../../scripts/app";
app.registerExtension({
name: "Comfy.Keybinds",
init() {
const keybindListener = function (event) {
const modifierPressed = event.ctrlKey || event.metaKey;
name: "Comfy.Keybinds",
init() {
const keybindListener = function (event) {
const modifierPressed = event.ctrlKey || event.metaKey;
// Queue prompt using ctrl or command + enter
if (modifierPressed && event.key === "Enter") {
app.queuePrompt(event.shiftKey ? -1 : 0).then();
return;
}
// Queue prompt using ctrl or command + enter
if (modifierPressed && event.key === "Enter") {
app.queuePrompt(event.shiftKey ? -1 : 0).then();
return;
}
const target = event.composedPath()[0];
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
return;
}
const target = event.composedPath()[0];
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
return;
}
const modifierKeyIdMap = {
s: "#comfy-save-button",
o: "#comfy-file-input",
Backspace: "#comfy-clear-button",
d: "#comfy-load-default-button",
};
const modifierKeyIdMap = {
s: "#comfy-save-button",
o: "#comfy-file-input",
Backspace: "#comfy-clear-button",
d: "#comfy-load-default-button",
};
const modifierKeybindId = modifierKeyIdMap[event.key];
if (modifierPressed && modifierKeybindId) {
event.preventDefault();
const modifierKeybindId = modifierKeyIdMap[event.key];
if (modifierPressed && modifierKeybindId) {
event.preventDefault();
const elem = document.querySelector(modifierKeybindId);
elem.click();
return;
}
const elem = document.querySelector(modifierKeybindId);
elem.click();
return;
}
// Finished Handling all modifier keybinds, now handle the rest
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
// Finished Handling all modifier keybinds, now handle the rest
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}
// Close out of modals using escape
if (event.key === "Escape") {
const modals = document.querySelectorAll<HTMLElement>(".comfy-modal");
const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none");
if (modal) {
modal.style.display = "none";
}
// Close out of modals using escape
if (event.key === "Escape") {
const modals = document.querySelectorAll<HTMLElement>(".comfy-modal");
const modal = Array.from(modals).find(
(modal) =>
window.getComputedStyle(modal).getPropertyValue("display") !==
"none"
);
if (modal) {
modal.style.display = "none";
}
[...document.querySelectorAll("dialog")].forEach(d => {
d.close();
});
}
[...document.querySelectorAll("dialog")].forEach((d) => {
d.close();
});
}
const keyIdMap = {
q: "#comfy-view-queue-button",
h: "#comfy-view-history-button",
r: "#comfy-refresh-button",
};
const keyIdMap = {
q: "#comfy-view-queue-button",
h: "#comfy-view-history-button",
r: "#comfy-refresh-button",
};
const buttonId = keyIdMap[event.key];
if (buttonId) {
const button = document.querySelector(buttonId);
button.click();
}
}
const buttonId = keyIdMap[event.key];
if (buttonId) {
const button = document.querySelector(buttonId);
button.click();
}
};
window.addEventListener("keydown", keybindListener, true);
}
window.addEventListener("keydown", keybindListener, true);
},
});

View File

@@ -2,25 +2,25 @@ import { app } from "../../scripts/app";
const id = "Comfy.LinkRenderMode";
const ext = {
name: id,
async setup(app) {
app.ui.settings.addSetting({
id,
name: "Link Render Mode",
defaultValue: 2,
type: "combo",
// @ts-ignore
options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
value: i,
text: m,
selected: i == app.canvas.links_render_mode,
})),
onChange(value) {
app.canvas.links_render_mode = +value;
app.graph.setDirtyCanvas(true);
},
});
},
name: id,
async setup(app) {
app.ui.settings.addSetting({
id,
name: "Link Render Mode",
defaultValue: 2,
type: "combo",
// @ts-ignore
options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
value: i,
text: m,
selected: i == app.canvas.links_render_mode,
})),
onChange(value) {
app.canvas.links_render_mode = +value;
app.graph.setDirtyCanvas(true);
},
});
},
};
app.registerExtension(ext);

File diff suppressed because it is too large Load Diff

View File

@@ -24,404 +24,416 @@ const id = "Comfy.NodeTemplates";
const file = "comfy.templates.json";
class ManageTemplates extends ComfyDialog {
templates: any[];
draggedEl: HTMLElement | null;
saveVisualCue: number | null;
emptyImg: HTMLImageElement;
importInput: HTMLInputElement;
templates: any[];
draggedEl: HTMLElement | null;
saveVisualCue: number | null;
emptyImg: HTMLImageElement;
importInput: HTMLInputElement;
constructor() {
super();
this.load().then((v) => {
this.templates = v;
});
constructor() {
super();
this.load().then((v) => {
this.templates = v;
});
this.element.classList.add("comfy-manage-templates");
this.draggedEl = null;
this.saveVisualCue = null;
this.emptyImg = new Image();
this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
this.element.classList.add("comfy-manage-templates");
this.draggedEl = null;
this.saveVisualCue = null;
this.emptyImg = new Image();
this.emptyImg.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
this.importInput = $el("input", {
type: "file",
accept: ".json",
multiple: true,
style: { display: "none" },
parent: document.body,
onchange: () => this.importAll(),
}) as HTMLInputElement;
}
this.importInput = $el("input", {
type: "file",
accept: ".json",
multiple: true,
style: { display: "none" },
parent: document.body,
onchange: () => this.importAll(),
}) as HTMLInputElement;
}
createButtons() {
const btns = super.createButtons();
btns[0].textContent = "Close";
btns[0].onclick = (e) => {
clearTimeout(this.saveVisualCue);
this.close();
};
btns.unshift(
$el("button", {
type: "button",
textContent: "Export",
onclick: () => this.exportAll(),
})
);
btns.unshift(
$el("button", {
type: "button",
textContent: "Import",
onclick: () => {
this.importInput.click();
},
})
);
return btns;
}
createButtons() {
const btns = super.createButtons();
btns[0].textContent = "Close";
btns[0].onclick = (e) => {
clearTimeout(this.saveVisualCue);
this.close();
};
btns.unshift(
$el("button", {
type: "button",
textContent: "Export",
onclick: () => this.exportAll(),
})
);
btns.unshift(
$el("button", {
type: "button",
textContent: "Import",
onclick: () => {
this.importInput.click();
},
})
);
return btns;
}
async load() {
let templates = [];
if (app.storageLocation === "server") {
if (app.isNewUserSession) {
// New user so migrate existing templates
const json = localStorage.getItem(id);
if (json) {
templates = JSON.parse(json);
}
await api.storeUserData(file, json, { stringify: false });
} else {
const res = await api.getUserData(file);
if (res.status === 200) {
try {
templates = await res.json();
} catch (error) {
}
} else if (res.status !== 404) {
console.error(res.status + " " + res.statusText);
}
}
} else {
const json = localStorage.getItem(id);
if (json) {
templates = JSON.parse(json);
}
}
async load() {
let templates = [];
if (app.storageLocation === "server") {
if (app.isNewUserSession) {
// New user so migrate existing templates
const json = localStorage.getItem(id);
if (json) {
templates = JSON.parse(json);
}
await api.storeUserData(file, json, { stringify: false });
} else {
const res = await api.getUserData(file);
if (res.status === 200) {
try {
templates = await res.json();
} catch (error) {}
} else if (res.status !== 404) {
console.error(res.status + " " + res.statusText);
}
}
} else {
const json = localStorage.getItem(id);
if (json) {
templates = JSON.parse(json);
}
}
return templates ?? [];
}
return templates ?? [];
}
async store() {
if(app.storageLocation === "server") {
const templates = JSON.stringify(this.templates, undefined, 4);
localStorage.setItem(id, templates); // Backwards compatibility
try {
await api.storeUserData(file, templates, { stringify: false });
} catch (error) {
console.error(error);
alert(error.message);
}
} else {
localStorage.setItem(id, JSON.stringify(this.templates));
}
}
async store() {
if (app.storageLocation === "server") {
const templates = JSON.stringify(this.templates, undefined, 4);
localStorage.setItem(id, templates); // Backwards compatibility
try {
await api.storeUserData(file, templates, { stringify: false });
} catch (error) {
console.error(error);
alert(error.message);
}
} else {
localStorage.setItem(id, JSON.stringify(this.templates));
}
}
async importAll() {
for (const file of this.importInput.files) {
if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader();
reader.onload = async () => {
const importFile = JSON.parse(reader.result as string);
if (importFile?.templates) {
for (const template of importFile.templates) {
if (template?.name && template?.data) {
this.templates.push(template);
}
}
await this.store();
}
};
await reader.readAsText(file);
}
}
async importAll() {
for (const file of this.importInput.files) {
if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader();
reader.onload = async () => {
const importFile = JSON.parse(reader.result as string);
if (importFile?.templates) {
for (const template of importFile.templates) {
if (template?.name && template?.data) {
this.templates.push(template);
}
}
await this.store();
}
};
await reader.readAsText(file);
}
}
this.importInput.value = null;
this.importInput.value = null;
this.close();
}
this.close();
}
exportAll() {
if (this.templates.length == 0) {
alert("No templates to export.");
return;
}
exportAll() {
if (this.templates.length == 0) {
alert("No templates to export.");
return;
}
const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: "node_templates.json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: "node_templates.json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
show() {
// Show list of template names + delete button
super.show(
$el(
"div",
{},
this.templates.flatMap((t,i) => {
let nameInput;
return [
$el(
"div",
{
dataset: { id: i.toString() },
className: "tempateManagerRow",
style: {
display: "grid",
gridTemplateColumns: "1fr auto",
border: "1px dashed transparent",
gap: "5px",
backgroundColor: "var(--comfy-menu-bg)"
},
ondragstart: (e) => {
this.draggedEl = e.currentTarget;
e.currentTarget.style.opacity = "0.6";
e.currentTarget.style.border = "1px dashed yellow";
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
},
ondragend: (e) => {
e.target.style.opacity = "1";
e.currentTarget.style.border = "1px dashed transparent";
e.currentTarget.removeAttribute("draggable");
show() {
// Show list of template names + delete button
super.show(
$el(
"div",
{},
this.templates.flatMap((t, i) => {
let nameInput;
return [
$el(
"div",
{
dataset: { id: i.toString() },
className: "tempateManagerRow",
style: {
display: "grid",
gridTemplateColumns: "1fr auto",
border: "1px dashed transparent",
gap: "5px",
backgroundColor: "var(--comfy-menu-bg)",
},
ondragstart: (e) => {
this.draggedEl = e.currentTarget;
e.currentTarget.style.opacity = "0.6";
e.currentTarget.style.border = "1px dashed yellow";
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
},
ondragend: (e) => {
e.target.style.opacity = "1";
e.currentTarget.style.border = "1px dashed transparent";
e.currentTarget.removeAttribute("draggable");
// rearrange the elements
this.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => {
var prev_i = Number.parseInt(el.dataset.id);
// rearrange the elements
this.element
.querySelectorAll(".tempateManagerRow")
.forEach((el: HTMLElement, i) => {
var prev_i = Number.parseInt(el.dataset.id);
if ( el == this.draggedEl && prev_i != i ) {
this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]);
}
el.dataset.id = i.toString();
});
this.store();
},
ondragover: (e) => {
e.preventDefault();
if ( e.currentTarget == this.draggedEl )
return;
if (el == this.draggedEl && prev_i != i) {
this.templates.splice(
i,
0,
this.templates.splice(prev_i, 1)[0]
);
}
el.dataset.id = i.toString();
});
this.store();
},
ondragover: (e) => {
e.preventDefault();
if (e.currentTarget == this.draggedEl) return;
let rect = e.currentTarget.getBoundingClientRect();
if (e.clientY > rect.top + rect.height / 2) {
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling);
} else {
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget);
}
}
},
[
$el(
"label",
{
textContent: "Name: ",
style: {
cursor: "grab",
},
onmousedown: (e) => {
// enable dragging only from the label
if (e.target.localName == 'label')
e.currentTarget.parentNode.draggable = 'true';
}
},
[
$el("input", {
value: t.name,
dataset: { name: t.name },
style: {
transitionProperty: 'background-color',
transitionDuration: '0s',
},
onchange: (e) => {
clearTimeout(this.saveVisualCue);
var el = e.target;
var row = el.parentNode.parentNode;
this.templates[row.dataset.id].name = el.value.trim() || 'untitled';
this.store();
el.style.backgroundColor = 'rgb(40, 95, 40)';
el.style.transitionDuration = '0s';
// @ts-expect-error
// In browser env the return value is number.
this.saveVisualCue = setTimeout(function () {
el.style.transitionDuration = '.7s';
el.style.backgroundColor = 'var(--comfy-input-bg)';
}, 15);
},
onkeypress: (e) => {
var el = e.target;
clearTimeout(this.saveVisualCue);
el.style.transitionDuration = '0s';
el.style.backgroundColor = 'var(--comfy-input-bg)';
},
$: (el) => (nameInput = el),
})
]
),
$el(
"div",
{},
[
$el("button", {
textContent: "Export",
style: {
fontSize: "12px",
fontWeight: "normal",
},
onclick: (e) => {
const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string
const blob = new Blob([json], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: (nameInput.value || t.name) + ".json",
style: {display: "none"},
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
},
}),
$el("button", {
textContent: "Delete",
style: {
fontSize: "12px",
color: "red",
fontWeight: "normal",
},
onclick: (e) => {
const item = e.target.parentNode.parentNode;
item.parentNode.removeChild(item);
this.templates.splice(item.dataset.id*1, 1);
this.store();
// update the rows index, setTimeout ensures that the list is updated
var that = this;
setTimeout(function (){
that.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => {
el.dataset.id = i.toString();
});
}, 0);
},
}),
]
),
]
)
];
})
)
);
}
let rect = e.currentTarget.getBoundingClientRect();
if (e.clientY > rect.top + rect.height / 2) {
e.currentTarget.parentNode.insertBefore(
this.draggedEl,
e.currentTarget.nextSibling
);
} else {
e.currentTarget.parentNode.insertBefore(
this.draggedEl,
e.currentTarget
);
}
},
},
[
$el(
"label",
{
textContent: "Name: ",
style: {
cursor: "grab",
},
onmousedown: (e) => {
// enable dragging only from the label
if (e.target.localName == "label")
e.currentTarget.parentNode.draggable = "true";
},
},
[
$el("input", {
value: t.name,
dataset: { name: t.name },
style: {
transitionProperty: "background-color",
transitionDuration: "0s",
},
onchange: (e) => {
clearTimeout(this.saveVisualCue);
var el = e.target;
var row = el.parentNode.parentNode;
this.templates[row.dataset.id].name =
el.value.trim() || "untitled";
this.store();
el.style.backgroundColor = "rgb(40, 95, 40)";
el.style.transitionDuration = "0s";
// @ts-expect-error
// In browser env the return value is number.
this.saveVisualCue = setTimeout(function () {
el.style.transitionDuration = ".7s";
el.style.backgroundColor = "var(--comfy-input-bg)";
}, 15);
},
onkeypress: (e) => {
var el = e.target;
clearTimeout(this.saveVisualCue);
el.style.transitionDuration = "0s";
el.style.backgroundColor = "var(--comfy-input-bg)";
},
$: (el) => (nameInput = el),
}),
]
),
$el("div", {}, [
$el("button", {
textContent: "Export",
style: {
fontSize: "12px",
fontWeight: "normal",
},
onclick: (e) => {
const json = JSON.stringify({ templates: [t] }, null, 2); // convert the data to a JSON string
const blob = new Blob([json], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: (nameInput.value || t.name) + ".json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
},
}),
$el("button", {
textContent: "Delete",
style: {
fontSize: "12px",
color: "red",
fontWeight: "normal",
},
onclick: (e) => {
const item = e.target.parentNode.parentNode;
item.parentNode.removeChild(item);
this.templates.splice(item.dataset.id * 1, 1);
this.store();
// update the rows index, setTimeout ensures that the list is updated
var that = this;
setTimeout(function () {
that.element
.querySelectorAll(".tempateManagerRow")
.forEach((el: HTMLElement, i) => {
el.dataset.id = i.toString();
});
}, 0);
},
}),
]),
]
),
];
})
)
);
}
}
app.registerExtension({
name: id,
setup() {
const manage = new ManageTemplates();
name: id,
setup() {
const manage = new ManageTemplates();
const clipboardAction = async (cb) => {
// We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback
const old = localStorage.getItem("litegrapheditor_clipboard");
await cb();
localStorage.setItem("litegrapheditor_clipboard", old);
};
const clipboardAction = async (cb) => {
// We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback
const old = localStorage.getItem("litegrapheditor_clipboard");
await cb();
localStorage.setItem("litegrapheditor_clipboard", old);
};
// @ts-ignore
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
// @ts-ignore
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
// @ts-ignore
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
// @ts-ignore
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
options.push(null);
options.push({
content: `Save Selected as Template`,
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => {
const name = prompt("Enter name");
if (!name?.trim()) return;
options.push(null);
options.push({
content: `Save Selected as Template`,
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => {
const name = prompt("Enter name");
if (!name?.trim()) return;
clipboardAction(() => {
app.canvas.copyToClipboard();
let data = localStorage.getItem("litegrapheditor_clipboard");
data = JSON.parse(data);
const nodeIds = Object.keys(app.canvas.selected_nodes);
for (let i = 0; i < nodeIds.length; i++) {
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]));
// @ts-ignore
const nodeData = node?.constructor.nodeData;
clipboardAction(() => {
app.canvas.copyToClipboard();
let data = localStorage.getItem("litegrapheditor_clipboard");
data = JSON.parse(data);
const nodeIds = Object.keys(app.canvas.selected_nodes);
for (let i = 0; i < nodeIds.length; i++) {
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]));
// @ts-ignore
const nodeData = node?.constructor.nodeData;
let groupData = GroupNodeHandler.getGroupData(node);
if (groupData) {
groupData = groupData.nodeData;
// @ts-ignore
if (!data.groupNodes) {
// @ts-ignore
data.groupNodes = {};
}
// @ts-ignore
data.groupNodes[nodeData.name] = groupData;
// @ts-ignore
data.nodes[i].type = nodeData.name;
}
}
let groupData = GroupNodeHandler.getGroupData(node);
if (groupData) {
groupData = groupData.nodeData;
// @ts-ignore
if (!data.groupNodes) {
// @ts-ignore
data.groupNodes = {};
}
// @ts-ignore
data.groupNodes[nodeData.name] = groupData;
// @ts-ignore
data.nodes[i].type = nodeData.name;
}
}
manage.templates.push({
name,
data: JSON.stringify(data),
});
manage.store();
});
},
});
manage.templates.push({
name,
data: JSON.stringify(data),
});
manage.store();
});
},
});
// Map each template to a menu item
const subItems = manage.templates.map((t) => {
return {
content: t.name,
callback: () => {
clipboardAction(async () => {
const data = JSON.parse(t.data);
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
localStorage.setItem("litegrapheditor_clipboard", t.data);
app.canvas.pasteFromClipboard();
});
},
};
});
// Map each template to a menu item
const subItems = manage.templates.map((t) => {
return {
content: t.name,
callback: () => {
clipboardAction(async () => {
const data = JSON.parse(t.data);
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
localStorage.setItem("litegrapheditor_clipboard", t.data);
app.canvas.pasteFromClipboard();
});
},
};
});
subItems.push(null, {
content: "Manage",
callback: () => manage.show(),
});
subItems.push(null, {
content: "Manage",
callback: () => manage.show(),
});
options.push({
content: "Node Templates",
submenu: {
options: subItems,
},
});
options.push({
content: "Node Templates",
submenu: {
options: subItems,
},
});
return options;
};
},
return options;
};
},
});

View File

@@ -1,53 +1,55 @@
import {app} from "../../scripts/app";
import {ComfyWidgets} from "../../scripts/widgets";
import { app } from "../../scripts/app";
import { ComfyWidgets } from "../../scripts/widgets";
// Node that add notes to your project
app.registerExtension({
name: "Comfy.NoteNode",
registerCustomNodes() {
class NoteNode {
static category: string;
// @ts-ignore
color=LGraphCanvas.node_colors.yellow.color;
// @ts-ignore
bgcolor=LGraphCanvas.node_colors.yellow.bgcolor;
// @ts-ignore
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
properties: { text: string };
serialize_widgets: boolean;
isVirtualNode: boolean;
collapsable: boolean;
title_mode: number;
constructor() {
if (!this.properties) {
this.properties = { text: "" };
}
// @ts-ignore
// Should we extends LGraphNode?
ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app)
this.serialize_widgets = true;
this.isVirtualNode = true;
}
name: "Comfy.NoteNode",
registerCustomNodes() {
class NoteNode {
static category: string;
// @ts-ignore
color = LGraphCanvas.node_colors.yellow.color;
// @ts-ignore
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
// @ts-ignore
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
properties: { text: string };
serialize_widgets: boolean;
isVirtualNode: boolean;
collapsable: boolean;
title_mode: number;
constructor() {
if (!this.properties) {
this.properties = { text: "" };
}
// Load default visibility
LiteGraph.registerNodeType(
"Note",
// @ts-ignore
Object.assign(NoteNode, {
title_mode: LiteGraph.NORMAL_TITLE,
title: "Note",
collapsable: true,
})
ComfyWidgets.STRING(
// @ts-ignore
// Should we extends LGraphNode?
this,
"",
["", { default: this.properties.text, multiline: true }],
app
);
NoteNode.category = "utils";
},
this.serialize_widgets = true;
this.isVirtualNode = true;
}
}
// Load default visibility
LiteGraph.registerNodeType(
"Note",
// @ts-ignore
Object.assign(NoteNode, {
title_mode: LiteGraph.NORMAL_TITLE,
title: "Note",
collapsable: true,
})
);
NoteNode.category = "utils";
},
});

View File

@@ -4,271 +4,311 @@ import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs";
// Node that allows you to redirect connections for cleaner graphs
app.registerExtension({
name: "Comfy.RerouteNode",
registerCustomNodes(app) {
class RerouteNode {
constructor() {
if (!this.properties) {
this.properties = {};
}
this.properties.showOutputText = RerouteNode.defaultVisibility;
this.properties.horizontal = false;
name: "Comfy.RerouteNode",
registerCustomNodes(app) {
class RerouteNode {
constructor() {
if (!this.properties) {
this.properties = {};
}
this.properties.showOutputText = RerouteNode.defaultVisibility;
this.properties.horizontal = false;
this.addInput("", "*");
this.addOutput(this.properties.showOutputText ? "*" : "", "*");
this.addInput("", "*");
this.addOutput(this.properties.showOutputText ? "*" : "", "*");
this.onAfterGraphConfigured = function () {
requestAnimationFrame(() => {
this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
});
};
this.onAfterGraphConfigured = function () {
requestAnimationFrame(() => {
this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
});
};
this.onConnectionsChange = function (type, index, connected, link_info) {
this.applyOrientation();
this.onConnectionsChange = function (
type,
index,
connected,
link_info
) {
this.applyOrientation();
// Prevent multiple connections to different types when we have no input
if (connected && type === LiteGraph.OUTPUT) {
// Ignore wildcard nodes as these will be updated to real types
const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
if (types.size > 1) {
const linksToDisconnect = [];
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
const linkId = this.outputs[0].links[i];
const link = app.graph.links[linkId];
linksToDisconnect.push(link);
}
for (const link of linksToDisconnect) {
const node = app.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
// Prevent multiple connections to different types when we have no input
if (connected && type === LiteGraph.OUTPUT) {
// Ignore wildcard nodes as these will be updated to real types
const types = new Set(
this.outputs[0].links
.map((l) => app.graph.links[l].type)
.filter((t) => t !== "*")
);
if (types.size > 1) {
const linksToDisconnect = [];
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
const linkId = this.outputs[0].links[i];
const link = app.graph.links[linkId];
linksToDisconnect.push(link);
}
for (const link of linksToDisconnect) {
const node = app.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
// Find root input
let currentNode = this;
let updateNodes = [];
let inputType = null;
let inputNode = null;
while (currentNode) {
updateNodes.unshift(currentNode);
const linkId = currentNode.inputs[0].link;
if (linkId !== null) {
const link = app.graph.links[linkId];
if (!link) return;
const node = app.graph.getNodeById(link.origin_id);
const type = node.constructor.type;
if (type === "Reroute") {
if (node === this) {
// We've found a circle
currentNode.disconnectInput(link.target_slot);
currentNode = null;
} else {
// Move the previous node
currentNode = node;
}
} else {
// We've found the end
inputNode = currentNode;
inputType = node.outputs[link.origin_slot]?.type ?? null;
break;
}
} else {
// This path has no input node
currentNode = null;
break;
}
}
// Find root input
let currentNode = this;
let updateNodes = [];
let inputType = null;
let inputNode = null;
while (currentNode) {
updateNodes.unshift(currentNode);
const linkId = currentNode.inputs[0].link;
if (linkId !== null) {
const link = app.graph.links[linkId];
if (!link) return;
const node = app.graph.getNodeById(link.origin_id);
const type = node.constructor.type;
if (type === "Reroute") {
if (node === this) {
// We've found a circle
currentNode.disconnectInput(link.target_slot);
currentNode = null;
} else {
// Move the previous node
currentNode = node;
}
} else {
// We've found the end
inputNode = currentNode;
inputType = node.outputs[link.origin_slot]?.type ?? null;
break;
}
} else {
// This path has no input node
currentNode = null;
break;
}
}
// Find all outputs
const nodes = [this];
let outputType = null;
while (nodes.length) {
currentNode = nodes.pop();
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
if (outputs.length) {
for (const linkId of outputs) {
const link = app.graph.links[linkId];
// Find all outputs
const nodes = [this];
let outputType = null;
while (nodes.length) {
currentNode = nodes.pop();
const outputs =
(currentNode.outputs ? currentNode.outputs[0].links : []) || [];
if (outputs.length) {
for (const linkId of outputs) {
const link = app.graph.links[linkId];
// When disconnecting sometimes the link is still registered
if (!link) continue;
// When disconnecting sometimes the link is still registered
if (!link) continue;
const node = app.graph.getNodeById(link.target_id);
const type = node.constructor.type;
const node = app.graph.getNodeById(link.target_id);
const type = node.constructor.type;
if (type === "Reroute") {
// Follow reroute nodes
nodes.push(node);
updateNodes.push(node);
} else {
// We've found an output
const nodeOutType =
node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type
? node.inputs[link.target_slot].type
: null;
if (inputType && inputType !== "*" && nodeOutType !== inputType) {
// The output doesnt match our input so disconnect it
node.disconnectInput(link.target_slot);
} else {
outputType = nodeOutType;
}
}
}
} else {
// No more outputs for this path
}
}
if (type === "Reroute") {
// Follow reroute nodes
nodes.push(node);
updateNodes.push(node);
} else {
// We've found an output
const nodeOutType =
node.inputs &&
node.inputs[link?.target_slot] &&
node.inputs[link.target_slot].type
? node.inputs[link.target_slot].type
: null;
if (
inputType &&
inputType !== "*" &&
nodeOutType !== inputType
) {
// The output doesnt match our input so disconnect it
node.disconnectInput(link.target_slot);
} else {
outputType = nodeOutType;
}
}
}
} else {
// No more outputs for this path
}
}
const displayType = inputType || outputType || "*";
const color = LGraphCanvas.link_type_colors[displayType];
const displayType = inputType || outputType || "*";
const color = LGraphCanvas.link_type_colors[displayType];
let widgetConfig;
let targetWidget;
let widgetType;
// Update the types of each node
for (const node of updateNodes) {
// If we dont have an input type we are always wildcard but we'll show the output type
// This lets you change the output link to a different type and all nodes will update
node.outputs[0].type = inputType || "*";
node.__outputType = displayType;
node.outputs[0].name = node.properties.showOutputText ? displayType : "";
node.size = node.computeSize();
node.applyOrientation();
let widgetConfig;
let targetWidget;
let widgetType;
// Update the types of each node
for (const node of updateNodes) {
// If we dont have an input type we are always wildcard but we'll show the output type
// This lets you change the output link to a different type and all nodes will update
node.outputs[0].type = inputType || "*";
node.__outputType = displayType;
node.outputs[0].name = node.properties.showOutputText
? displayType
: "";
node.size = node.computeSize();
node.applyOrientation();
for (const l of node.outputs[0].links || []) {
const link = app.graph.links[l];
if (link) {
link.color = color;
for (const l of node.outputs[0].links || []) {
const link = app.graph.links[l];
if (link) {
link.color = color;
if (app.configuringGraph) continue;
const targetNode = app.graph.getNodeById(link.target_id);
const targetInput = targetNode.inputs?.[link.target_slot];
if (targetInput?.widget) {
const config = getWidgetConfig(targetInput);
if (!widgetConfig) {
widgetConfig = config[1] ?? {};
widgetType = config[0];
}
if (!targetWidget) {
targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name);
}
if (app.configuringGraph) continue;
const targetNode = app.graph.getNodeById(link.target_id);
const targetInput = targetNode.inputs?.[link.target_slot];
if (targetInput?.widget) {
const config = getWidgetConfig(targetInput);
if (!widgetConfig) {
widgetConfig = config[1] ?? {};
widgetType = config[0];
}
if (!targetWidget) {
targetWidget = targetNode.widgets?.find(
(w) => w.name === targetInput.widget.name
);
}
const merged = mergeIfValid(targetInput, [config[0], widgetConfig]);
if (merged.customConfig) {
widgetConfig = merged.customConfig;
}
}
}
}
}
const merged = mergeIfValid(targetInput, [
config[0],
widgetConfig,
]);
if (merged.customConfig) {
widgetConfig = merged.customConfig;
}
}
}
}
}
for (const node of updateNodes) {
if (widgetConfig && outputType) {
node.inputs[0].widget = { name: "value" };
setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget);
} else {
setWidgetConfig(node.inputs[0], null);
}
}
for (const node of updateNodes) {
if (widgetConfig && outputType) {
node.inputs[0].widget = { name: "value" };
setWidgetConfig(
node.inputs[0],
[widgetType ?? displayType, widgetConfig],
targetWidget
);
} else {
setWidgetConfig(node.inputs[0], null);
}
}
if (inputNode) {
const link = app.graph.links[inputNode.inputs[0].link];
if (link) {
link.color = color;
}
}
};
if (inputNode) {
const link = app.graph.links[inputNode.inputs[0].link];
if (link) {
link.color = color;
}
}
};
this.clone = function () {
const cloned = RerouteNode.prototype.clone.apply(this);
cloned.removeOutput(0);
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
cloned.size = cloned.computeSize();
return cloned;
};
this.clone = function () {
const cloned = RerouteNode.prototype.clone.apply(this);
cloned.removeOutput(0);
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
cloned.size = cloned.computeSize();
return cloned;
};
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
this.isVirtualNode = true;
}
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
this.isVirtualNode = true;
}
getExtraMenuOptions(_, options) {
options.unshift(
{
content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
callback: () => {
this.properties.showOutputText = !this.properties.showOutputText;
if (this.properties.showOutputText) {
this.outputs[0].name = this.__outputType || this.outputs[0].type;
} else {
this.outputs[0].name = "";
}
this.size = this.computeSize();
this.applyOrientation();
app.graph.setDirtyCanvas(true, true);
},
},
{
content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default",
callback: () => {
RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility);
},
},
{
// naming is inverted with respect to LiteGraphNode.horizontal
// LiteGraphNode.horizontal == true means that
// each slot in the inputs and outputs are layed out horizontally,
// which is the opposite of the visual orientation of the inputs and outputs as a node
content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
callback: () => {
this.properties.horizontal = !this.properties.horizontal;
this.applyOrientation();
},
}
);
}
applyOrientation() {
this.horizontal = this.properties.horizontal;
if (this.horizontal) {
// we correct the input position, because LiteGraphNode.horizontal
// doesn't account for title presence
// which reroute nodes don't have
this.inputs[0].pos = [this.size[0] / 2, 0];
} else {
delete this.inputs[0].pos;
}
app.graph.setDirtyCanvas(true, true);
}
getExtraMenuOptions(_, options) {
options.unshift(
{
content:
(this.properties.showOutputText ? "Hide" : "Show") + " Type",
callback: () => {
this.properties.showOutputText = !this.properties.showOutputText;
if (this.properties.showOutputText) {
this.outputs[0].name =
this.__outputType || this.outputs[0].type;
} else {
this.outputs[0].name = "";
}
this.size = this.computeSize();
this.applyOrientation();
app.graph.setDirtyCanvas(true, true);
},
},
{
content:
(RerouteNode.defaultVisibility ? "Hide" : "Show") +
" Type By Default",
callback: () => {
RerouteNode.setDefaultTextVisibility(
!RerouteNode.defaultVisibility
);
},
},
{
// naming is inverted with respect to LiteGraphNode.horizontal
// LiteGraphNode.horizontal == true means that
// each slot in the inputs and outputs are layed out horizontally,
// which is the opposite of the visual orientation of the inputs and outputs as a node
content:
"Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
callback: () => {
this.properties.horizontal = !this.properties.horizontal;
this.applyOrientation();
},
}
);
}
applyOrientation() {
this.horizontal = this.properties.horizontal;
if (this.horizontal) {
// we correct the input position, because LiteGraphNode.horizontal
// doesn't account for title presence
// which reroute nodes don't have
this.inputs[0].pos = [this.size[0] / 2, 0];
} else {
delete this.inputs[0].pos;
}
app.graph.setDirtyCanvas(true, true);
}
computeSize() {
return [
this.properties.showOutputText && this.outputs && this.outputs.length
? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40)
: 75,
26,
];
}
computeSize() {
return [
this.properties.showOutputText && this.outputs && this.outputs.length
? Math.max(
75,
LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 +
40
)
: 75,
26,
];
}
static setDefaultTextVisibility(visible) {
RerouteNode.defaultVisibility = visible;
if (visible) {
localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
} else {
delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
}
}
}
static setDefaultTextVisibility(visible) {
RerouteNode.defaultVisibility = visible;
if (visible) {
localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
} else {
delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
}
}
}
// Load default visibility
RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]);
// Load default visibility
RerouteNode.setDefaultTextVisibility(
!!localStorage["Comfy.RerouteNode.DefaultVisibility"]
);
LiteGraph.registerNodeType(
"Reroute",
Object.assign(RerouteNode, {
title_mode: LiteGraph.NO_TITLE,
title: "Reroute",
collapsable: false,
})
);
LiteGraph.registerNodeType(
"Reroute",
Object.assign(RerouteNode, {
title_mode: LiteGraph.NO_TITLE,
title: "Reroute",
collapsable: false,
})
);
RerouteNode.category = "utils";
},
RerouteNode.category = "utils";
},
});

View File

@@ -3,33 +3,41 @@ import { applyTextReplacements } from "../../scripts/utils";
// Use widget values and dates in output filenames
app.registerExtension({
name: "Comfy.SaveImageExtraOutput",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SaveImage") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
name: "Comfy.SaveImageExtraOutput",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SaveImage") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
const widget = this.widgets.find((w) => w.name === "filename_prefix");
widget.serializeValue = () => {
return applyTextReplacements(app, widget.value);
};
const widget = this.widgets.find((w) => w.name === "filename_prefix");
widget.serializeValue = () => {
return applyTextReplacements(app, widget.value);
};
return r;
};
} else {
// When any other node is created add a property to alias the node
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
return r;
};
} else {
// When any other node is created add a property to alias the node
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
if (!this.properties || !("Node name for S&R" in this.properties)) {
this.addProperty("Node name for S&R", this.constructor.type, "string");
}
if (!this.properties || !("Node name for S&R" in this.properties)) {
this.addProperty(
"Node name for S&R",
this.constructor.type,
"string"
);
}
return r;
};
}
},
return r;
};
}
},
});

View File

@@ -4,108 +4,111 @@ let touchZooming;
let touchCount = 0;
app.registerExtension({
name: "Comfy.SimpleTouchSupport",
setup() {
let zoomPos;
let touchTime;
let lastTouch;
name: "Comfy.SimpleTouchSupport",
setup() {
let zoomPos;
let touchTime;
let lastTouch;
function getMultiTouchPos(e) {
return Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
}
function getMultiTouchPos(e) {
return Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
}
app.canvasEl.addEventListener(
"touchstart",
(e) => {
touchCount++;
lastTouch = null;
if (e.touches?.length === 1) {
// Store start time for press+hold for context menu
touchTime = new Date();
lastTouch = e.touches[0];
} else {
touchTime = null;
if (e.touches?.length === 2) {
// Store center pos for zoom
zoomPos = getMultiTouchPos(e);
app.canvas.pointer_is_down = false;
}
}
},
true
);
app.canvasEl.addEventListener(
"touchstart",
(e) => {
touchCount++;
lastTouch = null;
if (e.touches?.length === 1) {
// Store start time for press+hold for context menu
touchTime = new Date();
lastTouch = e.touches[0];
} else {
touchTime = null;
if (e.touches?.length === 2) {
// Store center pos for zoom
zoomPos = getMultiTouchPos(e);
app.canvas.pointer_is_down = false;
}
}
},
true
);
app.canvasEl.addEventListener("touchend", (e: TouchEvent) => {
touchZooming = false;
touchCount = e.touches?.length ?? touchCount - 1;
if (touchTime && !e.touches?.length) {
if ((new Date()).getTime() - touchTime > 600) {
try {
// hack to get litegraph to use this event
e.constructor = CustomEvent;
} catch (error) {}
// @ts-ignore
e.clientX = lastTouch.clientX;
// @ts-ignore
e.clientY = lastTouch.clientY;
app.canvasEl.addEventListener("touchend", (e: TouchEvent) => {
touchZooming = false;
touchCount = e.touches?.length ?? touchCount - 1;
if (touchTime && !e.touches?.length) {
if (new Date().getTime() - touchTime > 600) {
try {
// hack to get litegraph to use this event
e.constructor = CustomEvent;
} catch (error) {}
// @ts-ignore
e.clientX = lastTouch.clientX;
// @ts-ignore
e.clientY = lastTouch.clientY;
app.canvas.pointer_is_down = true;
// @ts-ignore
app.canvas._mousedown_callback(e);
}
touchTime = null;
}
});
app.canvas.pointer_is_down = true;
// @ts-ignore
app.canvas._mousedown_callback(e);
}
touchTime = null;
}
});
app.canvasEl.addEventListener(
"touchmove",
(e) => {
touchTime = null;
if (e.touches?.length === 2) {
app.canvas.pointer_is_down = false;
touchZooming = true;
// @ts-ignore
LiteGraph.closeAllContextMenus();
// @ts-ignore
app.canvas.search_box?.close();
const newZoomPos = getMultiTouchPos(e);
app.canvasEl.addEventListener(
"touchmove",
(e) => {
touchTime = null;
if (e.touches?.length === 2) {
app.canvas.pointer_is_down = false;
touchZooming = true;
// @ts-ignore
LiteGraph.closeAllContextMenus();
// @ts-ignore
app.canvas.search_box?.close();
const newZoomPos = getMultiTouchPos(e);
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
let scale = app.canvas.ds.scale;
const diff = zoomPos - newZoomPos;
if (diff > 0.5) {
scale *= 1 / 1.07;
} else if (diff < -0.5) {
scale *= 1.07;
}
app.canvas.ds.changeScale(scale, [midX, midY]);
app.canvas.setDirty(true, true);
zoomPos = newZoomPos;
}
},
true
);
},
let scale = app.canvas.ds.scale;
const diff = zoomPos - newZoomPos;
if (diff > 0.5) {
scale *= 1 / 1.07;
} else if (diff < -0.5) {
scale *= 1.07;
}
app.canvas.ds.changeScale(scale, [midX, midY]);
app.canvas.setDirty(true, true);
zoomPos = newZoomPos;
}
},
true
);
},
});
// @ts-ignore
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
// @ts-ignore
LGraphCanvas.prototype.processMouseDown = function (e) {
if (touchZooming || touchCount) {
return;
}
return processMouseDown.apply(this, arguments);
if (touchZooming || touchCount) {
return;
}
return processMouseDown.apply(this, arguments);
};
// @ts-ignore
const processMouseMove = LGraphCanvas.prototype.processMouseMove;
// @ts-ignore
LGraphCanvas.prototype.processMouseMove = function (e) {
if (touchZooming || touchCount > 1) {
return;
}
return processMouseMove.apply(this, arguments);
if (touchZooming || touchCount > 1) {
return;
}
return processMouseMove.apply(this, arguments);
};

View File

@@ -3,89 +3,94 @@ import { ComfyWidgets } from "../../scripts/widgets";
// Adds defaults for quickly adding nodes with middle click on the input/output
app.registerExtension({
name: "Comfy.SlotDefaults",
suggestionsNumber: null,
init() {
LiteGraph.search_filter_enabled = true;
LiteGraph.middle_click_slot_add_default_node = true;
this.suggestionsNumber = app.ui.settings.addSetting({
id: "Comfy.NodeSuggestions.number",
name: "Number of nodes suggestions",
type: "slider",
attrs: {
min: 1,
max: 100,
step: 1,
},
defaultValue: 5,
onChange: (newVal, oldVal) => {
this.setDefaults(newVal);
}
});
},
slot_types_default_out: {},
slot_types_default_in: {},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
var nodeId = nodeData.name;
var inputs = [];
inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs
for (const inputKey in inputs) {
var input = (inputs[inputKey]);
if (typeof input[0] !== "string") continue;
name: "Comfy.SlotDefaults",
suggestionsNumber: null,
init() {
LiteGraph.search_filter_enabled = true;
LiteGraph.middle_click_slot_add_default_node = true;
this.suggestionsNumber = app.ui.settings.addSetting({
id: "Comfy.NodeSuggestions.number",
name: "Number of nodes suggestions",
type: "slider",
attrs: {
min: 1,
max: 100,
step: 1,
},
defaultValue: 5,
onChange: (newVal, oldVal) => {
this.setDefaults(newVal);
},
});
},
slot_types_default_out: {},
slot_types_default_in: {},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
var nodeId = nodeData.name;
var inputs = [];
inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs
for (const inputKey in inputs) {
var input = inputs[inputKey];
if (typeof input[0] !== "string") continue;
var type = input[0]
if (type in ComfyWidgets) {
var customProperties = input[1]
if (!(customProperties?.forceInput)) continue; //ignore widgets that don't force input
}
var type = input[0];
if (type in ComfyWidgets) {
var customProperties = input[1];
if (!customProperties?.forceInput) continue; //ignore widgets that don't force input
}
if (!(type in this.slot_types_default_out)) {
this.slot_types_default_out[type] = ["Reroute"];
}
if (this.slot_types_default_out[type].includes(nodeId)) continue;
this.slot_types_default_out[type].push(nodeId);
if (!(type in this.slot_types_default_out)) {
this.slot_types_default_out[type] = ["Reroute"];
}
if (this.slot_types_default_out[type].includes(nodeId)) continue;
this.slot_types_default_out[type].push(nodeId);
// Input types have to be stored as lower case
// Store each node that can handle this input type
const lowerType = type.toLocaleLowerCase();
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
}
LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeType.comfyClass);
}
// Input types have to be stored as lower case
// Store each node that can handle this input type
const lowerType = type.toLocaleLowerCase();
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
}
LiteGraph.registered_slot_in_types[lowerType].nodes.push(
nodeType.comfyClass
);
}
var outputs = nodeData["output"];
for (const key in outputs) {
var type = outputs[key];
if (!(type in this.slot_types_default_in)) {
this.slot_types_default_in[type] = ["Reroute"];// ["Reroute", "Primitive"]; primitive doesn't always work :'()
}
var outputs = nodeData["output"];
for (const key in outputs) {
var type = outputs[key];
if (!(type in this.slot_types_default_in)) {
this.slot_types_default_in[type] = ["Reroute"]; // ["Reroute", "Primitive"]; primitive doesn't always work :'()
}
this.slot_types_default_in[type].push(nodeId);
this.slot_types_default_in[type].push(nodeId);
// Store each node that can handle this output type
if (!(type in LiteGraph.registered_slot_out_types)) {
LiteGraph.registered_slot_out_types[type] = { nodes: [] };
}
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
// Store each node that can handle this output type
if (!(type in LiteGraph.registered_slot_out_types)) {
LiteGraph.registered_slot_out_types[type] = { nodes: [] };
}
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
if(!LiteGraph.slot_types_out.includes(type)) {
LiteGraph.slot_types_out.push(type);
}
}
var maxNum = this.suggestionsNumber.value;
this.setDefaults(maxNum);
},
setDefaults(maxNum) {
if (!LiteGraph.slot_types_out.includes(type)) {
LiteGraph.slot_types_out.push(type);
}
}
var maxNum = this.suggestionsNumber.value;
this.setDefaults(maxNum);
},
setDefaults(maxNum) {
LiteGraph.slot_types_default_out = {};
LiteGraph.slot_types_default_in = {};
LiteGraph.slot_types_default_out = {};
LiteGraph.slot_types_default_in = {};
for (const type in this.slot_types_default_out) {
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum);
}
for (const type in this.slot_types_default_in) {
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum);
}
}
for (const type in this.slot_types_default_out) {
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[
type
].slice(0, maxNum);
}
for (const type in this.slot_types_default_in) {
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[
type
].slice(0, maxNum);
}
},
});

View File

@@ -4,178 +4,192 @@ import { app } from "../../scripts/app";
/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
function roundVectorToGrid(vec) {
vec[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE);
vec[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE);
return vec;
vec[0] =
LiteGraph.CANVAS_GRID_SIZE *
Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE);
vec[1] =
LiteGraph.CANVAS_GRID_SIZE *
Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE);
return vec;
}
app.registerExtension({
name: "Comfy.SnapToGrid",
init() {
// Add setting to control grid size
app.ui.settings.addSetting({
id: "Comfy.SnapToGrid.GridSize",
name: "Grid Size",
type: "slider",
attrs: {
min: 1,
max: 500,
},
tooltip:
"When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
onChange(value) {
LiteGraph.CANVAS_GRID_SIZE = +value;
},
});
name: "Comfy.SnapToGrid",
init() {
// Add setting to control grid size
app.ui.settings.addSetting({
id: "Comfy.SnapToGrid.GridSize",
name: "Grid Size",
type: "slider",
attrs: {
min: 1,
max: 500,
},
tooltip:
"When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
onChange(value) {
LiteGraph.CANVAS_GRID_SIZE = +value;
},
});
// After moving a node, if the shift key is down align it to grid
const onNodeMoved = app.canvas.onNodeMoved;
app.canvas.onNodeMoved = function (node) {
const r = onNodeMoved?.apply(this, arguments);
// After moving a node, if the shift key is down align it to grid
const onNodeMoved = app.canvas.onNodeMoved;
app.canvas.onNodeMoved = function (node) {
const r = onNodeMoved?.apply(this, arguments);
if (app.shiftDown) {
// Ensure all selected nodes are realigned
for (const id in this.selected_nodes) {
this.selected_nodes[id].alignToGrid();
}
}
if (app.shiftDown) {
// Ensure all selected nodes are realigned
for (const id in this.selected_nodes) {
this.selected_nodes[id].alignToGrid();
}
}
return r;
};
return r;
};
// When a node is added, add a resize handler to it so we can fix align the size with the grid
const onNodeAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function (node) {
const onResize = node.onResize;
node.onResize = function () {
if (app.shiftDown) {
roundVectorToGrid(node.size);
}
return onResize?.apply(this, arguments);
};
return onNodeAdded?.apply(this, arguments);
};
// When a node is added, add a resize handler to it so we can fix align the size with the grid
const onNodeAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function (node) {
const onResize = node.onResize;
node.onResize = function () {
if (app.shiftDown) {
roundVectorToGrid(node.size);
}
return onResize?.apply(this, arguments);
};
return onNodeAdded?.apply(this, arguments);
};
// Draw a preview of where the node will go if holding shift and the node is selected
// @ts-ignore
const origDrawNode = LGraphCanvas.prototype.drawNode;
// @ts-ignore
LGraphCanvas.prototype.drawNode = function (node, ctx) {
if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
const [x, y] = roundVectorToGrid([...node.pos]);
const shiftX = x - node.pos[0];
let shiftY = y - node.pos[1];
// Draw a preview of where the node will go if holding shift and the node is selected
// @ts-ignore
const origDrawNode = LGraphCanvas.prototype.drawNode;
// @ts-ignore
LGraphCanvas.prototype.drawNode = function (node, ctx) {
if (
app.shiftDown &&
this.node_dragged &&
node.id in this.selected_nodes
) {
const [x, y] = roundVectorToGrid([...node.pos]);
const shiftX = x - node.pos[0];
let shiftY = y - node.pos[1];
let w, h;
if (node.flags.collapsed) {
w = node._collapsed_width;
h = LiteGraph.NODE_TITLE_HEIGHT;
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
} else {
w = node.size[0];
h = node.size[1];
let titleMode = node.constructor.title_mode;
if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) {
h += LiteGraph.NODE_TITLE_HEIGHT;
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
}
}
const f = ctx.fillStyle;
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
ctx.fillRect(shiftX, shiftY, w, h);
ctx.fillStyle = f;
}
let w, h;
if (node.flags.collapsed) {
w = node._collapsed_width;
h = LiteGraph.NODE_TITLE_HEIGHT;
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
} else {
w = node.size[0];
h = node.size[1];
let titleMode = node.constructor.title_mode;
if (
titleMode !== LiteGraph.TRANSPARENT_TITLE &&
titleMode !== LiteGraph.NO_TITLE
) {
h += LiteGraph.NODE_TITLE_HEIGHT;
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
}
}
const f = ctx.fillStyle;
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
ctx.fillRect(shiftX, shiftY, w, h);
ctx.fillStyle = f;
}
return origDrawNode.apply(this, arguments);
};
return origDrawNode.apply(this, arguments);
};
/**
* The currently moving, selected group only. Set after the `selected_group` has actually started
* moving.
*/
let selectedAndMovingGroup = null;
/**
* The currently moving, selected group only. Set after the `selected_group` has actually started
* moving.
*/
let selectedAndMovingGroup = null;
/**
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
* below) as well as handle the last move call from LiteGraph's `processMouseUp`.
*/
// @ts-ignore
const groupMove = LGraphGroup.prototype.move;
// @ts-ignore
LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
const v = groupMove.apply(this, arguments);
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
// too eagerly and we don't want to behave like we're moving until we get a delta.
if (!selectedAndMovingGroup && app.canvas.selected_group === this && (deltax || deltay)) {
selectedAndMovingGroup = this;
}
/**
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
* below) as well as handle the last move call from LiteGraph's `processMouseUp`.
*/
// @ts-ignore
const groupMove = LGraphGroup.prototype.move;
// @ts-ignore
LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
const v = groupMove.apply(this, arguments);
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
// too eagerly and we don't want to behave like we're moving until we get a delta.
if (
!selectedAndMovingGroup &&
app.canvas.selected_group === this &&
(deltax || deltay)
) {
selectedAndMovingGroup = this;
}
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
// has been set to `false`. Essentially, this check here is the equivilant to calling an
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
// align the group itself.
this.recomputeInsideNodes();
for (const node of this._nodes) {
node.alignToGrid();
}
// @ts-ignore
LGraphNode.prototype.alignToGrid.apply(this);
}
return v;
};
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
// has been set to `false`. Essentially, this check here is the equivilant to calling an
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
// align the group itself.
this.recomputeInsideNodes();
for (const node of this._nodes) {
node.alignToGrid();
}
// @ts-ignore
LGraphNode.prototype.alignToGrid.apply(this);
}
return v;
};
/**
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
* drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
* both.
*/
// @ts-ignore
const drawGroups = LGraphCanvas.prototype.drawGroups;
// @ts-ignore
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
if (this.selected_group && app.shiftDown) {
if (this.selected_group_resizing) {
roundVectorToGrid(this.selected_group.size);
} else if (selectedAndMovingGroup) {
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
const f = ctx.fillStyle;
const s = ctx.strokeStyle;
ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
ctx.rect(x, y, ...selectedAndMovingGroup.size);
ctx.fill();
ctx.stroke();
ctx.fillStyle = f;
ctx.strokeStyle = s;
}
} else if (!this.selected_group) {
selectedAndMovingGroup = null;
}
return drawGroups.apply(this, arguments);
};
/**
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
* drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
* both.
*/
// @ts-ignore
const drawGroups = LGraphCanvas.prototype.drawGroups;
// @ts-ignore
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
if (this.selected_group && app.shiftDown) {
if (this.selected_group_resizing) {
roundVectorToGrid(this.selected_group.size);
} else if (selectedAndMovingGroup) {
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
const f = ctx.fillStyle;
const s = ctx.strokeStyle;
ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
ctx.rect(x, y, ...selectedAndMovingGroup.size);
ctx.fill();
ctx.stroke();
ctx.fillStyle = f;
ctx.strokeStyle = s;
}
} else if (!this.selected_group) {
selectedAndMovingGroup = null;
}
return drawGroups.apply(this, arguments);
};
/** Handles adding a group in a snapping-enabled state. */
// @ts-ignore
const onGroupAdd = LGraphCanvas.onGroupAdd;
// @ts-ignore
LGraphCanvas.onGroupAdd = function() {
const v = onGroupAdd.apply(app.canvas, arguments);
if (app.shiftDown) {
// @ts-ignore
const lastGroup = app.graph._groups[app.graph._groups.length - 1];
if (lastGroup) {
// @ts-ignore
roundVectorToGrid(lastGroup.pos);
// @ts-ignore
roundVectorToGrid(lastGroup.size);
}
}
return v;
};
},
/** Handles adding a group in a snapping-enabled state. */
// @ts-ignore
const onGroupAdd = LGraphCanvas.onGroupAdd;
// @ts-ignore
LGraphCanvas.onGroupAdd = function () {
const v = onGroupAdd.apply(app.canvas, arguments);
if (app.shiftDown) {
// @ts-ignore
const lastGroup = app.graph._groups[app.graph._groups.length - 1];
if (lastGroup) {
// @ts-ignore
roundVectorToGrid(lastGroup.pos);
// @ts-ignore
roundVectorToGrid(lastGroup.size);
}
}
return v;
};
},
});

View File

@@ -11,10 +11,17 @@ function splitFilePath(path: string): [string, string] {
if (folder_separator === -1) {
return ["", path];
}
return [path.substring(0, folder_separator), path.substring(folder_separator + 1)];
return [
path.substring(0, folder_separator),
path.substring(folder_separator + 1),
];
}
function getResourceURL(subfolder: string, filename: string, type: FolderType = "input"): string {
function getResourceURL(
subfolder: string,
filename: string,
type: FolderType = "input"
): string {
const params = [
"filename=" + encodeURIComponent(filename),
"type=" + type,
@@ -54,7 +61,9 @@ async function uploadFile(
}
if (updateNode) {
audioUIWidget.element.src = api.apiURL(getResourceURL(...splitFilePath(path)));
audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(path))
);
audioWidget.value = path;
}
} else {
@@ -70,7 +79,9 @@ async function uploadFile(
app.registerExtension({
name: "Comfy.AudioWidget",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) {
if (
["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)
) {
nodeData.input.required.audioUI = ["AUDIO_UI"];
}
},
@@ -82,7 +93,11 @@ app.registerExtension({
audio.classList.add("comfy-audio");
audio.setAttribute("name", "media");
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.addDOMWidget(inputName, /* name=*/ "audioUI", audio);
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.addDOMWidget(
inputName,
/* name=*/ "audioUI",
audio
);
// @ts-ignore
// TODO: Sort out the DOMWidget type.
audioUIWidget.serialize = false;
@@ -98,21 +113,27 @@ app.registerExtension({
const audios = message.audio;
if (!audios) return;
const audio = audios[0];
audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type));
audioUIWidget.element.src = api.apiURL(
getResourceURL(audio.subfolder, audio.filename, audio.type)
);
audioUIWidget.element.classList.remove("empty-audio-widget");
}
};
}
return { widget: audioUIWidget };
}
}
},
};
},
onNodeOutputsUpdated(nodeOutputs: Record<number, any>) {
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
const node = app.graph.getNodeById(Number.parseInt(nodeId));
if ("audio" in output) {
const audioUIWidget = node.widgets.find((w) => w.name === "audioUI") as unknown as DOMWidget<HTMLAudioElement>;
const audioUIWidget = node.widgets.find(
(w) => w.name === "audioUI"
) as unknown as DOMWidget<HTMLAudioElement>;
const audio = output.audio[0];
audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type));
audioUIWidget.element.src = api.apiURL(
getResourceURL(audio.subfolder, audio.filename, audio.type)
);
audioUIWidget.element.classList.remove("empty-audio-widget");
}
}
@@ -130,11 +151,17 @@ app.registerExtension({
return {
AUDIOUPLOAD(node, inputName: string) {
// The widget that allows user to select file.
const audioWidget: IWidget = node.widgets.find((w: IWidget) => w.name === "audio");
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.widgets.find((w: IWidget) => w.name === "audioUI");
const audioWidget: IWidget = node.widgets.find(
(w: IWidget) => w.name === "audio"
);
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.widgets.find(
(w: IWidget) => w.name === "audioUI"
);
const onAudioWidgetUpdate = () => {
audioUIWidget.element.src = api.apiURL(getResourceURL(...splitFilePath(audioWidget.value)));
audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(audioWidget.value))
);
};
// Initially load default audio file to audioUIWidget.
if (audioWidget.value) {
@@ -152,14 +179,19 @@ app.registerExtension({
}
};
// The widget to pop up the upload dialog.
const uploadWidget = node.addWidget("button", inputName, /* value=*/"", () => {
fileInput.click();
});
const uploadWidget = node.addWidget(
"button",
inputName,
/* value=*/ "",
() => {
fileInput.click();
}
);
uploadWidget.label = "choose file to upload";
uploadWidget.serialize = false;
return { widget: uploadWidget };
}
}
},
};
},
});

View File

@@ -4,10 +4,10 @@ import { ComfyNodeDef } from "/types/apiTypes";
// Adds an upload button to the nodes
app.registerExtension({
name: "Comfy.UploadImage",
async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef, app) {
if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
nodeData.input.required.upload = ["IMAGEUPLOAD"];
}
},
name: "Comfy.UploadImage",
async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef, app) {
if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
nodeData.input.required.upload = ["IMAGEUPLOAD"];
}
},
});

View File

@@ -4,123 +4,137 @@ import { api } from "../../scripts/api";
const WEBCAM_READY = Symbol();
app.registerExtension({
name: "Comfy.WebcamCapture",
getCustomWidgets(app) {
return {
WEBCAM(node, inputName) {
let res;
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve));
name: "Comfy.WebcamCapture",
getCustomWidgets(app) {
return {
WEBCAM(node, inputName) {
let res;
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve));
const container = document.createElement("div");
container.style.background = "rgba(0,0,0,0.25)";
container.style.textAlign = "center";
const container = document.createElement("div");
container.style.background = "rgba(0,0,0,0.25)";
container.style.textAlign = "center";
const video = document.createElement("video");
video.style.height = video.style.width = "100%";
const video = document.createElement("video");
video.style.height = video.style.width = "100%";
const loadVideo = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
container.replaceChildren(video);
const loadVideo = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
container.replaceChildren(video);
setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener("loadedmetadata", () => res(video), false);
video.srcObject = stream;
video.play();
} catch (error) {
const label = document.createElement("div");
label.style.color = "red";
label.style.overflow = "auto";
label.style.maxHeight = "100%";
label.style.whiteSpace = "pre-wrap";
setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener("loadedmetadata", () => res(video), false);
video.srcObject = stream;
video.play();
} catch (error) {
const label = document.createElement("div");
label.style.color = "red";
label.style.overflow = "auto";
label.style.maxHeight = "100%";
label.style.whiteSpace = "pre-wrap";
if (window.isSecureContext) {
label.textContent = "Unable to load webcam, please ensure access is granted:\n" + error.message;
} else {
label.textContent = "Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" + error.message;
}
if (window.isSecureContext) {
label.textContent =
"Unable to load webcam, please ensure access is granted:\n" +
error.message;
} else {
label.textContent =
"Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" +
error.message;
}
container.replaceChildren(label);
}
};
container.replaceChildren(label);
}
};
loadVideo();
loadVideo();
return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
},
};
},
nodeCreated(node) {
if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return;
return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
},
};
},
nodeCreated(node) {
if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return;
let video;
const camera = node.widgets.find((w) => w.name === "image");
const w = node.widgets.find((w) => w.name === "width");
const h = node.widgets.find((w) => w.name === "height");
const captureOnQueue = node.widgets.find((w) => w.name === "capture_on_queue");
let video;
const camera = node.widgets.find((w) => w.name === "image");
const w = node.widgets.find((w) => w.name === "width");
const h = node.widgets.find((w) => w.name === "height");
const captureOnQueue = node.widgets.find(
(w) => w.name === "capture_on_queue"
);
const canvas = document.createElement("canvas");
const canvas = document.createElement("canvas");
const capture = () => {
canvas.width = w.value;
canvas.height = h.value;
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, w.value, h.value);
const data = canvas.toDataURL("image/png");
const capture = () => {
canvas.width = w.value;
canvas.height = h.value;
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, w.value, h.value);
const data = canvas.toDataURL("image/png");
const img = new Image();
img.onload = () => {
node.imgs = [img];
app.graph.setDirtyCanvas(true);
requestAnimationFrame(() => {
node.setSizeForImage?.();
});
};
img.src = data;
};
const img = new Image();
img.onload = () => {
node.imgs = [img];
app.graph.setDirtyCanvas(true);
requestAnimationFrame(() => {
node.setSizeForImage?.();
});
};
img.src = data;
};
const btn = node.addWidget("button", "waiting for camera...", "capture", capture);
btn.disabled = true;
btn.serializeValue = () => undefined;
const btn = node.addWidget(
"button",
"waiting for camera...",
"capture",
capture
);
btn.disabled = true;
btn.serializeValue = () => undefined;
camera.serializeValue = async () => {
if (captureOnQueue.value) {
capture();
} else if (!node.imgs?.length) {
const err = `No webcam image captured`;
alert(err);
throw new Error(err);
}
camera.serializeValue = async () => {
if (captureOnQueue.value) {
capture();
} else if (!node.imgs?.length) {
const err = `No webcam image captured`;
alert(err);
throw new Error(err);
}
// Upload image to temp storage
const blob = await new Promise<Blob>((r) => canvas.toBlob(r));
const name = `${+new Date()}.png`;
const file = new File([blob], name);
const body = new FormData();
body.append("image", file);
body.append("subfolder", "webcam");
body.append("type", "temp");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status !== 200) {
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
alert(err);
throw new Error(err);
}
return `webcam/${name} [temp]`;
};
// Upload image to temp storage
const blob = await new Promise<Blob>((r) => canvas.toBlob(r));
const name = `${+new Date()}.png`;
const file = new File([blob], name);
const body = new FormData();
body.append("image", file);
body.append("subfolder", "webcam");
body.append("type", "temp");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status !== 200) {
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
alert(err);
throw new Error(err);
}
return `webcam/${name} [temp]`;
};
node[WEBCAM_READY].then((v) => {
video = v;
// If width isnt specified then use video output resolution
if (!w.value) {
w.value = video.videoWidth || 640;
h.value = video.videoHeight || 480;
}
btn.disabled = false;
btn.label = "capture";
});
},
node[WEBCAM_READY].then((v) => {
video = v;
// If width isnt specified then use video output resolution
if (!w.value) {
w.value = video.videoWidth || 640;
h.value = video.videoHeight || 480;
}
btn.disabled = false;
btn.label = "capture";
});
},
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,16 +6,19 @@
* @param {clearBackgroundColor} String
* @
*/
LGraphCanvas.prototype.updateBackground = function (image, clearBackgroundColor) {
this._bg_img = new Image();
this._bg_img.name = image;
this._bg_img.src = image;
this._bg_img.onload = () => {
this.draw(true, true);
};
this.background_image = image;
LGraphCanvas.prototype.updateBackground = function (
image,
clearBackgroundColor
) {
this._bg_img = new Image();
this._bg_img.name = image;
this._bg_img.src = image;
this._bg_img.onload = () => {
this.draw(true, true);
};
this.background_image = image;
this.clear_background = true;
this.clear_background_color = clearBackgroundColor;
this._pattern = null
}
this.clear_background = true;
this.clear_background_color = clearBackgroundColor;
this._pattern = null;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,255 +3,271 @@
import { api } from "./api";
import { clone } from "./utils";
export class ChangeTracker {
static MAX_HISTORY = 50;
#app;
undo = [];
redo = [];
activeState = null;
isOurLoad = false;
/** @type { import("./workflows").ComfyWorkflow | null } */
workflow;
static MAX_HISTORY = 50;
#app;
undo = [];
redo = [];
activeState = null;
isOurLoad = false;
/** @type { import("./workflows").ComfyWorkflow | null } */
workflow;
ds;
nodeOutputs;
ds;
nodeOutputs;
get app() {
return this.#app ?? this.workflow.manager.app;
}
get app() {
return this.#app ?? this.workflow.manager.app;
}
constructor(workflow) {
this.workflow = workflow;
}
constructor(workflow) {
this.workflow = workflow;
}
#setApp(app) {
this.#app = app;
}
#setApp(app) {
this.#app = app;
}
store() {
this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] };
}
store() {
this.ds = {
scale: this.app.canvas.ds.scale,
offset: [...this.app.canvas.ds.offset],
};
}
restore() {
if (this.ds) {
this.app.canvas.ds.scale = this.ds.scale;
this.app.canvas.ds.offset = this.ds.offset;
}
if (this.nodeOutputs) {
this.app.nodeOutputs = this.nodeOutputs;
}
}
restore() {
if (this.ds) {
this.app.canvas.ds.scale = this.ds.scale;
this.app.canvas.ds.offset = this.ds.offset;
}
if (this.nodeOutputs) {
this.app.nodeOutputs = this.nodeOutputs;
}
}
checkState() {
if (!this.app.graph) return;
checkState() {
if (!this.app.graph) return;
const currentState = this.app.graph.serialize();
if (!this.activeState) {
this.activeState = clone(currentState);
return;
}
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
this.undo.push(this.activeState);
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
this.undo.shift();
}
this.activeState = clone(currentState);
this.redo.length = 0;
this.workflow.unsaved = true;
api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState }));
}
}
const currentState = this.app.graph.serialize();
if (!this.activeState) {
this.activeState = clone(currentState);
return;
}
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
this.undo.push(this.activeState);
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
this.undo.shift();
}
this.activeState = clone(currentState);
this.redo.length = 0;
this.workflow.unsaved = true;
api.dispatchEvent(
new CustomEvent("graphChanged", { detail: this.activeState })
);
}
}
async updateState(source, target) {
const prevState = source.pop();
if (prevState) {
target.push(this.activeState);
this.isOurLoad = true;
await this.app.loadGraphData(prevState, false, false, this.workflow);
this.activeState = prevState;
}
}
async updateState(source, target) {
const prevState = source.pop();
if (prevState) {
target.push(this.activeState);
this.isOurLoad = true;
await this.app.loadGraphData(prevState, false, false, this.workflow);
this.activeState = prevState;
}
}
async undoRedo(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === "y") {
this.updateState(this.redo, this.undo);
return true;
} else if (e.key === "z") {
this.updateState(this.undo, this.redo);
return true;
}
}
}
async undoRedo(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === "y") {
this.updateState(this.redo, this.undo);
return true;
} else if (e.key === "z") {
this.updateState(this.undo, this.redo);
return true;
}
}
}
/** @param { import("./app").ComfyApp } app */
static init(app) {
const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
globalTracker.#setApp(app);
/** @param { import("./app").ComfyApp } app */
static init(app) {
const changeTracker = () =>
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
globalTracker.#setApp(app);
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
const v = await loadGraphData.apply(this, arguments);
const ct = changeTracker();
if (ct.isOurLoad) {
ct.isOurLoad = false;
} else {
ct.checkState();
}
return v;
};
const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () {
const v = await loadGraphData.apply(this, arguments);
const ct = changeTracker();
if (ct.isOurLoad) {
ct.isOurLoad = false;
} else {
ct.checkState();
}
return v;
};
let keyIgnored = false;
window.addEventListener(
"keydown",
(e) => {
requestAnimationFrame(async () => {
let activeEl;
// If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
activeEl = document.activeElement;
if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") {
// Ignore events on inputs, they have their native history
return;
}
}
let keyIgnored = false;
window.addEventListener(
"keydown",
(e) => {
requestAnimationFrame(async () => {
let activeEl;
// If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
activeEl = document.activeElement;
if (
activeEl?.tagName === "INPUT" ||
activeEl?.["type"] === "textarea"
) {
// Ignore events on inputs, they have their native history
return;
}
}
keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta";
if (keyIgnored) return;
keyIgnored =
e.key === "Control" ||
e.key === "Shift" ||
e.key === "Alt" ||
e.key === "Meta";
if (keyIgnored) return;
// Check if this is a ctrl+z ctrl+y
if (await changeTracker().undoRedo(e)) return;
// Check if this is a ctrl+z ctrl+y
if (await changeTracker().undoRedo(e)) return;
// If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(activeEl)) return;
changeTracker().checkState();
});
},
true
);
// If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(activeEl)) return;
changeTracker().checkState();
});
},
true
);
window.addEventListener("keyup", (e) => {
if (keyIgnored) {
keyIgnored = false;
changeTracker().checkState();
}
});
window.addEventListener("keyup", (e) => {
if (keyIgnored) {
keyIgnored = false;
changeTracker().checkState();
}
});
// Handle clicking DOM elements (e.g. widgets)
window.addEventListener("mouseup", () => {
changeTracker().checkState();
});
// Handle clicking DOM elements (e.g. widgets)
window.addEventListener("mouseup", () => {
changeTracker().checkState();
});
// Handle prompt queue event for dynamic widget changes
api.addEventListener("promptQueued", () => {
changeTracker().checkState();
});
// Handle prompt queue event for dynamic widget changes
api.addEventListener("promptQueued", () => {
changeTracker().checkState();
});
// Handle litegraph clicks
// @ts-ignore
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
// @ts-ignore
LGraphCanvas.prototype.processMouseUp = function (e) {
const v = processMouseUp.apply(this, arguments);
changeTracker().checkState();
return v;
};
// @ts-ignore
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
// @ts-ignore
LGraphCanvas.prototype.processMouseDown = function (e) {
const v = processMouseDown.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Handle litegraph clicks
// @ts-ignore
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
// @ts-ignore
LGraphCanvas.prototype.processMouseUp = function (e) {
const v = processMouseUp.apply(this, arguments);
changeTracker().checkState();
return v;
};
// @ts-ignore
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
// @ts-ignore
LGraphCanvas.prototype.processMouseDown = function (e) {
const v = processMouseDown.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Handle litegraph context menu for COMBO widgets
const close = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function (e) {
const v = close.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Handle litegraph context menu for COMBO widgets
const close = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function (e) {
const v = close.apply(this, arguments);
changeTracker().checkState();
return v;
};
// Detects nodes being added via the node search dialog
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
LiteGraph.LGraph.prototype.onNodeAdded = function () {
const v = onNodeAdded?.apply(this, arguments);
const ct = changeTracker();
if (!ct.isOurLoad) {
ct.checkState();
}
return v;
};
// Detects nodes being added via the node search dialog
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
LiteGraph.LGraph.prototype.onNodeAdded = function () {
const v = onNodeAdded?.apply(this, arguments);
const ct = changeTracker();
if (!ct.isOurLoad) {
ct.checkState();
}
return v;
};
// Store node outputs
api.addEventListener("executed", ({ detail }) => {
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
if (!prompt?.workflow) return;
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
const output = nodeOutputs[detail.node];
if (detail.merge && output) {
for (const k in detail.output ?? {}) {
const v = output[k];
if (v instanceof Array) {
output[k] = v.concat(detail.output[k]);
} else {
output[k] = detail.output[k];
}
}
} else {
nodeOutputs[detail.node] = detail.output;
}
});
}
// Store node outputs
api.addEventListener("executed", ({ detail }) => {
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
if (!prompt?.workflow) return;
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
const output = nodeOutputs[detail.node];
if (detail.merge && output) {
for (const k in detail.output ?? {}) {
const v = output[k];
if (v instanceof Array) {
output[k] = v.concat(detail.output[k]);
} else {
output[k] = detail.output[k];
}
}
} else {
nodeOutputs[detail.node] = detail.output;
}
});
}
static bindInput(app, activeEl) {
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") {
for (const evt of ["change", "input", "blur"]) {
if (`on${evt}` in activeEl) {
const listener = () => {
app.workflowManager.activeWorkflow.changeTracker.checkState();
activeEl.removeEventListener(evt, listener);
};
activeEl.addEventListener(evt, listener);
return true;
}
}
}
}
static bindInput(app, activeEl) {
if (
activeEl &&
activeEl.tagName !== "CANVAS" &&
activeEl.tagName !== "BODY"
) {
for (const evt of ["change", "input", "blur"]) {
if (`on${evt}` in activeEl) {
const listener = () => {
app.workflowManager.activeWorkflow.changeTracker.checkState();
activeEl.removeEventListener(evt, listener);
};
activeEl.addEventListener(evt, listener);
return true;
}
}
}
}
static graphEqual(a, b, path = "") {
if (a === b) return true;
static graphEqual(a, b, path = "") {
if (a === b) return true;
if (typeof a == "object" && a && typeof b == "object" && b) {
const keys = Object.getOwnPropertyNames(a);
if (typeof a == "object" && a && typeof b == "object" && b) {
const keys = Object.getOwnPropertyNames(a);
if (keys.length != Object.getOwnPropertyNames(b).length) {
return false;
}
if (keys.length != Object.getOwnPropertyNames(b).length) {
return false;
}
for (const key of keys) {
let av = a[key];
let bv = b[key];
if (!path && key === "nodes") {
// Nodes need to be sorted as the order changes when selecting nodes
av = [...av].sort((a, b) => a.id - b.id);
bv = [...bv].sort((a, b) => a.id - b.id);
} else if (path === "extra.ds") {
// Ignore view changes
continue;
}
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
return false;
}
}
for (const key of keys) {
let av = a[key];
let bv = b[key];
if (!path && key === "nodes") {
// Nodes need to be sorted as the order changes when selecting nodes
av = [...av].sort((a, b) => a.id - b.id);
bv = [...bv].sort((a, b) => a.id - b.id);
} else if (path === "extra.ds") {
// Ignore view changes
continue;
}
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
return false;
}
}
return true;
}
return true;
}
return false;
}
return false;
}
}
const globalTracker = new ChangeTracker({});
const globalTracker = new ChangeTracker({});

View File

@@ -1,121 +1,137 @@
import type { ComfyWorkflow } from "/types/comfyWorkflow";
export const defaultGraph: ComfyWorkflow = {
last_node_id: 9,
last_link_id: 9,
nodes: [
{
id: 7,
type: "CLIPTextEncode",
pos: [413, 389],
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
flags: {},
order: 3,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
properties: {},
widgets_values: ["text, watermark"],
},
{
id: 6,
type: "CLIPTextEncode",
pos: [415, 186],
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
flags: {},
order: 2,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
properties: {},
widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"],
},
{
id: 5,
type: "EmptyLatentImage",
pos: [473, 609],
size: { 0: 315, 1: 106 },
flags: {},
order: 1,
mode: 0,
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
properties: {},
widgets_values: [512, 512, 1],
},
{
id: 3,
type: "KSampler",
pos: [863, 186],
size: { 0: 315, 1: 262 },
flags: {},
order: 4,
mode: 0,
inputs: [
{ name: "model", type: "MODEL", link: 1 },
{ name: "positive", type: "CONDITIONING", link: 4 },
{ name: "negative", type: "CONDITIONING", link: 6 },
{ name: "latent_image", type: "LATENT", link: 2 },
],
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {},
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
},
{
id: 8,
type: "VAEDecode",
pos: [1209, 188],
size: { 0: 210, 1: 46 },
flags: {},
order: 5,
mode: 0,
inputs: [
{ name: "samples", type: "LATENT", link: 7 },
{ name: "vae", type: "VAE", link: 8 },
],
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
properties: {},
},
{
id: 9,
type: "SaveImage",
pos: [1451, 189],
size: { 0: 210, 1: 26 },
flags: {},
order: 6,
mode: 0,
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
properties: {},
},
{
id: 4,
type: "CheckpointLoaderSimple",
pos: [26, 474],
size: { 0: 315, 1: 98 },
flags: {},
order: 0,
mode: 0,
outputs: [
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
],
properties: {},
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
},
],
links: [
[1, 4, 0, 3, 0, "MODEL"],
[2, 5, 0, 3, 3, "LATENT"],
[3, 4, 1, 6, 0, "CLIP"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[5, 4, 1, 7, 0, "CLIP"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[8, 4, 2, 8, 1, "VAE"],
[9, 8, 0, 9, 0, "IMAGE"],
],
groups: [],
config: {},
extra: {},
version: 0.4,
last_node_id: 9,
last_link_id: 9,
nodes: [
{
id: 7,
type: "CLIPTextEncode",
pos: [413, 389],
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
flags: {},
order: 3,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [6],
slot_index: 0,
},
],
properties: {},
widgets_values: ["text, watermark"],
},
{
id: 6,
type: "CLIPTextEncode",
pos: [415, 186],
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
flags: {},
order: 2,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [4],
slot_index: 0,
},
],
properties: {},
widgets_values: [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
],
},
{
id: 5,
type: "EmptyLatentImage",
pos: [473, 609],
size: { 0: 315, 1: 106 },
flags: {},
order: 1,
mode: 0,
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
properties: {},
widgets_values: [512, 512, 1],
},
{
id: 3,
type: "KSampler",
pos: [863, 186],
size: { 0: 315, 1: 262 },
flags: {},
order: 4,
mode: 0,
inputs: [
{ name: "model", type: "MODEL", link: 1 },
{ name: "positive", type: "CONDITIONING", link: 4 },
{ name: "negative", type: "CONDITIONING", link: 6 },
{ name: "latent_image", type: "LATENT", link: 2 },
],
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {},
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
},
{
id: 8,
type: "VAEDecode",
pos: [1209, 188],
size: { 0: 210, 1: 46 },
flags: {},
order: 5,
mode: 0,
inputs: [
{ name: "samples", type: "LATENT", link: 7 },
{ name: "vae", type: "VAE", link: 8 },
],
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
properties: {},
},
{
id: 9,
type: "SaveImage",
pos: [1451, 189],
size: { 0: 210, 1: 26 },
flags: {},
order: 6,
mode: 0,
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
properties: {},
},
{
id: 4,
type: "CheckpointLoaderSimple",
pos: [26, 474],
size: { 0: 315, 1: 98 },
flags: {},
order: 0,
mode: 0,
outputs: [
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
],
properties: {},
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
},
],
links: [
[1, 4, 0, 3, 0, "MODEL"],
[2, 5, 0, 3, 3, "LATENT"],
[3, 4, 1, 6, 0, "CLIP"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[5, 4, 1, 7, 0, "CLIP"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[8, 4, 2, 8, 1, "VAE"],
[9, 8, 0, 9, 0, "IMAGE"],
],
groups: [],
config: {},
extra: {},
version: 0.4,
};

View File

@@ -1,194 +1,216 @@
import { app, ANIM_PREVIEW_WIDGET } from "./app";
import type { LGraphNode, Vector4 } from "/types/litegraph";
const SIZE = Symbol();
interface Rect {
height: number;
width: number;
x: number;
y: number;
height: number;
width: number;
x: number;
y: number;
}
export interface DOMWidget<T = HTMLElement> {
type: string;
name: string;
computedHeight?: number;
element?: T;
options: any;
value?: any;
y?: number;
callback?: (value: any) => void;
draw?: (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) => void;
onRemove?: () => void;
type: string;
name: string;
computedHeight?: number;
element?: T;
options: any;
value?: any;
y?: number;
callback?: (value: any) => void;
draw?: (
ctx: CanvasRenderingContext2D,
node: LGraphNode,
widgetWidth: number,
y: number,
widgetHeight: number
) => void;
onRemove?: () => void;
}
function intersect(a: Rect, b: Rect): Vector4 | null {
const x = Math.max(a.x, b.x);
const num1 = Math.min(a.x + a.width, b.x + b.width);
const y = Math.max(a.y, b.y);
const num2 = Math.min(a.y + a.height, b.y + b.height);
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
else return null;
const x = Math.max(a.x, b.x);
const num1 = Math.min(a.x + a.width, b.x + b.width);
const y = Math.max(a.y, b.y);
const num2 = Math.min(a.y + a.height, b.y + b.height);
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
else return null;
}
function getClipPath(node: LGraphNode, element: HTMLElement): string {
const selectedNode: LGraphNode = Object.values(app.canvas.selected_nodes)[0] as LGraphNode;
if (selectedNode && selectedNode !== node) {
const elRect = element.getBoundingClientRect();
const MARGIN = 7;
const scale = app.canvas.ds.scale;
const selectedNode: LGraphNode = Object.values(
app.canvas.selected_nodes
)[0] as LGraphNode;
if (selectedNode && selectedNode !== node) {
const elRect = element.getBoundingClientRect();
const MARGIN = 7;
const scale = app.canvas.ds.scale;
const bounding = selectedNode.getBounding();
const intersection = intersect(
{ x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale },
{
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN,
width: bounding[2] + MARGIN + MARGIN,
height: bounding[3] + MARGIN + MARGIN,
}
);
const bounding = selectedNode.getBounding();
const intersection = intersect(
{
x: elRect.x / scale,
y: elRect.y / scale,
width: elRect.width / scale,
height: elRect.height / scale,
},
{
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
y:
selectedNode.pos[1] +
app.canvas.ds.offset[1] -
LiteGraph.NODE_TITLE_HEIGHT -
MARGIN,
width: bounding[2] + MARGIN + MARGIN,
height: bounding[3] + MARGIN + MARGIN,
}
);
if (!intersection) {
return "";
}
if (!intersection) {
return "";
}
const widgetRect = element.getBoundingClientRect();
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
const clipWidth = intersection[2] + "px";
const clipHeight = intersection[3] + "px";
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
return path;
}
return "";
const widgetRect = element.getBoundingClientRect();
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
const clipWidth = intersection[2] + "px";
const clipHeight = intersection[3] + "px";
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
return path;
}
return "";
}
function computeSize(size: [number, number]): void {
if (this.widgets?.[0]?.last_y == null) return;
if (this.widgets?.[0]?.last_y == null) return;
let y = this.widgets[0].last_y;
let freeSpace = size[1] - y;
let y = this.widgets[0].last_y;
let freeSpace = size[1] - y;
let widgetHeight = 0;
let dom = [];
for (const w of this.widgets) {
if (w.type === "converted-widget") {
// Ignore
delete w.computedHeight;
} else if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else if (w.element) {
// Extract DOM widget size info
const styles = getComputedStyle(w.element);
let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
let widgetHeight = 0;
let dom = [];
for (const w of this.widgets) {
if (w.type === "converted-widget") {
// Ignore
delete w.computedHeight;
} else if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else if (w.element) {
// Extract DOM widget size info
const styles = getComputedStyle(w.element);
let minHeight =
w.options.getMinHeight?.() ??
parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
let maxHeight =
w.options.getMaxHeight?.() ??
parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height");
if (prefHeight.endsWith?.("%")) {
prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
} else {
prefHeight = parseInt(prefHeight);
if (isNaN(minHeight)) {
minHeight = prefHeight;
}
}
if (isNaN(minHeight)) {
minHeight = 50;
}
if (!isNaN(maxHeight)) {
if (!isNaN(prefHeight)) {
prefHeight = Math.min(prefHeight, maxHeight);
} else {
prefHeight = maxHeight;
}
}
dom.push({
minHeight,
prefHeight,
w,
});
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
let prefHeight =
w.options.getHeight?.() ??
styles.getPropertyValue("--comfy-widget-height");
if (prefHeight.endsWith?.("%")) {
prefHeight =
size[1] *
(parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
} else {
prefHeight = parseInt(prefHeight);
if (isNaN(minHeight)) {
minHeight = prefHeight;
}
}
if (isNaN(minHeight)) {
minHeight = 50;
}
if (!isNaN(maxHeight)) {
if (!isNaN(prefHeight)) {
prefHeight = Math.min(prefHeight, maxHeight);
} else {
prefHeight = maxHeight;
}
}
dom.push({
minHeight,
prefHeight,
w,
});
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
freeSpace -= widgetHeight;
freeSpace -= widgetHeight;
// Calculate sizes with all widgets at their min height
const prefGrow = []; // Nodes that want to grow to their prefd size
const canGrow = []; // Nodes that can grow to auto size
let growBy = 0;
for (const d of dom) {
freeSpace -= d.minHeight;
if (isNaN(d.prefHeight)) {
canGrow.push(d);
d.w.computedHeight = d.minHeight;
} else {
const diff = d.prefHeight - d.minHeight;
if (diff > 0) {
prefGrow.push(d);
growBy += diff;
d.diff = diff;
} else {
d.w.computedHeight = d.minHeight;
}
}
}
// Calculate sizes with all widgets at their min height
const prefGrow = []; // Nodes that want to grow to their prefd size
const canGrow = []; // Nodes that can grow to auto size
let growBy = 0;
for (const d of dom) {
freeSpace -= d.minHeight;
if (isNaN(d.prefHeight)) {
canGrow.push(d);
d.w.computedHeight = d.minHeight;
} else {
const diff = d.prefHeight - d.minHeight;
if (diff > 0) {
prefGrow.push(d);
growBy += diff;
d.diff = diff;
} else {
d.w.computedHeight = d.minHeight;
}
}
}
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
// Allocate space for image
freeSpace -= 220;
}
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
// Allocate space for image
freeSpace -= 220;
}
this.freeWidgetSpace = freeSpace;
this.freeWidgetSpace = freeSpace;
if (freeSpace < 0) {
// Not enough space for all widgets so we need to grow
size[1] -= freeSpace;
this.graph.setDirtyCanvas(true);
} else {
// Share the space between each
const growDiff = freeSpace - growBy;
if (growDiff > 0) {
// All pref sizes can be fulfilled
freeSpace = growDiff;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight;
}
} else {
// We need to grow evenly
const shared = -growDiff / prefGrow.length;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight - shared;
}
freeSpace = 0;
}
if (freeSpace < 0) {
// Not enough space for all widgets so we need to grow
size[1] -= freeSpace;
this.graph.setDirtyCanvas(true);
} else {
// Share the space between each
const growDiff = freeSpace - growBy;
if (growDiff > 0) {
// All pref sizes can be fulfilled
freeSpace = growDiff;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight;
}
} else {
// We need to grow evenly
const shared = -growDiff / prefGrow.length;
for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight - shared;
}
freeSpace = 0;
}
if (freeSpace > 0 && canGrow.length) {
// Grow any that are auto height
const shared = freeSpace / canGrow.length;
for (const d of canGrow) {
d.w.computedHeight += shared;
}
}
}
if (freeSpace > 0 && canGrow.length) {
// Grow any that are auto height
const shared = freeSpace / canGrow.length;
for (const d of canGrow) {
d.w.computedHeight += shared;
}
}
}
// Position each of the widgets
for (const w of this.widgets) {
w.y = y;
if (w.computedHeight) {
y += w.computedHeight;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
// Position each of the widgets
for (const w of this.widgets) {
w.y = y;
if (w.computedHeight) {
y += w.computedHeight;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
}
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
@@ -197,170 +219,179 @@ const elementWidgets = new Set();
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
//@ts-ignore
LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
const visibleNodes = computeVisibleNodes.apply(this, arguments);
// @ts-ignore
for (const node of app.graph._nodes) {
if (elementWidgets.has(node)) {
const hidden = visibleNodes.indexOf(node) === -1;
for (const w of node.widgets) {
// @ts-ignore
if (w.element) {
// @ts-ignore
w.element.hidden = hidden;
// @ts-ignore
w.element.style.display = hidden ? "none" : undefined;
if (hidden) {
w.options.onHide?.(w);
}
}
}
}
}
const visibleNodes = computeVisibleNodes.apply(this, arguments);
// @ts-ignore
for (const node of app.graph._nodes) {
if (elementWidgets.has(node)) {
const hidden = visibleNodes.indexOf(node) === -1;
for (const w of node.widgets) {
// @ts-ignore
if (w.element) {
// @ts-ignore
w.element.hidden = hidden;
// @ts-ignore
w.element.style.display = hidden ? "none" : undefined;
if (hidden) {
w.options.onHide?.(w);
}
}
}
}
}
return visibleNodes;
return visibleNodes;
};
let enableDomClipping = true;
export function addDomClippingSetting(): void {
app.ui.settings.addSetting({
id: "Comfy.DOMClippingEnabled",
name: "Enable DOM element clipping (enabling may reduce performance)",
type: "boolean",
defaultValue: enableDomClipping,
onChange(value) {
enableDomClipping = !!value;
},
});
app.ui.settings.addSetting({
id: "Comfy.DOMClippingEnabled",
name: "Enable DOM element clipping (enabling may reduce performance)",
type: "boolean",
defaultValue: enableDomClipping,
onChange(value) {
enableDomClipping = !!value;
},
});
}
//@ts-ignore
LGraphNode.prototype.addDOMWidget = function (
name: string,
type: string,
element: HTMLElement,
options: Record<string, any>
name: string,
type: string,
element: HTMLElement,
options: Record<string, any>
): DOMWidget {
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
if (!element.parentElement) {
document.body.append(element);
}
element.hidden = true;
element.style.display = "none";
if (!element.parentElement) {
document.body.append(element);
}
element.hidden = true;
element.style.display = "none";
let mouseDownHandler;
if (element.blur) {
mouseDownHandler = (event) => {
if (!element.contains(event.target)) {
element.blur();
}
};
document.addEventListener("mousedown", mouseDownHandler);
}
let mouseDownHandler;
if (element.blur) {
mouseDownHandler = (event) => {
if (!element.contains(event.target)) {
element.blur();
}
};
document.addEventListener("mousedown", mouseDownHandler);
}
const widget: DOMWidget = {
type,
name,
get value() {
return options.getValue?.() ?? undefined;
},
set value(v) {
options.setValue?.(v);
widget.callback?.(widget.value);
},
draw: function (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) {
if (widget.computedHeight == null) {
computeSize.call(node, node.size);
}
const widget: DOMWidget = {
type,
name,
get value() {
return options.getValue?.() ?? undefined;
},
set value(v) {
options.setValue?.(v);
widget.callback?.(widget.value);
},
draw: function (
ctx: CanvasRenderingContext2D,
node: LGraphNode,
widgetWidth: number,
y: number,
widgetHeight: number
) {
if (widget.computedHeight == null) {
computeSize.call(node, node.size);
}
const hidden =
node.flags?.collapsed ||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
widget.computedHeight <= 0 ||
widget.type === "converted-widget" ||
widget.type === "hidden";
element.hidden = hidden;
element.style.display = hidden ? "none" : null;
if (hidden) {
widget.options.onHide?.(widget);
return;
}
const hidden =
node.flags?.collapsed ||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
widget.computedHeight <= 0 ||
widget.type === "converted-widget" ||
widget.type === "hidden";
element.hidden = hidden;
element.style.display = hidden ? "none" : null;
if (hidden) {
widget.options.onHide?.(widget);
return;
}
const margin = 10;
const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y);
const margin = 10;
const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(
elRect.width / ctx.canvas.width,
elRect.height / ctx.canvas.height
)
.multiplySelf(ctx.getTransform())
.translateSelf(margin, margin + y);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
Object.assign(element.style, {
transformOrigin: "0 0",
transform: scale,
left: `${transform.a + transform.e + elRect.left}px`,
top: `${transform.d + transform.f + elRect.top}px`,
width: `${widgetWidth - margin * 2}px`,
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
position: "absolute",
// @ts-ignore
zIndex: app.graph._nodes.indexOf(node),
});
Object.assign(element.style, {
transformOrigin: "0 0",
transform: scale,
left: `${transform.a + transform.e + elRect.left}px`,
top: `${transform.d + transform.f + elRect.top}px`,
width: `${widgetWidth - margin * 2}px`,
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
position: "absolute",
// @ts-ignore
zIndex: app.graph._nodes.indexOf(node),
});
if (enableDomClipping) {
element.style.clipPath = getClipPath(node, element);
element.style.willChange = "clip-path";
}
if (enableDomClipping) {
element.style.clipPath = getClipPath(node, element);
element.style.willChange = "clip-path";
}
this.options.onDraw?.(widget);
},
element,
options,
onRemove() {
if (mouseDownHandler) {
document.removeEventListener("mousedown", mouseDownHandler);
}
element.remove();
},
};
this.options.onDraw?.(widget);
},
element,
options,
onRemove() {
if (mouseDownHandler) {
document.removeEventListener("mousedown", mouseDownHandler);
}
element.remove();
},
};
for (const evt of options.selectOn) {
element.addEventListener(evt, () => {
app.canvas.selectNode(this);
app.canvas.bringToFront(this);
});
}
for (const evt of options.selectOn) {
element.addEventListener(evt, () => {
app.canvas.selectNode(this);
app.canvas.bringToFront(this);
});
}
this.addCustomWidget(widget);
elementWidgets.add(this);
this.addCustomWidget(widget);
elementWidgets.add(this);
const collapse = this.collapse;
this.collapse = function () {
collapse.apply(this, arguments);
if (this.flags?.collapsed) {
element.hidden = true;
element.style.display = "none";
}
}
const collapse = this.collapse;
this.collapse = function () {
collapse.apply(this, arguments);
if (this.flags?.collapsed) {
element.hidden = true;
element.style.display = "none";
}
};
const onRemoved = this.onRemoved;
this.onRemoved = function () {
element.remove();
elementWidgets.delete(this);
onRemoved?.apply(this, arguments);
};
const onRemoved = this.onRemoved;
this.onRemoved = function () {
element.remove();
elementWidgets.delete(this);
onRemoved?.apply(this, arguments);
};
if (!this[SIZE]) {
this[SIZE] = true;
const onResize = this.onResize;
this.onResize = function (size) {
options.beforeResize?.call(widget, this);
computeSize.call(this, size);
onResize?.apply(this, arguments);
options.afterResize?.call(widget, this);
};
}
if (!this[SIZE]) {
this[SIZE] = true;
const onResize = this.onResize;
this.onResize = function (size) {
options.beforeResize?.call(widget, this);
computeSize.call(this, size);
onResize?.apply(this, arguments);
options.afterResize?.call(widget, this);
};
}
return widget;
return widget;
};

View File

@@ -41,13 +41,13 @@ function stringify(val, depth, replacer, space, onGetObjID?) {
r
? (o = (onGetObjID && onGetObjID(val)) || null)
: JSON.stringify(val, function (k, v) {
if (a || depth > 0) {
if (replacer) v = replacer(k, v);
if (!k) return (a = Array.isArray(v)), (val = v);
!o && (o = a ? [] : {});
o[k] = _build(v, a ? depth : depth - 1);
}
}),
if (a || depth > 0) {
if (replacer) v = replacer(k, v);
if (!k) return (a = Array.isArray(v)), (val = v);
!o && (o = a ? [] : {});
o[k] = _build(v, a ? depth : depth - 1);
}
}),
o === void 0 ? (a ? [] : {}) : o);
}
return JSON.stringify(_build(val, depth), null, space);
@@ -97,9 +97,12 @@ class ComfyLoggingDialog extends ComfyDialog {
}
export() {
const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], {
type: "application/json",
});
const blob = new Blob(
[stringify([...this.logging.entries], 20, jsonReplacer, "\t")],
{
type: "application/json",
}
);
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
@@ -147,230 +150,234 @@ class ComfyLoggingDialog extends ComfyDialog {
textContent: "Export logs...",
onclick: () => this.export(),
}),
$el("button", {
type: "button",
textContent: "View exported logs...",
onclick: () => this.import(),
}),
...super.createButtons(),
];
}
$el("button", {
type: "button",
textContent: "View exported logs...",
onclick: () => this.import(),
}),
...super.createButtons(),
];
}
getTypeColor(type) {
switch (type) {
case "error":
return "red";
case "warn":
return "orange";
case "debug":
return "dodgerblue";
}
}
getTypeColor(type) {
switch (type) {
case "error":
return "red";
case "warn":
return "orange";
case "debug":
return "dodgerblue";
}
}
show(entries?: any[]) {
if (!entries) entries = this.logging.entries;
this.element.style.width = "100%";
const cols = {
source: "Source",
type: "Type",
timestamp: "Timestamp",
message: "Message",
};
const keys = Object.keys(cols);
const headers = Object.values(cols).map((title) =>
$el("div.comfy-logging-title", {
textContent: title,
})
);
const rows = entries.map((entry, i) => {
return $el(
"div.comfy-logging-log",
{
$: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`),
},
keys.map((key) => {
let v = entry[key];
let color;
if (key === "type") {
color = this.getTypeColor(v);
} else {
v = jsonReplacer(key, v, true);
show(entries?: any[]) {
if (!entries) entries = this.logging.entries;
this.element.style.width = "100%";
const cols = {
source: "Source",
type: "Type",
timestamp: "Timestamp",
message: "Message",
};
const keys = Object.keys(cols);
const headers = Object.values(cols).map((title) =>
$el("div.comfy-logging-title", {
textContent: title,
})
);
const rows = entries.map((entry, i) => {
return $el(
"div.comfy-logging-log",
{
$: (el) =>
el.style.setProperty(
"--row-bg",
`var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`
),
},
keys.map((key) => {
let v = entry[key];
let color;
if (key === "type") {
color = this.getTypeColor(v);
} else {
v = jsonReplacer(key, v, true);
if (typeof v === "object") {
v = stringify(v, 5, jsonReplacer, " ");
}
}
if (typeof v === "object") {
v = stringify(v, 5, jsonReplacer, " ");
}
}
return $el("div", {
style: {
color,
},
textContent: v,
});
})
);
});
return $el("div", {
style: {
color,
},
textContent: v,
});
})
);
});
const grid = $el(
"div.comfy-logging-logs",
{
style: {
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
},
},
[...headers, ...rows]
);
const els = [grid];
if (!this.logging.enabled) {
els.unshift(
$el("h3", {
style: { textAlign: "center" },
textContent: "Logging is disabled",
})
);
}
super.show($el("div", els));
}
const grid = $el(
"div.comfy-logging-logs",
{
style: {
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
},
},
[...headers, ...rows]
);
const els = [grid];
if (!this.logging.enabled) {
els.unshift(
$el("h3", {
style: { textAlign: "center" },
textContent: "Logging is disabled",
})
);
}
super.show($el("div", els));
}
}
export class ComfyLogging {
/**
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
*/
entries = [];
/**
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
*/
entries = [];
#enabled;
#console = {};
#enabled;
#console = {};
app: ComfyApp;
dialog: ComfyLoggingDialog;
app: ComfyApp;
dialog: ComfyLoggingDialog;
get enabled() {
return this.#enabled;
}
get enabled() {
return this.#enabled;
}
set enabled(value) {
if (value === this.#enabled) return;
if (value) {
this.patchConsole();
} else {
this.unpatchConsole();
}
this.#enabled = value;
}
set enabled(value) {
if (value === this.#enabled) return;
if (value) {
this.patchConsole();
} else {
this.unpatchConsole();
}
this.#enabled = value;
}
constructor(app) {
this.app = app;
constructor(app) {
this.app = app;
this.dialog = new ComfyLoggingDialog(this);
this.addSetting();
this.catchUnhandled();
this.addInitData();
}
this.dialog = new ComfyLoggingDialog(this);
this.addSetting();
this.catchUnhandled();
this.addInitData();
}
addSetting() {
const settingId: string = "Comfy.Logging.Enabled";
const htmlSettingId = settingId.replaceAll(".", "-");
const setting = this.app.ui.settings.addSetting({
id: settingId,
name: settingId,
defaultValue: true,
onChange: (value) => {
this.enabled = value;
},
type: (name, setter, value) => {
return $el("tr", [
$el("td", [
$el("label", {
textContent: "Logging",
for: htmlSettingId,
}),
]),
$el("td", [
$el("input", {
id: htmlSettingId,
type: "checkbox",
checked: value,
onchange: (event) => {
setter(event.target.checked);
},
}),
$el("button", {
textContent: "View Logs",
onclick: () => {
this.app.ui.settings.element.close();
this.dialog.show();
},
style: {
fontSize: "14px",
display: "block",
marginTop: "5px",
},
}),
]),
]);
},
});
this.enabled = setting.value;
}
addSetting() {
const settingId: string = "Comfy.Logging.Enabled";
const htmlSettingId = settingId.replaceAll(".", "-");
const setting = this.app.ui.settings.addSetting({
id: settingId,
name: settingId,
defaultValue: true,
onChange: (value) => {
this.enabled = value;
},
type: (name, setter, value) => {
return $el("tr", [
$el("td", [
$el("label", {
textContent: "Logging",
for: htmlSettingId,
}),
]),
$el("td", [
$el("input", {
id: htmlSettingId,
type: "checkbox",
checked: value,
onchange: (event) => {
setter(event.target.checked);
},
}),
$el("button", {
textContent: "View Logs",
onclick: () => {
this.app.ui.settings.element.close();
this.dialog.show();
},
style: {
fontSize: "14px",
display: "block",
marginTop: "5px",
},
}),
]),
]);
},
});
this.enabled = setting.value;
}
patchConsole() {
// Capture common console outputs
const self = this;
for (const type of ["log", "warn", "error", "debug"]) {
const orig = console[type];
this.#console[type] = orig;
console[type] = function () {
orig.apply(console, arguments);
self.addEntry("console", type, ...arguments);
};
}
}
patchConsole() {
// Capture common console outputs
const self = this;
for (const type of ["log", "warn", "error", "debug"]) {
const orig = console[type];
this.#console[type] = orig;
console[type] = function () {
orig.apply(console, arguments);
self.addEntry("console", type, ...arguments);
};
}
}
unpatchConsole() {
// Restore original console functions
for (const type of Object.keys(this.#console)) {
console[type] = this.#console[type];
}
this.#console = {};
}
unpatchConsole() {
// Restore original console functions
for (const type of Object.keys(this.#console)) {
console[type] = this.#console[type];
}
this.#console = {};
}
catchUnhandled() {
// Capture uncaught errors
window.addEventListener("error", (e) => {
this.addEntry("window", "error", e.error ?? "Unknown error");
return false;
});
catchUnhandled() {
// Capture uncaught errors
window.addEventListener("error", (e) => {
this.addEntry("window", "error", e.error ?? "Unknown error");
return false;
});
window.addEventListener("unhandledrejection", (e) => {
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
});
}
window.addEventListener("unhandledrejection", (e) => {
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
});
}
clear() {
this.entries = [];
}
clear() {
this.entries = [];
}
addEntry(source, type, ...args) {
if (this.enabled) {
this.entries.push({
source,
type,
timestamp: new Date(),
message: args,
});
}
}
addEntry(source, type, ...args) {
if (this.enabled) {
this.entries.push({
source,
type,
timestamp: new Date(),
message: args,
});
}
}
log(source, ...args) {
this.addEntry(source, "log", ...args);
}
log(source, ...args) {
this.addEntry(source, "log", ...args);
}
async addInitData() {
if (!this.enabled) return;
const source = "ComfyUI.Logging";
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
const systemStats = await api.getSystemStats();
this.addEntry(source, "debug", systemStats);
}
async addInitData() {
if (!this.enabled) return;
const source = "ComfyUI.Logging";
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
const systemStats = await api.getSystemStats();
this.addEntry(source, "debug", systemStats);
}
}

View File

@@ -23,17 +23,26 @@ export function getPngMetadata(file) {
// Get the length of the chunk
const length = dataView.getUint32(offset);
// Get the chunk type
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
const type = String.fromCharCode(
...pngData.slice(offset + 4, offset + 8)
);
if (type === "tEXt" || type == "comf" || type === "iTXt") {
// Get the keyword
let keyword_end = offset + 8;
while (pngData[keyword_end] !== 0) {
keyword_end++;
}
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
const keyword = String.fromCharCode(
...pngData.slice(offset + 8, keyword_end)
);
// Get the text
const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
const contentJson = new TextDecoder("utf-8").decode(contentArraySegment);
const contentArraySegment = pngData.slice(
keyword_end + 1,
offset + 8 + length
);
const contentJson = new TextDecoder("utf-8").decode(
contentArraySegment
);
txt_chunks[keyword] = contentJson;
}
@@ -53,11 +62,17 @@ function parseExifData(exifData) {
// Function to read 16-bit and 32-bit integers from binary data
function readInt(offset, isLittleEndian, length) {
let arr = exifData.slice(offset, offset + length)
let arr = exifData.slice(offset, offset + length);
if (length === 2) {
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(0, isLittleEndian);
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
0,
isLittleEndian
);
} else if (length === 4) {
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(0, isLittleEndian);
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(
0,
isLittleEndian
);
}
}
@@ -79,7 +94,9 @@ function parseExifData(exifData) {
let value;
if (type === 2) {
// ASCII string
value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1));
value = String.fromCharCode(
...exifData.slice(valueOffset, valueOffset + numValues - 1)
);
}
result[tag] = value;
@@ -94,13 +111,13 @@ function parseExifData(exifData) {
}
function splitValues(input) {
var output = {};
for (var key in input) {
var output = {};
for (var key in input) {
var value = input[key];
var splitValues = value.split(':', 2);
var splitValues = value.split(":", 2);
output[splitValues[0]] = splitValues[1];
}
return output;
}
return output;
}
export function getWebpMetadata(file) {
@@ -111,7 +128,10 @@ export function getWebpMetadata(file) {
const dataView = new DataView(webp.buffer);
// Check that the WEBP signature is present
if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) {
if (
dataView.getUint32(0) !== 0x52494646 ||
dataView.getUint32(8) !== 0x57454250
) {
console.error("Not a valid WEBP file");
r({});
return;
@@ -123,15 +143,22 @@ export function getWebpMetadata(file) {
// Loop through the chunks in the WEBP file
while (offset < webp.length) {
const chunk_length = dataView.getUint32(offset + 4, true);
const chunk_type = String.fromCharCode(...webp.slice(offset, offset + 4));
const chunk_type = String.fromCharCode(
...webp.slice(offset, offset + 4)
);
if (chunk_type === "EXIF") {
if (String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == "Exif\0\0") {
if (
String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) ==
"Exif\0\0"
) {
offset += 6;
}
let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length));
let data = parseExifData(
webp.slice(offset + 8, offset + 8 + chunk_length)
);
for (var key in data) {
var value = data[key] as string;
let index = value.indexOf(':');
let index = value.indexOf(":");
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
}
}
@@ -150,11 +177,17 @@ export function getLatentMetadata(file) {
return new Promise((r) => {
const reader = new FileReader();
reader.onload = (event) => {
const safetensorsData = new Uint8Array(event.target.result as ArrayBuffer);
const safetensorsData = new Uint8Array(
event.target.result as ArrayBuffer
);
const dataView = new DataView(safetensorsData.buffer);
let header_size = dataView.getUint32(0, true);
let offset = 8;
let header = JSON.parse(new TextDecoder().decode(safetensorsData.slice(offset, offset + header_size)));
let header = JSON.parse(
new TextDecoder().decode(
safetensorsData.slice(offset, offset + header_size)
)
);
r(header.__metadata__);
};
@@ -164,7 +197,7 @@ export function getLatentMetadata(file) {
}
function getString(dataView: DataView, offset: number, length: number): string {
let string = '';
let string = "";
for (let i = 0; i < length; i++) {
string += String.fromCharCode(dataView.getUint8(offset + i));
}
@@ -188,7 +221,7 @@ function parseVorbisComment(dataView: DataView): Record<string, string> {
const comment = getString(dataView, offset, commentLength);
offset += commentLength;
const [key, value] = comment.split('=');
const [key, value] = comment.split("=");
comments[key] = value;
}
@@ -200,14 +233,16 @@ function parseVorbisComment(dataView: DataView): Record<string, string> {
export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
return new Promise((r) => {
const reader = new FileReader();
reader.onload = function(event) {
reader.onload = function (event) {
const arrayBuffer = event.target.result as ArrayBuffer;
const dataView = new DataView(arrayBuffer);
// Verify the FLAC signature
const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4));
if (signature !== 'fLaC') {
console.error('Not a valid FLAC file');
const signature = String.fromCharCode(
...new Uint8Array(arrayBuffer, 0, 4)
);
if (signature !== "fLaC") {
console.error("Not a valid FLAC file");
return;
}
@@ -216,12 +251,15 @@ export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
let vorbisComment = null;
while (offset < dataView.byteLength) {
const isLastBlock = dataView.getUint8(offset) & 0x80;
const blockType = dataView.getUint8(offset) & 0x7F;
const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF;
const blockType = dataView.getUint8(offset) & 0x7f;
const blockSize = dataView.getUint32(offset, false) & 0xffffff;
offset += 4;
if (blockType === 4) { // Vorbis Comment block type
vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize));
if (blockType === 4) {
// Vorbis Comment block type
vorbisComment = parseVorbisComment(
new DataView(arrayBuffer, offset, blockSize)
);
}
offset += blockSize;
@@ -241,11 +279,13 @@ export async function importA1111(graph, parameters) {
const opts = parameters
.substr(p)
.split("\n")[1]
.match(new RegExp("\\s*([^:]+:\\s*([^\"\\{].*?|\".*?\"|\\{.*?\\}))\\s*(,|$)", "g"))
.match(
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', "g")
)
.reduce((p, n) => {
const s = n.split(":");
if (s[1].endsWith(',')) {
s[1] = s[1].substr(0, s[1].length -1);
if (s[1].endsWith(",")) {
s[1] = s[1].substr(0, s[1].length - 1);
}
p[s[0].trim().toLowerCase()] = s[1].trim();
return p;
@@ -271,7 +311,7 @@ export async function importA1111(graph, parameters) {
const getWidget = (node, name) => {
return node.widgets.find((w) => w.name === name);
}
};
const setWidgetValue = (node, name, value, isOptionPrefix?) => {
const w = getWidget(node, name);
@@ -286,7 +326,7 @@ export async function importA1111(graph, parameters) {
} else {
w.value = value;
}
}
};
const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
const loras = [];
@@ -320,24 +360,28 @@ export async function importA1111(graph, parameters) {
}
return { text, prevModel, prevClip };
}
};
const replaceEmbeddings = (text) => {
if(!embeddings.length) return text;
if (!embeddings.length) return text;
return text.replaceAll(
new RegExp(
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
"\\b(" +
embeddings
.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join("\\b|\\b") +
")\\b",
"ig"
),
"embedding:$1"
);
}
};
const popOpt = (name) => {
const v = opts[name];
delete opts[name];
return v;
}
};
graph.clear();
graph.add(ckptNode);
@@ -365,7 +409,7 @@ export async function importA1111(graph, parameters) {
model(v) {
setWidgetValue(ckptNode, "ckpt_name", v, true);
},
"vae"(v) {
vae(v) {
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
},
"cfg scale"(v) {
@@ -383,7 +427,9 @@ export async function importA1111(graph, parameters) {
setWidgetValue(samplerNode, "scheduler", "normal");
}
const w = getWidget(samplerNode, "sampler_name");
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
const o = w.options.values.find(
(w) => w === name || w === "sample_" + name
);
if (o) {
setWidgetValue(samplerNode, "sampler_name", o);
}
@@ -431,11 +477,14 @@ export async function importA1111(graph, parameters) {
samplerNode.connect(0, decode, 0);
vaeLoaderNode.connect(0, decode, 1);
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
const upscaleLoaderNode =
LiteGraph.createNode("UpscaleModelLoader");
graph.add(upscaleLoaderNode);
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
const modelUpscaleNode = LiteGraph.createNode(
"ImageUpscaleWithModel"
);
graph.add(modelUpscaleNode);
decode.connect(0, modelUpscaleNode, 1);
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
@@ -444,7 +493,8 @@ export async function importA1111(graph, parameters) {
graph.add(upscaleNode);
modelUpscaleNode.connect(0, upscaleNode, 0);
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
const vaeEncodeNode = (latentNode =
LiteGraph.createNode("VAEEncodeTiled"));
graph.add(vaeEncodeNode);
upscaleNode.connect(0, vaeEncodeNode, 0);
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
@@ -477,14 +527,39 @@ export async function importA1111(graph, parameters) {
}
if (hrSamplerNode) {
setWidgetValue(hrSamplerNode, "steps", hrSteps? +hrSteps : getWidget(samplerNode, "steps").value);
setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value);
setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value);
setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value);
setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1"));
setWidgetValue(
hrSamplerNode,
"steps",
hrSteps ? +hrSteps : getWidget(samplerNode, "steps").value
);
setWidgetValue(
hrSamplerNode,
"cfg",
getWidget(samplerNode, "cfg").value
);
setWidgetValue(
hrSamplerNode,
"scheduler",
getWidget(samplerNode, "scheduler").value
);
setWidgetValue(
hrSamplerNode,
"sampler_name",
getWidget(samplerNode, "sampler_name").value
);
setWidgetValue(
hrSamplerNode,
"denoise",
+(popOpt("denoising strength") || "1")
);
}
let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 });
let n = createLoraNodes(
positiveNode,
positive,
{ node: clipSkipNode, index: 0 },
{ node: ckptNode, index: 0 }
);
positive = n.text;
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
negative = n.text;
@@ -494,7 +569,15 @@ export async function importA1111(graph, parameters) {
graph.arrange();
for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) {
for (const opt of [
"model hash",
"ensd",
"version",
"vae hash",
"ti hashes",
"lora hashes",
"hashes",
]) {
delete opts[opt];
}

View File

@@ -8,67 +8,77 @@ import { TaskItem } from "/types/apiTypes";
export const ComfyDialog = _ComfyDialog;
type Position2D = {
x: number,
y: number
x: number;
y: number;
};
type Props = {
parent?: HTMLElement,
$?: (el: HTMLElement) => void,
dataset?: DOMStringMap,
style?: Partial<CSSStyleDeclaration>,
for?: string,
textContent?: string,
[key: string]: any
parent?: HTMLElement;
$?: (el: HTMLElement) => void;
dataset?: DOMStringMap;
style?: Partial<CSSStyleDeclaration>;
for?: string;
textContent?: string;
[key: string]: any;
};
type Children = Element[] | Element | string | string[];
export function $el(tag: string, propsOrChildren?: Children | Props, children?: Children): HTMLElement {
const split = tag.split(".");
const element = document.createElement(split.shift() as string);
if (split.length > 0) {
element.classList.add(...split);
export function $el(
tag: string,
propsOrChildren?: Children | Props,
children?: Children
): HTMLElement {
const split = tag.split(".");
const element = document.createElement(split.shift() as string);
if (split.length > 0) {
element.classList.add(...split);
}
if (propsOrChildren) {
if (typeof propsOrChildren === "string") {
propsOrChildren = { textContent: propsOrChildren };
} else if (propsOrChildren instanceof Element) {
propsOrChildren = [propsOrChildren];
}
if (Array.isArray(propsOrChildren)) {
element.append(...propsOrChildren);
} else {
const {
parent,
$: cb,
dataset,
style,
...rest
} = propsOrChildren as Props;
if (propsOrChildren) {
if (typeof propsOrChildren === "string") {
propsOrChildren = { textContent: propsOrChildren };
} else if (propsOrChildren instanceof Element) {
propsOrChildren = [propsOrChildren];
}
if (Array.isArray(propsOrChildren)) {
element.append(...propsOrChildren);
} else {
const { parent, $: cb, dataset, style, ...rest } = propsOrChildren as Props;
if (rest.for) {
element.setAttribute("for", rest.for);
}
if (rest.for) {
element.setAttribute("for", rest.for)
}
if (style) {
Object.assign(element.style, style);
}
if (style) {
Object.assign(element.style, style);
}
if (dataset) {
Object.assign(element.dataset, dataset);
}
if (dataset) {
Object.assign(element.dataset, dataset);
}
Object.assign(element, rest);
if (children) {
element.append(...(Array.isArray(children) ? children : [children]));
}
Object.assign(element, rest);
if (children) {
element.append(...(Array.isArray(children) ? children : [children]));
}
if (parent) {
parent.append(element);
}
if (parent) {
parent.append(element);
}
if (cb) {
cb(element);
}
}
if (cb) {
cb(element);
}
}
return element;
}
return element;
}
function dragElement(dragEl, settings) {
@@ -93,12 +103,17 @@ function dragElement(dragEl, settings) {
function ensureInBounds() {
try {
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft));
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop));
newPosX = Math.min(
document.body.clientWidth - dragEl.clientWidth,
Math.max(0, dragEl.offsetLeft)
);
newPosY = Math.min(
document.body.clientHeight - dragEl.clientHeight,
Math.max(0, dragEl.offsetTop)
);
positionElement();
}
catch (exception) {
} catch (exception) {
// robust
}
}
@@ -112,7 +127,8 @@ function dragElement(dragEl, settings) {
// set the element's new position:
if (anchorRight) {
dragEl.style.left = "unset";
dragEl.style.right = document.body.clientWidth - newPosX - dragEl.clientWidth + "px";
dragEl.style.right =
document.body.clientWidth - newPosX - dragEl.clientWidth + "px";
} else {
dragEl.style.left = newPosX + "px";
dragEl.style.right = "unset";
@@ -180,8 +196,14 @@ function dragElement(dragEl, settings) {
posStartX = e.clientX;
posStartY = e.clientY;
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft + posDiffX));
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop + posDiffY));
newPosX = Math.min(
document.body.clientWidth - dragEl.clientWidth,
Math.max(0, dragEl.offsetLeft + posDiffX)
);
newPosY = Math.min(
document.body.clientHeight - dragEl.clientHeight,
Math.max(0, dragEl.offsetTop + posDiffY)
);
positionElement();
}
@@ -226,31 +248,40 @@ class ComfyList {
textContent: section,
}),
$el("div.comfy-list-items", [
...(this.#reverse ? items[section].reverse() : items[section]).map((item: TaskItem) => {
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
const removeAction = "remove" in item ? item.remove : {
name: "Delete",
cb: () => api.deleteItem(this.#type, item.prompt[1]),
};
return $el("div", { textContent: item.prompt[0] + ": " }, [
$el("button", {
textContent: "Load",
onclick: async () => {
await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow, true, false);
if ("outputs" in item) {
app.nodeOutputs = item.outputs;
}
},
}),
$el("button", {
textContent: removeAction.name,
onclick: async () => {
await removeAction.cb();
await this.update();
},
}),
]);
}),
...(this.#reverse ? items[section].reverse() : items[section]).map(
(item: TaskItem) => {
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
const removeAction =
"remove" in item
? item.remove
: {
name: "Delete",
cb: () => api.deleteItem(this.#type, item.prompt[1]),
};
return $el("div", { textContent: item.prompt[0] + ": " }, [
$el("button", {
textContent: "Load",
onclick: async () => {
await app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow,
true,
false
);
if ("outputs" in item) {
app.nodeOutputs = item.outputs;
}
},
}),
$el("button", {
textContent: removeAction.name,
onclick: async () => {
await removeAction.cb();
await this.update();
},
}),
]);
}
),
]),
]),
$el("div.comfy-list-actions", [
@@ -400,8 +431,15 @@ export class ComfyUI {
const autoQueueModeEl = toggleSwitch(
"autoQueueMode",
[
{ text: "instant", tooltip: "A new prompt will be queued as soon as the queue reaches 0" },
{ text: "change", tooltip: "A new prompt will be queued when the queue is at 0 and the graph is/has changed" },
{
text: "instant",
tooltip: "A new prompt will be queued as soon as the queue reaches 0",
},
{
text: "change",
tooltip:
"A new prompt will be queued when the queue is at 0 and the graph is/has changed",
},
],
{
onChange: (value) => {
@@ -435,30 +473,34 @@ export class ComfyUI {
) as HTMLDivElement;
this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
$el("div.drag-handle.comfy-menu-header", {
style: {
overflow: "hidden",
position: "relative",
width: "100%",
cursor: "default"
}
}, [
$el("span.drag-handle"),
$el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }),
$el("div.comfy-menu-actions", [
$el("button.comfy-settings-btn", {
textContent: "⚙️",
onclick: () => this.settings.show(),
}),
$el("button.comfy-close-menu-btn", {
textContent: "\u00d7",
onclick: () => {
this.menuContainer.style.display = "none";
this.menuHamburger.style.display = "flex";
},
}),
]),
]),
$el(
"div.drag-handle.comfy-menu-header",
{
style: {
overflow: "hidden",
position: "relative",
width: "100%",
cursor: "default",
},
},
[
$el("span.drag-handle"),
$el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }),
$el("div.comfy-menu-actions", [
$el("button.comfy-settings-btn", {
textContent: "⚙️",
onclick: () => this.settings.show(),
}),
$el("button.comfy-close-menu-btn", {
textContent: "\u00d7",
onclick: () => {
this.menuContainer.style.display = "none";
this.menuHamburger.style.display = "flex";
},
}),
]),
]
),
$el("button.comfy-queue-btn", {
id: "queue-button",
textContent: "Queue Prompt",
@@ -469,70 +511,95 @@ export class ComfyUI {
$el("input", {
type: "checkbox",
onchange: (i) => {
document.getElementById("extraOptions").style.display = i.srcElement.checked ? "block" : "none";
this.batchCount = i.srcElement.checked ?
Number.parseInt((document.getElementById("batchCountInputRange") as HTMLInputElement).value) : 1;
(document.getElementById("autoQueueCheckbox") as HTMLInputElement).checked = false;
document.getElementById("extraOptions").style.display = i
.srcElement.checked
? "block"
: "none";
this.batchCount = i.srcElement.checked
? Number.parseInt(
(
document.getElementById(
"batchCountInputRange"
) as HTMLInputElement
).value
)
: 1;
(
document.getElementById("autoQueueCheckbox") as HTMLInputElement
).checked = false;
this.autoQueueEnabled = false;
},
}),
]),
]),
$el("div", { id: "extraOptions", style: { width: "100%", display: "none" } }, [
$el("div", [
$el("label", { innerHTML: "Batch count" }),
$el("input", {
id: "batchCountInputNumber",
type: "number",
value: this.batchCount,
min: "1",
style: { width: "35%", "marginLeft": "0.4em" },
oninput: (i) => {
this.batchCount = i.target.value;
/* Even though an <input> element with a type of range logically represents a number (since
$el(
"div",
{ id: "extraOptions", style: { width: "100%", display: "none" } },
[
$el("div", [
$el("label", { innerHTML: "Batch count" }),
$el("input", {
id: "batchCountInputNumber",
type: "number",
value: this.batchCount,
min: "1",
style: { width: "35%", marginLeft: "0.4em" },
oninput: (i) => {
this.batchCount = i.target.value;
/* Even though an <input> element with a type of range logically represents a number (since
it's used for numeric input), the value it holds is still treated as a string in HTML and
JavaScript. This behavior is consistent across all <input> elements regardless of their type
(like text, number, or range), where the .value property is always a string. */
(document.getElementById("batchCountInputRange") as HTMLInputElement).value = this.batchCount.toString();
},
}),
$el("input", {
id: "batchCountInputRange",
type: "range",
min: "1",
max: "100",
value: this.batchCount,
oninput: (i) => {
this.batchCount = i.srcElement.value;
// Note
(document.getElementById("batchCountInputNumber") as HTMLInputElement).value = i.srcElement.value;
},
}),
]),
$el("div", [
$el("label", {
for: "autoQueueCheckbox",
innerHTML: "Auto Queue"
}),
$el("input", {
id: "autoQueueCheckbox",
type: "checkbox",
checked: false,
title: "Automatically queue prompt when the queue size hits 0",
onchange: (e) => {
this.autoQueueEnabled = e.target.checked;
autoQueueModeEl.style.display = this.autoQueueEnabled ? "" : "none";
}
}),
autoQueueModeEl
])
]),
(
document.getElementById(
"batchCountInputRange"
) as HTMLInputElement
).value = this.batchCount.toString();
},
}),
$el("input", {
id: "batchCountInputRange",
type: "range",
min: "1",
max: "100",
value: this.batchCount,
oninput: (i) => {
this.batchCount = i.srcElement.value;
// Note
(
document.getElementById(
"batchCountInputNumber"
) as HTMLInputElement
).value = i.srcElement.value;
},
}),
]),
$el("div", [
$el("label", {
for: "autoQueueCheckbox",
innerHTML: "Auto Queue",
}),
$el("input", {
id: "autoQueueCheckbox",
type: "checkbox",
checked: false,
title: "Automatically queue prompt when the queue size hits 0",
onchange: (e) => {
this.autoQueueEnabled = e.target.checked;
autoQueueModeEl.style.display = this.autoQueueEnabled
? ""
: "none";
},
}),
autoQueueModeEl,
]),
]
),
$el("div.comfy-menu-btns", [
$el("button", {
id: "queue-front-button",
textContent: "Queue Front",
onclick: () => app.queuePrompt(-1, this.batchCount)
onclick: () => app.queuePrompt(-1, this.batchCount),
}),
$el("button", {
$: (b) => (this.queue.button = b as HTMLButtonElement),
@@ -567,7 +634,7 @@ export class ComfyUI {
filename += ".json";
}
}
app.graphToPrompt().then(p => {
app.graphToPrompt().then((p) => {
const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
@@ -598,7 +665,7 @@ export class ComfyUI {
filename += ".json";
}
}
app.graphToPrompt().then(p => {
app.graphToPrompt().then((p) => {
const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
@@ -616,34 +683,48 @@ export class ComfyUI {
});
},
}),
$el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }),
$el("button", {
id: "comfy-load-button",
textContent: "Load",
onclick: () => fileInput.click(),
}),
$el("button", {
id: "comfy-refresh-button",
textContent: "Refresh",
onclick: () => app.refreshComboInNodes()
onclick: () => app.refreshComboInNodes(),
}),
$el("button", { id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace() }),
$el("button", {
id: "comfy-clear-button", textContent: "Clear", onclick: () => {
id: "comfy-clipspace-button",
textContent: "Clipspace",
onclick: () => app.openClipspace(),
}),
$el("button", {
id: "comfy-clear-button",
textContent: "Clear",
onclick: () => {
if (!confirmClear.value || confirm("Clear workflow?")) {
app.clean();
app.graph.clear();
app.resetView();
}
}
},
}),
$el("button", {
id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => {
id: "comfy-load-default-button",
textContent: "Load Default",
onclick: async () => {
if (!confirmClear.value || confirm("Load default workflow?")) {
app.resetView();
await app.loadGraphData()
await app.loadGraphData();
}
}
},
}),
$el("button", {
id: "comfy-reset-view-button", textContent: "Reset View", onclick: async () => {
id: "comfy-reset-view-button",
textContent: "Reset View",
onclick: async () => {
app.resetView();
}
},
}),
]) as HTMLDivElement;
@@ -652,7 +733,10 @@ export class ComfyUI {
name: "Enable Dev mode Options",
type: "boolean",
defaultValue: false,
onChange: function (value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none" },
onChange: function (value) {
document.getElementById("comfy-dev-save-api-button").style.display =
value ? "flex" : "none";
},
});
// @ts-ignore
@@ -662,7 +746,8 @@ export class ComfyUI {
}
setStatus(status) {
this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
this.queueSize.textContent =
"Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
if (status) {
if (
this.lastQueueSize != 0 &&

View File

@@ -2,63 +2,63 @@ import { ComfyDialog } from "../dialog";
import { $el } from "../../ui";
export class ComfyAsyncDialog extends ComfyDialog {
#resolve;
#resolve;
constructor(actions) {
super(
"dialog.comfy-dialog.comfyui-dialog",
actions?.map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
return $el("button.comfyui-button", {
type: "button",
textContent: opt.text,
onclick: () => this.close(opt.value ?? opt.text),
});
})
);
}
constructor(actions) {
super(
"dialog.comfy-dialog.comfyui-dialog",
actions?.map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
return $el("button.comfyui-button", {
type: "button",
textContent: opt.text,
onclick: () => this.close(opt.value ?? opt.text),
});
})
);
}
show(html) {
this.element.addEventListener("close", () => {
this.close();
});
show(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
super.show(html);
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
showModal(html) {
this.element.addEventListener("close", () => {
this.close();
});
showModal(html) {
this.element.addEventListener("close", () => {
this.close();
});
super.show(html);
this.element.showModal();
super.show(html);
this.element.showModal();
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
return new Promise((resolve) => {
this.#resolve = resolve;
});
}
close(result = null) {
this.#resolve(result);
this.element.close();
super.close();
}
close(result = null) {
this.#resolve(result);
this.element.close();
super.close();
}
static async prompt({ title = null, message, actions }) {
const dialog = new ComfyAsyncDialog(actions);
const content = [$el("span", message)];
if (title) {
content.unshift($el("h3", title));
}
const res = await dialog.showModal(content);
dialog.element.remove();
return res;
}
static async prompt({ title = null, message, actions }) {
const dialog = new ComfyAsyncDialog(actions);
const content = [$el("span", message)];
if (title) {
content.unshift($el("h3", title));
}
const res = await dialog.showModal(content);
dialog.element.remove();
return res;
}
}

View File

@@ -19,145 +19,159 @@ import { prop } from "../../utils";
* }} ComfyButtonProps
*/
export class ComfyButton {
#over = 0;
#popupOpen = false;
isOver = false;
iconElement = $el("i.mdi");
contentElement = $el("span");
/**
* @type {import("./popup").ComfyPopup}
*/
popup;
#over = 0;
#popupOpen = false;
isOver = false;
iconElement = $el("i.mdi");
contentElement = $el("span");
/**
* @type {import("./popup").ComfyPopup}
*/
popup;
/**
* @param {ComfyButtonProps} opts
*/
constructor({
icon,
overIcon,
iconSize,
content,
tooltip,
action,
classList = "comfyui-button",
visibilitySetting,
app,
enabled = true,
}) {
this.element = $el("button", {
onmouseenter: () => {
this.isOver = true;
if(this.overIcon) {
this.updateIcon();
}
},
onmouseleave: () => {
this.isOver = false;
if(this.overIcon) {
this.updateIcon();
}
}
/**
* @param {ComfyButtonProps} opts
*/
constructor({
icon,
overIcon,
iconSize,
content,
tooltip,
action,
classList = "comfyui-button",
visibilitySetting,
app,
enabled = true,
}) {
this.element = $el(
"button",
{
onmouseenter: () => {
this.isOver = true;
if (this.overIcon) {
this.updateIcon();
}
},
onmouseleave: () => {
this.isOver = false;
if (this.overIcon) {
this.updateIcon();
}
},
},
[this.iconElement, this.contentElement]
);
}, [this.iconElement, this.contentElement]);
this.icon = prop(
this,
"icon",
icon,
toggleElement(this.iconElement, { onShow: this.updateIcon })
);
this.overIcon = prop(this, "overIcon", overIcon, () => {
if (this.isOver) {
this.updateIcon();
}
});
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
this.content = prop(
this,
"content",
content,
toggleElement(this.contentElement, {
onShow: (el, v) => {
if (typeof v === "string") {
el.textContent = v;
} else {
el.replaceChildren(v);
}
},
})
);
this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon }));
this.overIcon = prop(this, "overIcon", overIcon, () => {
if(this.isOver) {
this.updateIcon();
}
});
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
this.content = prop(
this,
"content",
content,
toggleElement(this.contentElement, {
onShow: (el, v) => {
if (typeof v === "string") {
el.textContent = v;
} else {
el.replaceChildren(v);
}
},
})
);
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
if (v) {
this.element.title = v;
} else {
this.element.removeAttribute("title");
}
});
this.classList = prop(this, "classList", classList, this.updateClasses);
this.hidden = prop(this, "hidden", false, this.updateClasses);
this.enabled = prop(this, "enabled", enabled, () => {
this.updateClasses();
this.element.disabled = !this.enabled;
});
this.action = prop(this, "action", action);
this.element.addEventListener("click", (e) => {
if (this.popup) {
// we are either a touch device or triggered by click not hover
if (!this.#over) {
this.popup.toggle();
}
}
this.action?.(e, this);
});
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
if (v) {
this.element.title = v;
} else {
this.element.removeAttribute("title");
}
});
this.classList = prop(this, "classList", classList, this.updateClasses);
this.hidden = prop(this, "hidden", false, this.updateClasses);
this.enabled = prop(this, "enabled", enabled, () => {
this.updateClasses();
this.element.disabled = !this.enabled;
});
this.action = prop(this, "action", action);
this.element.addEventListener("click", (e) => {
if (this.popup) {
// we are either a touch device or triggered by click not hover
if (!this.#over) {
this.popup.toggle();
}
}
this.action?.(e, this);
});
if (visibilitySetting?.id) {
const settingUpdated = () => {
this.hidden =
app.ui.settings.getSettingValue(visibilitySetting.id) !==
visibilitySetting.showValue;
};
app.ui.settings.addEventListener(
visibilitySetting.id + ".change",
settingUpdated
);
settingUpdated();
}
}
if (visibilitySetting?.id) {
const settingUpdated = () => {
this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue;
};
app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated);
settingUpdated();
}
}
updateIcon = () =>
(this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
updateClasses = () => {
const internalClasses = [];
if (this.hidden) {
internalClasses.push("hidden");
}
if (!this.enabled) {
internalClasses.push("disabled");
}
if (this.popup) {
if (this.#popupOpen) {
internalClasses.push("popup-open");
} else {
internalClasses.push("popup-closed");
}
}
applyClasses(this.element, this.classList, ...internalClasses);
};
updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
updateClasses = () => {
const internalClasses = [];
if (this.hidden) {
internalClasses.push("hidden");
}
if (!this.enabled) {
internalClasses.push("disabled");
}
if (this.popup) {
if (this.#popupOpen) {
internalClasses.push("popup-open");
} else {
internalClasses.push("popup-closed");
}
}
applyClasses(this.element, this.classList, ...internalClasses);
};
/**
*
* @param { import("./popup").ComfyPopup } popup
* @param { "click" | "hover" } mode
*/
withPopup(popup, mode = "click") {
this.popup = popup;
/**
*
* @param { import("./popup").ComfyPopup } popup
* @param { "click" | "hover" } mode
*/
withPopup(popup, mode = "click") {
this.popup = popup;
if (mode === "hover") {
for (const el of [this.element, this.popup.element]) {
el.addEventListener("mouseenter", () => {
this.popup.open = !!++this.#over;
});
el.addEventListener("mouseleave", () => {
this.popup.open = !!--this.#over;
});
}
}
if (mode === "hover") {
for (const el of [this.element, this.popup.element]) {
el.addEventListener("mouseenter", () => {
this.popup.open = !!++this.#over;
});
el.addEventListener("mouseleave", () => {
this.popup.open = !!--this.#over;
});
}
}
popup.addEventListener("change", () => {
this.#popupOpen = popup.open;
this.updateClasses();
});
popup.addEventListener("change", () => {
this.#popupOpen = popup.open;
this.updateClasses();
});
return this;
}
return this;
}
}

View File

@@ -5,41 +5,41 @@ import { ComfyButton } from "./button";
import { prop } from "../../utils";
export class ComfyButtonGroup {
element = $el("div.comfyui-button-group");
element = $el("div.comfyui-button-group");
/** @param {Array<ComfyButton | HTMLElement>} buttons */
constructor(...buttons) {
this.buttons = prop(this, "buttons", buttons, () => this.update());
}
/** @param {Array<ComfyButton | HTMLElement>} buttons */
constructor(...buttons) {
this.buttons = prop(this, "buttons", buttons, () => this.update());
}
/**
* @param {ComfyButton} button
* @param {number} index
*/
insert(button, index) {
this.buttons.splice(index, 0, button);
this.update();
}
/**
* @param {ComfyButton} button
* @param {number} index
*/
insert(button, index) {
this.buttons.splice(index, 0, button);
this.update();
}
/** @param {ComfyButton} button */
append(button) {
this.buttons.push(button);
this.update();
}
/** @param {ComfyButton} button */
append(button) {
this.buttons.push(button);
this.update();
}
/** @param {ComfyButton|number} indexOrButton */
remove(indexOrButton) {
if (typeof indexOrButton !== "number") {
indexOrButton = this.buttons.indexOf(indexOrButton);
}
if (indexOrButton > -1) {
const r = this.buttons.splice(indexOrButton, 1);
this.update();
return r;
}
}
/** @param {ComfyButton|number} indexOrButton */
remove(indexOrButton) {
if (typeof indexOrButton !== "number") {
indexOrButton = this.buttons.indexOf(indexOrButton);
}
if (indexOrButton > -1) {
const r = this.buttons.splice(indexOrButton, 1);
this.update();
return r;
}
}
update() {
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
}
update() {
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
}
}

View File

@@ -5,124 +5,133 @@ import { $el } from "../../ui";
import { applyClasses } from "../utils";
export class ComfyPopup extends EventTarget {
element = $el("div.comfyui-popup");
element = $el("div.comfyui-popup");
/**
* @param {{
* target: HTMLElement,
* container?: HTMLElement,
* classList?: import("../utils").ClassList,
* ignoreTarget?: boolean,
* closeOnEscape?: boolean,
* position?: "absolute" | "relative",
* horizontal?: "left" | "right"
* }} param0
* @param {...HTMLElement} children
*/
constructor(
{
target,
container = document.body,
classList = "",
ignoreTarget = true,
closeOnEscape = true,
position = "absolute",
horizontal = "left",
},
...children
) {
super();
this.target = target;
this.ignoreTarget = ignoreTarget;
this.container = container;
this.position = position;
this.closeOnEscape = closeOnEscape;
this.horizontal = horizontal;
/**
* @param {{
* target: HTMLElement,
* container?: HTMLElement,
* classList?: import("../utils").ClassList,
* ignoreTarget?: boolean,
* closeOnEscape?: boolean,
* position?: "absolute" | "relative",
* horizontal?: "left" | "right"
* }} param0
* @param {...HTMLElement} children
*/
constructor(
{
target,
container = document.body,
classList = "",
ignoreTarget = true,
closeOnEscape = true,
position = "absolute",
horizontal = "left",
},
...children
) {
super();
this.target = target;
this.ignoreTarget = ignoreTarget;
this.container = container;
this.position = position;
this.closeOnEscape = closeOnEscape;
this.horizontal = horizontal;
container.append(this.element);
container.append(this.element);
this.children = prop(this, "children", children, () => {
this.element.replaceChildren(...this.children);
this.update();
});
this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal));
this.open = prop(this, "open", false, (v, o) => {
if (v === o) return;
if (v) {
this.#show();
} else {
this.#hide();
}
});
}
this.children = prop(this, "children", children, () => {
this.element.replaceChildren(...this.children);
this.update();
});
this.classList = prop(this, "classList", classList, () =>
applyClasses(this.element, this.classList, "comfyui-popup", horizontal)
);
this.open = prop(this, "open", false, (v, o) => {
if (v === o) return;
if (v) {
this.#show();
} else {
this.#hide();
}
});
}
toggle() {
this.open = !this.open;
}
toggle() {
this.open = !this.open;
}
#hide() {
this.element.classList.remove("open");
window.removeEventListener("resize", this.update);
window.removeEventListener("click", this.#clickHandler, { capture: true });
window.removeEventListener("keydown", this.#escHandler, { capture: true });
#hide() {
this.element.classList.remove("open");
window.removeEventListener("resize", this.update);
window.removeEventListener("click", this.#clickHandler, { capture: true });
window.removeEventListener("keydown", this.#escHandler, { capture: true });
this.dispatchEvent(new CustomEvent("close"));
this.dispatchEvent(new CustomEvent("change"));
}
this.dispatchEvent(new CustomEvent("close"));
this.dispatchEvent(new CustomEvent("change"));
}
#show() {
this.element.classList.add("open");
this.update();
#show() {
this.element.classList.add("open");
this.update();
window.addEventListener("resize", this.update);
window.addEventListener("click", this.#clickHandler, { capture: true });
if (this.closeOnEscape) {
window.addEventListener("keydown", this.#escHandler, { capture: true });
}
window.addEventListener("resize", this.update);
window.addEventListener("click", this.#clickHandler, { capture: true });
if (this.closeOnEscape) {
window.addEventListener("keydown", this.#escHandler, { capture: true });
}
this.dispatchEvent(new CustomEvent("open"));
this.dispatchEvent(new CustomEvent("change"));
}
this.dispatchEvent(new CustomEvent("open"));
this.dispatchEvent(new CustomEvent("change"));
}
#escHandler = (e) => {
if (e.key === "Escape") {
this.open = false;
e.preventDefault();
e.stopImmediatePropagation();
}
};
#escHandler = (e) => {
if (e.key === "Escape") {
this.open = false;
e.preventDefault();
e.stopImmediatePropagation();
}
};
#clickHandler = (e) => {
/** @type {any} */
const target = e.target;
if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) {
this.open = false;
}
};
#clickHandler = (e) => {
/** @type {any} */
const target = e.target;
if (
!this.element.contains(target) &&
this.ignoreTarget &&
!this.target.contains(target)
) {
this.open = false;
}
};
update = () => {
const rect = this.target.getBoundingClientRect();
this.element.style.setProperty("--bottom", "unset");
if (this.position === "absolute") {
if (this.horizontal === "left") {
this.element.style.setProperty("--left", rect.left + "px");
} else {
this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px");
}
this.element.style.setProperty("--top", rect.bottom + "px");
this.element.style.setProperty("--limit", rect.bottom + "px");
} else {
this.element.style.setProperty("--left", 0 + "px");
this.element.style.setProperty("--top", rect.height + "px");
this.element.style.setProperty("--limit", rect.height + "px");
}
update = () => {
const rect = this.target.getBoundingClientRect();
this.element.style.setProperty("--bottom", "unset");
if (this.position === "absolute") {
if (this.horizontal === "left") {
this.element.style.setProperty("--left", rect.left + "px");
} else {
this.element.style.setProperty(
"--left",
rect.right - this.element.clientWidth + "px"
);
}
this.element.style.setProperty("--top", rect.bottom + "px");
this.element.style.setProperty("--limit", rect.bottom + "px");
} else {
this.element.style.setProperty("--left", 0 + "px");
this.element.style.setProperty("--top", rect.height + "px");
this.element.style.setProperty("--limit", rect.height + "px");
}
const thisRect = this.element.getBoundingClientRect();
if (thisRect.height < 30) {
// Move up instead
this.element.style.setProperty("--top", "unset");
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
this.element.style.setProperty("--limit", rect.height + 5 + "px");
}
};
const thisRect = this.element.getBoundingClientRect();
if (thisRect.height < 30) {
// Move up instead
this.element.style.setProperty("--top", "unset");
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
this.element.style.setProperty("--limit", rect.height + 5 + "px");
}
};
}

View File

@@ -6,38 +6,47 @@ import { prop } from "../../utils";
import { ComfyPopup } from "./popup";
export class ComfySplitButton {
/**
* @param {{
* primary: ComfyButton,
* mode?: "hover" | "click",
* horizontal?: "left" | "right",
* position?: "relative" | "absolute"
* }} param0
* @param {Array<ComfyButton> | Array<HTMLElement>} items
*/
constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) {
this.arrow = new ComfyButton({
icon: "chevron-down",
});
this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [
$el("div.comfyui-split-primary", primary.element),
$el("div.comfyui-split-arrow", this.arrow.element),
]);
this.popup = new ComfyPopup({
target: this.element,
container: position === "relative" ? this.element : document.body,
classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
closeOnEscape: mode === "click",
position,
horizontal,
});
/**
* @param {{
* primary: ComfyButton,
* mode?: "hover" | "click",
* horizontal?: "left" | "right",
* position?: "relative" | "absolute"
* }} param0
* @param {Array<ComfyButton> | Array<HTMLElement>} items
*/
constructor(
{ primary, mode, horizontal = "left", position = "relative" },
...items
) {
this.arrow = new ComfyButton({
icon: "chevron-down",
});
this.element = $el(
"div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""),
[
$el("div.comfyui-split-primary", primary.element),
$el("div.comfyui-split-arrow", this.arrow.element),
]
);
this.popup = new ComfyPopup({
target: this.element,
container: position === "relative" ? this.element : document.body,
classList:
"comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
closeOnEscape: mode === "click",
position,
horizontal,
});
this.arrow.withPopup(this.popup, mode);
this.arrow.withPopup(this.popup, mode);
this.items = prop(this, "items", items, () => this.update());
}
this.items = prop(this, "items", items, () => this.update());
}
update() {
this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b));
}
update() {
this.popup.element.replaceChildren(
...this.items.map((b) => b.element ?? b)
);
}
}

View File

@@ -1,6 +1,8 @@
import { $el } from "../ui";
export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarget {
export class ComfyDialog<
T extends HTMLElement = HTMLElement,
> extends EventTarget {
element: T;
textElement: HTMLElement;
#buttons: HTMLButtonElement[] | null;
@@ -9,7 +11,10 @@ export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarge
super();
this.#buttons = buttons;
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
$el("div.comfy-modal-content", [
$el("p", { $: (p) => (this.textElement = p) }),
...this.createButtons(),
]),
]) as T;
}
@@ -33,7 +38,9 @@ export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarge
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
this.textElement.replaceChildren(
...(html instanceof Array ? html : [html])
);
}
this.element.style.display = "flex";
}

View File

@@ -27,8 +27,8 @@
import { $el } from "../ui";
$el("style", {
parent: document.head,
textContent: `
parent: document.head,
textContent: `
.draggable-item {
position: relative;
will-change: transform;
@@ -40,7 +40,7 @@ $el("style", {
.draggable-item.is-draggable {
z-index: 10;
}
`
`,
});
export class DraggableList extends EventTarget {
@@ -57,9 +57,9 @@ export class DraggableList extends EventTarget {
offDrag = [];
constructor(element, itemSelector) {
super();
super();
this.listContainer = element;
this.itemSelector = itemSelector;
this.itemSelector = itemSelector;
if (!this.listContainer) return;
@@ -71,7 +71,9 @@ export class DraggableList extends EventTarget {
getAllItems() {
if (!this.items?.length) {
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
this.items = Array.from(
this.listContainer.querySelectorAll(this.itemSelector)
);
this.items.forEach((element) => {
element.classList.add("is-idle");
});
@@ -80,7 +82,9 @@ export class DraggableList extends EventTarget {
}
getIdleItems() {
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
return this.getAllItems().filter((item) =>
item.classList.contains("is-idle")
);
}
isItemAbove(item) {
@@ -106,18 +110,24 @@ export class DraggableList extends EventTarget {
this.pointerStartX = e.clientX || e.touches[0].clientX;
this.pointerStartY = e.clientY || e.touches[0].clientY;
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
this.scrollYMax =
this.listContainer.scrollHeight - this.listContainer.clientHeight;
this.setItemsGap();
this.initDraggableItem();
this.initItemsState();
this.offDrag.push(this.on(document, "mousemove", this.drag));
this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false }));
this.offDrag.push(
this.on(document, "touchmove", this.drag, { passive: false })
);
this.dispatchEvent(
new CustomEvent("dragstart", {
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
detail: {
element: this.draggableItem,
position: this.getAllItems().indexOf(this.draggableItem),
},
})
);
}
@@ -250,7 +260,11 @@ export class DraggableList extends EventTarget {
this.dispatchEvent(
new CustomEvent("dragend", {
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
detail: {
element: this.draggableItem,
oldPosition,
newPosition: reorderedItems.indexOf(this.draggableItem),
},
})
);
}

View File

@@ -57,7 +57,11 @@ export function createImageHost(node) {
}
const nw = node.size[0];
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
({ cellWidth: w, cellHeight: h } = calculateImageGrid(
currentImgs,
nw - 20,
elH
));
w += "px";
h += "px";
@@ -86,10 +90,13 @@ export function createImageHost(node) {
onDraw() {
// Element from point uses a hittest find elements so we need to toggle pointer events
el.style.pointerEvents = "all";
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
const over = document.elementFromPoint(
app.canvas.mouse[0],
app.canvas.mouse[1]
);
el.style.pointerEvents = "none";
if(!over) return;
if (!over) return;
// Set the overIndex so Open Image etc work
const idx = currentImgs.indexOf(over);
node.overIndex = idx;

View File

@@ -48,7 +48,11 @@ export class ComfyAppMenu {
content: t,
});
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
this.logo = $el(
"h1.comfyui-logo.nlg-hide",
{ title: "ComfyUI" },
"ComfyUI"
);
this.saveButton = new ComfySplitButton(
{
primary: getSaveButton(),
@@ -71,7 +75,8 @@ export class ComfyAppMenu {
new ComfyButton({
icon: "api",
content: "Export (API Format)",
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
tooltip:
"Export the current workflow as JSON for use with the ComfyUI API",
action: () => this.exportWorkflow("workflow_api", "output"),
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
app,
@@ -101,7 +106,10 @@ export class ComfyAppMenu {
content: "Clear",
tooltip: "Clears current workflow",
action: () => {
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
if (
!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) ||
confirm("Clear workflow?")
) {
app.clean();
app.graph.clear();
}
@@ -126,7 +134,9 @@ export class ComfyAppMenu {
this.mobileMenuButton = new ComfyButton({
icon: "menu",
action: (_, btn) => {
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
btn.icon = this.element.classList.toggle("expanded")
? "menu-open"
: "menu";
window.dispatchEvent(new Event("resize"));
},
classList: "comfyui-button comfyui-menu-button",
@@ -239,7 +249,10 @@ export class ComfyAppMenu {
idx--;
}
} else if (innerSize > this.element.clientWidth) {
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(
window.innerWidth,
innerSize
);
// We need to shrink
if (idx < this.#sizeBreaks.length - 1) {
idx++;
@@ -254,19 +267,26 @@ export class ComfyAppMenu {
clearTimeout(this.#cacheTimeout);
if (this.#cachedInnerSize) {
// Extend cache time
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
this.#cacheTimeout = setTimeout(
() => (this.#cachedInnerSize = null),
100
);
} else {
let innerSize = 0;
let count = 1;
for (const c of this.element.children) {
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items
if (idx && c.classList.contains("comfyui-menu-mobile-collapse"))
continue; // ignore collapse items
innerSize += c.clientWidth;
count++;
}
innerSize += 8 * count;
this.#cachedInnerSize = innerSize;
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
this.#cacheTimeout = setTimeout(
() => (this.#cachedInnerSize = null),
100
);
}
return this.#cachedInnerSize;
}

View File

@@ -14,7 +14,10 @@ export class ComfyQueueButton {
queuePrompt = async (e) => {
this.#internalQueueSize += this.queueOptions.batchCount;
// Hold shift to queue front, event is undefined when auto-queue is enabled
await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount);
await this.app.queuePrompt(
e?.shiftKey ? -1 : 0,
this.queueOptions.batchCount
);
};
constructor(app) {
@@ -63,7 +66,10 @@ export class ComfyQueueButton {
}
});
this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"]));
this.queueOptions.addEventListener(
"autoQueueMode",
(e) => (this.autoQueueMode = e["detail"])
);
api.addEventListener("graphChanged", () => {
if (this.autoQueueMode === "change") {
@@ -79,10 +85,14 @@ export class ComfyQueueButton {
api.addEventListener("status", ({ detail }) => {
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
if (this.#internalQueueSize != null) {
this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
this.queueSizeElement.textContent =
this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
if (!this.#internalQueueSize && !app.lastExecutionError) {
if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) {
if (
this.autoQueueMode === "instant" ||
(this.autoQueueMode === "change" && this.graphHasChanged)
) {
this.graphHasChanged = false;
this.queuePrompt();
}

View File

@@ -69,11 +69,14 @@ export class ComfyViewList {
},
});
this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [
$el("h3", mode),
$el("header", [this.clear.element, this.refresh.element]),
this.items,
]);
this.element = $el(
`div.comfyui-${this.type}-popup.comfyui-view-list-popup`,
[
$el("h3", mode),
$el("header", [this.clear.element, this.refresh.element]),
this.items,
]
);
api.addEventListener("status", () => {
if (this.popup.open) {
@@ -155,7 +158,9 @@ export class ComfyViewList {
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
await this.app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow
);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}

View File

@@ -31,7 +31,9 @@ export class ComfyViewQueueList extends ComfyViewList {
text: "Load",
action: async () => {
try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
await this.app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow
);
if (item.outputs) {
this.app.nodeOutputs = item.outputs;
}
@@ -51,5 +53,5 @@ export class ComfyViewQueueList extends ComfyViewList {
},
],
};
}
};
}

View File

@@ -37,14 +37,21 @@ export class ComfyWorkflowsMenu {
this.buttonProgress = $el("div.comfyui-workflows-button-progress");
this.workflowLabel = $el("span.comfyui-workflows-label", "");
this.button = new ComfyButton({
content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]),
content: $el("div.comfyui-workflows-button-inner", [
$el("i.mdi.mdi-graph"),
this.workflowLabel,
this.buttonProgress,
]),
icon: "chevron-down",
classList,
});
this.element.append(this.button.element);
this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" });
this.popup = new ComfyPopup({
target: this.element,
classList: "comfyui-workflows-popup",
});
this.content = new ComfyWorkflowsContent(app, this.popup);
this.popup.children = [this.content.element];
this.popup.addEventListener("change", () => {
@@ -85,7 +92,10 @@ export class ComfyWorkflowsMenu {
};
#bindEvents() {
this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive);
this.app.workflowManager.addEventListener(
"changeWorkflow",
this.#updateActive
);
this.app.workflowManager.addEventListener("rename", this.#updateActive);
this.app.workflowManager.addEventListener("delete", this.#updateActive);
@@ -157,10 +167,15 @@ export class ComfyWorkflowsMenu {
name: "Comfy.Workflows",
async beforeRegisterNodeDef(nodeType) {
function getImageWidget(node) {
const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional };
const inputs = {
...node.constructor?.nodeData?.input?.required,
...node.constructor?.nodeData?.input?.optional,
};
for (const input in inputs) {
if (inputs[input][0] === "IMAGEUPLOAD") {
const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image"));
const imageWidget = node.widgets.find(
(w) => w.name === (inputs[input]?.[1]?.widget ?? "image")
);
if (imageWidget) return imageWidget;
}
}
@@ -213,8 +228,13 @@ export class ComfyWorkflowsMenu {
const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"];
nodeType.prototype["getExtraMenuOptions"] = function (_, options) {
const r = getExtraMenuOptions?.apply?.(this, arguments);
if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) {
const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this);
if (
app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true
) {
const t =
/** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (
this
);
let img;
if (t.imageIndex != null) {
// An image is selected so select that
@@ -238,10 +258,13 @@ export class ComfyWorkflowsMenu {
submenu: {
options: [
{
callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow),
callback: () =>
sendToWorkflow(img, app.workflowManager.activeWorkflow),
title: "[Current workflow]",
},
...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)),
...self.#getFavoriteMenuOptions(
sendToWorkflow.bind(null, img)
),
null,
...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
],
@@ -315,7 +338,9 @@ export class ComfyWorkflowsContent {
this.element.replaceChildren(this.actions, this.spinner);
this.popup.addEventListener("open", () => this.load());
this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner));
this.popup.addEventListener("close", () =>
this.element.replaceChildren(this.actions, this.spinner)
);
this.app.workflowManager.addEventListener("favorite", (e) => {
const workflow = e["detail"];
@@ -331,7 +356,9 @@ export class ComfyWorkflowsContent {
app.workflowManager.addEventListener(e, () => this.updateOpen());
}
this.app.workflowManager.addEventListener("rename", () => this.load());
this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive());
this.app.workflowManager.addEventListener("execute", (e) =>
this.#updateActive()
);
}
async load() {
@@ -339,7 +366,12 @@ export class ComfyWorkflowsContent {
this.updateTree();
this.updateFavorites();
this.updateOpen();
this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement);
this.element.replaceChildren(
this.actions,
this.openElement,
this.favoritesElement,
this.treeElement
);
}
updateOpen() {
@@ -368,7 +400,7 @@ export class ComfyWorkflowsContent {
if (w.unsaved) {
wrapper.element.classList.add("unsaved");
}
if(w === this.app.workflowManager.activeWorkflow) {
if (w === this.app.workflowManager.activeWorkflow) {
wrapper.element.classList.add("active");
}
@@ -383,7 +415,9 @@ export class ComfyWorkflowsContent {
updateFavorites() {
const current = this.favoritesElement;
const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)];
const favorites = [
...this.app.workflowManager.workflows.filter((w) => w.isFavorite),
];
this.favoritesElement = $el("div.comfyui-workflows-favorites", [
$el("h3", "Favorites"),
@@ -437,7 +471,10 @@ export class ComfyWorkflowsContent {
hideTreeParents(element) {
// Hide all parents if no children are visible
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
if (
element.parentElement?.classList.contains("comfyui-workflows-tree") ===
false
) {
for (let i = 1; i < element.parentElement.children.length; i++) {
const c = element.parentElement.children[i];
if (c.style.display !== "none") {
@@ -450,7 +487,10 @@ export class ComfyWorkflowsContent {
}
showTreeParents(element) {
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
if (
element.parentElement?.classList.contains("comfyui-workflows-tree") ===
false
) {
element.parentElement.style.removeProperty("display");
this.showTreeParents(element.parentElement);
}
@@ -490,7 +530,9 @@ export class ComfyWorkflowsContent {
for (let i = 0; i < workflow.pathParts.length; i++) {
currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i];
const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot);
const parentNode =
nodes[currentPath] ??
this.#createNode(currentPath, workflow, i, currentRoot);
nodes[currentPath] = parentNode;
currentRoot = parentNode;
@@ -559,7 +601,9 @@ export class ComfyWorkflowsContent {
/** @param {ComfyWorkflow} workflow */
#getFavoriteTooltip(workflow) {
return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites";
return workflow.isFavorite
? "Remove this workflow from your favorites"
: "Add this workflow to your favorites";
}
/** @param {ComfyWorkflow} workflow */
@@ -568,7 +612,9 @@ export class ComfyWorkflowsContent {
icon: this.#getFavoriteIcon(workflow),
overIcon: this.#getFavoriteOverIcon(workflow),
iconSize: 18,
classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""),
classList:
"comfyui-button comfyui-workflows-file-action-favorite" +
(primary ? " comfyui-workflows-file-action-primary" : ""),
tooltip: this.#getFavoriteTooltip(workflow),
action: (e) => {
e.stopImmediatePropagation();
@@ -628,7 +674,9 @@ export class ComfyWorkflowsContent {
#getRenameButton(workflow) {
return new ComfyButton({
icon: "pencil",
tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.",
tooltip: workflow.path
? "Rename this workflow"
: "This workflow can't be renamed as it hasn't been saved.",
classList: "comfyui-button comfyui-workflows-file-action",
iconSize: 18,
enabled: !!workflow.path,
@@ -646,7 +694,11 @@ export class ComfyWorkflowsContent {
#getWorkflowElement(workflow) {
return new WorkflowElement(this, workflow, {
primary: this.#getFavoriteButton(workflow, true),
buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)],
buttons: [
this.#getInsertButton(workflow),
this.#getRenameButton(workflow),
this.#getDeleteButton(workflow),
],
});
}
@@ -660,14 +712,17 @@ export class ComfyWorkflowsContent {
#createNode(currentPath, workflow, i, currentRoot) {
const part = workflow.pathParts[i];
const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), {
$: (el) => {
el.onclick = (e) => {
this.#expandNode(el, workflow, currentPath, i);
e.stopImmediatePropagation();
};
},
});
const parentNode = $el(
"ul" + (this.treeState[currentPath] ? "" : ".closed"),
{
$: (el) => {
el.onclick = (e) => {
this.#expandNode(el, workflow, currentPath, i);
e.stopImmediatePropagation();
};
},
}
);
currentRoot.append(parentNode);
// Create a node for the current part and an inner UL for its children if it isnt a leaf node
@@ -676,7 +731,10 @@ export class ComfyWorkflowsContent {
if (leaf) {
nodeElement = this.#createLeafNode(workflow).element;
} else {
nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]);
nodeElement = $el("li", [
$el("i.mdi.mdi-18px.mdi-folder"),
$el("span", part),
]);
}
parentNode.append(nodeElement);
return parentNode;
@@ -703,7 +761,11 @@ class WorkflowElement {
},
title: this.workflow.path,
},
[this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)]
[
this.primary?.element,
$el("span", workflow.name),
...buttons.map((b) => b.element),
]
);
}
}
@@ -732,7 +794,11 @@ class WidgetSelectionDialog extends ComfyAsyncDialog {
"section",
this.#options.map((opt) => {
return $el("div.comfy-widget-selection-item", [
$el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`),
$el(
"span",
{ dataset: { id: opt.node.id } },
`${opt.node.title ?? opt.node.type} ${opt.widget.name}`
),
$el(
"button.comfyui-button",
{
@@ -760,4 +826,4 @@ class WidgetSelectionDialog extends ComfyAsyncDialog {
])
);
}
}
}

View File

@@ -19,7 +19,14 @@ interface SettingOption {
interface SettingParams {
id: string;
name: string;
type: string | ((name: string, setter: (v: any) => void, value: any, attrs: any) => HTMLElement);
type:
| string
| ((
name: string,
setter: (v: any) => void,
value: any,
attrs: any
) => HTMLElement);
defaultValue: any;
onChange?: (newValue: any, oldValue?: any) => void;
attrs?: any;
@@ -81,7 +88,7 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
new CustomEvent(id + ".change", {
detail: {
value,
oldValue
oldValue,
},
})
);
@@ -115,8 +122,7 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
if (this.app.storageLocation === "browser") {
try {
value = JSON.parse(value);
} catch (error) {
}
} catch (error) {}
}
}
return value ?? defaultValue;
@@ -145,7 +151,16 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
}
addSetting(params: SettingParams) {
const { id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined } = params;
const {
id,
name,
type,
defaultValue,
onChange,
attrs = {},
tooltip = "",
options = undefined,
} = params;
if (!id) {
throw new Error("Settings must have an ID");
}
@@ -272,7 +287,8 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
style: { maxWidth: "4rem" },
oninput: (e) => {
setter(e.target.value);
e.target.previousElementSibling.value = e.target.value;
e.target.previousElementSibling.value =
e.target.value;
},
}),
]
@@ -291,7 +307,10 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
setter(e.target.value);
},
},
(typeof options === "function" ? options(value) : options || []).map((opt) => {
(typeof options === "function"
? options(value)
: options || []
).map((opt) => {
if (typeof opt === "string") {
opt = { text: opt };
}
@@ -309,7 +328,9 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
case "text":
default:
if (type !== "text") {
console.warn(`Unsupported setting type '${type}, defaulting to text`);
console.warn(
`Unsupported setting type '${type}, defaulting to text`
);
}
element = $el("tr", [
@@ -356,7 +377,10 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
},
[$el("th"), $el("th", { style: { width: "33%" } })]
),
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean)
...this.settings
.sort((a, b) => a.name.localeCompare(b.name))
.map((s) => s.render())
.filter(Boolean)
);
this.element.showModal();
}

View File

@@ -1,6 +1,5 @@
import "./spinner.css";
export function createSpinner() {
const div = document.createElement("div");
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;

View File

@@ -20,7 +20,10 @@ export function toggleSwitch(name, items, e?) {
if (selectedIndex != null) {
elements[selectedIndex].classList.remove("comfy-toggle-selected");
}
onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] });
onChange?.({
item: items[index],
prev: selectedIndex == null ? undefined : items[selectedIndex],
});
selectedIndex = index;
elements[selectedIndex].classList.add("comfy-toggle-selected");
}

View File

@@ -3,16 +3,14 @@ import { $el } from "../ui";
import { createSpinner } from "./spinner";
import "./userSelection.css";
interface SelectedUser {
username: string;
userId: string;
created: boolean;
}
export class UserSelectionScreen {
async show(users, user): Promise<SelectedUser>{
async show(users, user): Promise<SelectedUser> {
const userSelection = document.getElementById("comfy-user-selection");
userSelection.style.display = "";
return new Promise((resolve) => {
@@ -22,7 +20,9 @@ export class UserSelectionScreen {
const selectSection = select.closest("section");
const form = userSelection.getElementsByTagName("form")[0];
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
const button = userSelection.getElementsByClassName("comfy-user-button-next")[0];
const button = userSelection.getElementsByClassName(
"comfy-user-button-next"
)[0];
let inputActive = null;
input.addEventListener("focus", () => {
@@ -45,7 +45,8 @@ export class UserSelectionScreen {
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (inputActive == null) {
error.textContent = "Please enter a username or select an existing user.";
error.textContent =
"Please enter a username or select an existing user.";
} else if (inputActive) {
const username = input.value.trim();
if (!username) {
@@ -54,41 +55,59 @@ export class UserSelectionScreen {
}
// Create new user
// @ts-ignore
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
input.disabled = select.disabled = input.readonly = select.readonly = true;
input.disabled =
select.disabled =
// @ts-ignore
input.readonly =
// @ts-ignore
select.readonly =
true;
const spinner = createSpinner();
button.prepend(spinner);
try {
const resp = await api.createUser(username);
if (resp.status >= 300) {
let message = "Error creating user: " + resp.status + " " + resp.statusText;
let message =
"Error creating user: " + resp.status + " " + resp.statusText;
try {
const res = await resp.json();
if(res.error) {
if (res.error) {
message = res.error;
}
} catch (error) {
}
} catch (error) {}
throw new Error(message);
}
resolve({ username, userId: await resp.json(), created: true });
} catch (err) {
spinner.remove();
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred.";
// @ts-ignore
error.textContent =
err.message ??
err.statusText ??
err ??
"An unknown error occurred.";
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
input.disabled = select.disabled = input.readonly = select.readonly = false;
input.disabled =
select.disabled =
// @ts-ignore
input.readonly =
// @ts-ignore
select.readonly =
false;
return;
}
} else if (!select.value) {
error.textContent = "Please select an existing user.";
return;
} else {
resolve({ username: users[select.value], userId: select.value, created: false });
resolve({
username: users[select.value],
userId: select.value,
created: false,
});
}
});

View File

@@ -57,7 +57,9 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
// Find node with matching S&R property name
// @ts-ignore
let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]);
let nodes = app.graph._nodes.filter(
(n) => n.properties?.["Node name for S&R"] === split[0]
);
// If we cant, see if there is a node with that title
if (!nodes.length) {
// @ts-ignore
@@ -76,7 +78,13 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
const widget = node.widgets?.find((w) => w.name === split[1]);
if (!widget) {
console.warn("Unable to find widget", split[1], "on node", split[0], node);
console.warn(
"Unable to find widget",
split[1],
"on node",
split[0],
node
);
return match;
}
@@ -84,13 +92,19 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
});
}
export async function addStylesheet(urlOrFile: string, relativeTo?: string): Promise<void> {
export async function addStylesheet(
urlOrFile: string,
relativeTo?: string
): Promise<void> {
return new Promise((res, rej) => {
let url;
if (urlOrFile.endsWith(".js")) {
url = urlOrFile.substr(0, urlOrFile.length - 2) + "css";
} else {
url = new URL(urlOrFile, relativeTo ?? `${window.location.protocol}//${window.location.host}`).toString();
url = new URL(
urlOrFile,
relativeTo ?? `${window.location.protocol}//${window.location.host}`
).toString();
}
$el("link", {
parent: document.head,
@@ -103,7 +117,6 @@ export async function addStylesheet(urlOrFile: string, relativeTo?: string): Pro
});
}
/**
* @param { string } filename
* @param { Blob } blob
@@ -147,7 +160,10 @@ export function prop(target, name, defaultValue, onChanged) {
export function getStorageValue(id) {
const clientId = api.clientId ?? api.initialClientId;
return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id);
return (
(clientId && sessionStorage.getItem(`${id}:${clientId}`)) ??
localStorage.getItem(id)
);
}
export function setStorageValue(id, value) {
@@ -156,4 +172,4 @@ export function setStorageValue(id, value) {
sessionStorage.setItem(`${id}:${clientId}`, value);
}
localStorage.setItem(id, value);
}
}

View File

@@ -1,20 +1,23 @@
import { api } from "./api"
import { api } from "./api";
import "./domWidget";
import type { ComfyApp } from "./app";
import type { IWidget, LGraphNode } from "/types/litegraph";
import { ComfyNodeDef } from "/types/apiTypes";
export type ComfyWidgetConstructor = (
node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app?: ComfyApp, widgetName?: string) =>
{widget: IWidget, minWidth?: number; minHeight?: number };
node: LGraphNode,
inputName: string,
inputData: ComfyNodeDef,
app?: ComfyApp,
widgetName?: string
) => { widget: IWidget; minWidth?: number; minHeight?: number };
let controlValueRunBefore = false;
export function updateControlWidgetLabel(widget) {
let replacement = "after";
let find = "before";
if (controlValueRunBefore) {
[find, replacement] = [replacement, find]
[find, replacement] = [replacement, find];
}
widget.label = (widget.label ?? widget.name).replace(find, replacement);
}
@@ -22,9 +25,14 @@ export function updateControlWidgetLabel(widget) {
const IS_CONTROL_WIDGET = Symbol();
const HAS_EXECUTED = Symbol();
function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enable_rounding) {
function getNumberDefaults(
inputData: ComfyNodeDef,
defaultStep,
precision,
enable_rounding
) {
let defaultVal = inputData[1]["default"];
let { min, max, step, round} = inputData[1];
let { min, max, step, round } = inputData[1];
if (defaultVal == undefined) defaultVal = 0;
if (min == undefined) min = 0;
@@ -33,30 +41,52 @@ function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enab
// precision is the number of decimal places to show.
// by default, display the the smallest number of decimal places such that changes of size step are visible.
if (precision == undefined) {
precision = Math.max(-Math.floor(Math.log10(step)),0);
precision = Math.max(-Math.floor(Math.log10(step)), 0);
}
if (enable_rounding && (round == undefined || round === true)) {
// by default, round the value to those decimal places shown.
round = Math.round(1000000*Math.pow(0.1,precision))/1000000;
round = Math.round(1000000 * Math.pow(0.1, precision)) / 1000000;
}
return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } };
return {
val: defaultVal,
config: { min, max, step: 10.0 * step, round, precision },
};
}
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values, widgetName, inputData: ComfyNodeDef) {
export function addValueControlWidget(
node,
targetWidget,
defaultValue = "randomize",
values,
widgetName,
inputData: ComfyNodeDef
) {
let name = inputData[1]?.control_after_generate;
if(typeof name !== "string") {
if (typeof name !== "string") {
name = widgetName;
}
const widgets = addValueControlWidgets(node, targetWidget, defaultValue, {
addFilterList: false,
controlAfterGenerateName: name
}, inputData);
const widgets = addValueControlWidgets(
node,
targetWidget,
defaultValue,
{
addFilterList: false,
controlAfterGenerateName: name,
},
inputData
);
return widgets[0];
}
export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData: ComfyNodeDef) {
export function addValueControlWidgets(
node,
targetWidget,
defaultValue = "randomize",
options,
inputData: ComfyNodeDef
) {
if (!defaultValue) defaultValue = "randomize";
if (!options) options = {};
@@ -67,10 +97,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
} else if (typeof inputData?.[1]?.[defaultName] === "string") {
name = inputData?.[1]?.[defaultName];
} else if (inputData?.[1]?.control_prefix) {
name = inputData?.[1]?.control_prefix + " " + name
name = inputData?.[1]?.control_prefix + " " + name;
}
return name;
}
};
const widgets = [];
const valueControl = node.addWidget(
@@ -120,16 +150,23 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
const regex = new RegExp(filter.substring(1, filter.length - 1));
check = (item) => regex.test(item);
} catch (error) {
console.error("Error constructing RegExp filter for node " + node.id, filter, error);
console.error(
"Error constructing RegExp filter for node " + node.id,
filter,
error
);
}
}
if (!check) {
const lower = filter.toLocaleLowerCase();
check = (item) => item.toLocaleLowerCase().includes(lower);
}
values = values.filter(item => check(item));
values = values.filter((item) => check(item));
if (!values.length && targetWidget.options.values.length) {
console.warn("Filter for node " + node.id + " has filtered out all items", filter);
console.warn(
"Filter for node " + node.id + " has filtered out all items",
filter
);
}
}
let current_index = values.indexOf(targetWidget.value);
@@ -141,8 +178,8 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
break;
case "increment-wrap":
current_index += 1;
if ( current_index >= current_length ) {
current_index = 0;
if (current_index >= current_length) {
current_index = 0;
}
break;
case "decrement":
@@ -181,7 +218,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
targetWidget.value -= targetWidget.options.step / 10;
break;
case "randomize":
targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min;
targetWidget.value =
Math.floor(Math.random() * range) *
(targetWidget.options.step / 10) +
min;
break;
default:
break;
@@ -190,8 +230,7 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
* ranges and set them to min or max.*/
if (targetWidget.value < min) targetWidget.value = min;
if (targetWidget.value > max)
targetWidget.value = max;
if (targetWidget.value > max) targetWidget.value = max;
targetWidget.callback(targetWidget.value);
}
};
@@ -213,20 +252,39 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
};
return widgets;
};
}
function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) {
const seed = createIntWidget(node, inputName, inputData, app, true);
const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData);
const seedControl = addValueControlWidget(
node,
seed.widget,
"randomize",
undefined,
widgetName,
inputData
);
seed.widget.linkedWidgets = [seedControl];
return seed;
}
function createIntWidget(node, inputName, inputData: ComfyNodeDef, app, isSeedInput: boolean = false) {
function createIntWidget(
node,
inputName,
inputData: ComfyNodeDef,
app,
isSeedInput: boolean = false
) {
const control = inputData[1]?.control_after_generate;
if (!isSeedInput && control) {
return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined);
return seedWidget(
node,
inputName,
inputData,
app,
typeof control === "string" ? control : undefined
);
}
let widgetType = isSlider(inputData[1]["display"], app);
@@ -275,10 +333,10 @@ function addMultilineWidget(node, name, opts, app) {
function isSlider(display, app) {
if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
return "number"
return "number";
}
return (display==="slider") ? "slider" : "number"
return display === "slider" ? "slider" : "number";
}
export function initWidgets(app) {
@@ -288,7 +346,8 @@ export function initWidgets(app) {
type: "combo",
defaultValue: "after",
options: ["before", "after"],
tooltip: "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
tooltip:
"Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
onChange(value) {
controlValueRunBefore = value === "before";
for (const n of app.graph._nodes) {
@@ -313,21 +372,41 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
"INT:seed": seedWidget,
"INT:noise_seed": seedWidget,
FLOAT(node, inputName, inputData: ComfyNodeDef, app) {
let widgetType: "number" | "slider" = isSlider(inputData[1]["display"], app);
let precision = app.ui.settings.getSettingValue("Comfy.FloatRoundingPrecision");
let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding")
let widgetType: "number" | "slider" = isSlider(
inputData[1]["display"],
app
);
let precision = app.ui.settings.getSettingValue(
"Comfy.FloatRoundingPrecision"
);
let disable_rounding = app.ui.settings.getSettingValue(
"Comfy.DisableFloatRounding"
);
if (precision == 0) precision = undefined;
const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding);
return { widget: node.addWidget(widgetType, inputName, val,
function (v) {
if (config.round) {
this.value = Math.round((v + Number.EPSILON)/config.round)*config.round;
if (this.value > config.max) this.value = config.max;
if (this.value < config.min) this.value = config.min;
} else {
this.value = v;
}
}, config) };
const { val, config } = getNumberDefaults(
inputData,
0.5,
precision,
!disable_rounding
);
return {
widget: node.addWidget(
widgetType,
inputName,
val,
function (v) {
if (config.round) {
this.value =
Math.round((v + Number.EPSILON) / config.round) * config.round;
if (this.value > config.max) this.value = config.max;
if (this.value < config.min) this.value = config.min;
} else {
this.value = v;
}
},
config
),
};
},
INT(node, inputName, inputData: ComfyNodeDef, app) {
return createIntWidget(node, inputName, inputData, app);
@@ -336,12 +415,9 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
let defaultVal = false;
let options = {};
if (inputData[1]) {
if (inputData[1].default)
defaultVal = inputData[1].default;
if (inputData[1].label_on)
options["on"] = inputData[1].label_on;
if (inputData[1].label_off)
options["off"] = inputData[1].label_off;
if (inputData[1].default) defaultVal = inputData[1].default;
if (inputData[1].label_on) options["on"] = inputData[1].label_on;
if (inputData[1].label_off) options["off"] = inputData[1].label_off;
}
return {
widget: node.addWidget(
@@ -349,8 +425,8 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
inputName,
defaultVal,
() => {},
options,
)
options
),
};
},
STRING(node, inputName, inputData: ComfyNodeDef, app) {
@@ -359,12 +435,19 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
let res;
if (multiline) {
res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
res = addMultilineWidget(
node,
inputName,
{ defaultVal, ...inputData[1] },
app
);
} else {
res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
res = {
widget: node.addWidget("text", inputName, defaultVal, () => {}, {}),
};
}
if(inputData[1].dynamicPrompts != undefined)
if (inputData[1].dynamicPrompts != undefined)
res.widget.dynamicPrompts = inputData[1].dynamicPrompts;
return res;
@@ -375,18 +458,35 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
if (inputData[1] && inputData[1].default) {
defaultValue = inputData[1].default;
}
const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
const res = {
widget: node.addWidget("combo", inputName, defaultValue, () => {}, {
values: type,
}),
};
if (inputData[1]?.control_after_generate) {
// TODO make combo handle a widget node type?
// @ts-ignore
res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData);
res.widget.linkedWidgets = addValueControlWidgets(
node,
res.widget,
undefined,
undefined,
inputData
);
}
return res;
},
IMAGEUPLOAD(node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app) {
IMAGEUPLOAD(
node: LGraphNode,
inputName: string,
inputData: ComfyNodeDef,
app
) {
// TODO make image upload handle a custom node type?
// @ts-ignore
const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image"));
const imageWidget = node.widgets.find(
(w) => w.name === (inputData[1]?.widget ?? "image")
);
let uploadWidget;
function showImage(name) {
@@ -402,18 +502,20 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
subfolder = name.substring(0, folder_separator);
name = name.substring(folder_separator + 1);
}
img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`);
img.src = api.apiURL(
`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
);
// @ts-ignore
node.setSizeForImage?.();
}
var default_value = imageWidget.value;
Object.defineProperty(imageWidget, "value", {
set : function(value) {
set: function (value) {
this._real_value = value;
},
get : function() {
get: function () {
if (!this._real_value) {
return default_value;
}
@@ -428,11 +530,11 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
value += real_value.filename;
if(real_value.type && real_value.type !== "input")
if (real_value.type && real_value.type !== "input")
value += ` [${real_value.type}]`;
}
return value;
}
},
});
// Add our own callback to the combo widget to render an image when it changes
@@ -535,15 +637,15 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
};
// @ts-ignore
node.pasteFile = function(file) {
node.pasteFile = function (file) {
if (file.type.startsWith("image/")) {
const is_pasted = (file.name === "image.png") &&
(file.lastModified - Date.now() < 2000);
const is_pasted =
file.name === "image.png" && file.lastModified - Date.now() < 2000;
uploadFile(file, true, is_pasted);
return true;
}
return false;
}
};
return { widget: uploadWidget };
},

View File

@@ -57,7 +57,10 @@ export class ComfyWorkflowManager extends EventTarget {
#bindExecutionEvents() {
// TODO: on reload, set active prompt based on the latest ws message
const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt }));
const emit = () =>
this.dispatchEvent(
new CustomEvent("execute", { detail: this.activePrompt })
);
let executing = null;
api.addEventListener("execution_start", (e) => {
this.#activePromptId = e.detail.prompt_id;
@@ -106,14 +109,21 @@ export class ComfyWorkflowManager extends EventTarget {
favorites = new Set();
}
const workflows = (await api.listUserData("workflows", true, true)).map((w) => {
let workflow = this.workflowLookup[w[0]];
if (!workflow) {
workflow = new ComfyWorkflow(this, w[0], w.slice(1), favorites.has(w[0]));
this.workflowLookup[workflow.path] = workflow;
const workflows = (await api.listUserData("workflows", true, true)).map(
(w) => {
let workflow = this.workflowLookup[w[0]];
if (!workflow) {
workflow = new ComfyWorkflow(
this,
w[0],
w.slice(1),
favorites.has(w[0])
);
this.workflowLookup[workflow.path] = workflow;
}
return workflow;
}
return workflow;
});
);
this.workflows = workflows;
} catch (error) {
@@ -124,7 +134,9 @@ export class ComfyWorkflowManager extends EventTarget {
async saveWorkflowMetadata() {
await api.storeUserData("workflows/.index.json", {
favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)],
favorites: [
...this.workflows.filter((w) => w.isFavorite).map((w) => w.path),
],
});
}
@@ -137,13 +149,20 @@ export class ComfyWorkflowManager extends EventTarget {
const found = this.workflows.find((w) => w.path === workflow);
if (found) {
workflow = found;
workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
workflow.unsaved =
!workflow ||
getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
}
}
if (!(workflow instanceof ComfyWorkflow)) {
// Still not found, either reloading a deleted workflow or blank
workflow = new ComfyWorkflow(this, workflow || "Unsaved Workflow" + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : ""));
workflow = new ComfyWorkflow(
this,
workflow ||
"Unsaved Workflow" +
(this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "")
);
}
const index = this.openWorkflows.indexOf(workflow);
@@ -293,7 +312,9 @@ export class ComfyWorkflow {
async getWorkflowData() {
const resp = await api.getUserData("workflows/" + this.path);
if (resp.status !== 200) {
alert(`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
alert(
`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`
);
return;
}
return await resp.json();
@@ -301,7 +322,12 @@ export class ComfyWorkflow {
load = async () => {
if (this.isOpen) {
await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this);
await this.manager.app.loadGraphData(
this.changeTracker.activeState,
true,
true,
this
);
} else {
const data = await this.getWorkflowData();
if (!data) return;
@@ -327,7 +353,12 @@ export class ComfyWorkflow {
await this.manager.saveWorkflowMetadata();
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
} catch (error) {
alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error));
alert(
"Error favoriting workflow " +
this.path +
"\n" +
(error.message ?? error)
);
}
}
@@ -336,15 +367,29 @@ export class ComfyWorkflow {
*/
async rename(path) {
path = appendJsonExt(path);
let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path);
let resp = await api.moveUserData(
"workflows/" + this.path,
"workflows/" + path
);
if (resp.status === 409) {
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return resp;
resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path, { overwrite: true });
if (
!confirm(
`Workflow '${path}' already exists, do you want to overwrite it?`
)
)
return resp;
resp = await api.moveUserData(
"workflows/" + this.path,
"workflows/" + path,
{ overwrite: true }
);
}
if (resp.status !== 200) {
alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
alert(
`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`
);
return;
}
@@ -367,7 +412,10 @@ export class ComfyWorkflow {
const old = localStorage.getItem("litegrapheditor_clipboard");
const graph = new LGraph(data);
const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true });
const canvas = new LGraphCanvas(null, graph, {
skip_events: true,
skip_render: true,
});
canvas.selectNodes();
canvas.copyToClipboard();
this.manager.app.canvas.pasteFromClipboard();
@@ -406,7 +454,10 @@ export class ComfyWorkflow {
*/
async #save(path, overwrite) {
if (!path) {
path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow");
path = prompt(
"Save workflow as:",
trimJsonExt(this.path) ?? this.name ?? "workflow"
);
if (!path) return;
}
@@ -414,14 +465,27 @@ export class ComfyWorkflow {
const p = await this.manager.app.graphToPrompt();
const json = JSON.stringify(p.workflow, null, 2);
let resp = await api.storeUserData("workflows/" + path, json, { stringify: false, throwOnError: false, overwrite });
let resp = await api.storeUserData("workflows/" + path, json, {
stringify: false,
throwOnError: false,
overwrite,
});
if (resp.status === 409) {
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return;
resp = await api.storeUserData("workflows/" + path, json, { stringify: false });
if (
!confirm(
`Workflow '${path}' already exists, do you want to overwrite it?`
)
)
return;
resp = await api.storeUserData("workflows/" + path, json, {
stringify: false,
});
}
if (resp.status !== 200) {
alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`);
alert(
`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`
);
return;
}

View File

@@ -7,107 +7,113 @@ const zQueueIndex = z.number();
const zPromptId = z.string();
const zPromptItem = z.object({
inputs: z.record(z.string(), z.any()),
class_type: zNodeType,
inputs: z.record(z.string(), z.any()),
class_type: zNodeType,
});
const zPrompt = z.array(zPromptItem);
const zExtraPngInfo = z.object({
const zExtraPngInfo = z
.object({
workflow: zComfyWorkflow,
}).passthrough();
})
.passthrough();
const zExtraData = z.object({
extra_pnginfo: zExtraPngInfo,
client_id: z.string(),
extra_pnginfo: zExtraPngInfo,
client_id: z.string(),
});
const zOutputsToExecute = z.array(zNodeId);
const zExecutionStartMessage = z.tuple([
z.literal("execution_start"),
z.object({
prompt_id: zPromptId,
}),
z.literal("execution_start"),
z.object({
prompt_id: zPromptId,
}),
]);
const zExecutionCachedMessage = z.tuple([
z.literal("execution_cached"),
z.object({
prompt_id: zPromptId,
nodes: z.array(zNodeId),
}),
z.literal("execution_cached"),
z.object({
prompt_id: zPromptId,
nodes: z.array(zNodeId),
}),
]);
const zExecutionInterruptedMessage = z.tuple([
z.literal("execution_interrupted"),
z.object({
// InterruptProcessingException
prompt_id: zPromptId,
node_id: zNodeId,
node_type: zNodeType,
executed: z.array(zNodeId),
}),
z.literal("execution_interrupted"),
z.object({
// InterruptProcessingException
prompt_id: zPromptId,
node_id: zNodeId,
node_type: zNodeType,
executed: z.array(zNodeId),
}),
]);
const zExecutionErrorMessage = z.tuple([
z.literal("execution_error"),
z.object({
prompt_id: zPromptId,
node_id: zNodeId,
node_type: zNodeType,
executed: z.array(zNodeId),
z.literal("execution_error"),
z.object({
prompt_id: zPromptId,
node_id: zNodeId,
node_type: zNodeType,
executed: z.array(zNodeId),
exception_message: z.string(),
exception_type: z.string(),
traceback: z.string(),
current_inputs: z.any(),
current_outputs: z.any(),
}),
exception_message: z.string(),
exception_type: z.string(),
traceback: z.string(),
current_inputs: z.any(),
current_outputs: z.any(),
}),
]);
const zStatusMessage = z.union([
zExecutionStartMessage,
zExecutionCachedMessage,
zExecutionInterruptedMessage,
zExecutionErrorMessage,
zExecutionStartMessage,
zExecutionCachedMessage,
zExecutionInterruptedMessage,
zExecutionErrorMessage,
]);
const zStatus = z.object({
status_str: z.enum(["success", "error"]),
completed: z.boolean(),
messages: z.array(zStatusMessage),
status_str: z.enum(["success", "error"]),
completed: z.boolean(),
messages: z.array(zStatusMessage),
});
// TODO: this is a placeholder
const zOutput = z.any();
const zTaskPrompt = z.tuple([
zQueueIndex,
zPromptId,
zPrompt,
zExtraData,
zOutputsToExecute,
zQueueIndex,
zPromptId,
zPrompt,
zExtraData,
zOutputsToExecute,
]);
const zRunningTaskItem = z.object({
prompt: zTaskPrompt,
remove: z.object({
name: z.literal("Cancel"),
cb: z.function(),
}),
prompt: zTaskPrompt,
remove: z.object({
name: z.literal("Cancel"),
cb: z.function(),
}),
});
const zPendingTaskItem = z.object({
prompt: zTaskPrompt,
prompt: zTaskPrompt,
});
const zHistoryTaskItem = z.object({
prompt: zTaskPrompt,
status: zStatus.optional(),
outputs: z.record(zNodeId, zOutput),
prompt: zTaskPrompt,
status: zStatus.optional(),
outputs: z.record(zNodeId, zOutput),
});
const zTaskItem = z.union([zRunningTaskItem, zPendingTaskItem, zHistoryTaskItem]);
const zTaskItem = z.union([
zRunningTaskItem,
zPendingTaskItem,
zHistoryTaskItem,
]);
// `/queue`
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>;
@@ -119,101 +125,100 @@ export type TaskItem = z.infer<typeof zTaskItem>;
// TODO: validate `/history` `/queue` API endpoint responses.
function inputSpec(spec: [ZodType, ZodType]): ZodType {
const [inputType, inputSpec] = spec;
return z.union([
z.tuple([inputType, inputSpec]),
z.tuple([inputType]),
]);
const [inputType, inputSpec] = spec;
return z.union([z.tuple([inputType, inputSpec]), z.tuple([inputType])]);
}
const zIntInputSpec = inputSpec([
z.literal("INT"),
z.object({
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
default: z.number().optional(),
forceInput: z.boolean().optional(),
}),
z.literal("INT"),
z.object({
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
default: z.number().optional(),
forceInput: z.boolean().optional(),
}),
]);
const zFloatInputSpec = inputSpec([
z.literal("FLOAT"),
z.object({
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
round: z.number().optional(),
default: z.number().optional(),
forceInput: z.boolean().optional(),
}),
z.literal("FLOAT"),
z.object({
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
round: z.number().optional(),
default: z.number().optional(),
forceInput: z.boolean().optional(),
}),
]);
const zBooleanInputSpec = inputSpec([
z.literal("BOOLEAN"),
z.object({
label_on: z.string().optional(),
label_off: z.string().optional(),
default: z.boolean().optional(),
forceInput: z.boolean().optional(),
})
z.literal("BOOLEAN"),
z.object({
label_on: z.string().optional(),
label_off: z.string().optional(),
default: z.boolean().optional(),
forceInput: z.boolean().optional(),
}),
]);
const zStringInputSpec = inputSpec([
z.literal("STRING"),
z.object({
default: z.string().optional(),
multiline: z.boolean().optional(),
dynamicPrompts: z.boolean().optional(),
forceInput: z.boolean().optional(),
}),
z.literal("STRING"),
z.object({
default: z.string().optional(),
multiline: z.boolean().optional(),
dynamicPrompts: z.boolean().optional(),
forceInput: z.boolean().optional(),
}),
]);
// Dropdown Selection.
const zComboInputSpec = inputSpec([
z.array(z.any()),
z.object({
default: z.any().optional(),
control_after_generate: z.boolean().optional(),
image_upload: z.boolean().optional(),
forceInput: z.boolean().optional(),
}),
z.array(z.any()),
z.object({
default: z.any().optional(),
control_after_generate: z.boolean().optional(),
image_upload: z.boolean().optional(),
forceInput: z.boolean().optional(),
}),
]);
const zCustomInputSpec = inputSpec([
z.string(),
z.object({
default: z.any().optional(),
forceInput: z.boolean().optional(),
}),
z.string(),
z.object({
default: z.any().optional(),
forceInput: z.boolean().optional(),
}),
]);
const zInputSpec = z.union([
zIntInputSpec,
zFloatInputSpec,
zBooleanInputSpec,
zStringInputSpec,
zComboInputSpec,
zCustomInputSpec,
zIntInputSpec,
zFloatInputSpec,
zBooleanInputSpec,
zStringInputSpec,
zComboInputSpec,
zCustomInputSpec,
]);
const zComfyNodeDataType = z.string();
const zComfyComboOutput = z.array(z.any());
const zComfyOutputSpec = z.array(z.union([zComfyNodeDataType, zComfyComboOutput]));
const zComfyOutputSpec = z.array(
z.union([zComfyNodeDataType, zComfyComboOutput])
);
const zComfyNodeDef = z.object({
input: z.object({
required: z.record(zInputSpec).optional(),
optional: z.record(zInputSpec).optional(),
}),
output: zComfyOutputSpec,
output_is_list: z.array(z.boolean()),
output_name: z.array(z.string()),
name: z.string(),
display_name: z.string(),
description: z.string(),
category: z.string(),
output_node: z.boolean(),
input: z.object({
required: z.record(zInputSpec).optional(),
optional: z.record(zInputSpec).optional(),
}),
output: zComfyOutputSpec,
output_is_list: z.array(z.boolean()),
output_name: z.array(z.string()),
name: z.string(),
display_name: z.string(),
description: z.string(),
category: z.string(),
output_node: z.boolean(),
});
// `/object_info`

View File

@@ -1,6 +1,7 @@
import { z } from 'zod';
import { z } from "zod";
const nodeSlotSchema = z.object({
const nodeSlotSchema = z
.object({
BOOLEAN: z.string().optional(),
CLIP: z.string(),
CLIP_VISION: z.string(),
@@ -25,10 +26,12 @@ const nodeSlotSchema = z.object({
TAESD: z.string(),
TIMESTEP_KEYFRAME: z.string().optional(),
UPSCALE_MODEL: z.string().optional(),
VAE: z.string()
}).passthrough();
VAE: z.string(),
})
.passthrough();
const litegraphBaseSchema = z.object({
const litegraphBaseSchema = z
.object({
BACKGROUND_IMAGE: z.string(),
CLEAR_BACKGROUND_COLOR: z.string(),
NODE_TITLE_COLOR: z.string(),
@@ -49,37 +52,40 @@ const litegraphBaseSchema = z.object({
WIDGET_SECONDARY_TEXT_COLOR: z.string(),
LINK_COLOR: z.string(),
EVENT_LINK_COLOR: z.string(),
CONNECTING_LINK_COLOR: z.string()
}).passthrough();
CONNECTING_LINK_COLOR: z.string(),
})
.passthrough();
const comfyBaseSchema = z.object({
["fg-color"]: z.string(),
["bg-color"]: z.string(),
["comfy-menu-bg"]: z.string(),
["comfy-input-bg"]: z.string(),
["input-text"]: z.string(),
["descrip-text"]: z.string(),
["drag-text"]: z.string(),
["error-text"]: z.string(),
["border-color"]: z.string(),
["tr-even-bg-color"]: z.string(),
["tr-odd-bg-color"]: z.string(),
["content-bg"]: z.string(),
["content-fg"]: z.string(),
["content-hover-bg"]: z.string(),
["content-hover-fg"]: z.string(),
["fg-color"]: z.string(),
["bg-color"]: z.string(),
["comfy-menu-bg"]: z.string(),
["comfy-input-bg"]: z.string(),
["input-text"]: z.string(),
["descrip-text"]: z.string(),
["drag-text"]: z.string(),
["error-text"]: z.string(),
["border-color"]: z.string(),
["tr-even-bg-color"]: z.string(),
["tr-odd-bg-color"]: z.string(),
["content-bg"]: z.string(),
["content-fg"]: z.string(),
["content-hover-bg"]: z.string(),
["content-hover-fg"]: z.string(),
});
const colorsSchema = z.object({
const colorsSchema = z
.object({
node_slot: nodeSlotSchema,
litegraph_base: litegraphBaseSchema,
comfy_base: comfyBaseSchema
}).passthrough();
comfy_base: comfyBaseSchema,
})
.passthrough();
const paletteSchema = z.object({
id: z.string(),
name: z.string(),
colors: colorsSchema
id: z.string(),
name: z.string(),
colors: colorsSchema,
});
const colorPalettesSchema = z.record(paletteSchema);

147
src/types/comfy.d.ts vendored
View File

@@ -2,75 +2,90 @@ import { LGraphNode, IWidget } from "./litegraph";
import { ComfyApp } from "../../scripts/app";
export interface ComfyExtension {
/**
* The name of the extension
*/
name: string;
/**
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
* @param app The ComfyUI app instance
*/
init?(app: ComfyApp): Promise<void>;
/**
* Allows any additonal setup, called after the application is fully set up and running
* @param app The ComfyUI app instance
*/
setup?(app: ComfyApp): Promise<void>;
/**
* Called before nodes are registered with the graph
* @param defs The collection of node definitions, add custom ones or edit existing ones
* @param app The ComfyUI app instance
*/
addCustomNodeDefs?(defs: Record<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>;
/**
* Allows the extension to add custom widgets
* @param app The ComfyUI app instance
* @returns An array of {[widget name]: widget data}
*/
getCustomWidgets?(
app: ComfyApp
): Promise<
Record<string, (node, inputName, inputData, app) => { widget?: IWidget; minWidth?: number; minHeight?: number }>
>;
/**
* Allows the extension to add additional handling to the node before it is registered with LGraph
* @param nodeType The node class (not an instance)
* @param nodeData The original node object info config object
* @param app The ComfyUI app instance
*/
beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise<void>;
/**
* Allows the extension to register additional nodes with LGraph after standard nodes are added
* @param app The ComfyUI app instance
*/
registerCustomNodes?(app: ComfyApp): Promise<void>;
/**
* Allows the extension to modify a node that has been reloaded onto the graph.
* If you break something in the backend and want to patch workflows in the frontend
* This is the place to do this
* @param node The node that has been loaded
* @param app The ComfyUI app instance
*/
loadedGraphNode?(node: LGraphNode, app: ComfyApp);
/**
* Allows the extension to run code after the constructor of the node
* @param node The node that has been created
* @param app The ComfyUI app instance
*/
nodeCreated?(node: LGraphNode, app: ComfyApp);
/**
* The name of the extension
*/
name: string;
/**
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
* @param app The ComfyUI app instance
*/
init?(app: ComfyApp): Promise<void>;
/**
* Allows any additonal setup, called after the application is fully set up and running
* @param app The ComfyUI app instance
*/
setup?(app: ComfyApp): Promise<void>;
/**
* Called before nodes are registered with the graph
* @param defs The collection of node definitions, add custom ones or edit existing ones
* @param app The ComfyUI app instance
*/
addCustomNodeDefs?(
defs: Record<string, ComfyObjectInfo>,
app: ComfyApp
): Promise<void>;
/**
* Allows the extension to add custom widgets
* @param app The ComfyUI app instance
* @returns An array of {[widget name]: widget data}
*/
getCustomWidgets?(
app: ComfyApp
): Promise<
Record<
string,
(
node,
inputName,
inputData,
app
) => { widget?: IWidget; minWidth?: number; minHeight?: number }
>
>;
/**
* Allows the extension to add additional handling to the node before it is registered with LGraph
* @param nodeType The node class (not an instance)
* @param nodeData The original node object info config object
* @param app The ComfyUI app instance
*/
beforeRegisterNodeDef?(
nodeType: typeof LGraphNode,
nodeData: ComfyObjectInfo,
app: ComfyApp
): Promise<void>;
/**
* Allows the extension to register additional nodes with LGraph after standard nodes are added
* @param app The ComfyUI app instance
*/
registerCustomNodes?(app: ComfyApp): Promise<void>;
/**
* Allows the extension to modify a node that has been reloaded onto the graph.
* If you break something in the backend and want to patch workflows in the frontend
* This is the place to do this
* @param node The node that has been loaded
* @param app The ComfyUI app instance
*/
loadedGraphNode?(node: LGraphNode, app: ComfyApp);
/**
* Allows the extension to run code after the constructor of the node
* @param node The node that has been created
* @param app The ComfyUI app instance
*/
nodeCreated?(node: LGraphNode, app: ComfyApp);
}
export type ComfyObjectInfo = {
name: string;
display_name?: string;
description?: string;
category: string;
input?: {
required?: Record<string, ComfyObjectInfoConfig>;
optional?: Record<string, ComfyObjectInfoConfig>;
};
output?: string[];
output_name: string[];
name: string;
display_name?: string;
description?: string;
category: string;
input?: {
required?: Record<string, ComfyObjectInfoConfig>;
optional?: Record<string, ComfyObjectInfoConfig>;
};
output?: string[];
output_name: string[];
};
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];

View File

@@ -1,47 +1,56 @@
import { z } from 'zod';
import { fromZodError } from 'zod-validation-error';
import { z } from "zod";
import { fromZodError } from "zod-validation-error";
const zComfyLink = z.tuple([
z.number(), // Link id
z.number(), // Node id of source node
z.number(), // Output slot# of source node
z.number(), // Node id of destination node
z.number(), // Input slot# of destination node
z.string(), // Data type
z.number(), // Link id
z.number(), // Node id of source node
z.number(), // Output slot# of source node
z.number(), // Node id of destination node
z.number(), // Input slot# of destination node
z.string(), // Data type
]);
const zNodeOutput = z.object({
const zNodeOutput = z
.object({
name: z.string(),
type: z.string(),
links: z.array(z.number()).nullable(),
slot_index: z.number().optional(),
}).passthrough();
})
.passthrough();
const zNodeInput = z.object({
const zNodeInput = z
.object({
name: z.string(),
type: z.string(),
link: z.number().nullable(),
slot_index: z.number().optional(),
}).passthrough();
})
.passthrough();
const zFlags = z.object({
const zFlags = z
.object({
collapsed: z.boolean().optional(),
pinned: z.boolean().optional(),
allow_interaction: z.boolean().optional(),
horizontal: z.boolean().optional(),
skip_repeated_outputs: z.boolean().optional(),
}).passthrough();
})
.passthrough();
const zProperties = z.object({
const zProperties = z
.object({
["Node name for S&R"]: z.string().optional(),
}).passthrough();
})
.passthrough();
const zVector2 = z.union([
z.object({ 0: z.number(), 1: z.number() }),
z.tuple([z.number(), z.number()]),
z.object({ 0: z.number(), 1: z.number() }),
z.tuple([z.number(), z.number()]),
]);
const zComfyNode = z.object({
const zComfyNode = z
.object({
id: z.number(),
type: z.string(),
pos: z.tuple([z.number(), z.number()]),
@@ -52,20 +61,24 @@ const zComfyNode = z.object({
inputs: z.array(zNodeInput).optional(),
outputs: z.array(zNodeOutput).optional(),
properties: zProperties,
widgets_values: z.array(z.any()).optional(), // This could contain mixed types
widgets_values: z.array(z.any()).optional(), // This could contain mixed types
color: z.string().optional(),
bgcolor: z.string().optional(),
}).passthrough();
})
.passthrough();
const zGroup = z.object({
const zGroup = z
.object({
title: z.string(),
bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]),
color: z.string(),
font_size: z.number(),
locked: z.boolean().optional(),
}).passthrough();
})
.passthrough();
const zInfo = z.object({
const zInfo = z
.object({
name: z.string(),
author: z.string(),
description: z.string(),
@@ -73,24 +86,32 @@ const zInfo = z.object({
created: z.string(),
modified: z.string(),
software: z.string(),
}).passthrough();
})
.passthrough();
const zDS = z.object({
const zDS = z
.object({
scale: z.number(),
offset: zVector2,
}).passthrough();
})
.passthrough();
const zConfig = z.object({
const zConfig = z
.object({
links_ontop: z.boolean().optional(),
align_to_grid: z.boolean().optional(),
}).passthrough();
})
.passthrough();
const zExtra = z.object({
const zExtra = z
.object({
ds: zDS.optional(),
info: zInfo.optional(),
}).passthrough();
})
.passthrough();
export const zComfyWorkflow = z.object({
export const zComfyWorkflow = z
.object({
last_node_id: z.number(),
last_link_id: z.number(),
nodes: z.array(zComfyNode),
@@ -99,7 +120,8 @@ export const zComfyWorkflow = z.object({
config: zConfig.optional().nullable(),
extra: zExtra.optional().nullable(),
version: z.number(),
}).passthrough();
})
.passthrough();
export type NodeInput = z.infer<typeof zNodeInput>;
export type NodeOutput = z.infer<typeof zNodeOutput>;
@@ -107,15 +129,14 @@ export type ComfyLink = z.infer<typeof zComfyLink>;
export type ComfyNode = z.infer<typeof zComfyNode>;
export type ComfyWorkflow = z.infer<typeof zComfyWorkflow>;
export async function parseComfyWorkflow(data: string): Promise<ComfyWorkflow> {
// Validate
const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data));
if (!result.success) {
// TODO: Pretty print the error on UI modal.
const error = fromZodError(result.error);
alert(`Invalid workflow against zod schema:\n${error}`);
throw error;
}
return result.data;
// Validate
const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data));
if (!result.success) {
// TODO: Pretty print the error on UI modal.
const error = fromZodError(result.error);
alert(`Invalid workflow against zod schema:\n${error}`);
throw error;
}
return result.data;
}

2767
src/types/litegraph.d.ts vendored

File diff suppressed because it is too large Load Diff