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", "@types/node": "^20.14.8",
"babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-import-meta": "^2.2.1",
"babel-plugin-transform-rename-import": "^2.3.0", "babel-plugin-transform-rename-import": "^2.3.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.7",
"prettier": "^3.3.2",
"ts-jest": "^29.1.4", "ts-jest": "^29.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.15.6", "tsx": "^4.15.6",
@@ -3884,6 +3887,87 @@
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
"dev": true "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": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -3929,6 +4013,12 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3941,6 +4031,15 @@
"node": ">= 0.8" "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4380,6 +4479,12 @@
"node": ">=0.10.0" "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": { "node_modules/execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -4577,6 +4682,18 @@
"node": "6.* || 8.* || >= 10.*" "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": { "node_modules/get-package-type": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -4739,6 +4856,21 @@
"node": ">=10.17.0" "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": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -6758,12 +6890,293 @@
"immediate": "~3.0.5" "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": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true "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": { "node_modules/locate-path": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -6788,6 +7201,147 @@
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true "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": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -7140,6 +7694,18 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/pirates": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -7233,6 +7799,21 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/pretty-format": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -7510,6 +8091,22 @@
"node": ">=10" "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": { "node_modules/reusify": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -7520,6 +8117,12 @@
"node": ">=0.10.0" "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": { "node_modules/rollup": {
"version": "4.18.0", "version": "4.18.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
@@ -7659,6 +8262,46 @@
"node": ">=8" "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": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -7723,6 +8366,15 @@
"safe-buffer": "~5.1.0" "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": { "node_modules/string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -8850,6 +9502,18 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true "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": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",

View File

@@ -8,6 +8,7 @@
"build": "npm run typecheck && vite build", "build": "npm run typecheck && vite build",
"zipdist": "node scripts/zipdist.js", "zipdist": "node scripts/zipdist.js",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"format": "prettier --write 'src/**/*.{js,ts,tsx}'",
"test": "npm run build && jest", "test": "npm run build && jest",
"test:generate": "npx tsx tests-ui/setup", "test:generate": "npx tsx tests-ui/setup",
"test:browser": "npx playwright test", "test:browser": "npx playwright test",
@@ -21,9 +22,12 @@
"@types/node": "^20.14.8", "@types/node": "^20.14.8",
"babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-import-meta": "^2.2.1",
"babel-plugin-transform-rename-import": "^2.3.0", "babel-plugin-transform-rename-import": "^2.3.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.7",
"prettier": "^3.3.2",
"ts-jest": "^29.1.4", "ts-jest": "^29.1.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.15.6", "tsx": "^4.15.6",
@@ -36,5 +40,11 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"zod": "^3.23.8", "zod": "^3.23.8",
"zod-validation-error": "^3.3.0" "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"; import { ComfyApp } from "../../scripts/app";
export class ClipspaceDialog extends ComfyDialog { export class ClipspaceDialog extends ComfyDialog {
static items = []; static items = [];
static instance = null; static instance = null;
static registerButton(name, contextPredicate, callback) { static registerButton(name, contextPredicate, callback) {
const item = const item = $el("button", {
$el("button", { type: "button",
type: "button", textContent: name,
textContent: name, contextPredicate: contextPredicate,
contextPredicate: contextPredicate, onclick: callback,
onclick: callback });
})
ClipspaceDialog.items.push(item); ClipspaceDialog.items.push(item);
} }
static invalidatePreview() { static invalidatePreview() {
if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) { if (
const img_preview = document.getElementById("clipspace_preview") as HTMLImageElement; ComfyApp.clipspace &&
if(img_preview) { ComfyApp.clipspace.imgs &&
img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src; ComfyApp.clipspace.imgs.length > 0
img_preview.style.maxHeight = "100%"; ) {
img_preview.style.maxWidth = "100%"; 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() { static invalidate() {
if(ClipspaceDialog.instance) { if (ClipspaceDialog.instance) {
const self = ClipspaceDialog.instance; const self = ClipspaceDialog.instance;
// allow reconstruct controls when copying from non-image to image content. // allow reconstruct controls when copying from non-image to image content.
const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]); const children = $el("div.comfy-modal-content", [
self.createImgSettings(),
...self.createButtons(),
]);
if(self.element) { if (self.element) {
// update // update
self.element.removeChild(self.element.firstChild); self.element.removeChild(self.element.firstChild);
self.element.appendChild(children); self.element.appendChild(children);
} } else {
else { // new
// new self.element = $el("div.comfy-modal", { parent: document.body }, [
self.element = $el("div.comfy-modal", { parent: document.body }, [children,]); children,
} ]);
}
if(self.element.children[0].children.length <= 1) { 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."])); 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() { constructor() {
super(); super();
} }
createButtons() { createButtons() {
const buttons = []; const buttons = [];
for(let idx in ClipspaceDialog.items) { for (let idx in ClipspaceDialog.items) {
const item = ClipspaceDialog.items[idx]; const item = ClipspaceDialog.items[idx];
if(!item.contextPredicate || item.contextPredicate()) if (!item.contextPredicate || item.contextPredicate())
buttons.push(ClipspaceDialog.items[idx]); buttons.push(ClipspaceDialog.items[idx]);
} }
buttons.push( buttons.push(
$el("button", { $el("button", {
type: "button", type: "button",
textContent: "Close", textContent: "Close",
onclick: () => { this.close(); } onclick: () => {
}) this.close();
); },
})
);
return buttons; return buttons;
} }
createImgSettings() { createImgSettings() {
if(ComfyApp.clipspace.imgs) { if (ComfyApp.clipspace.imgs) {
const combo_items = []; const combo_items = [];
const imgs = ComfyApp.clipspace.imgs; const imgs = ComfyApp.clipspace.imgs;
for(let i=0; i < imgs.length; i++) { for (let i = 0; i < imgs.length; i++) {
combo_items.push($el("option", {value:i}, [`${i}`])); combo_items.push($el("option", { value: i }, [`${i}`]));
} }
const combo1 = $el("select", const combo1 = $el(
{id:"clipspace_img_selector", onchange:(event) => { "select",
ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex; {
ClipspaceDialog.invalidatePreview(); id: "clipspace_img_selector",
} }, combo_items); onchange: (event) => {
ComfyApp.clipspace["selectedIndex"] = event.target.selectedIndex;
ClipspaceDialog.invalidatePreview();
},
},
combo_items
);
const row1 = const row1 = $el("tr", {}, [
$el("tr", {}, $el("td", {}, [$el("font", { color: "white" }, ["Select Image"])]),
[ $el("td", {}, [combo1]),
$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", const row2 = $el("tr", {}, [
{id:"clipspace_img_paste_mode", onchange:(event) => { $el("td", {}, [$el("font", { color: "white" }, ["Paste Mode"])]),
ComfyApp.clipspace['img_paste_mode'] = event.target.value; $el("td", {}, [combo2]),
} }, ]);
[
$el("option", {value:'selected'}, 'selected'),
$el("option", {value:'all'}, 'all')
]) as HTMLSelectElement;
combo2.value = ComfyApp.clipspace['img_paste_mode'];
const row2 = const td = $el(
$el("tr", {}, "td",
[ { align: "center", width: "100px", height: "100px", colSpan: "2" },
$el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]), [$el("img", { id: "clipspace_preview", ondragstart: () => false }, [])]
$el("td", {}, [combo2]) );
]);
const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'}, const row3 = $el("tr", {}, [td]);
[ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]);
const row3 = return $el("table", {}, [row1, row2, row3]);
$el("tr", {}, [td]); } else {
return [];
}
}
return $el("table", {}, [row1, row2, row3]); createImgPreview() {
} if (ComfyApp.clipspace.imgs) {
else { return $el("img", { id: "clipspace_preview", ondragstart: () => false });
return []; } else return [];
} }
}
createImgPreview() { show() {
if(ComfyApp.clipspace.imgs) { const img_preview = document.getElementById("clipspace_preview");
return $el("img",{id:"clipspace_preview", ondragstart:() => false}); ClipspaceDialog.invalidate();
}
else
return [];
}
show() { this.element.style.display = "block";
const img_preview = document.getElementById("clipspace_preview"); }
ClipspaceDialog.invalidate();
this.element.style.display = "block";
}
} }
app.registerExtension({ app.registerExtension({
name: "Comfy.Clipspace", name: "Comfy.Clipspace",
init(app) { init(app) {
app.openClipspace = app.openClipspace = function () {
function () { if (!ClipspaceDialog.instance) {
if(!ClipspaceDialog.instance) { ClipspaceDialog.instance = new ClipspaceDialog();
ClipspaceDialog.instance = new ClipspaceDialog(); ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate; }
}
if(ComfyApp.clipspace) { if (ComfyApp.clipspace) {
ClipspaceDialog.instance.show(); ClipspaceDialog.instance.show();
} } else app.ui.dialog.show("Clipspace is Empty!");
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 // Adds filtering to combo context menus
const ext = { const ext = {
name: "Comfy.ContextMenuFilter", name: "Comfy.ContextMenuFilter",
init() { init() {
const ctxMenu = LiteGraph.ContextMenu; const ctxMenu = LiteGraph.ContextMenu;
// @ts-ignore // @ts-ignore
// TODO Very hacky way to modify Litegraph behaviour. Fix this later. // TODO Very hacky way to modify Litegraph behaviour. Fix this later.
LiteGraph.ContextMenu = function (values, options) { LiteGraph.ContextMenu = function (values, options) {
const ctx = ctxMenu.call(this, 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 we are a dark menu (only used for combo boxes) then add a filter input
if (options?.className === "dark" && values?.length > 10) { if (options?.className === "dark" && values?.length > 10) {
const filter = document.createElement("input"); const filter = document.createElement("input");
filter.classList.add("comfy-context-menu-filter"); filter.classList.add("comfy-context-menu-filter");
filter.placeholder = "Filter list"; filter.placeholder = "Filter list";
this.root.prepend(filter); this.root.prepend(filter);
const items = Array.from(this.root.querySelectorAll(".litemenu-entry")) as HTMLElement[]; const items = Array.from(
let displayedItems = [...items]; this.root.querySelectorAll(".litemenu-entry")
let itemCount = displayedItems.length; ) 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. // We must request an animation frame for the current node of the active canvas to update.
requestAnimationFrame(() => { requestAnimationFrame(() => {
// @ts-ignore // @ts-ignore
const currentNode = LGraphCanvas.active_canvas.current_node; const currentNode = LGraphCanvas.active_canvas.current_node;
const clickedComboValue = currentNode.widgets const clickedComboValue = currentNode.widgets
?.filter(w => w.type === "combo" && w.options.values.length === values.length) ?.filter(
.find(w => w.options.values.every((v, i) => v === values[i])) (w) =>
?.value; 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; let selectedIndex = clickedComboValue
if (selectedIndex < 0) { ? values.findIndex((v) => v === clickedComboValue)
selectedIndex = 0; : 0;
} if (selectedIndex < 0) {
let selectedItem = displayedItems[selectedIndex]; selectedIndex = 0;
updateSelected(); }
let selectedItem = displayedItems[selectedIndex];
updateSelected();
// Apply highlighting to the selected item // Apply highlighting to the selected item
function updateSelected() { function updateSelected() {
selectedItem?.style.setProperty("background-color", ""); selectedItem?.style.setProperty("background-color", "");
selectedItem?.style.setProperty("color", ""); selectedItem?.style.setProperty("color", "");
selectedItem = displayedItems[selectedIndex]; selectedItem = displayedItems[selectedIndex];
selectedItem?.style.setProperty("background-color", "#ccc", "important"); selectedItem?.style.setProperty(
selectedItem?.style.setProperty("color", "#000", "important"); "background-color",
} "#ccc",
"important"
);
selectedItem?.style.setProperty("color", "#000", "important");
}
const positionList = () => { const positionList = () => {
const rect = this.root.getBoundingClientRect(); const rect = this.root.getBoundingClientRect();
// If the top is off-screen then shift the element with scaling applied // If the top is off-screen then shift the element with scaling applied
if (rect.top < 0) { if (rect.top < 0) {
const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight; const scale =
const shift = (this.root.clientHeight * scale) / 2; 1 -
this.root.style.top = -shift + "px"; 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 // Arrow up/down to select items
filter.addEventListener("keydown", (event) => { filter.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "ArrowUp": case "ArrowUp":
event.preventDefault(); event.preventDefault();
if (selectedIndex === 0) { if (selectedIndex === 0) {
selectedIndex = itemCount - 1; selectedIndex = itemCount - 1;
} else { } else {
selectedIndex--; selectedIndex--;
} }
updateSelected(); updateSelected();
break; break;
case "ArrowRight": case "ArrowRight":
event.preventDefault(); event.preventDefault();
selectedIndex = itemCount - 1; selectedIndex = itemCount - 1;
updateSelected(); updateSelected();
break; break;
case "ArrowDown": case "ArrowDown":
event.preventDefault(); event.preventDefault();
if (selectedIndex === itemCount - 1) { if (selectedIndex === itemCount - 1) {
selectedIndex = 0; selectedIndex = 0;
} else { } else {
selectedIndex++; selectedIndex++;
} }
updateSelected(); updateSelected();
break; break;
case "ArrowLeft": case "ArrowLeft":
event.preventDefault(); event.preventDefault();
selectedIndex = 0; selectedIndex = 0;
updateSelected(); updateSelected();
break; break;
case "Enter": case "Enter":
selectedItem?.click(); selectedItem?.click();
break; break;
case "Escape": case "Escape":
this.close(); this.close();
break; break;
} }
}); });
filter.addEventListener("input", () => { filter.addEventListener("input", () => {
// Hide all items that don't match our filter // Hide all items that don't match our filter
const term = filter.value.toLocaleLowerCase(); const term = filter.value.toLocaleLowerCase();
// When filtering, recompute which items are visible for arrow up/down and maintain selection. // When filtering, recompute which items are visible for arrow up/down and maintain selection.
displayedItems = items.filter(item => { displayedItems = items.filter((item) => {
const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term); const isVisible =
item.style.display = isVisible ? "block" : "none"; !term || item.textContent.toLocaleLowerCase().includes(term);
return isVisible; item.style.display = isVisible ? "block" : "none";
}); return isVisible;
});
selectedIndex = 0; selectedIndex = 0;
if (displayedItems.includes(selectedItem)) { if (displayedItems.includes(selectedItem)) {
selectedIndex = displayedItems.findIndex(d => d === selectedItem); selectedIndex = displayedItems.findIndex(
} (d) => d === selectedItem
itemCount = displayedItems.length; );
}
itemCount = displayedItems.length;
updateSelected(); updateSelected();
// If we have an event then we can try and position the list under the source // If we have an event then we can try and position the list under the source
if (options.event) { if (options.event) {
let top = options.event.clientY - 10; let top = options.event.clientY - 10;
const bodyRect = document.body.getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect();
const rootRect = this.root.getBoundingClientRect(); const rootRect = this.root.getBoundingClientRect();
if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) { if (
top = Math.max(0, bodyRect.height - rootRect.height - 10); bodyRect.height &&
} top > bodyRect.height - rootRect.height - 10
) {
top = Math.max(0, bodyRect.height - rootRect.height - 10);
}
this.root.style.top = top + "px"; this.root.style.top = top + "px";
positionList(); positionList();
} }
}); });
requestAnimationFrame(() => { requestAnimationFrame(() => {
// Focus the filter box when opening // Focus the filter box when opening
filter.focus(); filter.focus();
positionList(); positionList();
}); });
}) });
} }
return ctx; return ctx;
}; };
LiteGraph.ContextMenu.prototype = ctxMenu.prototype; LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
}, },
} };
app.registerExtension(ext); app.registerExtension(ext);

View File

@@ -7,42 +7,46 @@ import { app } from "../../scripts/app";
* Strips C-style line and block comments from a string * Strips C-style line and block comments from a string
*/ */
function stripComments(str) { function stripComments(str) {
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,''); return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
} }
app.registerExtension({ app.registerExtension({
name: "Comfy.DynamicPrompts", name: "Comfy.DynamicPrompts",
nodeCreated(node) { nodeCreated(node) {
if (node.widgets) { if (node.widgets) {
// Locate dynamic prompt text widgets // Locate dynamic prompt text widgets
// Include any widgets with dynamicPrompts set to true, and customtext // Include any widgets with dynamicPrompts set to true, and customtext
const widgets = node.widgets.filter( const widgets = node.widgets.filter((n) => n.dynamicPrompts);
(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
for (const widget of widgets) { widget.serializeValue = (workflowNode, widgetIndex) => {
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node let prompt = stripComments(widget.value);
widget.serializeValue = (workflowNode, widgetIndex) => { while (
let prompt = stripComments(widget.value); prompt.replace("\\{", "").includes("{") &&
while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) { prompt.replace("\\}", "").includes("}")
const startIndex = prompt.replace("\\{", "00").indexOf("{"); ) {
const endIndex = prompt.replace("\\}", "00").indexOf("}"); const startIndex = prompt.replace("\\{", "00").indexOf("{");
const endIndex = prompt.replace("\\}", "00").indexOf("}");
const optionsString = prompt.substring(startIndex + 1, endIndex); const optionsString = prompt.substring(startIndex + 1, endIndex);
const options = optionsString.split("|"); const options = optionsString.split("|");
const randomIndex = Math.floor(Math.random() * options.length); const randomIndex = Math.floor(Math.random() * options.length);
const randomOption = options[randomIndex]; 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 // Overwrite the value in the serialized workflow pnginfo
if (workflowNode?.widgets_values) if (workflowNode?.widgets_values)
workflowNode.widgets_values[widgetIndex] = prompt; 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 // Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
app.registerExtension({ app.registerExtension({
name: "Comfy.EditAttention", name: "Comfy.EditAttention",
init() { init() {
const editAttentionDelta = app.ui.settings.addSetting({ const editAttentionDelta = app.ui.settings.addSetting({
id: "Comfy.EditAttention.Delta", id: "Comfy.EditAttention.Delta",
name: "Ctrl+up/down precision", name: "Ctrl+up/down precision",
type: "slider", type: "slider",
attrs: { attrs: {
min: 0.01, min: 0.01,
max: 0.5, max: 0.5,
step: 0.01, step: 0.01,
}, },
defaultValue: 0.05, defaultValue: 0.05,
}); });
function incrementWeight(weight, delta) { function incrementWeight(weight, delta) {
const floatWeight = parseFloat(weight); const floatWeight = parseFloat(weight);
if (isNaN(floatWeight)) return weight; if (isNaN(floatWeight)) return weight;
const newWeight = floatWeight + delta; const newWeight = floatWeight + delta;
if (newWeight < 0) return "0"; if (newWeight < 0) return "0";
return String(Number(newWeight.toFixed(10))); 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) { // If the selection ends with a space, remove it
let start = cursorPos, end = cursorPos; if (selectedText[selectedText.length - 1] === " ") {
let openCount = 0, closeCount = 0; selectedText = selectedText.substring(0, selectedText.length - 1);
end -= 1;
}
// Find opening parenthesis before cursor // If there are parentheses left and right of the selection, select them
while (start >= 0) { if (
start--; inputField.value[start - 1] === "(" &&
if (text[start] === "(" && openCount === closeCount) break; inputField.value[end] === ")"
if (text[start] === "(") openCount++; ) {
if (text[start] === ")") closeCount++; start -= 1;
} end += 1;
if (start < 0) return false; selectedText = inputField.value.substring(start, end);
}
openCount = 0; // If the selection is not enclosed in parentheses, add them
closeCount = 0; if (
selectedText[0] !== "(" ||
selectedText[selectedText.length - 1] !== ")"
) {
selectedText = `(${selectedText})`;
}
// Find closing parenthesis after cursor // If the selection does not have a weight, add a weight of 1.0
while (end < text.length) { selectedText = addWeightToParentheses(selectedText);
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 }; // 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) { inputField.setRangeText(updatedText, start, end, "select");
const parenRegex = /^\((.*)\)$/; }
const parenMatch = text.match(parenRegex); window.addEventListener("keydown", editAttention);
},
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);
},
}); });

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 { app, type ComfyApp } from "../../scripts/app";
import type { LGraphNode, LGraphNodeConstructor } from "/types/litegraph"; import type { LGraphNode, LGraphNodeConstructor } from "/types/litegraph";
const ORDER: symbol = Symbol(); const ORDER: symbol = Symbol();
function merge(target, source) { function merge(target, source) {
@@ -26,11 +25,23 @@ function merge(target, source) {
} }
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> { 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; selectedNodeIndex: number | null | undefined;
selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs"; selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs";
selectedGroup: string | undefined; 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[]; nodeItems: any[];
app: ComfyApp; app: ComfyApp;
groupNodeType: LGraphNodeConstructor<LGraphNode>; groupNodeType: LGraphNodeConstructor<LGraphNode>;
@@ -86,7 +97,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
} }
getGroupData() { 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.groupNodeDef = this.groupNodeType.nodeData;
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType); this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
} }
@@ -131,24 +143,39 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
this.changeNode(0); this.changeNode(0);
} else { } else {
const items = this.draggable.getAllItems(); const items = this.draggable.getAllItems();
let index = items.findIndex(item => item.classList.contains("selected")); let index = items.findIndex((item) =>
if(index === -1) index = this.selectedNodeIndex; item.classList.contains("selected")
);
if (index === -1) index = this.selectedNodeIndex;
this.changeNode(index, true); this.changeNode(index, true);
} }
const ordered = [...nodes]; const ordered = [...nodes];
this.draggable?.dispose(); this.draggable?.dispose();
this.draggable = new DraggableList(this.innerNodesList, "li"); this.draggable = new DraggableList(this.innerNodesList, "li");
this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => { this.draggable.addEventListener(
if (oldPosition === newPosition) return; "dragend",
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]); ({ detail: { oldPosition, newPosition } }) => {
for (let i = 0; i < ordered.length; i++) { if (oldPosition === newPosition) return;
this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i }); 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 { nodeIndex, section, prop, value } = props;
const groupMod = (this.modifications[this.selectedGroup] ??= {}); const groupMod = (this.modifications[this.selectedGroup] ??= {});
const nodesMod = (groupMod.nodes ??= {}); const nodesMod = (groupMod.nodes ??= {});
@@ -165,7 +192,10 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
getEditElement(section, prop, value, placeholder, checked, checkable = true) { getEditElement(section, prop, value, placeholder, checked, checkable = true) {
if (value === placeholder) value = ""; 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) {
if (mods.name != null) { if (mods.name != null) {
value = mods.name; value = mods.name;
@@ -181,7 +211,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
placeholder, placeholder,
type: "text", type: "text",
onchange: (e) => { onchange: (e) => {
this.storeModification({ section, prop, value: { name: e.target.value } }); this.storeModification({
section,
prop,
value: { name: e.target.value },
});
}, },
}), }),
$el("label", { textContent: "Visible" }, [ $el("label", { textContent: "Visible" }, [
@@ -190,7 +224,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
checked, checked,
disabled: !checkable, disabled: !checkable,
onchange: (e) => { 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() { buildWidgetsPage() {
const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]; const widgets =
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
const items = Object.keys(widgets ?? {}); const items = Object.keys(widgets ?? {});
const type = app.graph.extra.groupNodes[this.selectedGroup]; const type = app.graph.extra.groupNodes[this.selectedGroup];
const config = type.config?.[this.selectedNodeInnerIndex]?.input; const config = type.config?.[this.selectedNodeInnerIndex]?.input;
this.widgetsPage.replaceChildren( this.widgetsPage.replaceChildren(
...items.map((oldName) => { ...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; return !!items.length;
@@ -223,7 +268,13 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
return; 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) .filter(Boolean)
); );
@@ -232,9 +283,12 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
buildOutputsPage() { buildOutputsPage() {
const nodes = this.groupData.nodeData.nodes; 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 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 type = app.graph.extra.groupNodes[this.selectedGroup];
const config = type.config?.[this.selectedNodeInnerIndex]?.output; const config = type.config?.[this.selectedNodeInnerIndex]?.output;
@@ -250,7 +304,14 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
if (!value || value === oldName) { if (!value || value === oldName) {
value = ""; value = "";
} }
return this.getEditElement("output", slot, value, oldName, visible, checkable); return this.getEditElement(
"output",
slot,
value,
oldName,
visible,
checkable
);
}) })
.filter(Boolean) .filter(Boolean)
); );
@@ -258,13 +319,21 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
} }
show(type?) { 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.widgetsPage = $el("section.comfy-group-manage-node-page");
this.inputsPage = $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"); 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 = [ this.tabs = [
["Inputs", this.inputsPage], ["Inputs", this.inputsPage],
@@ -318,12 +387,20 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
{ {
onclick: (e) => { onclick: (e) => {
// @ts-ignore // @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) { 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; 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]; delete app.graph.extra.groupNodes[this.selectedGroup];
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup); LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
} }
@@ -416,16 +493,22 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
}, },
"Save" "Save"
), ),
$el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"), $el(
"button.comfy-btn",
{ onclick: () => this.element.close() },
"Close"
),
]), ]),
]); ]);
this.element.replaceChildren(outer); 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.showModal();
this.element.addEventListener("close", () => { this.element.addEventListener("close", () => {
this.draggable?.dispose(); this.draggable?.dispose();
}); });
} }
} }

View File

@@ -1,262 +1,265 @@
import {app} from "../../scripts/app"; import { app } from "../../scripts/app";
function setNodeMode(node, mode) { function setNodeMode(node, mode) {
node.mode = mode; node.mode = mode;
node.graph.change(); node.graph.change();
} }
function addNodesToGroup(group, nodes=[]) { function addNodesToGroup(group, nodes = []) {
var x1, y1, x2, y2; var x1, y1, x2, y2;
var nx1, ny1, nx2, ny2; var nx1, ny1, nx2, ny2;
var node; var node;
x1 = y1 = x2 = y2 = -1; x1 = y1 = x2 = y2 = -1;
nx1 = ny1 = nx2 = ny2 = -1; nx1 = ny1 = nx2 = ny2 = -1;
for (var n of [group._nodes, nodes]) { for (var n of [group._nodes, nodes]) {
for (var i in n) { for (var i in n) {
node = n[i] node = n[i];
nx1 = node.pos[0] nx1 = node.pos[0];
ny1 = node.pos[1] ny1 = node.pos[1];
nx2 = node.pos[0] + node.size[0] nx2 = node.pos[0] + node.size[0];
ny2 = node.pos[1] + node.size[1] ny2 = node.pos[1] + node.size[1];
if (node.type != "Reroute") { if (node.type != "Reroute") {
ny1 -= LiteGraph.NODE_TITLE_HEIGHT; ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
} }
if (node.flags?.collapsed) { if (node.flags?.collapsed) {
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT; ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
if (node?._collapsed_width) { if (node?._collapsed_width) {
nx2 = nx1 + Math.round(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 (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.pos = [x1 - padding, y1 - padding];
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2]; group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
} }
app.registerExtension({ app.registerExtension({
name: "Comfy.GroupOptions", name: "Comfy.GroupOptions",
setup() { setup() {
// @ts-ignore // @ts-ignore
const orig = LGraphCanvas.prototype.getCanvasMenuOptions; const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
// graph_mouse // graph_mouse
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.getCanvasMenuOptions = function () { LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments); const options = orig.apply(this, arguments);
const group = this.graph.getGroupOnPos(this.graph_mouse[0], this.graph_mouse[1]); const group = this.graph.getGroupOnPos(
if (!group) { this.graph_mouse[0],
options.push({ this.graph_mouse[1]
content: "Add Group For Selected Nodes", );
disabled: !Object.keys(app.canvas.selected_nodes || {}).length, if (!group) {
callback: () => { options.push({
// @ts-ignore content: "Add Group For Selected Nodes",
var group = new LiteGraph.LGraphGroup(); disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
addNodesToGroup(group, this.selected_nodes) callback: () => {
app.canvas.graph.add(group); // @ts-ignore
this.graph.change(); 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 nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
group.recomputeInsideNodes(); group.recomputeInsideNodes();
const nodesInGroup = group._nodes; const nodesInGroup = group._nodes;
options.push({ options.push({
content: "Add Selected Nodes To Group", content: "Add Selected Nodes To Group",
disabled: !Object.keys(app.canvas.selected_nodes || {}).length, disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => { callback: () => {
addNodesToGroup(group, this.selected_nodes) addNodesToGroup(group, this.selected_nodes);
this.graph.change(); this.graph.change();
} },
}); });
// No nodes in group, return default options // No nodes in group, return default options
if (nodesInGroup.length === 0) { if (nodesInGroup.length === 0) {
return options; return options;
} else { } else {
// Add a separator between the default options and the group options // Add a separator between the default options and the group options
options.push(null); options.push(null);
} }
// Check if all nodes are the same mode // Check if all nodes are the same mode
let allNodesAreSameMode = true; let allNodesAreSameMode = true;
for (let i = 1; i < nodesInGroup.length; i++) { for (let i = 1; i < nodesInGroup.length; i++) {
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) { if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
allNodesAreSameMode = false; allNodesAreSameMode = false;
break; 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
} }
} }
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"; const id = "Comfy.InvertMenuScrolling";
app.registerExtension({ app.registerExtension({
name: id, name: id,
init() { init() {
const ctxMenu = LiteGraph.ContextMenu; const ctxMenu = LiteGraph.ContextMenu;
const replace = () => { const replace = () => {
// @ts-ignore // @ts-ignore
LiteGraph.ContextMenu = function (values, options) { LiteGraph.ContextMenu = function (values, options) {
options = options || {}; options = options || {};
if (options.scroll_speed) { if (options.scroll_speed) {
options.scroll_speed *= -1; options.scroll_speed *= -1;
} else { } else {
options.scroll_speed = -0.1; options.scroll_speed = -0.1;
} }
return ctxMenu.call(this, values, options); return ctxMenu.call(this, values, options);
}; };
LiteGraph.ContextMenu.prototype = ctxMenu.prototype; LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
}; };
app.ui.settings.addSetting({ app.ui.settings.addSetting({
id, id,
name: "Invert Menu Scrolling", name: "Invert Menu Scrolling",
type: "boolean", type: "boolean",
defaultValue: false, defaultValue: false,
onChange(value) { onChange(value) {
if (value) { if (value) {
replace(); replace();
} else { } else {
LiteGraph.ContextMenu = ctxMenu; LiteGraph.ContextMenu = ctxMenu;
} }
}, },
}); });
}, },
}); });

View File

@@ -1,69 +1,73 @@
import {app} from "../../scripts/app"; import { app } from "../../scripts/app";
app.registerExtension({ app.registerExtension({
name: "Comfy.Keybinds", name: "Comfy.Keybinds",
init() { init() {
const keybindListener = function (event) { const keybindListener = function (event) {
const modifierPressed = event.ctrlKey || event.metaKey; const modifierPressed = event.ctrlKey || event.metaKey;
// Queue prompt using ctrl or command + enter // Queue prompt using ctrl or command + enter
if (modifierPressed && event.key === "Enter") { if (modifierPressed && event.key === "Enter") {
app.queuePrompt(event.shiftKey ? -1 : 0).then(); app.queuePrompt(event.shiftKey ? -1 : 0).then();
return; return;
} }
const target = event.composedPath()[0]; const target = event.composedPath()[0];
if (["INPUT", "TEXTAREA"].includes(target.tagName)) { if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
return; return;
} }
const modifierKeyIdMap = { const modifierKeyIdMap = {
s: "#comfy-save-button", s: "#comfy-save-button",
o: "#comfy-file-input", o: "#comfy-file-input",
Backspace: "#comfy-clear-button", Backspace: "#comfy-clear-button",
d: "#comfy-load-default-button", d: "#comfy-load-default-button",
}; };
const modifierKeybindId = modifierKeyIdMap[event.key]; const modifierKeybindId = modifierKeyIdMap[event.key];
if (modifierPressed && modifierKeybindId) { if (modifierPressed && modifierKeybindId) {
event.preventDefault(); event.preventDefault();
const elem = document.querySelector(modifierKeybindId); const elem = document.querySelector(modifierKeybindId);
elem.click(); elem.click();
return; return;
} }
// Finished Handling all modifier keybinds, now handle the rest // Finished Handling all modifier keybinds, now handle the rest
if (event.ctrlKey || event.altKey || event.metaKey) { if (event.ctrlKey || event.altKey || event.metaKey) {
return; return;
} }
// Close out of modals using escape // Close out of modals using escape
if (event.key === "Escape") { if (event.key === "Escape") {
const modals = document.querySelectorAll<HTMLElement>(".comfy-modal"); const modals = document.querySelectorAll<HTMLElement>(".comfy-modal");
const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none"); const modal = Array.from(modals).find(
if (modal) { (modal) =>
modal.style.display = "none"; window.getComputedStyle(modal).getPropertyValue("display") !==
} "none"
);
if (modal) {
modal.style.display = "none";
}
[...document.querySelectorAll("dialog")].forEach(d => { [...document.querySelectorAll("dialog")].forEach((d) => {
d.close(); d.close();
}); });
} }
const keyIdMap = { const keyIdMap = {
q: "#comfy-view-queue-button", q: "#comfy-view-queue-button",
h: "#comfy-view-history-button", h: "#comfy-view-history-button",
r: "#comfy-refresh-button", r: "#comfy-refresh-button",
}; };
const buttonId = keyIdMap[event.key]; const buttonId = keyIdMap[event.key];
if (buttonId) { if (buttonId) {
const button = document.querySelector(buttonId); const button = document.querySelector(buttonId);
button.click(); 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 id = "Comfy.LinkRenderMode";
const ext = { const ext = {
name: id, name: id,
async setup(app) { async setup(app) {
app.ui.settings.addSetting({ app.ui.settings.addSetting({
id, id,
name: "Link Render Mode", name: "Link Render Mode",
defaultValue: 2, defaultValue: 2,
type: "combo", type: "combo",
// @ts-ignore // @ts-ignore
options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({ options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
value: i, value: i,
text: m, text: m,
selected: i == app.canvas.links_render_mode, selected: i == app.canvas.links_render_mode,
})), })),
onChange(value) { onChange(value) {
app.canvas.links_render_mode = +value; app.canvas.links_render_mode = +value;
app.graph.setDirtyCanvas(true); app.graph.setDirtyCanvas(true);
}, },
}); });
}, },
}; };
app.registerExtension(ext); 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"; const file = "comfy.templates.json";
class ManageTemplates extends ComfyDialog { class ManageTemplates extends ComfyDialog {
templates: any[]; templates: any[];
draggedEl: HTMLElement | null; draggedEl: HTMLElement | null;
saveVisualCue: number | null; saveVisualCue: number | null;
emptyImg: HTMLImageElement; emptyImg: HTMLImageElement;
importInput: HTMLInputElement; importInput: HTMLInputElement;
constructor() { constructor() {
super(); super();
this.load().then((v) => { this.load().then((v) => {
this.templates = v; this.templates = v;
}); });
this.element.classList.add("comfy-manage-templates"); this.element.classList.add("comfy-manage-templates");
this.draggedEl = null; this.draggedEl = null;
this.saveVisualCue = null; this.saveVisualCue = null;
this.emptyImg = new Image(); this.emptyImg = new Image();
this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; this.emptyImg.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
this.importInput = $el("input", { this.importInput = $el("input", {
type: "file", type: "file",
accept: ".json", accept: ".json",
multiple: true, multiple: true,
style: { display: "none" }, style: { display: "none" },
parent: document.body, parent: document.body,
onchange: () => this.importAll(), onchange: () => this.importAll(),
}) as HTMLInputElement; }) as HTMLInputElement;
} }
createButtons() { createButtons() {
const btns = super.createButtons(); const btns = super.createButtons();
btns[0].textContent = "Close"; btns[0].textContent = "Close";
btns[0].onclick = (e) => { btns[0].onclick = (e) => {
clearTimeout(this.saveVisualCue); clearTimeout(this.saveVisualCue);
this.close(); this.close();
}; };
btns.unshift( btns.unshift(
$el("button", { $el("button", {
type: "button", type: "button",
textContent: "Export", textContent: "Export",
onclick: () => this.exportAll(), onclick: () => this.exportAll(),
}) })
); );
btns.unshift( btns.unshift(
$el("button", { $el("button", {
type: "button", type: "button",
textContent: "Import", textContent: "Import",
onclick: () => { onclick: () => {
this.importInput.click(); this.importInput.click();
}, },
}) })
); );
return btns; return btns;
} }
async load() { async load() {
let templates = []; let templates = [];
if (app.storageLocation === "server") { if (app.storageLocation === "server") {
if (app.isNewUserSession) { if (app.isNewUserSession) {
// New user so migrate existing templates // New user so migrate existing templates
const json = localStorage.getItem(id); const json = localStorage.getItem(id);
if (json) { if (json) {
templates = JSON.parse(json); templates = JSON.parse(json);
} }
await api.storeUserData(file, json, { stringify: false }); await api.storeUserData(file, json, { stringify: false });
} else { } else {
const res = await api.getUserData(file); const res = await api.getUserData(file);
if (res.status === 200) { if (res.status === 200) {
try { try {
templates = await res.json(); templates = await res.json();
} catch (error) { } catch (error) {}
} } else if (res.status !== 404) {
} else if (res.status !== 404) { console.error(res.status + " " + res.statusText);
console.error(res.status + " " + res.statusText); }
} }
} } else {
} else { const json = localStorage.getItem(id);
const json = localStorage.getItem(id); if (json) {
if (json) { templates = JSON.parse(json);
templates = JSON.parse(json); }
} }
}
return templates ?? []; return templates ?? [];
} }
async store() { async store() {
if(app.storageLocation === "server") { if (app.storageLocation === "server") {
const templates = JSON.stringify(this.templates, undefined, 4); const templates = JSON.stringify(this.templates, undefined, 4);
localStorage.setItem(id, templates); // Backwards compatibility localStorage.setItem(id, templates); // Backwards compatibility
try { try {
await api.storeUserData(file, templates, { stringify: false }); await api.storeUserData(file, templates, { stringify: false });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
alert(error.message); alert(error.message);
} }
} else { } else {
localStorage.setItem(id, JSON.stringify(this.templates)); localStorage.setItem(id, JSON.stringify(this.templates));
} }
} }
async importAll() { async importAll() {
for (const file of this.importInput.files) { for (const file of this.importInput.files) {
if (file.type === "application/json" || file.name.endsWith(".json")) { if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
const importFile = JSON.parse(reader.result as string); const importFile = JSON.parse(reader.result as string);
if (importFile?.templates) { if (importFile?.templates) {
for (const template of importFile.templates) { for (const template of importFile.templates) {
if (template?.name && template?.data) { if (template?.name && template?.data) {
this.templates.push(template); this.templates.push(template);
} }
} }
await this.store(); await this.store();
} }
}; };
await reader.readAsText(file); await reader.readAsText(file);
} }
} }
this.importInput.value = null; this.importInput.value = null;
this.close(); this.close();
} }
exportAll() { exportAll() {
if (this.templates.length == 0) { if (this.templates.length == 0) {
alert("No templates to export."); alert("No templates to export.");
return; return;
} }
const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string 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 blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = $el("a", { const a = $el("a", {
href: url, href: url,
download: "node_templates.json", download: "node_templates.json",
style: { display: "none" }, style: { display: "none" },
parent: document.body, parent: document.body,
}); });
a.click(); a.click();
setTimeout(function () { setTimeout(function () {
a.remove(); a.remove();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}, 0); }, 0);
} }
show() { show() {
// Show list of template names + delete button // Show list of template names + delete button
super.show( super.show(
$el( $el(
"div", "div",
{}, {},
this.templates.flatMap((t,i) => { this.templates.flatMap((t, i) => {
let nameInput; let nameInput;
return [ return [
$el( $el(
"div", "div",
{ {
dataset: { id: i.toString() }, dataset: { id: i.toString() },
className: "tempateManagerRow", className: "tempateManagerRow",
style: { style: {
display: "grid", display: "grid",
gridTemplateColumns: "1fr auto", gridTemplateColumns: "1fr auto",
border: "1px dashed transparent", border: "1px dashed transparent",
gap: "5px", gap: "5px",
backgroundColor: "var(--comfy-menu-bg)" backgroundColor: "var(--comfy-menu-bg)",
}, },
ondragstart: (e) => { ondragstart: (e) => {
this.draggedEl = e.currentTarget; this.draggedEl = e.currentTarget;
e.currentTarget.style.opacity = "0.6"; e.currentTarget.style.opacity = "0.6";
e.currentTarget.style.border = "1px dashed yellow"; e.currentTarget.style.border = "1px dashed yellow";
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setDragImage(this.emptyImg, 0, 0); e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
}, },
ondragend: (e) => { ondragend: (e) => {
e.target.style.opacity = "1"; e.target.style.opacity = "1";
e.currentTarget.style.border = "1px dashed transparent"; e.currentTarget.style.border = "1px dashed transparent";
e.currentTarget.removeAttribute("draggable"); e.currentTarget.removeAttribute("draggable");
// rearrange the elements // rearrange the elements
this.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => { this.element
var prev_i = Number.parseInt(el.dataset.id); .querySelectorAll(".tempateManagerRow")
.forEach((el: HTMLElement, i) => {
var prev_i = Number.parseInt(el.dataset.id);
if ( el == this.draggedEl && prev_i != i ) { if (el == this.draggedEl && prev_i != i) {
this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]); this.templates.splice(
} i,
el.dataset.id = i.toString(); 0,
}); this.templates.splice(prev_i, 1)[0]
this.store(); );
}, }
ondragover: (e) => { el.dataset.id = i.toString();
e.preventDefault(); });
if ( e.currentTarget == this.draggedEl ) this.store();
return; },
ondragover: (e) => {
e.preventDefault();
if (e.currentTarget == this.draggedEl) return;
let rect = e.currentTarget.getBoundingClientRect(); let rect = e.currentTarget.getBoundingClientRect();
if (e.clientY > rect.top + rect.height / 2) { if (e.clientY > rect.top + rect.height / 2) {
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling); e.currentTarget.parentNode.insertBefore(
} else { this.draggedEl,
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget); e.currentTarget.nextSibling
} );
} } else {
}, e.currentTarget.parentNode.insertBefore(
[ this.draggedEl,
$el( e.currentTarget
"label", );
{ }
textContent: "Name: ", },
style: { },
cursor: "grab", [
}, $el(
onmousedown: (e) => { "label",
// enable dragging only from the label {
if (e.target.localName == 'label') textContent: "Name: ",
e.currentTarget.parentNode.draggable = 'true'; style: {
} cursor: "grab",
}, },
[ onmousedown: (e) => {
$el("input", { // enable dragging only from the label
value: t.name, if (e.target.localName == "label")
dataset: { name: t.name }, e.currentTarget.parentNode.draggable = "true";
style: { },
transitionProperty: 'background-color', },
transitionDuration: '0s', [
}, $el("input", {
onchange: (e) => { value: t.name,
clearTimeout(this.saveVisualCue); dataset: { name: t.name },
var el = e.target; style: {
var row = el.parentNode.parentNode; transitionProperty: "background-color",
this.templates[row.dataset.id].name = el.value.trim() || 'untitled'; transitionDuration: "0s",
this.store(); },
el.style.backgroundColor = 'rgb(40, 95, 40)'; onchange: (e) => {
el.style.transitionDuration = '0s'; clearTimeout(this.saveVisualCue);
// @ts-expect-error var el = e.target;
// In browser env the return value is number. var row = el.parentNode.parentNode;
this.saveVisualCue = setTimeout(function () { this.templates[row.dataset.id].name =
el.style.transitionDuration = '.7s'; el.value.trim() || "untitled";
el.style.backgroundColor = 'var(--comfy-input-bg)'; this.store();
}, 15); el.style.backgroundColor = "rgb(40, 95, 40)";
}, el.style.transitionDuration = "0s";
onkeypress: (e) => { // @ts-expect-error
var el = e.target; // In browser env the return value is number.
clearTimeout(this.saveVisualCue); this.saveVisualCue = setTimeout(function () {
el.style.transitionDuration = '0s'; el.style.transitionDuration = ".7s";
el.style.backgroundColor = 'var(--comfy-input-bg)'; el.style.backgroundColor = "var(--comfy-input-bg)";
}, }, 15);
$: (el) => (nameInput = el), },
}) onkeypress: (e) => {
] var el = e.target;
), clearTimeout(this.saveVisualCue);
$el( el.style.transitionDuration = "0s";
"div", el.style.backgroundColor = "var(--comfy-input-bg)";
{}, },
[ $: (el) => (nameInput = el),
$el("button", { }),
textContent: "Export", ]
style: { ),
fontSize: "12px", $el("div", {}, [
fontWeight: "normal", $el("button", {
}, textContent: "Export",
onclick: (e) => { style: {
const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string fontSize: "12px",
const blob = new Blob([json], {type: "application/json"}); fontWeight: "normal",
const url = URL.createObjectURL(blob); },
const a = $el("a", { onclick: (e) => {
href: url, const json = JSON.stringify({ templates: [t] }, null, 2); // convert the data to a JSON string
download: (nameInput.value || t.name) + ".json", const blob = new Blob([json], {
style: {display: "none"}, type: "application/json",
parent: document.body, });
}); const url = URL.createObjectURL(blob);
a.click(); const a = $el("a", {
setTimeout(function () { href: url,
a.remove(); download: (nameInput.value || t.name) + ".json",
window.URL.revokeObjectURL(url); style: { display: "none" },
}, 0); parent: document.body,
}, });
}), a.click();
$el("button", { setTimeout(function () {
textContent: "Delete", a.remove();
style: { window.URL.revokeObjectURL(url);
fontSize: "12px", }, 0);
color: "red", },
fontWeight: "normal", }),
}, $el("button", {
onclick: (e) => { textContent: "Delete",
const item = e.target.parentNode.parentNode; style: {
item.parentNode.removeChild(item); fontSize: "12px",
this.templates.splice(item.dataset.id*1, 1); color: "red",
this.store(); fontWeight: "normal",
// update the rows index, setTimeout ensures that the list is updated },
var that = this; onclick: (e) => {
setTimeout(function (){ const item = e.target.parentNode.parentNode;
that.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => { item.parentNode.removeChild(item);
el.dataset.id = i.toString(); this.templates.splice(item.dataset.id * 1, 1);
}); this.store();
}, 0); // 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({ app.registerExtension({
name: id, name: id,
setup() { setup() {
const manage = new ManageTemplates(); const manage = new ManageTemplates();
const clipboardAction = async (cb) => { const clipboardAction = async (cb) => {
// We use the clipboard functions but dont want to overwrite the current user clipboard // We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback // Restore it after we've run our callback
const old = localStorage.getItem("litegrapheditor_clipboard"); const old = localStorage.getItem("litegrapheditor_clipboard");
await cb(); await cb();
localStorage.setItem("litegrapheditor_clipboard", old); localStorage.setItem("litegrapheditor_clipboard", old);
}; };
// @ts-ignore // @ts-ignore
const orig = LGraphCanvas.prototype.getCanvasMenuOptions; const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.getCanvasMenuOptions = function () { LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments); const options = orig.apply(this, arguments);
options.push(null); options.push(null);
options.push({ options.push({
content: `Save Selected as Template`, content: `Save Selected as Template`,
disabled: !Object.keys(app.canvas.selected_nodes || {}).length, disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => { callback: () => {
const name = prompt("Enter name"); const name = prompt("Enter name");
if (!name?.trim()) return; if (!name?.trim()) return;
clipboardAction(() => { clipboardAction(() => {
app.canvas.copyToClipboard(); app.canvas.copyToClipboard();
let data = localStorage.getItem("litegrapheditor_clipboard"); let data = localStorage.getItem("litegrapheditor_clipboard");
data = JSON.parse(data); data = JSON.parse(data);
const nodeIds = Object.keys(app.canvas.selected_nodes); const nodeIds = Object.keys(app.canvas.selected_nodes);
for (let i = 0; i < nodeIds.length; i++) { for (let i = 0; i < nodeIds.length; i++) {
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i])); const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]));
// @ts-ignore // @ts-ignore
const nodeData = node?.constructor.nodeData; const nodeData = node?.constructor.nodeData;
let groupData = GroupNodeHandler.getGroupData(node); let groupData = GroupNodeHandler.getGroupData(node);
if (groupData) { if (groupData) {
groupData = groupData.nodeData; groupData = groupData.nodeData;
// @ts-ignore // @ts-ignore
if (!data.groupNodes) { if (!data.groupNodes) {
// @ts-ignore // @ts-ignore
data.groupNodes = {}; data.groupNodes = {};
} }
// @ts-ignore // @ts-ignore
data.groupNodes[nodeData.name] = groupData; data.groupNodes[nodeData.name] = groupData;
// @ts-ignore // @ts-ignore
data.nodes[i].type = nodeData.name; data.nodes[i].type = nodeData.name;
} }
} }
manage.templates.push({ manage.templates.push({
name, name,
data: JSON.stringify(data), data: JSON.stringify(data),
}); });
manage.store(); manage.store();
}); });
}, },
}); });
// Map each template to a menu item // Map each template to a menu item
const subItems = manage.templates.map((t) => { const subItems = manage.templates.map((t) => {
return { return {
content: t.name, content: t.name,
callback: () => { callback: () => {
clipboardAction(async () => { clipboardAction(async () => {
const data = JSON.parse(t.data); const data = JSON.parse(t.data);
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}); await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
localStorage.setItem("litegrapheditor_clipboard", t.data); localStorage.setItem("litegrapheditor_clipboard", t.data);
app.canvas.pasteFromClipboard(); app.canvas.pasteFromClipboard();
}); });
}, },
}; };
}); });
subItems.push(null, { subItems.push(null, {
content: "Manage", content: "Manage",
callback: () => manage.show(), callback: () => manage.show(),
}); });
options.push({ options.push({
content: "Node Templates", content: "Node Templates",
submenu: { submenu: {
options: subItems, options: subItems,
}, },
}); });
return options; return options;
}; };
}, },
}); });

View File

@@ -1,53 +1,55 @@
import {app} from "../../scripts/app"; import { app } from "../../scripts/app";
import {ComfyWidgets} from "../../scripts/widgets"; import { ComfyWidgets } from "../../scripts/widgets";
// Node that add notes to your project // Node that add notes to your project
app.registerExtension({ app.registerExtension({
name: "Comfy.NoteNode", name: "Comfy.NoteNode",
registerCustomNodes() { registerCustomNodes() {
class NoteNode { class NoteNode {
static category: string; 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;
}
// @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: "" };
} }
ComfyWidgets.STRING(
// Load default visibility // @ts-ignore
// Should we extends LGraphNode?
LiteGraph.registerNodeType( this,
"Note", "",
// @ts-ignore ["", { default: this.properties.text, multiline: true }],
Object.assign(NoteNode, { app
title_mode: LiteGraph.NORMAL_TITLE,
title: "Note",
collapsable: true,
})
); );
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 // Node that allows you to redirect connections for cleaner graphs
app.registerExtension({ app.registerExtension({
name: "Comfy.RerouteNode", name: "Comfy.RerouteNode",
registerCustomNodes(app) { registerCustomNodes(app) {
class RerouteNode { class RerouteNode {
constructor() { constructor() {
if (!this.properties) { if (!this.properties) {
this.properties = {}; this.properties = {};
} }
this.properties.showOutputText = RerouteNode.defaultVisibility; this.properties.showOutputText = RerouteNode.defaultVisibility;
this.properties.horizontal = false; this.properties.horizontal = false;
this.addInput("", "*"); this.addInput("", "*");
this.addOutput(this.properties.showOutputText ? "*" : "", "*"); this.addOutput(this.properties.showOutputText ? "*" : "", "*");
this.onAfterGraphConfigured = function () { this.onAfterGraphConfigured = function () {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.onConnectionsChange(LiteGraph.INPUT, null, true, null); this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
}); });
}; };
this.onConnectionsChange = function (type, index, connected, link_info) { this.onConnectionsChange = function (
this.applyOrientation(); type,
index,
connected,
link_info
) {
this.applyOrientation();
// Prevent multiple connections to different types when we have no input // Prevent multiple connections to different types when we have no input
if (connected && type === LiteGraph.OUTPUT) { if (connected && type === LiteGraph.OUTPUT) {
// Ignore wildcard nodes as these will be updated to real types // 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 !== "*")); const types = new Set(
if (types.size > 1) { this.outputs[0].links
const linksToDisconnect = []; .map((l) => app.graph.links[l].type)
for (let i = 0; i < this.outputs[0].links.length - 1; i++) { .filter((t) => t !== "*")
const linkId = this.outputs[0].links[i]; );
const link = app.graph.links[linkId]; if (types.size > 1) {
linksToDisconnect.push(link); const linksToDisconnect = [];
} for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
for (const link of linksToDisconnect) { const linkId = this.outputs[0].links[i];
const node = app.graph.getNodeById(link.target_id); const link = app.graph.links[linkId];
node.disconnectInput(link.target_slot); linksToDisconnect.push(link);
} }
} for (const link of linksToDisconnect) {
} const node = app.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
// Find root input // Find root input
let currentNode = this; let currentNode = this;
let updateNodes = []; let updateNodes = [];
let inputType = null; let inputType = null;
let inputNode = null; let inputNode = null;
while (currentNode) { while (currentNode) {
updateNodes.unshift(currentNode); updateNodes.unshift(currentNode);
const linkId = currentNode.inputs[0].link; const linkId = currentNode.inputs[0].link;
if (linkId !== null) { if (linkId !== null) {
const link = app.graph.links[linkId]; const link = app.graph.links[linkId];
if (!link) return; if (!link) return;
const node = app.graph.getNodeById(link.origin_id); const node = app.graph.getNodeById(link.origin_id);
const type = node.constructor.type; const type = node.constructor.type;
if (type === "Reroute") { if (type === "Reroute") {
if (node === this) { if (node === this) {
// We've found a circle // We've found a circle
currentNode.disconnectInput(link.target_slot); currentNode.disconnectInput(link.target_slot);
currentNode = null; currentNode = null;
} else { } else {
// Move the previous node // Move the previous node
currentNode = node; currentNode = node;
} }
} else { } else {
// We've found the end // We've found the end
inputNode = currentNode; inputNode = currentNode;
inputType = node.outputs[link.origin_slot]?.type ?? null; inputType = node.outputs[link.origin_slot]?.type ?? null;
break; break;
} }
} else { } else {
// This path has no input node // This path has no input node
currentNode = null; currentNode = null;
break; break;
} }
} }
// Find all outputs // Find all outputs
const nodes = [this]; const nodes = [this];
let outputType = null; let outputType = null;
while (nodes.length) { while (nodes.length) {
currentNode = nodes.pop(); currentNode = nodes.pop();
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || []; const outputs =
if (outputs.length) { (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
for (const linkId of outputs) { if (outputs.length) {
const link = app.graph.links[linkId]; for (const linkId of outputs) {
const link = app.graph.links[linkId];
// When disconnecting sometimes the link is still registered // When disconnecting sometimes the link is still registered
if (!link) continue; if (!link) continue;
const node = app.graph.getNodeById(link.target_id); const node = app.graph.getNodeById(link.target_id);
const type = node.constructor.type; const type = node.constructor.type;
if (type === "Reroute") { if (type === "Reroute") {
// Follow reroute nodes // Follow reroute nodes
nodes.push(node); nodes.push(node);
updateNodes.push(node); updateNodes.push(node);
} else { } else {
// We've found an output // We've found an output
const nodeOutType = const nodeOutType =
node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type node.inputs &&
? node.inputs[link.target_slot].type node.inputs[link?.target_slot] &&
: null; node.inputs[link.target_slot].type
if (inputType && inputType !== "*" && nodeOutType !== inputType) { ? node.inputs[link.target_slot].type
// The output doesnt match our input so disconnect it : null;
node.disconnectInput(link.target_slot); if (
} else { inputType &&
outputType = nodeOutType; inputType !== "*" &&
} nodeOutType !== inputType
} ) {
} // The output doesnt match our input so disconnect it
} else { node.disconnectInput(link.target_slot);
// No more outputs for this path } else {
} outputType = nodeOutType;
} }
}
}
} else {
// No more outputs for this path
}
}
const displayType = inputType || outputType || "*"; const displayType = inputType || outputType || "*";
const color = LGraphCanvas.link_type_colors[displayType]; const color = LGraphCanvas.link_type_colors[displayType];
let widgetConfig; let widgetConfig;
let targetWidget; let targetWidget;
let widgetType; let widgetType;
// Update the types of each node // Update the types of each node
for (const node of updateNodes) { for (const node of updateNodes) {
// If we dont have an input type we are always wildcard but we'll show the output type // 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 // This lets you change the output link to a different type and all nodes will update
node.outputs[0].type = inputType || "*"; node.outputs[0].type = inputType || "*";
node.__outputType = displayType; node.__outputType = displayType;
node.outputs[0].name = node.properties.showOutputText ? displayType : ""; node.outputs[0].name = node.properties.showOutputText
node.size = node.computeSize(); ? displayType
node.applyOrientation(); : "";
node.size = node.computeSize();
node.applyOrientation();
for (const l of node.outputs[0].links || []) { for (const l of node.outputs[0].links || []) {
const link = app.graph.links[l]; const link = app.graph.links[l];
if (link) { if (link) {
link.color = color; link.color = color;
if (app.configuringGraph) continue; if (app.configuringGraph) continue;
const targetNode = app.graph.getNodeById(link.target_id); const targetNode = app.graph.getNodeById(link.target_id);
const targetInput = targetNode.inputs?.[link.target_slot]; const targetInput = targetNode.inputs?.[link.target_slot];
if (targetInput?.widget) { if (targetInput?.widget) {
const config = getWidgetConfig(targetInput); const config = getWidgetConfig(targetInput);
if (!widgetConfig) { if (!widgetConfig) {
widgetConfig = config[1] ?? {}; widgetConfig = config[1] ?? {};
widgetType = config[0]; widgetType = config[0];
} }
if (!targetWidget) { if (!targetWidget) {
targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name); targetWidget = targetNode.widgets?.find(
} (w) => w.name === targetInput.widget.name
);
}
const merged = mergeIfValid(targetInput, [config[0], widgetConfig]); const merged = mergeIfValid(targetInput, [
if (merged.customConfig) { config[0],
widgetConfig = merged.customConfig; widgetConfig,
} ]);
} if (merged.customConfig) {
} widgetConfig = merged.customConfig;
} }
} }
}
}
}
for (const node of updateNodes) { for (const node of updateNodes) {
if (widgetConfig && outputType) { if (widgetConfig && outputType) {
node.inputs[0].widget = { name: "value" }; node.inputs[0].widget = { name: "value" };
setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget); setWidgetConfig(
} else { node.inputs[0],
setWidgetConfig(node.inputs[0], null); [widgetType ?? displayType, widgetConfig],
} targetWidget
} );
} else {
setWidgetConfig(node.inputs[0], null);
}
}
if (inputNode) { if (inputNode) {
const link = app.graph.links[inputNode.inputs[0].link]; const link = app.graph.links[inputNode.inputs[0].link];
if (link) { if (link) {
link.color = color; link.color = color;
} }
} }
}; };
this.clone = function () { this.clone = function () {
const cloned = RerouteNode.prototype.clone.apply(this); const cloned = RerouteNode.prototype.clone.apply(this);
cloned.removeOutput(0); cloned.removeOutput(0);
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*"); cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
cloned.size = cloned.computeSize(); cloned.size = cloned.computeSize();
return cloned; return cloned;
}; };
// This node is purely frontend and does not impact the resulting prompt so should not be serialized // This node is purely frontend and does not impact the resulting prompt so should not be serialized
this.isVirtualNode = true; this.isVirtualNode = true;
} }
getExtraMenuOptions(_, options) { getExtraMenuOptions(_, options) {
options.unshift( options.unshift(
{ {
content: (this.properties.showOutputText ? "Hide" : "Show") + " Type", content:
callback: () => { (this.properties.showOutputText ? "Hide" : "Show") + " Type",
this.properties.showOutputText = !this.properties.showOutputText; callback: () => {
if (this.properties.showOutputText) { this.properties.showOutputText = !this.properties.showOutputText;
this.outputs[0].name = this.__outputType || this.outputs[0].type; if (this.properties.showOutputText) {
} else { this.outputs[0].name =
this.outputs[0].name = ""; this.__outputType || this.outputs[0].type;
} } else {
this.size = this.computeSize(); this.outputs[0].name = "";
this.applyOrientation(); }
app.graph.setDirtyCanvas(true, true); this.size = this.computeSize();
}, this.applyOrientation();
}, app.graph.setDirtyCanvas(true, true);
{ },
content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default", },
callback: () => { {
RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility); content:
}, (RerouteNode.defaultVisibility ? "Hide" : "Show") +
}, " Type By Default",
{ callback: () => {
// naming is inverted with respect to LiteGraphNode.horizontal RerouteNode.setDefaultTextVisibility(
// LiteGraphNode.horizontal == true means that !RerouteNode.defaultVisibility
// 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; // naming is inverted with respect to LiteGraphNode.horizontal
this.applyOrientation(); // 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"),
applyOrientation() { callback: () => {
this.horizontal = this.properties.horizontal; this.properties.horizontal = !this.properties.horizontal;
if (this.horizontal) { this.applyOrientation();
// 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 { applyOrientation() {
delete this.inputs[0].pos; this.horizontal = this.properties.horizontal;
} if (this.horizontal) {
app.graph.setDirtyCanvas(true, true); // 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() { computeSize() {
return [ return [
this.properties.showOutputText && this.outputs && this.outputs.length this.properties.showOutputText && this.outputs && this.outputs.length
? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40) ? Math.max(
: 75, 75,
26, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 +
]; 40
} )
: 75,
26,
];
}
static setDefaultTextVisibility(visible) { static setDefaultTextVisibility(visible) {
RerouteNode.defaultVisibility = visible; RerouteNode.defaultVisibility = visible;
if (visible) { if (visible) {
localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true"; localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
} else { } else {
delete localStorage["Comfy.RerouteNode.DefaultVisibility"]; delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
} }
} }
} }
// Load default visibility // Load default visibility
RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]); RerouteNode.setDefaultTextVisibility(
!!localStorage["Comfy.RerouteNode.DefaultVisibility"]
);
LiteGraph.registerNodeType( LiteGraph.registerNodeType(
"Reroute", "Reroute",
Object.assign(RerouteNode, { Object.assign(RerouteNode, {
title_mode: LiteGraph.NO_TITLE, title_mode: LiteGraph.NO_TITLE,
title: "Reroute", title: "Reroute",
collapsable: false, 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 // Use widget values and dates in output filenames
app.registerExtension({ app.registerExtension({
name: "Comfy.SaveImageExtraOutput", name: "Comfy.SaveImageExtraOutput",
async beforeRegisterNodeDef(nodeType, nodeData, app) { async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SaveImage") { if (nodeData.name === "SaveImage") {
const onNodeCreated = nodeType.prototype.onNodeCreated; 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 // 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 () { nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
const widget = this.widgets.find((w) => w.name === "filename_prefix"); const widget = this.widgets.find((w) => w.name === "filename_prefix");
widget.serializeValue = () => { widget.serializeValue = () => {
return applyTextReplacements(app, widget.value); return applyTextReplacements(app, widget.value);
}; };
return r; return r;
}; };
} else { } else {
// When any other node is created add a property to alias the node // When any other node is created add a property to alias the node
const onNodeCreated = nodeType.prototype.onNodeCreated; const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () { nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
if (!this.properties || !("Node name for S&R" in this.properties)) { if (!this.properties || !("Node name for S&R" in this.properties)) {
this.addProperty("Node name for S&R", this.constructor.type, "string"); 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; let touchCount = 0;
app.registerExtension({ app.registerExtension({
name: "Comfy.SimpleTouchSupport", name: "Comfy.SimpleTouchSupport",
setup() { setup() {
let zoomPos; let zoomPos;
let touchTime; let touchTime;
let lastTouch; let lastTouch;
function getMultiTouchPos(e) { function getMultiTouchPos(e) {
return Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); return Math.hypot(
} e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
}
app.canvasEl.addEventListener( app.canvasEl.addEventListener(
"touchstart", "touchstart",
(e) => { (e) => {
touchCount++; touchCount++;
lastTouch = null; lastTouch = null;
if (e.touches?.length === 1) { if (e.touches?.length === 1) {
// Store start time for press+hold for context menu // Store start time for press+hold for context menu
touchTime = new Date(); touchTime = new Date();
lastTouch = e.touches[0]; lastTouch = e.touches[0];
} else { } else {
touchTime = null; touchTime = null;
if (e.touches?.length === 2) { if (e.touches?.length === 2) {
// Store center pos for zoom // Store center pos for zoom
zoomPos = getMultiTouchPos(e); zoomPos = getMultiTouchPos(e);
app.canvas.pointer_is_down = false; app.canvas.pointer_is_down = false;
} }
} }
}, },
true true
); );
app.canvasEl.addEventListener("touchend", (e: TouchEvent) => { app.canvasEl.addEventListener("touchend", (e: TouchEvent) => {
touchZooming = false; touchZooming = false;
touchCount = e.touches?.length ?? touchCount - 1; touchCount = e.touches?.length ?? touchCount - 1;
if (touchTime && !e.touches?.length) { if (touchTime && !e.touches?.length) {
if ((new Date()).getTime() - touchTime > 600) { if (new Date().getTime() - touchTime > 600) {
try { try {
// hack to get litegraph to use this event // hack to get litegraph to use this event
e.constructor = CustomEvent; e.constructor = CustomEvent;
} catch (error) {} } catch (error) {}
// @ts-ignore // @ts-ignore
e.clientX = lastTouch.clientX; e.clientX = lastTouch.clientX;
// @ts-ignore // @ts-ignore
e.clientY = lastTouch.clientY; e.clientY = lastTouch.clientY;
app.canvas.pointer_is_down = true; app.canvas.pointer_is_down = true;
// @ts-ignore // @ts-ignore
app.canvas._mousedown_callback(e); app.canvas._mousedown_callback(e);
} }
touchTime = null; touchTime = null;
} }
}); });
app.canvasEl.addEventListener( app.canvasEl.addEventListener(
"touchmove", "touchmove",
(e) => { (e) => {
touchTime = null; touchTime = null;
if (e.touches?.length === 2) { if (e.touches?.length === 2) {
app.canvas.pointer_is_down = false; app.canvas.pointer_is_down = false;
touchZooming = true; touchZooming = true;
// @ts-ignore // @ts-ignore
LiteGraph.closeAllContextMenus(); LiteGraph.closeAllContextMenus();
// @ts-ignore // @ts-ignore
app.canvas.search_box?.close(); app.canvas.search_box?.close();
const newZoomPos = getMultiTouchPos(e); const newZoomPos = getMultiTouchPos(e);
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2; const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2; const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
let scale = app.canvas.ds.scale; let scale = app.canvas.ds.scale;
const diff = zoomPos - newZoomPos; const diff = zoomPos - newZoomPos;
if (diff > 0.5) { if (diff > 0.5) {
scale *= 1 / 1.07; scale *= 1 / 1.07;
} else if (diff < -0.5) { } else if (diff < -0.5) {
scale *= 1.07; scale *= 1.07;
} }
app.canvas.ds.changeScale(scale, [midX, midY]); app.canvas.ds.changeScale(scale, [midX, midY]);
app.canvas.setDirty(true, true); app.canvas.setDirty(true, true);
zoomPos = newZoomPos; zoomPos = newZoomPos;
} }
}, },
true true
); );
}, },
}); });
// @ts-ignore // @ts-ignore
const processMouseDown = LGraphCanvas.prototype.processMouseDown; const processMouseDown = LGraphCanvas.prototype.processMouseDown;
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.processMouseDown = function (e) { LGraphCanvas.prototype.processMouseDown = function (e) {
if (touchZooming || touchCount) { if (touchZooming || touchCount) {
return; return;
} }
return processMouseDown.apply(this, arguments); return processMouseDown.apply(this, arguments);
}; };
// @ts-ignore // @ts-ignore
const processMouseMove = LGraphCanvas.prototype.processMouseMove; const processMouseMove = LGraphCanvas.prototype.processMouseMove;
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.processMouseMove = function (e) { LGraphCanvas.prototype.processMouseMove = function (e) {
if (touchZooming || touchCount > 1) { if (touchZooming || touchCount > 1) {
return; return;
} }
return processMouseMove.apply(this, arguments); 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 // Adds defaults for quickly adding nodes with middle click on the input/output
app.registerExtension({ app.registerExtension({
name: "Comfy.SlotDefaults", name: "Comfy.SlotDefaults",
suggestionsNumber: null, suggestionsNumber: null,
init() { init() {
LiteGraph.search_filter_enabled = true; LiteGraph.search_filter_enabled = true;
LiteGraph.middle_click_slot_add_default_node = true; LiteGraph.middle_click_slot_add_default_node = true;
this.suggestionsNumber = app.ui.settings.addSetting({ this.suggestionsNumber = app.ui.settings.addSetting({
id: "Comfy.NodeSuggestions.number", id: "Comfy.NodeSuggestions.number",
name: "Number of nodes suggestions", name: "Number of nodes suggestions",
type: "slider", type: "slider",
attrs: { attrs: {
min: 1, min: 1,
max: 100, max: 100,
step: 1, step: 1,
}, },
defaultValue: 5, defaultValue: 5,
onChange: (newVal, oldVal) => { onChange: (newVal, oldVal) => {
this.setDefaults(newVal); this.setDefaults(newVal);
} },
}); });
}, },
slot_types_default_out: {}, slot_types_default_out: {},
slot_types_default_in: {}, slot_types_default_in: {},
async beforeRegisterNodeDef(nodeType, nodeData, app) { async beforeRegisterNodeDef(nodeType, nodeData, app) {
var nodeId = nodeData.name; var nodeId = nodeData.name;
var inputs = []; var inputs = [];
inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional 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) { for (const inputKey in inputs) {
var input = (inputs[inputKey]); var input = inputs[inputKey];
if (typeof input[0] !== "string") continue; if (typeof input[0] !== "string") continue;
var type = input[0] var type = input[0];
if (type in ComfyWidgets) { if (type in ComfyWidgets) {
var customProperties = input[1] var customProperties = input[1];
if (!(customProperties?.forceInput)) continue; //ignore widgets that don't force input if (!customProperties?.forceInput) continue; //ignore widgets that don't force input
} }
if (!(type in this.slot_types_default_out)) { if (!(type in this.slot_types_default_out)) {
this.slot_types_default_out[type] = ["Reroute"]; this.slot_types_default_out[type] = ["Reroute"];
} }
if (this.slot_types_default_out[type].includes(nodeId)) continue; if (this.slot_types_default_out[type].includes(nodeId)) continue;
this.slot_types_default_out[type].push(nodeId); this.slot_types_default_out[type].push(nodeId);
// Input types have to be stored as lower case // Input types have to be stored as lower case
// Store each node that can handle this input type // Store each node that can handle this input type
const lowerType = type.toLocaleLowerCase(); const lowerType = type.toLocaleLowerCase();
if (!(lowerType in LiteGraph.registered_slot_in_types)) { if (!(lowerType in LiteGraph.registered_slot_in_types)) {
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }; LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
} }
LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeType.comfyClass); LiteGraph.registered_slot_in_types[lowerType].nodes.push(
} nodeType.comfyClass
);
}
var outputs = nodeData["output"]; var outputs = nodeData["output"];
for (const key in outputs) { for (const key in outputs) {
var type = outputs[key]; var type = outputs[key];
if (!(type in this.slot_types_default_in)) { 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] = ["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 // Store each node that can handle this output type
if (!(type in LiteGraph.registered_slot_out_types)) { if (!(type in LiteGraph.registered_slot_out_types)) {
LiteGraph.registered_slot_out_types[type] = { nodes: [] }; LiteGraph.registered_slot_out_types[type] = { nodes: [] };
} }
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass); LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
if(!LiteGraph.slot_types_out.includes(type)) { if (!LiteGraph.slot_types_out.includes(type)) {
LiteGraph.slot_types_out.push(type); LiteGraph.slot_types_out.push(type);
} }
} }
var maxNum = this.suggestionsNumber.value; var maxNum = this.suggestionsNumber.value;
this.setDefaults(maxNum); this.setDefaults(maxNum);
}, },
setDefaults(maxNum) { setDefaults(maxNum) {
LiteGraph.slot_types_default_out = {};
LiteGraph.slot_types_default_in = {};
LiteGraph.slot_types_default_out = {}; for (const type in this.slot_types_default_out) {
LiteGraph.slot_types_default_in = {}; LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[
type
for (const type in this.slot_types_default_out) { ].slice(0, maxNum);
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum); }
} for (const type in this.slot_types_default_in) {
for (const type in this.slot_types_default_in) { LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum); 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. */ /** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
function roundVectorToGrid(vec) { function roundVectorToGrid(vec) {
vec[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE); vec[0] =
vec[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE); LiteGraph.CANVAS_GRID_SIZE *
return vec; 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({ app.registerExtension({
name: "Comfy.SnapToGrid", name: "Comfy.SnapToGrid",
init() { init() {
// Add setting to control grid size // Add setting to control grid size
app.ui.settings.addSetting({ app.ui.settings.addSetting({
id: "Comfy.SnapToGrid.GridSize", id: "Comfy.SnapToGrid.GridSize",
name: "Grid Size", name: "Grid Size",
type: "slider", type: "slider",
attrs: { attrs: {
min: 1, min: 1,
max: 500, max: 500,
}, },
tooltip: tooltip:
"When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.", "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, defaultValue: LiteGraph.CANVAS_GRID_SIZE,
onChange(value) { onChange(value) {
LiteGraph.CANVAS_GRID_SIZE = +value; LiteGraph.CANVAS_GRID_SIZE = +value;
}, },
}); });
// After moving a node, if the shift key is down align it to grid // After moving a node, if the shift key is down align it to grid
const onNodeMoved = app.canvas.onNodeMoved; const onNodeMoved = app.canvas.onNodeMoved;
app.canvas.onNodeMoved = function (node) { app.canvas.onNodeMoved = function (node) {
const r = onNodeMoved?.apply(this, arguments); const r = onNodeMoved?.apply(this, arguments);
if (app.shiftDown) { if (app.shiftDown) {
// Ensure all selected nodes are realigned // Ensure all selected nodes are realigned
for (const id in this.selected_nodes) { for (const id in this.selected_nodes) {
this.selected_nodes[id].alignToGrid(); 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 // 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; const onNodeAdded = app.graph.onNodeAdded;
app.graph.onNodeAdded = function (node) { app.graph.onNodeAdded = function (node) {
const onResize = node.onResize; const onResize = node.onResize;
node.onResize = function () { node.onResize = function () {
if (app.shiftDown) { if (app.shiftDown) {
roundVectorToGrid(node.size); roundVectorToGrid(node.size);
} }
return onResize?.apply(this, arguments); return onResize?.apply(this, arguments);
}; };
return onNodeAdded?.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 // Draw a preview of where the node will go if holding shift and the node is selected
// @ts-ignore // @ts-ignore
const origDrawNode = LGraphCanvas.prototype.drawNode; const origDrawNode = LGraphCanvas.prototype.drawNode;
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.drawNode = function (node, ctx) { LGraphCanvas.prototype.drawNode = function (node, ctx) {
if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) { if (
const [x, y] = roundVectorToGrid([...node.pos]); app.shiftDown &&
const shiftX = x - node.pos[0]; this.node_dragged &&
let shiftY = y - node.pos[1]; 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; let w, h;
if (node.flags.collapsed) { if (node.flags.collapsed) {
w = node._collapsed_width; w = node._collapsed_width;
h = LiteGraph.NODE_TITLE_HEIGHT; h = LiteGraph.NODE_TITLE_HEIGHT;
shiftY -= LiteGraph.NODE_TITLE_HEIGHT; shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
} else { } else {
w = node.size[0]; w = node.size[0];
h = node.size[1]; h = node.size[1];
let titleMode = node.constructor.title_mode; let titleMode = node.constructor.title_mode;
if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) { if (
h += LiteGraph.NODE_TITLE_HEIGHT; titleMode !== LiteGraph.TRANSPARENT_TITLE &&
shiftY -= LiteGraph.NODE_TITLE_HEIGHT; titleMode !== LiteGraph.NO_TITLE
} ) {
} h += LiteGraph.NODE_TITLE_HEIGHT;
const f = ctx.fillStyle; shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
ctx.fillStyle = "rgba(100, 100, 100, 0.5)"; }
ctx.fillRect(shiftX, shiftY, w, h); }
ctx.fillStyle = f; 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 * The currently moving, selected group only. Set after the `selected_group` has actually started
* moving. * moving.
*/ */
let selectedAndMovingGroup = null; let selectedAndMovingGroup = null;
/** /**
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups` * 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`. * below) as well as handle the last move call from LiteGraph's `processMouseUp`.
*/ */
// @ts-ignore // @ts-ignore
const groupMove = LGraphGroup.prototype.move; const groupMove = LGraphGroup.prototype.move;
// @ts-ignore // @ts-ignore
LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
const v = groupMove.apply(this, arguments); const v = groupMove.apply(this, arguments);
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group` // 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. // 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)) { if (
selectedAndMovingGroup = this; !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 // 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` // 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 // has been set to `false`. Essentially, this check here is the equivilant to calling an
// `LGraphGroup.prototype.onNodeMoved` if it had existed. // `LGraphGroup.prototype.onNodeMoved` if it had existed.
if (app.canvas.last_mouse_dragging === false && app.shiftDown) { if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
// After moving a group (while app.shiftDown), snap all the child nodes and, finally, // After moving a group (while app.shiftDown), snap all the child nodes and, finally,
// align the group itself. // align the group itself.
this.recomputeInsideNodes(); this.recomputeInsideNodes();
for (const node of this._nodes) { for (const node of this._nodes) {
node.alignToGrid(); node.alignToGrid();
} }
// @ts-ignore // @ts-ignore
LGraphNode.prototype.alignToGrid.apply(this); LGraphNode.prototype.alignToGrid.apply(this);
} }
return v; return v;
}; };
/** /**
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or * 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 * drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
* both. * both.
*/ */
// @ts-ignore // @ts-ignore
const drawGroups = LGraphCanvas.prototype.drawGroups; const drawGroups = LGraphCanvas.prototype.drawGroups;
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) { LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
if (this.selected_group && app.shiftDown) { if (this.selected_group && app.shiftDown) {
if (this.selected_group_resizing) { if (this.selected_group_resizing) {
roundVectorToGrid(this.selected_group.size); roundVectorToGrid(this.selected_group.size);
} else if (selectedAndMovingGroup) { } else if (selectedAndMovingGroup) {
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]); const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
const f = ctx.fillStyle; const f = ctx.fillStyle;
const s = ctx.strokeStyle; const s = ctx.strokeStyle;
ctx.fillStyle = "rgba(100, 100, 100, 0.33)"; ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
ctx.strokeStyle = "rgba(100, 100, 100, 0.66)"; ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
ctx.rect(x, y, ...selectedAndMovingGroup.size); ctx.rect(x, y, ...selectedAndMovingGroup.size);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
ctx.fillStyle = f; ctx.fillStyle = f;
ctx.strokeStyle = s; ctx.strokeStyle = s;
} }
} else if (!this.selected_group) { } else if (!this.selected_group) {
selectedAndMovingGroup = null; selectedAndMovingGroup = null;
} }
return drawGroups.apply(this, arguments); return drawGroups.apply(this, arguments);
}; };
/** Handles adding a group in a snapping-enabled state. */
/** Handles adding a group in a snapping-enabled state. */ // @ts-ignore
// @ts-ignore const onGroupAdd = LGraphCanvas.onGroupAdd;
const onGroupAdd = LGraphCanvas.onGroupAdd; // @ts-ignore
// @ts-ignore LGraphCanvas.onGroupAdd = function () {
LGraphCanvas.onGroupAdd = function() { const v = onGroupAdd.apply(app.canvas, arguments);
const v = onGroupAdd.apply(app.canvas, arguments); if (app.shiftDown) {
if (app.shiftDown) { // @ts-ignore
// @ts-ignore const lastGroup = app.graph._groups[app.graph._groups.length - 1];
const lastGroup = app.graph._groups[app.graph._groups.length - 1]; if (lastGroup) {
if (lastGroup) { // @ts-ignore
// @ts-ignore roundVectorToGrid(lastGroup.pos);
roundVectorToGrid(lastGroup.pos); // @ts-ignore
// @ts-ignore roundVectorToGrid(lastGroup.size);
roundVectorToGrid(lastGroup.size); }
} }
} return v;
return v; };
}; },
},
}); });

View File

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

View File

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

View File

@@ -4,123 +4,137 @@ import { api } from "../../scripts/api";
const WEBCAM_READY = Symbol(); const WEBCAM_READY = Symbol();
app.registerExtension({ app.registerExtension({
name: "Comfy.WebcamCapture", name: "Comfy.WebcamCapture",
getCustomWidgets(app) { getCustomWidgets(app) {
return { return {
WEBCAM(node, inputName) { WEBCAM(node, inputName) {
let res; let res;
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve)); node[WEBCAM_READY] = new Promise((resolve) => (res = resolve));
const container = document.createElement("div"); const container = document.createElement("div");
container.style.background = "rgba(0,0,0,0.25)"; container.style.background = "rgba(0,0,0,0.25)";
container.style.textAlign = "center"; container.style.textAlign = "center";
const video = document.createElement("video"); const video = document.createElement("video");
video.style.height = video.style.width = "100%"; video.style.height = video.style.width = "100%";
const loadVideo = async () => { const loadVideo = async () => {
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); const stream = await navigator.mediaDevices.getUserMedia({
container.replaceChildren(video); video: true,
audio: false,
});
container.replaceChildren(video);
setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes? setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener("loadedmetadata", () => res(video), false); video.addEventListener("loadedmetadata", () => res(video), false);
video.srcObject = stream; video.srcObject = stream;
video.play(); video.play();
} catch (error) { } catch (error) {
const label = document.createElement("div"); const label = document.createElement("div");
label.style.color = "red"; label.style.color = "red";
label.style.overflow = "auto"; label.style.overflow = "auto";
label.style.maxHeight = "100%"; label.style.maxHeight = "100%";
label.style.whiteSpace = "pre-wrap"; label.style.whiteSpace = "pre-wrap";
if (window.isSecureContext) { if (window.isSecureContext) {
label.textContent = "Unable to load webcam, please ensure access is granted:\n" + error.message; label.textContent =
} else { "Unable to load webcam, please ensure access is granted:\n" +
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; 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) }; return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
}, },
}; };
}, },
nodeCreated(node) { nodeCreated(node) {
if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return; if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return;
let video; let video;
const camera = node.widgets.find((w) => w.name === "image"); const camera = node.widgets.find((w) => w.name === "image");
const w = node.widgets.find((w) => w.name === "width"); const w = node.widgets.find((w) => w.name === "width");
const h = node.widgets.find((w) => w.name === "height"); const h = node.widgets.find((w) => w.name === "height");
const captureOnQueue = node.widgets.find((w) => w.name === "capture_on_queue"); const captureOnQueue = node.widgets.find(
(w) => w.name === "capture_on_queue"
);
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const capture = () => { const capture = () => {
canvas.width = w.value; canvas.width = w.value;
canvas.height = h.value; canvas.height = h.value;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, w.value, h.value); ctx.drawImage(video, 0, 0, w.value, h.value);
const data = canvas.toDataURL("image/png"); const data = canvas.toDataURL("image/png");
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
node.imgs = [img]; node.imgs = [img];
app.graph.setDirtyCanvas(true); app.graph.setDirtyCanvas(true);
requestAnimationFrame(() => { requestAnimationFrame(() => {
node.setSizeForImage?.(); node.setSizeForImage?.();
}); });
}; };
img.src = data; img.src = data;
}; };
const btn = node.addWidget("button", "waiting for camera...", "capture", capture); const btn = node.addWidget(
btn.disabled = true; "button",
btn.serializeValue = () => undefined; "waiting for camera...",
"capture",
capture
);
btn.disabled = true;
btn.serializeValue = () => undefined;
camera.serializeValue = async () => { camera.serializeValue = async () => {
if (captureOnQueue.value) { if (captureOnQueue.value) {
capture(); capture();
} else if (!node.imgs?.length) { } else if (!node.imgs?.length) {
const err = `No webcam image captured`; const err = `No webcam image captured`;
alert(err); alert(err);
throw new Error(err); throw new Error(err);
} }
// Upload image to temp storage // Upload image to temp storage
const blob = await new Promise<Blob>((r) => canvas.toBlob(r)); const blob = await new Promise<Blob>((r) => canvas.toBlob(r));
const name = `${+new Date()}.png`; const name = `${+new Date()}.png`;
const file = new File([blob], name); const file = new File([blob], name);
const body = new FormData(); const body = new FormData();
body.append("image", file); body.append("image", file);
body.append("subfolder", "webcam"); body.append("subfolder", "webcam");
body.append("type", "temp"); body.append("type", "temp");
const resp = await api.fetchApi("/upload/image", { const resp = await api.fetchApi("/upload/image", {
method: "POST", method: "POST",
body, body,
}); });
if (resp.status !== 200) { if (resp.status !== 200) {
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`; const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
alert(err); alert(err);
throw new Error(err); throw new Error(err);
} }
return `webcam/${name} [temp]`; return `webcam/${name} [temp]`;
}; };
node[WEBCAM_READY].then((v) => { node[WEBCAM_READY].then((v) => {
video = v; video = v;
// If width isnt specified then use video output resolution // If width isnt specified then use video output resolution
if (!w.value) { if (!w.value) {
w.value = video.videoWidth || 640; w.value = video.videoWidth || 640;
h.value = video.videoHeight || 480; h.value = video.videoHeight || 480;
} }
btn.disabled = false; btn.disabled = false;
btn.label = "capture"; 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 * @param {clearBackgroundColor} String
* @ * @
*/ */
LGraphCanvas.prototype.updateBackground = function (image, clearBackgroundColor) { LGraphCanvas.prototype.updateBackground = function (
this._bg_img = new Image(); image,
this._bg_img.name = image; clearBackgroundColor
this._bg_img.src = image; ) {
this._bg_img.onload = () => { this._bg_img = new Image();
this.draw(true, true); this._bg_img.name = image;
}; this._bg_img.src = image;
this.background_image = image; this._bg_img.onload = () => {
this.draw(true, true);
};
this.background_image = image;
this.clear_background = true; this.clear_background = true;
this.clear_background_color = clearBackgroundColor; this.clear_background_color = clearBackgroundColor;
this._pattern = null 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 { api } from "./api";
import { clone } from "./utils"; import { clone } from "./utils";
export class ChangeTracker { export class ChangeTracker {
static MAX_HISTORY = 50; static MAX_HISTORY = 50;
#app; #app;
undo = []; undo = [];
redo = []; redo = [];
activeState = null; activeState = null;
isOurLoad = false; isOurLoad = false;
/** @type { import("./workflows").ComfyWorkflow | null } */ /** @type { import("./workflows").ComfyWorkflow | null } */
workflow; workflow;
ds; ds;
nodeOutputs; nodeOutputs;
get app() { get app() {
return this.#app ?? this.workflow.manager.app; return this.#app ?? this.workflow.manager.app;
} }
constructor(workflow) { constructor(workflow) {
this.workflow = workflow; this.workflow = workflow;
} }
#setApp(app) { #setApp(app) {
this.#app = app; this.#app = app;
} }
store() { store() {
this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] }; this.ds = {
} scale: this.app.canvas.ds.scale,
offset: [...this.app.canvas.ds.offset],
};
}
restore() { restore() {
if (this.ds) { if (this.ds) {
this.app.canvas.ds.scale = this.ds.scale; this.app.canvas.ds.scale = this.ds.scale;
this.app.canvas.ds.offset = this.ds.offset; this.app.canvas.ds.offset = this.ds.offset;
} }
if (this.nodeOutputs) { if (this.nodeOutputs) {
this.app.nodeOutputs = this.nodeOutputs; this.app.nodeOutputs = this.nodeOutputs;
} }
} }
checkState() { checkState() {
if (!this.app.graph) return; if (!this.app.graph) return;
const currentState = this.app.graph.serialize(); const currentState = this.app.graph.serialize();
if (!this.activeState) { if (!this.activeState) {
this.activeState = clone(currentState); this.activeState = clone(currentState);
return; return;
} }
if (!ChangeTracker.graphEqual(this.activeState, currentState)) { if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
this.undo.push(this.activeState); this.undo.push(this.activeState);
if (this.undo.length > ChangeTracker.MAX_HISTORY) { if (this.undo.length > ChangeTracker.MAX_HISTORY) {
this.undo.shift(); this.undo.shift();
} }
this.activeState = clone(currentState); this.activeState = clone(currentState);
this.redo.length = 0; this.redo.length = 0;
this.workflow.unsaved = true; this.workflow.unsaved = true;
api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState })); api.dispatchEvent(
} new CustomEvent("graphChanged", { detail: this.activeState })
} );
}
}
async updateState(source, target) { async updateState(source, target) {
const prevState = source.pop(); const prevState = source.pop();
if (prevState) { if (prevState) {
target.push(this.activeState); target.push(this.activeState);
this.isOurLoad = true; this.isOurLoad = true;
await this.app.loadGraphData(prevState, false, false, this.workflow); await this.app.loadGraphData(prevState, false, false, this.workflow);
this.activeState = prevState; this.activeState = prevState;
} }
} }
async undoRedo(e) { async undoRedo(e) {
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
if (e.key === "y") { if (e.key === "y") {
this.updateState(this.redo, this.undo); this.updateState(this.redo, this.undo);
return true; return true;
} else if (e.key === "z") { } else if (e.key === "z") {
this.updateState(this.undo, this.redo); this.updateState(this.undo, this.redo);
return true; return true;
} }
} }
} }
/** @param { import("./app").ComfyApp } app */ /** @param { import("./app").ComfyApp } app */
static init(app) { static init(app) {
const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker; const changeTracker = () =>
globalTracker.#setApp(app); app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
globalTracker.#setApp(app);
const loadGraphData = app.loadGraphData; const loadGraphData = app.loadGraphData;
app.loadGraphData = async function () { app.loadGraphData = async function () {
const v = await loadGraphData.apply(this, arguments); const v = await loadGraphData.apply(this, arguments);
const ct = changeTracker(); const ct = changeTracker();
if (ct.isOurLoad) { if (ct.isOurLoad) {
ct.isOurLoad = false; ct.isOurLoad = false;
} else { } else {
ct.checkState(); ct.checkState();
} }
return v; return v;
}; };
let keyIgnored = false; let keyIgnored = false;
window.addEventListener( window.addEventListener(
"keydown", "keydown",
(e) => { (e) => {
requestAnimationFrame(async () => { requestAnimationFrame(async () => {
let activeEl; let activeEl;
// If we are auto queue in change mode then we do want to trigger on inputs // If we are auto queue in change mode then we do want to trigger on inputs
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
activeEl = document.activeElement; activeEl = document.activeElement;
if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") { if (
// Ignore events on inputs, they have their native history activeEl?.tagName === "INPUT" ||
return; 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"; keyIgnored =
if (keyIgnored) return; e.key === "Control" ||
e.key === "Shift" ||
e.key === "Alt" ||
e.key === "Meta";
if (keyIgnored) return;
// Check if this is a ctrl+z ctrl+y // Check if this is a ctrl+z ctrl+y
if (await changeTracker().undoRedo(e)) return; if (await changeTracker().undoRedo(e)) return;
// If our active element is some type of input then handle changes after they're done // If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(activeEl)) return; if (ChangeTracker.bindInput(activeEl)) return;
changeTracker().checkState(); changeTracker().checkState();
}); });
}, },
true true
); );
window.addEventListener("keyup", (e) => { window.addEventListener("keyup", (e) => {
if (keyIgnored) { if (keyIgnored) {
keyIgnored = false; keyIgnored = false;
changeTracker().checkState(); changeTracker().checkState();
} }
}); });
// Handle clicking DOM elements (e.g. widgets) // Handle clicking DOM elements (e.g. widgets)
window.addEventListener("mouseup", () => { window.addEventListener("mouseup", () => {
changeTracker().checkState(); changeTracker().checkState();
}); });
// Handle prompt queue event for dynamic widget changes // Handle prompt queue event for dynamic widget changes
api.addEventListener("promptQueued", () => { api.addEventListener("promptQueued", () => {
changeTracker().checkState(); changeTracker().checkState();
}); });
// Handle litegraph clicks // Handle litegraph clicks
// @ts-ignore // @ts-ignore
const processMouseUp = LGraphCanvas.prototype.processMouseUp; const processMouseUp = LGraphCanvas.prototype.processMouseUp;
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.processMouseUp = function (e) { LGraphCanvas.prototype.processMouseUp = function (e) {
const v = processMouseUp.apply(this, arguments); const v = processMouseUp.apply(this, arguments);
changeTracker().checkState(); changeTracker().checkState();
return v; return v;
}; };
// @ts-ignore // @ts-ignore
const processMouseDown = LGraphCanvas.prototype.processMouseDown; const processMouseDown = LGraphCanvas.prototype.processMouseDown;
// @ts-ignore // @ts-ignore
LGraphCanvas.prototype.processMouseDown = function (e) { LGraphCanvas.prototype.processMouseDown = function (e) {
const v = processMouseDown.apply(this, arguments); const v = processMouseDown.apply(this, arguments);
changeTracker().checkState(); changeTracker().checkState();
return v; return v;
}; };
// Handle litegraph context menu for COMBO widgets // Handle litegraph context menu for COMBO widgets
const close = LiteGraph.ContextMenu.prototype.close; const close = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function (e) { LiteGraph.ContextMenu.prototype.close = function (e) {
const v = close.apply(this, arguments); const v = close.apply(this, arguments);
changeTracker().checkState(); changeTracker().checkState();
return v; return v;
}; };
// Detects nodes being added via the node search dialog // Detects nodes being added via the node search dialog
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded; const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
LiteGraph.LGraph.prototype.onNodeAdded = function () { LiteGraph.LGraph.prototype.onNodeAdded = function () {
const v = onNodeAdded?.apply(this, arguments); const v = onNodeAdded?.apply(this, arguments);
const ct = changeTracker(); const ct = changeTracker();
if (!ct.isOurLoad) { if (!ct.isOurLoad) {
ct.checkState(); ct.checkState();
} }
return v; return v;
}; };
// Store node outputs // Store node outputs
api.addEventListener("executed", ({ detail }) => { api.addEventListener("executed", ({ detail }) => {
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id]; const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
if (!prompt?.workflow) return; if (!prompt?.workflow) return;
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {}); const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
const output = nodeOutputs[detail.node]; const output = nodeOutputs[detail.node];
if (detail.merge && output) { if (detail.merge && output) {
for (const k in detail.output ?? {}) { for (const k in detail.output ?? {}) {
const v = output[k]; const v = output[k];
if (v instanceof Array) { if (v instanceof Array) {
output[k] = v.concat(detail.output[k]); output[k] = v.concat(detail.output[k]);
} else { } else {
output[k] = detail.output[k]; output[k] = detail.output[k];
} }
} }
} else { } else {
nodeOutputs[detail.node] = detail.output; nodeOutputs[detail.node] = detail.output;
} }
}); });
} }
static bindInput(app, activeEl) { static bindInput(app, activeEl) {
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") { if (
for (const evt of ["change", "input", "blur"]) { activeEl &&
if (`on${evt}` in activeEl) { activeEl.tagName !== "CANVAS" &&
const listener = () => { activeEl.tagName !== "BODY"
app.workflowManager.activeWorkflow.changeTracker.checkState(); ) {
activeEl.removeEventListener(evt, listener); for (const evt of ["change", "input", "blur"]) {
}; if (`on${evt}` in activeEl) {
activeEl.addEventListener(evt, listener); const listener = () => {
return true; app.workflowManager.activeWorkflow.changeTracker.checkState();
} activeEl.removeEventListener(evt, listener);
} };
} activeEl.addEventListener(evt, listener);
} return true;
}
}
}
}
static graphEqual(a, b, path = "") { static graphEqual(a, b, path = "") {
if (a === b) return true; if (a === b) return true;
if (typeof a == "object" && a && typeof b == "object" && b) { if (typeof a == "object" && a && typeof b == "object" && b) {
const keys = Object.getOwnPropertyNames(a); const keys = Object.getOwnPropertyNames(a);
if (keys.length != Object.getOwnPropertyNames(b).length) { if (keys.length != Object.getOwnPropertyNames(b).length) {
return false; return false;
} }
for (const key of keys) { for (const key of keys) {
let av = a[key]; let av = a[key];
let bv = b[key]; let bv = b[key];
if (!path && key === "nodes") { if (!path && key === "nodes") {
// Nodes need to be sorted as the order changes when selecting nodes // Nodes need to be sorted as the order changes when selecting nodes
av = [...av].sort((a, b) => a.id - b.id); av = [...av].sort((a, b) => a.id - b.id);
bv = [...bv].sort((a, b) => a.id - b.id); bv = [...bv].sort((a, b) => a.id - b.id);
} else if (path === "extra.ds") { } else if (path === "extra.ds") {
// Ignore view changes // Ignore view changes
continue; continue;
} }
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) { if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
return false; 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"; import type { ComfyWorkflow } from "/types/comfyWorkflow";
export const defaultGraph: ComfyWorkflow = { export const defaultGraph: ComfyWorkflow = {
last_node_id: 9, last_node_id: 9,
last_link_id: 9, last_link_id: 9,
nodes: [ nodes: [
{ {
id: 7, id: 7,
type: "CLIPTextEncode", type: "CLIPTextEncode",
pos: [413, 389], pos: [413, 389],
size: { 0: 425.27801513671875, 1: 180.6060791015625 }, size: { 0: 425.27801513671875, 1: 180.6060791015625 },
flags: {}, flags: {},
order: 3, order: 3,
mode: 0, mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 5 }], inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }], outputs: [
properties: {}, {
widgets_values: ["text, watermark"], name: "CONDITIONING",
}, type: "CONDITIONING",
{ links: [6],
id: 6, slot_index: 0,
type: "CLIPTextEncode", },
pos: [415, 186], ],
size: { 0: 422.84503173828125, 1: 164.31304931640625 }, properties: {},
flags: {}, widgets_values: ["text, watermark"],
order: 2, },
mode: 0, {
inputs: [{ name: "clip", type: "CLIP", link: 3 }], id: 6,
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }], type: "CLIPTextEncode",
properties: {}, pos: [415, 186],
widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"], size: { 0: 422.84503173828125, 1: 164.31304931640625 },
}, flags: {},
{ order: 2,
id: 5, mode: 0,
type: "EmptyLatentImage", inputs: [{ name: "clip", type: "CLIP", link: 3 }],
pos: [473, 609], outputs: [
size: { 0: 315, 1: 106 }, {
flags: {}, name: "CONDITIONING",
order: 1, type: "CONDITIONING",
mode: 0, links: [4],
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }], slot_index: 0,
properties: {}, },
widgets_values: [512, 512, 1], ],
}, properties: {},
{ widgets_values: [
id: 3, "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
type: "KSampler", ],
pos: [863, 186], },
size: { 0: 315, 1: 262 }, {
flags: {}, id: 5,
order: 4, type: "EmptyLatentImage",
mode: 0, pos: [473, 609],
inputs: [ size: { 0: 315, 1: 106 },
{ name: "model", type: "MODEL", link: 1 }, flags: {},
{ name: "positive", type: "CONDITIONING", link: 4 }, order: 1,
{ name: "negative", type: "CONDITIONING", link: 6 }, mode: 0,
{ name: "latent_image", type: "LATENT", link: 2 }, outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
], properties: {},
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }], widgets_values: [512, 512, 1],
properties: {}, },
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1], {
}, id: 3,
{ type: "KSampler",
id: 8, pos: [863, 186],
type: "VAEDecode", size: { 0: 315, 1: 262 },
pos: [1209, 188], flags: {},
size: { 0: 210, 1: 46 }, order: 4,
flags: {}, mode: 0,
order: 5, inputs: [
mode: 0, { name: "model", type: "MODEL", link: 1 },
inputs: [ { name: "positive", type: "CONDITIONING", link: 4 },
{ name: "samples", type: "LATENT", link: 7 }, { name: "negative", type: "CONDITIONING", link: 6 },
{ name: "vae", type: "VAE", link: 8 }, { name: "latent_image", type: "LATENT", link: 2 },
], ],
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }], outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {}, properties: {},
}, widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
{ },
id: 9, {
type: "SaveImage", id: 8,
pos: [1451, 189], type: "VAEDecode",
size: { 0: 210, 1: 26 }, pos: [1209, 188],
flags: {}, size: { 0: 210, 1: 46 },
order: 6, flags: {},
mode: 0, order: 5,
inputs: [{ name: "images", type: "IMAGE", link: 9 }], mode: 0,
properties: {}, inputs: [
}, { name: "samples", type: "LATENT", link: 7 },
{ { name: "vae", type: "VAE", link: 8 },
id: 4, ],
type: "CheckpointLoaderSimple", outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
pos: [26, 474], properties: {},
size: { 0: 315, 1: 98 }, },
flags: {}, {
order: 0, id: 9,
mode: 0, type: "SaveImage",
outputs: [ pos: [1451, 189],
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 }, size: { 0: 210, 1: 26 },
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 }, flags: {},
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 }, order: 6,
], mode: 0,
properties: {}, inputs: [{ name: "images", type: "IMAGE", link: 9 }],
widgets_values: ["v1-5-pruned-emaonly.ckpt"], properties: {},
}, },
], {
links: [ id: 4,
[1, 4, 0, 3, 0, "MODEL"], type: "CheckpointLoaderSimple",
[2, 5, 0, 3, 3, "LATENT"], pos: [26, 474],
[3, 4, 1, 6, 0, "CLIP"], size: { 0: 315, 1: 98 },
[4, 6, 0, 3, 1, "CONDITIONING"], flags: {},
[5, 4, 1, 7, 0, "CLIP"], order: 0,
[6, 7, 0, 3, 2, "CONDITIONING"], mode: 0,
[7, 3, 0, 8, 0, "LATENT"], outputs: [
[8, 4, 2, 8, 1, "VAE"], { name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
[9, 8, 0, 9, 0, "IMAGE"], { name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
], { name: "VAE", type: "VAE", links: [8], slot_index: 2 },
groups: [], ],
config: {}, properties: {},
extra: {}, widgets_values: ["v1-5-pruned-emaonly.ckpt"],
version: 0.4, },
],
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 { app, ANIM_PREVIEW_WIDGET } from "./app";
import type { LGraphNode, Vector4 } from "/types/litegraph"; import type { LGraphNode, Vector4 } from "/types/litegraph";
const SIZE = Symbol(); const SIZE = Symbol();
interface Rect { interface Rect {
height: number; height: number;
width: number; width: number;
x: number; x: number;
y: number; y: number;
} }
export interface DOMWidget<T = HTMLElement> { export interface DOMWidget<T = HTMLElement> {
type: string; type: string;
name: string; name: string;
computedHeight?: number; computedHeight?: number;
element?: T; element?: T;
options: any; options: any;
value?: any; value?: any;
y?: number; y?: number;
callback?: (value: any) => void; callback?: (value: any) => void;
draw?: (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) => void; draw?: (
onRemove?: () => void; ctx: CanvasRenderingContext2D,
node: LGraphNode,
widgetWidth: number,
y: number,
widgetHeight: number
) => void;
onRemove?: () => void;
} }
function intersect(a: Rect, b: Rect): Vector4 | null { function intersect(a: Rect, b: Rect): Vector4 | null {
const x = Math.max(a.x, b.x); const x = Math.max(a.x, b.x);
const num1 = Math.min(a.x + a.width, b.x + b.width); const num1 = Math.min(a.x + a.width, b.x + b.width);
const y = Math.max(a.y, b.y); const y = Math.max(a.y, b.y);
const num2 = Math.min(a.y + a.height, b.y + b.height); const num2 = Math.min(a.y + a.height, b.y + b.height);
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y]; if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
else return null; else return null;
} }
function getClipPath(node: LGraphNode, element: HTMLElement): string { function getClipPath(node: LGraphNode, element: HTMLElement): string {
const selectedNode: LGraphNode = Object.values(app.canvas.selected_nodes)[0] as LGraphNode; const selectedNode: LGraphNode = Object.values(
if (selectedNode && selectedNode !== node) { app.canvas.selected_nodes
const elRect = element.getBoundingClientRect(); )[0] as LGraphNode;
const MARGIN = 7; if (selectedNode && selectedNode !== node) {
const scale = app.canvas.ds.scale; const elRect = element.getBoundingClientRect();
const MARGIN = 7;
const scale = app.canvas.ds.scale;
const bounding = selectedNode.getBounding(); const bounding = selectedNode.getBounding();
const intersection = intersect( const intersection = intersect(
{ x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale }, {
{ x: elRect.x / scale,
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN, y: elRect.y / scale,
y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN, width: elRect.width / scale,
width: bounding[2] + MARGIN + MARGIN, height: elRect.height / scale,
height: bounding[3] + MARGIN + MARGIN, },
} {
); 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) { if (!intersection) {
return ""; return "";
} }
const widgetRect = element.getBoundingClientRect(); const widgetRect = element.getBoundingClientRect();
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px"; const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px"; const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
const clipWidth = intersection[2] + "px"; const clipWidth = intersection[2] + "px";
const clipHeight = intersection[3] + "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%)`; 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 path;
} }
return ""; return "";
} }
function computeSize(size: [number, number]): void { 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 y = this.widgets[0].last_y;
let freeSpace = size[1] - y; let freeSpace = size[1] - y;
let widgetHeight = 0; let widgetHeight = 0;
let dom = []; let dom = [];
for (const w of this.widgets) { for (const w of this.widgets) {
if (w.type === "converted-widget") { if (w.type === "converted-widget") {
// Ignore // Ignore
delete w.computedHeight; delete w.computedHeight;
} else if (w.computeSize) { } else if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4; widgetHeight += w.computeSize()[1] + 4;
} else if (w.element) { } else if (w.element) {
// Extract DOM widget size info // Extract DOM widget size info
const styles = getComputedStyle(w.element); const styles = getComputedStyle(w.element);
let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height")); let minHeight =
let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height")); 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"); let prefHeight =
if (prefHeight.endsWith?.("%")) { w.options.getHeight?.() ??
prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100); styles.getPropertyValue("--comfy-widget-height");
} else { if (prefHeight.endsWith?.("%")) {
prefHeight = parseInt(prefHeight); prefHeight =
if (isNaN(minHeight)) { size[1] *
minHeight = prefHeight; (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
} } else {
} prefHeight = parseInt(prefHeight);
if (isNaN(minHeight)) { if (isNaN(minHeight)) {
minHeight = 50; minHeight = prefHeight;
} }
if (!isNaN(maxHeight)) { }
if (!isNaN(prefHeight)) { if (isNaN(minHeight)) {
prefHeight = Math.min(prefHeight, maxHeight); minHeight = 50;
} else { }
prefHeight = maxHeight; if (!isNaN(maxHeight)) {
} if (!isNaN(prefHeight)) {
} prefHeight = Math.min(prefHeight, maxHeight);
dom.push({ } else {
minHeight, prefHeight = maxHeight;
prefHeight, }
w, }
}); dom.push({
} else { minHeight,
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; prefHeight,
} w,
} });
} else {
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
}
}
freeSpace -= widgetHeight; freeSpace -= widgetHeight;
// Calculate sizes with all widgets at their min height // Calculate sizes with all widgets at their min height
const prefGrow = []; // Nodes that want to grow to their prefd size const prefGrow = []; // Nodes that want to grow to their prefd size
const canGrow = []; // Nodes that can grow to auto size const canGrow = []; // Nodes that can grow to auto size
let growBy = 0; let growBy = 0;
for (const d of dom) { for (const d of dom) {
freeSpace -= d.minHeight; freeSpace -= d.minHeight;
if (isNaN(d.prefHeight)) { if (isNaN(d.prefHeight)) {
canGrow.push(d); canGrow.push(d);
d.w.computedHeight = d.minHeight; d.w.computedHeight = d.minHeight;
} else { } else {
const diff = d.prefHeight - d.minHeight; const diff = d.prefHeight - d.minHeight;
if (diff > 0) { if (diff > 0) {
prefGrow.push(d); prefGrow.push(d);
growBy += diff; growBy += diff;
d.diff = diff; d.diff = diff;
} else { } else {
d.w.computedHeight = d.minHeight; d.w.computedHeight = d.minHeight;
} }
} }
} }
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) { if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
// Allocate space for image // Allocate space for image
freeSpace -= 220; freeSpace -= 220;
} }
this.freeWidgetSpace = freeSpace; this.freeWidgetSpace = freeSpace;
if (freeSpace < 0) { if (freeSpace < 0) {
// Not enough space for all widgets so we need to grow // Not enough space for all widgets so we need to grow
size[1] -= freeSpace; size[1] -= freeSpace;
this.graph.setDirtyCanvas(true); this.graph.setDirtyCanvas(true);
} else { } else {
// Share the space between each // Share the space between each
const growDiff = freeSpace - growBy; const growDiff = freeSpace - growBy;
if (growDiff > 0) { if (growDiff > 0) {
// All pref sizes can be fulfilled // All pref sizes can be fulfilled
freeSpace = growDiff; freeSpace = growDiff;
for (const d of prefGrow) { for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight; d.w.computedHeight = d.prefHeight;
} }
} else { } else {
// We need to grow evenly // We need to grow evenly
const shared = -growDiff / prefGrow.length; const shared = -growDiff / prefGrow.length;
for (const d of prefGrow) { for (const d of prefGrow) {
d.w.computedHeight = d.prefHeight - shared; d.w.computedHeight = d.prefHeight - shared;
} }
freeSpace = 0; freeSpace = 0;
} }
if (freeSpace > 0 && canGrow.length) { if (freeSpace > 0 && canGrow.length) {
// Grow any that are auto height // Grow any that are auto height
const shared = freeSpace / canGrow.length; const shared = freeSpace / canGrow.length;
for (const d of canGrow) { for (const d of canGrow) {
d.w.computedHeight += shared; d.w.computedHeight += shared;
} }
} }
} }
// Position each of the widgets // Position each of the widgets
for (const w of this.widgets) { for (const w of this.widgets) {
w.y = y; w.y = y;
if (w.computedHeight) { if (w.computedHeight) {
y += w.computedHeight; y += w.computedHeight;
} else if (w.computeSize) { } else if (w.computeSize) {
y += w.computeSize()[1] + 4; y += w.computeSize()[1] + 4;
} else { } else {
y += LiteGraph.NODE_WIDGET_HEIGHT + 4; 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 // 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; const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
//@ts-ignore //@ts-ignore
LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] { LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
const visibleNodes = computeVisibleNodes.apply(this, arguments); const visibleNodes = computeVisibleNodes.apply(this, arguments);
// @ts-ignore // @ts-ignore
for (const node of app.graph._nodes) { for (const node of app.graph._nodes) {
if (elementWidgets.has(node)) { if (elementWidgets.has(node)) {
const hidden = visibleNodes.indexOf(node) === -1; const hidden = visibleNodes.indexOf(node) === -1;
for (const w of node.widgets) { for (const w of node.widgets) {
// @ts-ignore // @ts-ignore
if (w.element) { if (w.element) {
// @ts-ignore // @ts-ignore
w.element.hidden = hidden; w.element.hidden = hidden;
// @ts-ignore // @ts-ignore
w.element.style.display = hidden ? "none" : undefined; w.element.style.display = hidden ? "none" : undefined;
if (hidden) { if (hidden) {
w.options.onHide?.(w); w.options.onHide?.(w);
} }
} }
} }
} }
} }
return visibleNodes; return visibleNodes;
}; };
let enableDomClipping = true; let enableDomClipping = true;
export function addDomClippingSetting(): void { export function addDomClippingSetting(): void {
app.ui.settings.addSetting({ app.ui.settings.addSetting({
id: "Comfy.DOMClippingEnabled", id: "Comfy.DOMClippingEnabled",
name: "Enable DOM element clipping (enabling may reduce performance)", name: "Enable DOM element clipping (enabling may reduce performance)",
type: "boolean", type: "boolean",
defaultValue: enableDomClipping, defaultValue: enableDomClipping,
onChange(value) { onChange(value) {
enableDomClipping = !!value; enableDomClipping = !!value;
}, },
}); });
} }
//@ts-ignore //@ts-ignore
LGraphNode.prototype.addDOMWidget = function ( LGraphNode.prototype.addDOMWidget = function (
name: string, name: string,
type: string, type: string,
element: HTMLElement, element: HTMLElement,
options: Record<string, any> options: Record<string, any>
): DOMWidget { ): DOMWidget {
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options }; options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
if (!element.parentElement) { if (!element.parentElement) {
document.body.append(element); document.body.append(element);
} }
element.hidden = true; element.hidden = true;
element.style.display = "none"; element.style.display = "none";
let mouseDownHandler; let mouseDownHandler;
if (element.blur) { if (element.blur) {
mouseDownHandler = (event) => { mouseDownHandler = (event) => {
if (!element.contains(event.target)) { if (!element.contains(event.target)) {
element.blur(); element.blur();
} }
}; };
document.addEventListener("mousedown", mouseDownHandler); document.addEventListener("mousedown", mouseDownHandler);
} }
const widget: DOMWidget = { const widget: DOMWidget = {
type, type,
name, name,
get value() { get value() {
return options.getValue?.() ?? undefined; return options.getValue?.() ?? undefined;
}, },
set value(v) { set value(v) {
options.setValue?.(v); options.setValue?.(v);
widget.callback?.(widget.value); widget.callback?.(widget.value);
}, },
draw: function (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) { draw: function (
if (widget.computedHeight == null) { ctx: CanvasRenderingContext2D,
computeSize.call(node, node.size); node: LGraphNode,
} widgetWidth: number,
y: number,
widgetHeight: number
) {
if (widget.computedHeight == null) {
computeSize.call(node, node.size);
}
const hidden = const hidden =
node.flags?.collapsed || node.flags?.collapsed ||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) || (!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
widget.computedHeight <= 0 || widget.computedHeight <= 0 ||
widget.type === "converted-widget" || widget.type === "converted-widget" ||
widget.type === "hidden"; widget.type === "hidden";
element.hidden = hidden; element.hidden = hidden;
element.style.display = hidden ? "none" : null; element.style.display = hidden ? "none" : null;
if (hidden) { if (hidden) {
widget.options.onHide?.(widget); widget.options.onHide?.(widget);
return; return;
} }
const margin = 10; const margin = 10;
const elRect = ctx.canvas.getBoundingClientRect(); const elRect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix() const transform = new DOMMatrix()
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height) .scaleSelf(
.multiplySelf(ctx.getTransform()) elRect.width / ctx.canvas.width,
.translateSelf(margin, margin + y); 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, { Object.assign(element.style, {
transformOrigin: "0 0", transformOrigin: "0 0",
transform: scale, transform: scale,
left: `${transform.a + transform.e + elRect.left}px`, left: `${transform.a + transform.e + elRect.left}px`,
top: `${transform.d + transform.f + elRect.top}px`, top: `${transform.d + transform.f + elRect.top}px`,
width: `${widgetWidth - margin * 2}px`, width: `${widgetWidth - margin * 2}px`,
height: `${(widget.computedHeight ?? 50) - margin * 2}px`, height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
position: "absolute", position: "absolute",
// @ts-ignore // @ts-ignore
zIndex: app.graph._nodes.indexOf(node), zIndex: app.graph._nodes.indexOf(node),
}); });
if (enableDomClipping) { if (enableDomClipping) {
element.style.clipPath = getClipPath(node, element); element.style.clipPath = getClipPath(node, element);
element.style.willChange = "clip-path"; element.style.willChange = "clip-path";
} }
this.options.onDraw?.(widget); this.options.onDraw?.(widget);
}, },
element, element,
options, options,
onRemove() { onRemove() {
if (mouseDownHandler) { if (mouseDownHandler) {
document.removeEventListener("mousedown", mouseDownHandler); document.removeEventListener("mousedown", mouseDownHandler);
} }
element.remove(); element.remove();
}, },
}; };
for (const evt of options.selectOn) { for (const evt of options.selectOn) {
element.addEventListener(evt, () => { element.addEventListener(evt, () => {
app.canvas.selectNode(this); app.canvas.selectNode(this);
app.canvas.bringToFront(this); app.canvas.bringToFront(this);
}); });
} }
this.addCustomWidget(widget); this.addCustomWidget(widget);
elementWidgets.add(this); elementWidgets.add(this);
const collapse = this.collapse; const collapse = this.collapse;
this.collapse = function () { this.collapse = function () {
collapse.apply(this, arguments); collapse.apply(this, arguments);
if (this.flags?.collapsed) { if (this.flags?.collapsed) {
element.hidden = true; element.hidden = true;
element.style.display = "none"; element.style.display = "none";
} }
} };
const onRemoved = this.onRemoved; const onRemoved = this.onRemoved;
this.onRemoved = function () { this.onRemoved = function () {
element.remove(); element.remove();
elementWidgets.delete(this); elementWidgets.delete(this);
onRemoved?.apply(this, arguments); onRemoved?.apply(this, arguments);
}; };
if (!this[SIZE]) { if (!this[SIZE]) {
this[SIZE] = true; this[SIZE] = true;
const onResize = this.onResize; const onResize = this.onResize;
this.onResize = function (size) { this.onResize = function (size) {
options.beforeResize?.call(widget, this); options.beforeResize?.call(widget, this);
computeSize.call(this, size); computeSize.call(this, size);
onResize?.apply(this, arguments); onResize?.apply(this, arguments);
options.afterResize?.call(widget, this); options.afterResize?.call(widget, this);
}; };
} }
return widget; return widget;
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,8 +27,8 @@
import { $el } from "../ui"; import { $el } from "../ui";
$el("style", { $el("style", {
parent: document.head, parent: document.head,
textContent: ` textContent: `
.draggable-item { .draggable-item {
position: relative; position: relative;
will-change: transform; will-change: transform;
@@ -40,7 +40,7 @@ $el("style", {
.draggable-item.is-draggable { .draggable-item.is-draggable {
z-index: 10; z-index: 10;
} }
` `,
}); });
export class DraggableList extends EventTarget { export class DraggableList extends EventTarget {
@@ -57,9 +57,9 @@ export class DraggableList extends EventTarget {
offDrag = []; offDrag = [];
constructor(element, itemSelector) { constructor(element, itemSelector) {
super(); super();
this.listContainer = element; this.listContainer = element;
this.itemSelector = itemSelector; this.itemSelector = itemSelector;
if (!this.listContainer) return; if (!this.listContainer) return;
@@ -71,7 +71,9 @@ export class DraggableList extends EventTarget {
getAllItems() { getAllItems() {
if (!this.items?.length) { 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) => { this.items.forEach((element) => {
element.classList.add("is-idle"); element.classList.add("is-idle");
}); });
@@ -80,7 +82,9 @@ export class DraggableList extends EventTarget {
} }
getIdleItems() { getIdleItems() {
return this.getAllItems().filter((item) => item.classList.contains("is-idle")); return this.getAllItems().filter((item) =>
item.classList.contains("is-idle")
);
} }
isItemAbove(item) { isItemAbove(item) {
@@ -106,18 +110,24 @@ export class DraggableList extends EventTarget {
this.pointerStartX = e.clientX || e.touches[0].clientX; this.pointerStartX = e.clientX || e.touches[0].clientX;
this.pointerStartY = e.clientY || e.touches[0].clientY; 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.setItemsGap();
this.initDraggableItem(); this.initDraggableItem();
this.initItemsState(); this.initItemsState();
this.offDrag.push(this.on(document, "mousemove", this.drag)); 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( this.dispatchEvent(
new CustomEvent("dragstart", { 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( this.dispatchEvent(
new CustomEvent("dragend", { 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]; 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"; w += "px";
h += "px"; h += "px";
@@ -86,10 +90,13 @@ export function createImageHost(node) {
onDraw() { onDraw() {
// Element from point uses a hittest find elements so we need to toggle pointer events // Element from point uses a hittest find elements so we need to toggle pointer events
el.style.pointerEvents = "all"; 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"; el.style.pointerEvents = "none";
if(!over) return; if (!over) return;
// Set the overIndex so Open Image etc work // Set the overIndex so Open Image etc work
const idx = currentImgs.indexOf(over); const idx = currentImgs.indexOf(over);
node.overIndex = idx; node.overIndex = idx;

View File

@@ -48,7 +48,11 @@ export class ComfyAppMenu {
content: t, 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( this.saveButton = new ComfySplitButton(
{ {
primary: getSaveButton(), primary: getSaveButton(),
@@ -71,7 +75,8 @@ export class ComfyAppMenu {
new ComfyButton({ new ComfyButton({
icon: "api", icon: "api",
content: "Export (API Format)", 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"), action: () => this.exportWorkflow("workflow_api", "output"),
visibilitySetting: { id: "Comfy.DevMode", showValue: true }, visibilitySetting: { id: "Comfy.DevMode", showValue: true },
app, app,
@@ -101,7 +106,10 @@ export class ComfyAppMenu {
content: "Clear", content: "Clear",
tooltip: "Clears current workflow", tooltip: "Clears current workflow",
action: () => { 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.clean();
app.graph.clear(); app.graph.clear();
} }
@@ -126,7 +134,9 @@ export class ComfyAppMenu {
this.mobileMenuButton = new ComfyButton({ this.mobileMenuButton = new ComfyButton({
icon: "menu", icon: "menu",
action: (_, btn) => { 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")); window.dispatchEvent(new Event("resize"));
}, },
classList: "comfyui-button comfyui-menu-button", classList: "comfyui-button comfyui-menu-button",
@@ -239,7 +249,10 @@ export class ComfyAppMenu {
idx--; idx--;
} }
} else if (innerSize > this.element.clientWidth) { } 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 // We need to shrink
if (idx < this.#sizeBreaks.length - 1) { if (idx < this.#sizeBreaks.length - 1) {
idx++; idx++;
@@ -254,19 +267,26 @@ export class ComfyAppMenu {
clearTimeout(this.#cacheTimeout); clearTimeout(this.#cacheTimeout);
if (this.#cachedInnerSize) { if (this.#cachedInnerSize) {
// Extend cache time // Extend cache time
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); this.#cacheTimeout = setTimeout(
() => (this.#cachedInnerSize = null),
100
);
} else { } else {
let innerSize = 0; let innerSize = 0;
let count = 1; let count = 1;
for (const c of this.element.children) { for (const c of this.element.children) {
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push 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; innerSize += c.clientWidth;
count++; count++;
} }
innerSize += 8 * count; innerSize += 8 * count;
this.#cachedInnerSize = innerSize; this.#cachedInnerSize = innerSize;
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100); this.#cacheTimeout = setTimeout(
() => (this.#cachedInnerSize = null),
100
);
} }
return this.#cachedInnerSize; return this.#cachedInnerSize;
} }

View File

@@ -14,7 +14,10 @@ export class ComfyQueueButton {
queuePrompt = async (e) => { queuePrompt = async (e) => {
this.#internalQueueSize += this.queueOptions.batchCount; this.#internalQueueSize += this.queueOptions.batchCount;
// Hold shift to queue front, event is undefined when auto-queue is enabled // 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) { 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", () => { api.addEventListener("graphChanged", () => {
if (this.autoQueueMode === "change") { if (this.autoQueueMode === "change") {
@@ -79,10 +85,14 @@ export class ComfyQueueButton {
api.addEventListener("status", ({ detail }) => { api.addEventListener("status", ({ detail }) => {
this.#internalQueueSize = detail?.exec_info?.queue_remaining; this.#internalQueueSize = detail?.exec_info?.queue_remaining;
if (this.#internalQueueSize != null) { 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`; this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
if (!this.#internalQueueSize && !app.lastExecutionError) { 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.graphHasChanged = false;
this.queuePrompt(); this.queuePrompt();
} }

View File

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

View File

@@ -31,7 +31,9 @@ export class ComfyViewQueueList extends ComfyViewList {
text: "Load", text: "Load",
action: async () => { action: async () => {
try { try {
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); await this.app.loadGraphData(
item.prompt[3].extra_pnginfo.workflow
);
if (item.outputs) { if (item.outputs) {
this.app.nodeOutputs = 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.buttonProgress = $el("div.comfyui-workflows-button-progress");
this.workflowLabel = $el("span.comfyui-workflows-label", ""); this.workflowLabel = $el("span.comfyui-workflows-label", "");
this.button = new ComfyButton({ 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", icon: "chevron-down",
classList, classList,
}); });
this.element.append(this.button.element); 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.content = new ComfyWorkflowsContent(app, this.popup);
this.popup.children = [this.content.element]; this.popup.children = [this.content.element];
this.popup.addEventListener("change", () => { this.popup.addEventListener("change", () => {
@@ -85,7 +92,10 @@ export class ComfyWorkflowsMenu {
}; };
#bindEvents() { #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("rename", this.#updateActive);
this.app.workflowManager.addEventListener("delete", this.#updateActive); this.app.workflowManager.addEventListener("delete", this.#updateActive);
@@ -157,10 +167,15 @@ export class ComfyWorkflowsMenu {
name: "Comfy.Workflows", name: "Comfy.Workflows",
async beforeRegisterNodeDef(nodeType) { async beforeRegisterNodeDef(nodeType) {
function getImageWidget(node) { 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) { for (const input in inputs) {
if (inputs[input][0] === "IMAGEUPLOAD") { 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; if (imageWidget) return imageWidget;
} }
} }
@@ -213,8 +228,13 @@ export class ComfyWorkflowsMenu {
const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"]; const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"];
nodeType.prototype["getExtraMenuOptions"] = function (_, options) { nodeType.prototype["getExtraMenuOptions"] = function (_, options) {
const r = getExtraMenuOptions?.apply?.(this, arguments); const r = getExtraMenuOptions?.apply?.(this, arguments);
if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) { if (
const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this); app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true
) {
const t =
/** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (
this
);
let img; let img;
if (t.imageIndex != null) { if (t.imageIndex != null) {
// An image is selected so select that // An image is selected so select that
@@ -238,10 +258,13 @@ export class ComfyWorkflowsMenu {
submenu: { submenu: {
options: [ options: [
{ {
callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow), callback: () =>
sendToWorkflow(img, app.workflowManager.activeWorkflow),
title: "[Current workflow]", title: "[Current workflow]",
}, },
...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)), ...self.#getFavoriteMenuOptions(
sendToWorkflow.bind(null, img)
),
null, null,
...self.#getMenuOptions(sendToWorkflow.bind(null, img)), ...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
], ],
@@ -315,7 +338,9 @@ export class ComfyWorkflowsContent {
this.element.replaceChildren(this.actions, this.spinner); this.element.replaceChildren(this.actions, this.spinner);
this.popup.addEventListener("open", () => this.load()); 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) => { this.app.workflowManager.addEventListener("favorite", (e) => {
const workflow = e["detail"]; const workflow = e["detail"];
@@ -331,7 +356,9 @@ export class ComfyWorkflowsContent {
app.workflowManager.addEventListener(e, () => this.updateOpen()); app.workflowManager.addEventListener(e, () => this.updateOpen());
} }
this.app.workflowManager.addEventListener("rename", () => this.load()); 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() { async load() {
@@ -339,7 +366,12 @@ export class ComfyWorkflowsContent {
this.updateTree(); this.updateTree();
this.updateFavorites(); this.updateFavorites();
this.updateOpen(); 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() { updateOpen() {
@@ -368,7 +400,7 @@ export class ComfyWorkflowsContent {
if (w.unsaved) { if (w.unsaved) {
wrapper.element.classList.add("unsaved"); wrapper.element.classList.add("unsaved");
} }
if(w === this.app.workflowManager.activeWorkflow) { if (w === this.app.workflowManager.activeWorkflow) {
wrapper.element.classList.add("active"); wrapper.element.classList.add("active");
} }
@@ -383,7 +415,9 @@ export class ComfyWorkflowsContent {
updateFavorites() { updateFavorites() {
const current = this.favoritesElement; 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", [ this.favoritesElement = $el("div.comfyui-workflows-favorites", [
$el("h3", "Favorites"), $el("h3", "Favorites"),
@@ -437,7 +471,10 @@ export class ComfyWorkflowsContent {
hideTreeParents(element) { hideTreeParents(element) {
// Hide all parents if no children are visible // 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++) { for (let i = 1; i < element.parentElement.children.length; i++) {
const c = element.parentElement.children[i]; const c = element.parentElement.children[i];
if (c.style.display !== "none") { if (c.style.display !== "none") {
@@ -450,7 +487,10 @@ export class ComfyWorkflowsContent {
} }
showTreeParents(element) { 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"); element.parentElement.style.removeProperty("display");
this.showTreeParents(element.parentElement); this.showTreeParents(element.parentElement);
} }
@@ -490,7 +530,9 @@ export class ComfyWorkflowsContent {
for (let i = 0; i < workflow.pathParts.length; i++) { for (let i = 0; i < workflow.pathParts.length; i++) {
currentPath += (currentPath ? "\\" : "") + workflow.pathParts[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; nodes[currentPath] = parentNode;
currentRoot = parentNode; currentRoot = parentNode;
@@ -559,7 +601,9 @@ export class ComfyWorkflowsContent {
/** @param {ComfyWorkflow} workflow */ /** @param {ComfyWorkflow} workflow */
#getFavoriteTooltip(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 */ /** @param {ComfyWorkflow} workflow */
@@ -568,7 +612,9 @@ export class ComfyWorkflowsContent {
icon: this.#getFavoriteIcon(workflow), icon: this.#getFavoriteIcon(workflow),
overIcon: this.#getFavoriteOverIcon(workflow), overIcon: this.#getFavoriteOverIcon(workflow),
iconSize: 18, 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), tooltip: this.#getFavoriteTooltip(workflow),
action: (e) => { action: (e) => {
e.stopImmediatePropagation(); e.stopImmediatePropagation();
@@ -628,7 +674,9 @@ export class ComfyWorkflowsContent {
#getRenameButton(workflow) { #getRenameButton(workflow) {
return new ComfyButton({ return new ComfyButton({
icon: "pencil", 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", classList: "comfyui-button comfyui-workflows-file-action",
iconSize: 18, iconSize: 18,
enabled: !!workflow.path, enabled: !!workflow.path,
@@ -646,7 +694,11 @@ export class ComfyWorkflowsContent {
#getWorkflowElement(workflow) { #getWorkflowElement(workflow) {
return new WorkflowElement(this, workflow, { return new WorkflowElement(this, workflow, {
primary: this.#getFavoriteButton(workflow, true), 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) { #createNode(currentPath, workflow, i, currentRoot) {
const part = workflow.pathParts[i]; const part = workflow.pathParts[i];
const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), { const parentNode = $el(
$: (el) => { "ul" + (this.treeState[currentPath] ? "" : ".closed"),
el.onclick = (e) => { {
this.#expandNode(el, workflow, currentPath, i); $: (el) => {
e.stopImmediatePropagation(); el.onclick = (e) => {
}; this.#expandNode(el, workflow, currentPath, i);
}, e.stopImmediatePropagation();
}); };
},
}
);
currentRoot.append(parentNode); currentRoot.append(parentNode);
// Create a node for the current part and an inner UL for its children if it isnt a leaf node // 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) { if (leaf) {
nodeElement = this.#createLeafNode(workflow).element; nodeElement = this.#createLeafNode(workflow).element;
} else { } 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); parentNode.append(nodeElement);
return parentNode; return parentNode;
@@ -703,7 +761,11 @@ class WorkflowElement {
}, },
title: this.workflow.path, 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", "section",
this.#options.map((opt) => { this.#options.map((opt) => {
return $el("div.comfy-widget-selection-item", [ 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( $el(
"button.comfyui-button", "button.comfyui-button",
{ {
@@ -760,4 +826,4 @@ class WidgetSelectionDialog extends ComfyAsyncDialog {
]) ])
); );
} }
} }

View File

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

View File

@@ -1,6 +1,5 @@
import "./spinner.css"; import "./spinner.css";
export function createSpinner() { export function createSpinner() {
const div = document.createElement("div"); const div = document.createElement("div");
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></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) { if (selectedIndex != null) {
elements[selectedIndex].classList.remove("comfy-toggle-selected"); 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; selectedIndex = index;
elements[selectedIndex].classList.add("comfy-toggle-selected"); elements[selectedIndex].classList.add("comfy-toggle-selected");
} }

View File

@@ -3,16 +3,14 @@ import { $el } from "../ui";
import { createSpinner } from "./spinner"; import { createSpinner } from "./spinner";
import "./userSelection.css"; import "./userSelection.css";
interface SelectedUser { interface SelectedUser {
username: string; username: string;
userId: string; userId: string;
created: boolean; created: boolean;
} }
export class UserSelectionScreen { export class UserSelectionScreen {
async show(users, user): Promise<SelectedUser>{ async show(users, user): Promise<SelectedUser> {
const userSelection = document.getElementById("comfy-user-selection"); const userSelection = document.getElementById("comfy-user-selection");
userSelection.style.display = ""; userSelection.style.display = "";
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -22,7 +20,9 @@ export class UserSelectionScreen {
const selectSection = select.closest("section"); const selectSection = select.closest("section");
const form = userSelection.getElementsByTagName("form")[0]; const form = userSelection.getElementsByTagName("form")[0];
const error = userSelection.getElementsByClassName("comfy-user-error")[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; let inputActive = null;
input.addEventListener("focus", () => { input.addEventListener("focus", () => {
@@ -45,7 +45,8 @@ export class UserSelectionScreen {
form.addEventListener("submit", async (e) => { form.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
if (inputActive == null) { 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) { } else if (inputActive) {
const username = input.value.trim(); const username = input.value.trim();
if (!username) { if (!username) {
@@ -54,41 +55,59 @@ export class UserSelectionScreen {
} }
// Create new user // Create new user
// @ts-ignore
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339) // Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551) // 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(); const spinner = createSpinner();
button.prepend(spinner); button.prepend(spinner);
try { try {
const resp = await api.createUser(username); const resp = await api.createUser(username);
if (resp.status >= 300) { if (resp.status >= 300) {
let message = "Error creating user: " + resp.status + " " + resp.statusText; let message =
"Error creating user: " + resp.status + " " + resp.statusText;
try { try {
const res = await resp.json(); const res = await resp.json();
if(res.error) { if (res.error) {
message = res.error; message = res.error;
} }
} catch (error) { } catch (error) {}
}
throw new Error(message); throw new Error(message);
} }
resolve({ username, userId: await resp.json(), created: true }); resolve({ username, userId: await resp.json(), created: true });
} catch (err) { } catch (err) {
spinner.remove(); spinner.remove();
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred."; error.textContent =
// @ts-ignore 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 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551) // 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; return;
} }
} else if (!select.value) { } else if (!select.value) {
error.textContent = "Please select an existing user."; error.textContent = "Please select an existing user.";
return; return;
} else { } 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 // Find node with matching S&R property name
// @ts-ignore // @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 we cant, see if there is a node with that title
if (!nodes.length) { if (!nodes.length) {
// @ts-ignore // @ts-ignore
@@ -76,7 +78,13 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
const widget = node.widgets?.find((w) => w.name === split[1]); const widget = node.widgets?.find((w) => w.name === split[1]);
if (!widget) { 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; 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) => { return new Promise((res, rej) => {
let url; let url;
if (urlOrFile.endsWith(".js")) { if (urlOrFile.endsWith(".js")) {
url = urlOrFile.substr(0, urlOrFile.length - 2) + "css"; url = urlOrFile.substr(0, urlOrFile.length - 2) + "css";
} else { } 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", { $el("link", {
parent: document.head, parent: document.head,
@@ -103,7 +117,6 @@ export async function addStylesheet(urlOrFile: string, relativeTo?: string): Pro
}); });
} }
/** /**
* @param { string } filename * @param { string } filename
* @param { Blob } blob * @param { Blob } blob
@@ -147,7 +160,10 @@ export function prop(target, name, defaultValue, onChanged) {
export function getStorageValue(id) { export function getStorageValue(id) {
const clientId = api.clientId ?? api.initialClientId; 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) { export function setStorageValue(id, value) {
@@ -156,4 +172,4 @@ export function setStorageValue(id, value) {
sessionStorage.setItem(`${id}:${clientId}`, value); sessionStorage.setItem(`${id}:${clientId}`, value);
} }
localStorage.setItem(id, value); localStorage.setItem(id, value);
} }

View File

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

View File

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

View File

@@ -7,107 +7,113 @@ const zQueueIndex = z.number();
const zPromptId = z.string(); const zPromptId = z.string();
const zPromptItem = z.object({ const zPromptItem = z.object({
inputs: z.record(z.string(), z.any()), inputs: z.record(z.string(), z.any()),
class_type: zNodeType, class_type: zNodeType,
}); });
const zPrompt = z.array(zPromptItem); const zPrompt = z.array(zPromptItem);
const zExtraPngInfo = z.object({ const zExtraPngInfo = z
.object({
workflow: zComfyWorkflow, workflow: zComfyWorkflow,
}).passthrough(); })
.passthrough();
const zExtraData = z.object({ const zExtraData = z.object({
extra_pnginfo: zExtraPngInfo, extra_pnginfo: zExtraPngInfo,
client_id: z.string(), client_id: z.string(),
}); });
const zOutputsToExecute = z.array(zNodeId); const zOutputsToExecute = z.array(zNodeId);
const zExecutionStartMessage = z.tuple([ const zExecutionStartMessage = z.tuple([
z.literal("execution_start"), z.literal("execution_start"),
z.object({ z.object({
prompt_id: zPromptId, prompt_id: zPromptId,
}), }),
]); ]);
const zExecutionCachedMessage = z.tuple([ const zExecutionCachedMessage = z.tuple([
z.literal("execution_cached"), z.literal("execution_cached"),
z.object({ z.object({
prompt_id: zPromptId, prompt_id: zPromptId,
nodes: z.array(zNodeId), nodes: z.array(zNodeId),
}), }),
]); ]);
const zExecutionInterruptedMessage = z.tuple([ const zExecutionInterruptedMessage = z.tuple([
z.literal("execution_interrupted"), z.literal("execution_interrupted"),
z.object({ z.object({
// InterruptProcessingException // InterruptProcessingException
prompt_id: zPromptId, prompt_id: zPromptId,
node_id: zNodeId, node_id: zNodeId,
node_type: zNodeType, node_type: zNodeType,
executed: z.array(zNodeId), executed: z.array(zNodeId),
}), }),
]); ]);
const zExecutionErrorMessage = z.tuple([ const zExecutionErrorMessage = z.tuple([
z.literal("execution_error"), z.literal("execution_error"),
z.object({ z.object({
prompt_id: zPromptId, prompt_id: zPromptId,
node_id: zNodeId, node_id: zNodeId,
node_type: zNodeType, node_type: zNodeType,
executed: z.array(zNodeId), executed: z.array(zNodeId),
exception_message: z.string(), exception_message: z.string(),
exception_type: z.string(), exception_type: z.string(),
traceback: z.string(), traceback: z.string(),
current_inputs: z.any(), current_inputs: z.any(),
current_outputs: z.any(), current_outputs: z.any(),
}), }),
]); ]);
const zStatusMessage = z.union([ const zStatusMessage = z.union([
zExecutionStartMessage, zExecutionStartMessage,
zExecutionCachedMessage, zExecutionCachedMessage,
zExecutionInterruptedMessage, zExecutionInterruptedMessage,
zExecutionErrorMessage, zExecutionErrorMessage,
]); ]);
const zStatus = z.object({ const zStatus = z.object({
status_str: z.enum(["success", "error"]), status_str: z.enum(["success", "error"]),
completed: z.boolean(), completed: z.boolean(),
messages: z.array(zStatusMessage), messages: z.array(zStatusMessage),
}); });
// TODO: this is a placeholder // TODO: this is a placeholder
const zOutput = z.any(); const zOutput = z.any();
const zTaskPrompt = z.tuple([ const zTaskPrompt = z.tuple([
zQueueIndex, zQueueIndex,
zPromptId, zPromptId,
zPrompt, zPrompt,
zExtraData, zExtraData,
zOutputsToExecute, zOutputsToExecute,
]); ]);
const zRunningTaskItem = z.object({ const zRunningTaskItem = z.object({
prompt: zTaskPrompt, prompt: zTaskPrompt,
remove: z.object({ remove: z.object({
name: z.literal("Cancel"), name: z.literal("Cancel"),
cb: z.function(), cb: z.function(),
}), }),
}); });
const zPendingTaskItem = z.object({ const zPendingTaskItem = z.object({
prompt: zTaskPrompt, prompt: zTaskPrompt,
}); });
const zHistoryTaskItem = z.object({ const zHistoryTaskItem = z.object({
prompt: zTaskPrompt, prompt: zTaskPrompt,
status: zStatus.optional(), status: zStatus.optional(),
outputs: z.record(zNodeId, zOutput), outputs: z.record(zNodeId, zOutput),
}); });
const zTaskItem = z.union([zRunningTaskItem, zPendingTaskItem, zHistoryTaskItem]); const zTaskItem = z.union([
zRunningTaskItem,
zPendingTaskItem,
zHistoryTaskItem,
]);
// `/queue` // `/queue`
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>; 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. // TODO: validate `/history` `/queue` API endpoint responses.
function inputSpec(spec: [ZodType, ZodType]): ZodType { function inputSpec(spec: [ZodType, ZodType]): ZodType {
const [inputType, inputSpec] = spec; const [inputType, inputSpec] = spec;
return z.union([ return z.union([z.tuple([inputType, inputSpec]), z.tuple([inputType])]);
z.tuple([inputType, inputSpec]),
z.tuple([inputType]),
]);
} }
const zIntInputSpec = inputSpec([ const zIntInputSpec = inputSpec([
z.literal("INT"), z.literal("INT"),
z.object({ z.object({
min: z.number().optional(), min: z.number().optional(),
max: z.number().optional(), max: z.number().optional(),
step: z.number().optional(), step: z.number().optional(),
default: z.number().optional(), default: z.number().optional(),
forceInput: z.boolean().optional(), forceInput: z.boolean().optional(),
}), }),
]); ]);
const zFloatInputSpec = inputSpec([ const zFloatInputSpec = inputSpec([
z.literal("FLOAT"), z.literal("FLOAT"),
z.object({ z.object({
min: z.number().optional(), min: z.number().optional(),
max: z.number().optional(), max: z.number().optional(),
step: z.number().optional(), step: z.number().optional(),
round: z.number().optional(), round: z.number().optional(),
default: z.number().optional(), default: z.number().optional(),
forceInput: z.boolean().optional(), forceInput: z.boolean().optional(),
}), }),
]); ]);
const zBooleanInputSpec = inputSpec([ const zBooleanInputSpec = inputSpec([
z.literal("BOOLEAN"), z.literal("BOOLEAN"),
z.object({ z.object({
label_on: z.string().optional(), label_on: z.string().optional(),
label_off: z.string().optional(), label_off: z.string().optional(),
default: z.boolean().optional(), default: z.boolean().optional(),
forceInput: z.boolean().optional(), forceInput: z.boolean().optional(),
}) }),
]); ]);
const zStringInputSpec = inputSpec([ const zStringInputSpec = inputSpec([
z.literal("STRING"), z.literal("STRING"),
z.object({ z.object({
default: z.string().optional(), default: z.string().optional(),
multiline: z.boolean().optional(), multiline: z.boolean().optional(),
dynamicPrompts: z.boolean().optional(), dynamicPrompts: z.boolean().optional(),
forceInput: z.boolean().optional(), forceInput: z.boolean().optional(),
}), }),
]); ]);
// Dropdown Selection. // Dropdown Selection.
const zComboInputSpec = inputSpec([ const zComboInputSpec = inputSpec([
z.array(z.any()), z.array(z.any()),
z.object({ z.object({
default: z.any().optional(), default: z.any().optional(),
control_after_generate: z.boolean().optional(), control_after_generate: z.boolean().optional(),
image_upload: z.boolean().optional(), image_upload: z.boolean().optional(),
forceInput: z.boolean().optional(), forceInput: z.boolean().optional(),
}), }),
]); ]);
const zCustomInputSpec = inputSpec([ const zCustomInputSpec = inputSpec([
z.string(), z.string(),
z.object({ z.object({
default: z.any().optional(), default: z.any().optional(),
forceInput: z.boolean().optional(), forceInput: z.boolean().optional(),
}), }),
]); ]);
const zInputSpec = z.union([ const zInputSpec = z.union([
zIntInputSpec, zIntInputSpec,
zFloatInputSpec, zFloatInputSpec,
zBooleanInputSpec, zBooleanInputSpec,
zStringInputSpec, zStringInputSpec,
zComboInputSpec, zComboInputSpec,
zCustomInputSpec, zCustomInputSpec,
]); ]);
const zComfyNodeDataType = z.string(); const zComfyNodeDataType = z.string();
const zComfyComboOutput = z.array(z.any()); 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({ const zComfyNodeDef = z.object({
input: z.object({ input: z.object({
required: z.record(zInputSpec).optional(), required: z.record(zInputSpec).optional(),
optional: z.record(zInputSpec).optional(), optional: z.record(zInputSpec).optional(),
}), }),
output: zComfyOutputSpec, output: zComfyOutputSpec,
output_is_list: z.array(z.boolean()), output_is_list: z.array(z.boolean()),
output_name: z.array(z.string()), output_name: z.array(z.string()),
name: z.string(), name: z.string(),
display_name: z.string(), display_name: z.string(),
description: z.string(), description: z.string(),
category: z.string(), category: z.string(),
output_node: z.boolean(), output_node: z.boolean(),
}); });
// `/object_info` // `/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(), BOOLEAN: z.string().optional(),
CLIP: z.string(), CLIP: z.string(),
CLIP_VISION: z.string(), CLIP_VISION: z.string(),
@@ -25,10 +26,12 @@ const nodeSlotSchema = z.object({
TAESD: z.string(), TAESD: z.string(),
TIMESTEP_KEYFRAME: z.string().optional(), TIMESTEP_KEYFRAME: z.string().optional(),
UPSCALE_MODEL: z.string().optional(), UPSCALE_MODEL: z.string().optional(),
VAE: z.string() VAE: z.string(),
}).passthrough(); })
.passthrough();
const litegraphBaseSchema = z.object({ const litegraphBaseSchema = z
.object({
BACKGROUND_IMAGE: z.string(), BACKGROUND_IMAGE: z.string(),
CLEAR_BACKGROUND_COLOR: z.string(), CLEAR_BACKGROUND_COLOR: z.string(),
NODE_TITLE_COLOR: z.string(), NODE_TITLE_COLOR: z.string(),
@@ -49,37 +52,40 @@ const litegraphBaseSchema = z.object({
WIDGET_SECONDARY_TEXT_COLOR: z.string(), WIDGET_SECONDARY_TEXT_COLOR: z.string(),
LINK_COLOR: z.string(), LINK_COLOR: z.string(),
EVENT_LINK_COLOR: z.string(), EVENT_LINK_COLOR: z.string(),
CONNECTING_LINK_COLOR: z.string() CONNECTING_LINK_COLOR: z.string(),
}).passthrough(); })
.passthrough();
const comfyBaseSchema = z.object({ const comfyBaseSchema = z.object({
["fg-color"]: z.string(), ["fg-color"]: z.string(),
["bg-color"]: z.string(), ["bg-color"]: z.string(),
["comfy-menu-bg"]: z.string(), ["comfy-menu-bg"]: z.string(),
["comfy-input-bg"]: z.string(), ["comfy-input-bg"]: z.string(),
["input-text"]: z.string(), ["input-text"]: z.string(),
["descrip-text"]: z.string(), ["descrip-text"]: z.string(),
["drag-text"]: z.string(), ["drag-text"]: z.string(),
["error-text"]: z.string(), ["error-text"]: z.string(),
["border-color"]: z.string(), ["border-color"]: z.string(),
["tr-even-bg-color"]: z.string(), ["tr-even-bg-color"]: z.string(),
["tr-odd-bg-color"]: z.string(), ["tr-odd-bg-color"]: z.string(),
["content-bg"]: z.string(), ["content-bg"]: z.string(),
["content-fg"]: z.string(), ["content-fg"]: z.string(),
["content-hover-bg"]: z.string(), ["content-hover-bg"]: z.string(),
["content-hover-fg"]: z.string(), ["content-hover-fg"]: z.string(),
}); });
const colorsSchema = z.object({ const colorsSchema = z
.object({
node_slot: nodeSlotSchema, node_slot: nodeSlotSchema,
litegraph_base: litegraphBaseSchema, litegraph_base: litegraphBaseSchema,
comfy_base: comfyBaseSchema comfy_base: comfyBaseSchema,
}).passthrough(); })
.passthrough();
const paletteSchema = z.object({ const paletteSchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
colors: colorsSchema colors: colorsSchema,
}); });
const colorPalettesSchema = z.record(paletteSchema); 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"; import { ComfyApp } from "../../scripts/app";
export interface ComfyExtension { export interface ComfyExtension {
/** /**
* The name of the extension * The name of the extension
*/ */
name: string; name: string;
/** /**
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added * Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
* @param app The ComfyUI app instance * @param app The ComfyUI app instance
*/ */
init?(app: ComfyApp): Promise<void>; init?(app: ComfyApp): Promise<void>;
/** /**
* Allows any additonal setup, called after the application is fully set up and running * Allows any additonal setup, called after the application is fully set up and running
* @param app The ComfyUI app instance * @param app The ComfyUI app instance
*/ */
setup?(app: ComfyApp): Promise<void>; setup?(app: ComfyApp): Promise<void>;
/** /**
* Called before nodes are registered with the graph * Called before nodes are registered with the graph
* @param defs The collection of node definitions, add custom ones or edit existing ones * @param defs The collection of node definitions, add custom ones or edit existing ones
* @param app The ComfyUI app instance * @param app The ComfyUI app instance
*/ */
addCustomNodeDefs?(defs: Record<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>; addCustomNodeDefs?(
/** defs: Record<string, ComfyObjectInfo>,
* Allows the extension to add custom widgets app: ComfyApp
* @param app The ComfyUI app instance ): Promise<void>;
* @returns An array of {[widget name]: widget data} /**
*/ * Allows the extension to add custom widgets
getCustomWidgets?( * @param app The ComfyUI app instance
app: ComfyApp * @returns An array of {[widget name]: widget data}
): Promise< */
Record<string, (node, inputName, inputData, app) => { widget?: IWidget; minWidth?: number; minHeight?: number }> getCustomWidgets?(
>; app: ComfyApp
/** ): Promise<
* Allows the extension to add additional handling to the node before it is registered with LGraph Record<
* @param nodeType The node class (not an instance) string,
* @param nodeData The original node object info config object (
* @param app The ComfyUI app instance node,
*/ inputName,
beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise<void>; inputData,
/** app
* Allows the extension to register additional nodes with LGraph after standard nodes are added ) => { widget?: IWidget; minWidth?: number; minHeight?: number }
* @param app The ComfyUI app instance >
*/ >;
registerCustomNodes?(app: ComfyApp): Promise<void>; /**
/** * Allows the extension to add additional handling to the node before it is registered with LGraph
* Allows the extension to modify a node that has been reloaded onto the graph. * @param nodeType The node class (not an instance)
* If you break something in the backend and want to patch workflows in the frontend * @param nodeData The original node object info config object
* This is the place to do this * @param app The ComfyUI app instance
* @param node The node that has been loaded */
* @param app The ComfyUI app instance beforeRegisterNodeDef?(
*/ nodeType: typeof LGraphNode,
loadedGraphNode?(node: LGraphNode, app: ComfyApp); nodeData: ComfyObjectInfo,
/** app: ComfyApp
* Allows the extension to run code after the constructor of the node ): Promise<void>;
* @param node The node that has been created /**
* @param app The ComfyUI app instance * Allows the extension to register additional nodes with LGraph after standard nodes are added
*/ * @param app The ComfyUI app instance
nodeCreated?(node: LGraphNode, app: ComfyApp); */
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 = { export type ComfyObjectInfo = {
name: string; name: string;
display_name?: string; display_name?: string;
description?: string; description?: string;
category: string; category: string;
input?: { input?: {
required?: Record<string, ComfyObjectInfoConfig>; required?: Record<string, ComfyObjectInfoConfig>;
optional?: Record<string, ComfyObjectInfoConfig>; optional?: Record<string, ComfyObjectInfoConfig>;
}; };
output?: string[]; output?: string[];
output_name: string[]; output_name: string[];
}; };
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any]; export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];

View File

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

2767
src/types/litegraph.d.ts vendored

File diff suppressed because it is too large Load Diff